mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-03-07 03:07:36 +08:00
Compare commits
3 Commits
819731163b
...
1cf80a2489
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cf80a2489 | ||
|
|
09ccaaae6c | ||
|
|
c4607078f6 |
@@ -1,7 +1,9 @@
|
||||
"""Admin routes - Management endpoints"""
|
||||
from fastapi import APIRouter, HTTPException, Depends, Header
|
||||
from fastapi.responses import FileResponse
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
from pydantic import BaseModel
|
||||
from ..core.auth import AuthManager
|
||||
@@ -93,10 +95,11 @@ class UpdateTokenRequest(BaseModel):
|
||||
video_concurrency: Optional[int] = None # Video concurrency limit
|
||||
|
||||
class ImportTokenItem(BaseModel):
|
||||
email: str # Email (primary key)
|
||||
access_token: str # Access Token (AT)
|
||||
email: str # Email (primary key, required)
|
||||
access_token: Optional[str] = None # Access Token (AT, optional for st/rt modes)
|
||||
session_token: Optional[str] = None # Session Token (ST)
|
||||
refresh_token: Optional[str] = None # Refresh Token (RT)
|
||||
client_id: Optional[str] = None # Client ID (optional, for compatibility)
|
||||
proxy_url: Optional[str] = None # Proxy URL (optional, for compatibility)
|
||||
remark: Optional[str] = None # Remark (optional, for compatibility)
|
||||
is_active: bool = True # Active status
|
||||
@@ -107,6 +110,7 @@ class ImportTokenItem(BaseModel):
|
||||
|
||||
class ImportTokensRequest(BaseModel):
|
||||
tokens: List[ImportTokenItem]
|
||||
mode: str = "at" # Import mode: offline/at/st/rt
|
||||
|
||||
class UpdateAdminConfigRequest(BaseModel):
|
||||
error_ban_threshold: int
|
||||
@@ -346,7 +350,8 @@ async def delete_token(token_id: int, token: str = Depends(verify_admin_token)):
|
||||
|
||||
@router.post("/api/tokens/import")
|
||||
async def import_tokens(request: ImportTokensRequest, token: str = Depends(verify_admin_token)):
|
||||
"""Import tokens in append mode (update if exists, add if not)"""
|
||||
"""Import tokens with different modes: offline/at/st/rt"""
|
||||
mode = request.mode # offline/at/st/rt
|
||||
added_count = 0
|
||||
updated_count = 0
|
||||
failed_count = 0
|
||||
@@ -354,22 +359,74 @@ async def import_tokens(request: ImportTokensRequest, token: str = Depends(verif
|
||||
|
||||
for import_item in request.tokens:
|
||||
try:
|
||||
# Check if token with this email already exists
|
||||
# Step 1: Get or convert access_token based on mode
|
||||
access_token = None
|
||||
skip_status = False
|
||||
|
||||
if mode == "offline":
|
||||
# Offline mode: use provided AT, skip status update
|
||||
if not import_item.access_token:
|
||||
raise ValueError("离线导入模式需要提供 access_token")
|
||||
access_token = import_item.access_token
|
||||
skip_status = True
|
||||
|
||||
elif mode == "at":
|
||||
# AT mode: use provided AT, update status (current logic)
|
||||
if not import_item.access_token:
|
||||
raise ValueError("AT导入模式需要提供 access_token")
|
||||
access_token = import_item.access_token
|
||||
skip_status = False
|
||||
|
||||
elif mode == "st":
|
||||
# ST mode: convert ST to AT, update status
|
||||
if not import_item.session_token:
|
||||
raise ValueError("ST导入模式需要提供 session_token")
|
||||
# Convert ST to AT
|
||||
st_result = await token_manager.st_to_at(import_item.session_token)
|
||||
access_token = st_result["access_token"]
|
||||
# Update email if API returned it
|
||||
if "email" in st_result and st_result["email"]:
|
||||
import_item.email = st_result["email"]
|
||||
skip_status = False
|
||||
|
||||
elif mode == "rt":
|
||||
# RT mode: convert RT to AT, update status
|
||||
if not import_item.refresh_token:
|
||||
raise ValueError("RT导入模式需要提供 refresh_token")
|
||||
# Convert RT to AT
|
||||
rt_result = await token_manager.rt_to_at(
|
||||
import_item.refresh_token,
|
||||
client_id=import_item.client_id
|
||||
)
|
||||
access_token = rt_result["access_token"]
|
||||
# Update RT if API returned new one
|
||||
if "refresh_token" in rt_result and rt_result["refresh_token"]:
|
||||
import_item.refresh_token = rt_result["refresh_token"]
|
||||
# Update email if API returned it
|
||||
if "email" in rt_result and rt_result["email"]:
|
||||
import_item.email = rt_result["email"]
|
||||
skip_status = False
|
||||
else:
|
||||
raise ValueError(f"不支持的导入模式: {mode}")
|
||||
|
||||
# Step 2: Check if token with this email already exists
|
||||
existing_token = await db.get_token_by_email(import_item.email)
|
||||
|
||||
if existing_token:
|
||||
# Update existing token
|
||||
await token_manager.update_token(
|
||||
token_id=existing_token.id,
|
||||
token=import_item.access_token,
|
||||
token=access_token,
|
||||
st=import_item.session_token,
|
||||
rt=import_item.refresh_token,
|
||||
client_id=import_item.client_id,
|
||||
proxy_url=import_item.proxy_url,
|
||||
remark=import_item.remark,
|
||||
image_enabled=import_item.image_enabled,
|
||||
video_enabled=import_item.video_enabled,
|
||||
image_concurrency=import_item.image_concurrency,
|
||||
video_concurrency=import_item.video_concurrency
|
||||
video_concurrency=import_item.video_concurrency,
|
||||
skip_status_update=skip_status
|
||||
)
|
||||
# Update active status
|
||||
await token_manager.update_token_status(existing_token.id, import_item.is_active)
|
||||
@@ -389,16 +446,19 @@ async def import_tokens(request: ImportTokensRequest, token: str = Depends(verif
|
||||
else:
|
||||
# Add new token
|
||||
new_token = await token_manager.add_token(
|
||||
token_value=import_item.access_token,
|
||||
token_value=access_token,
|
||||
st=import_item.session_token,
|
||||
rt=import_item.refresh_token,
|
||||
client_id=import_item.client_id,
|
||||
proxy_url=import_item.proxy_url,
|
||||
remark=import_item.remark,
|
||||
update_if_exists=False,
|
||||
image_enabled=import_item.image_enabled,
|
||||
video_enabled=import_item.video_enabled,
|
||||
image_concurrency=import_item.image_concurrency,
|
||||
video_concurrency=import_item.video_concurrency
|
||||
video_concurrency=import_item.video_concurrency,
|
||||
skip_status_update=skip_status,
|
||||
email=import_item.email # Pass email for offline mode
|
||||
)
|
||||
# Set active status
|
||||
if not import_item.is_active:
|
||||
@@ -427,7 +487,7 @@ async def import_tokens(request: ImportTokensRequest, token: str = Depends(verif
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Import completed: {added_count} added, {updated_count} updated, {failed_count} failed",
|
||||
"message": f"Import completed ({mode} mode): {added_count} added, {updated_count} updated, {failed_count} failed",
|
||||
"added": added_count,
|
||||
"updated": updated_count,
|
||||
"failed": failed_count,
|
||||
@@ -963,3 +1023,18 @@ async def update_at_auto_refresh_enabled(
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update AT auto refresh enabled status: {str(e)}")
|
||||
|
||||
# Debug logs download endpoint
|
||||
@router.get("/api/admin/logs/download")
|
||||
async def download_debug_logs(token: str = Depends(verify_admin_token)):
|
||||
"""Download debug logs file (logs.txt)"""
|
||||
log_file = Path("logs.txt")
|
||||
|
||||
if not log_file.exists():
|
||||
raise HTTPException(status_code=404, detail="日志文件不存在")
|
||||
|
||||
return FileResponse(
|
||||
path=str(log_file),
|
||||
filename="logs.txt",
|
||||
media_type="text/plain"
|
||||
)
|
||||
|
||||
@@ -198,6 +198,7 @@ class Database:
|
||||
("video_concurrency", "INTEGER DEFAULT -1"),
|
||||
("client_id", "TEXT"),
|
||||
("proxy_url", "TEXT"),
|
||||
("is_expired", "BOOLEAN DEFAULT 0"),
|
||||
]
|
||||
|
||||
for col_name, col_type in columns_to_add:
|
||||
@@ -310,7 +311,8 @@ class Database:
|
||||
image_enabled BOOLEAN DEFAULT 1,
|
||||
video_enabled BOOLEAN DEFAULT 1,
|
||||
image_concurrency INTEGER DEFAULT -1,
|
||||
video_concurrency INTEGER DEFAULT -1
|
||||
video_concurrency INTEGER DEFAULT -1,
|
||||
is_expired BOOLEAN DEFAULT 0
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -570,7 +572,23 @@ class Database:
|
||||
UPDATE tokens SET is_active = ? WHERE id = ?
|
||||
""", (is_active, token_id))
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def mark_token_expired(self, token_id: int):
|
||||
"""Mark token as expired and disable it"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("""
|
||||
UPDATE tokens SET is_expired = 1, is_active = 0 WHERE id = ?
|
||||
""", (token_id,))
|
||||
await db.commit()
|
||||
|
||||
async def clear_token_expired(self, token_id: int):
|
||||
"""Clear token expired flag"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("""
|
||||
UPDATE tokens SET is_expired = 0 WHERE id = ?
|
||||
""", (token_id,))
|
||||
await db.commit()
|
||||
|
||||
async def update_token_sora2(self, token_id: int, supported: bool, invite_code: Optional[str] = None,
|
||||
redeemed_count: int = 0, total_count: int = 0, remaining_count: int = 0):
|
||||
"""Update token Sora2 support info"""
|
||||
|
||||
@@ -15,17 +15,21 @@ class DebugLogger:
|
||||
|
||||
def _setup_logger(self):
|
||||
"""Setup file logger"""
|
||||
# Clear log file on startup
|
||||
if self.log_file.exists():
|
||||
self.log_file.unlink()
|
||||
|
||||
# Create logger
|
||||
self.logger = logging.getLogger("debug_logger")
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
# Remove existing handlers
|
||||
self.logger.handlers.clear()
|
||||
|
||||
|
||||
# Create file handler
|
||||
file_handler = logging.FileHandler(
|
||||
self.log_file,
|
||||
mode='a',
|
||||
self.log_file,
|
||||
mode='a',
|
||||
encoding='utf-8'
|
||||
)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
@@ -38,6 +38,8 @@ class Token(BaseModel):
|
||||
# 并发限制
|
||||
image_concurrency: int = -1 # 图片并发数限制,-1表示不限制
|
||||
video_concurrency: int = -1 # 视频并发数限制,-1表示不限制
|
||||
# 过期标记
|
||||
is_expired: bool = False # Token是否已过期(401 token_invalidated)
|
||||
|
||||
class TokenStats(BaseModel):
|
||||
"""Token statistics"""
|
||||
|
||||
@@ -86,6 +86,15 @@ class TokenManager:
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
# Check for token_invalidated error
|
||||
if response.status_code == 401:
|
||||
try:
|
||||
error_data = response.json()
|
||||
error_code = error_data.get("error", {}).get("code", "")
|
||||
if error_code == "token_invalidated":
|
||||
raise ValueError(f"401 token_invalidated: Token has been invalidated")
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
raise ValueError(f"Failed to get user info: {response.status_code}")
|
||||
|
||||
return response.json()
|
||||
@@ -649,7 +658,9 @@ class TokenManager:
|
||||
image_enabled: bool = True,
|
||||
video_enabled: bool = True,
|
||||
image_concurrency: int = -1,
|
||||
video_concurrency: int = -1) -> Token:
|
||||
video_concurrency: int = -1,
|
||||
skip_status_update: bool = False,
|
||||
email: Optional[str] = None) -> Token:
|
||||
"""Add a new Access Token to database
|
||||
|
||||
Args:
|
||||
@@ -690,101 +701,112 @@ class TokenManager:
|
||||
if "https://api.openai.com/profile" in decoded:
|
||||
jwt_email = decoded["https://api.openai.com/profile"].get("email")
|
||||
|
||||
# Get user info from Sora API
|
||||
try:
|
||||
user_info = await self.get_user_info(token_value, proxy_url=proxy_url)
|
||||
email = user_info.get("email", jwt_email or "")
|
||||
name = user_info.get("name") or ""
|
||||
except Exception as e:
|
||||
# If API call fails, use JWT data
|
||||
email = jwt_email or ""
|
||||
name = email.split("@")[0] if email else ""
|
||||
|
||||
# Get subscription info from Sora API
|
||||
# Initialize variables
|
||||
name = ""
|
||||
plan_type = None
|
||||
plan_title = None
|
||||
subscription_end = None
|
||||
try:
|
||||
sub_info = await self.get_subscription_info(token_value, proxy_url=proxy_url)
|
||||
plan_type = sub_info.get("plan_type")
|
||||
plan_title = sub_info.get("plan_title")
|
||||
# Parse subscription end time
|
||||
if sub_info.get("subscription_end"):
|
||||
from dateutil import parser
|
||||
subscription_end = parser.parse(sub_info["subscription_end"])
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
# Re-raise if it's a critical error (token expired)
|
||||
if "Token已过期" in error_msg:
|
||||
raise
|
||||
# If API call fails, subscription info will be None
|
||||
print(f"Failed to get subscription info: {e}")
|
||||
|
||||
# Get Sora2 invite code
|
||||
sora2_supported = None
|
||||
sora2_invite_code = None
|
||||
sora2_redeemed_count = 0
|
||||
sora2_total_count = 0
|
||||
sora2_remaining_count = 0
|
||||
try:
|
||||
sora2_info = await self.get_sora2_invite_code(token_value, proxy_url=proxy_url)
|
||||
sora2_supported = sora2_info.get("supported", False)
|
||||
sora2_invite_code = sora2_info.get("invite_code")
|
||||
sora2_redeemed_count = sora2_info.get("redeemed_count", 0)
|
||||
sora2_total_count = sora2_info.get("total_count", 0)
|
||||
sora2_redeemed_count = -1
|
||||
sora2_total_count = -1
|
||||
sora2_remaining_count = -1
|
||||
|
||||
# If Sora2 is supported, get remaining count
|
||||
if sora2_supported:
|
||||
try:
|
||||
remaining_info = await self.get_sora2_remaining_count(token_value, proxy_url=proxy_url)
|
||||
if remaining_info.get("success"):
|
||||
sora2_remaining_count = remaining_info.get("remaining_count", 0)
|
||||
print(f"✅ Sora2剩余次数: {sora2_remaining_count}")
|
||||
except Exception as e:
|
||||
print(f"Failed to get Sora2 remaining count: {e}")
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
# Re-raise if it's a critical error (unsupported country)
|
||||
if "Sora在您的国家/地区不可用" in error_msg:
|
||||
raise
|
||||
# If API call fails, Sora2 info will be None
|
||||
print(f"Failed to get Sora2 info: {e}")
|
||||
if skip_status_update:
|
||||
# Offline mode: use provided email or JWT email, skip API calls
|
||||
email = email or jwt_email or ""
|
||||
name = email.split("@")[0] if email else ""
|
||||
else:
|
||||
# Normal mode: get user info from Sora API
|
||||
try:
|
||||
user_info = await self.get_user_info(token_value, proxy_url=proxy_url)
|
||||
email = user_info.get("email", jwt_email or "")
|
||||
name = user_info.get("name") or ""
|
||||
except Exception as e:
|
||||
# If API call fails, use JWT data
|
||||
email = jwt_email or ""
|
||||
name = email.split("@")[0] if email else ""
|
||||
|
||||
# Check and set username if needed
|
||||
try:
|
||||
# Get fresh user info to check username
|
||||
user_info = await self.get_user_info(token_value, proxy_url=proxy_url)
|
||||
username = user_info.get("username")
|
||||
# Get subscription info from Sora API
|
||||
try:
|
||||
sub_info = await self.get_subscription_info(token_value, proxy_url=proxy_url)
|
||||
plan_type = sub_info.get("plan_type")
|
||||
plan_title = sub_info.get("plan_title")
|
||||
# Parse subscription end time
|
||||
if sub_info.get("subscription_end"):
|
||||
from dateutil import parser
|
||||
subscription_end = parser.parse(sub_info["subscription_end"])
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
# Re-raise if it's a critical error (token expired)
|
||||
if "Token已过期" in error_msg:
|
||||
raise
|
||||
# If API call fails, subscription info will be None
|
||||
print(f"Failed to get subscription info: {e}")
|
||||
|
||||
# If username is null, need to set one
|
||||
if username is None:
|
||||
print(f"⚠️ 检测到用户名为null,需要设置用户名")
|
||||
# Get Sora2 invite code
|
||||
sora2_redeemed_count = 0
|
||||
sora2_total_count = 0
|
||||
sora2_remaining_count = 0
|
||||
try:
|
||||
sora2_info = await self.get_sora2_invite_code(token_value, proxy_url=proxy_url)
|
||||
sora2_supported = sora2_info.get("supported", False)
|
||||
sora2_invite_code = sora2_info.get("invite_code")
|
||||
sora2_redeemed_count = sora2_info.get("redeemed_count", 0)
|
||||
sora2_total_count = sora2_info.get("total_count", 0)
|
||||
|
||||
# Generate random username
|
||||
max_attempts = 5
|
||||
for attempt in range(max_attempts):
|
||||
generated_username = self._generate_random_username()
|
||||
print(f"🔄 尝试用户名 ({attempt + 1}/{max_attempts}): {generated_username}")
|
||||
# If Sora2 is supported, get remaining count
|
||||
if sora2_supported:
|
||||
try:
|
||||
remaining_info = await self.get_sora2_remaining_count(token_value, proxy_url=proxy_url)
|
||||
if remaining_info.get("success"):
|
||||
sora2_remaining_count = remaining_info.get("remaining_count", 0)
|
||||
print(f"✅ Sora2剩余次数: {sora2_remaining_count}")
|
||||
except Exception as e:
|
||||
print(f"Failed to get Sora2 remaining count: {e}")
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
# Re-raise if it's a critical error (unsupported country)
|
||||
if "Sora在您的国家/地区不可用" in error_msg:
|
||||
raise
|
||||
# If API call fails, Sora2 info will be None
|
||||
print(f"Failed to get Sora2 info: {e}")
|
||||
|
||||
# Check if username is available
|
||||
if await self.check_username_available(token_value, generated_username):
|
||||
# Set the username
|
||||
try:
|
||||
await self.set_username(token_value, generated_username)
|
||||
print(f"✅ 用户名设置成功: {generated_username}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ 用户名设置失败: {e}")
|
||||
# Check and set username if needed
|
||||
try:
|
||||
# Get fresh user info to check username
|
||||
user_info = await self.get_user_info(token_value, proxy_url=proxy_url)
|
||||
username = user_info.get("username")
|
||||
|
||||
# If username is null, need to set one
|
||||
if username is None:
|
||||
print(f"⚠️ 检测到用户名为null,需要设置用户名")
|
||||
|
||||
# Generate random username
|
||||
max_attempts = 5
|
||||
for attempt in range(max_attempts):
|
||||
generated_username = self._generate_random_username()
|
||||
print(f"🔄 尝试用户名 ({attempt + 1}/{max_attempts}): {generated_username}")
|
||||
|
||||
# Check if username is available
|
||||
if await self.check_username_available(token_value, generated_username):
|
||||
# Set the username
|
||||
try:
|
||||
await self.set_username(token_value, generated_username)
|
||||
print(f"✅ 用户名设置成功: {generated_username}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ 用户名设置失败: {e}")
|
||||
if attempt == max_attempts - 1:
|
||||
print(f"⚠️ 达到最大尝试次数,跳过用户名设置")
|
||||
else:
|
||||
print(f"⚠️ 用户名 {generated_username} 已被占用,尝试下一个")
|
||||
if attempt == max_attempts - 1:
|
||||
print(f"⚠️ 达到最大尝试次数,跳过用户名设置")
|
||||
else:
|
||||
print(f"⚠️ 用户名 {generated_username} 已被占用,尝试下一个")
|
||||
if attempt == max_attempts - 1:
|
||||
print(f"⚠️ 达到最大尝试次数,跳过用户名设置")
|
||||
else:
|
||||
print(f"✅ 用户名已设置: {username}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 用户名检查/设置过程中出错: {e}")
|
||||
else:
|
||||
print(f"✅ 用户名已设置: {username}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 用户名检查/设置过程中出错: {e}")
|
||||
|
||||
# Create token object
|
||||
token = Token(
|
||||
@@ -885,7 +907,8 @@ class TokenManager:
|
||||
image_enabled: Optional[bool] = None,
|
||||
video_enabled: Optional[bool] = None,
|
||||
image_concurrency: Optional[int] = None,
|
||||
video_concurrency: Optional[int] = None):
|
||||
video_concurrency: Optional[int] = None,
|
||||
skip_status_update: bool = False):
|
||||
"""Update token (AT, ST, RT, client_id, proxy_url, remark, image_enabled, video_enabled, concurrency limits)"""
|
||||
# If token (AT) is updated, decode JWT to get new expiry time
|
||||
expiry_time = None
|
||||
@@ -900,6 +923,17 @@ class TokenManager:
|
||||
image_enabled=image_enabled, video_enabled=video_enabled,
|
||||
image_concurrency=image_concurrency, video_concurrency=video_concurrency)
|
||||
|
||||
# If token (AT) is updated and not in offline mode, test it and clear expired flag if valid
|
||||
if token and not skip_status_update:
|
||||
try:
|
||||
test_result = await self.test_token(token_id)
|
||||
if test_result.get("valid"):
|
||||
# Token is valid, enable it and clear expired flag
|
||||
await self.db.update_token_status(token_id, True)
|
||||
await self.db.clear_token_expired(token_id)
|
||||
except Exception:
|
||||
pass # Ignore test errors during update
|
||||
|
||||
async def get_active_tokens(self) -> List[Token]:
|
||||
"""Get all active tokens (not cooled down)"""
|
||||
return await self.db.get_active_tokens()
|
||||
@@ -917,13 +951,15 @@ class TokenManager:
|
||||
await self.db.update_token_status(token_id, True)
|
||||
# Reset error count when enabling (in token_stats table)
|
||||
await self.db.reset_error_count(token_id)
|
||||
# Clear expired flag when enabling
|
||||
await self.db.clear_token_expired(token_id)
|
||||
|
||||
async def disable_token(self, token_id: int):
|
||||
"""Disable a token"""
|
||||
await self.db.update_token_status(token_id, False)
|
||||
|
||||
async def test_token(self, token_id: int) -> dict:
|
||||
"""Test if a token is valid by calling Sora API and refresh Sora2 info"""
|
||||
"""Test if a token is valid by calling Sora API and refresh account info (subscription + Sora2)"""
|
||||
# Get token from database
|
||||
token_data = await self.db.get_token(token_id)
|
||||
if not token_data:
|
||||
@@ -933,6 +969,21 @@ class TokenManager:
|
||||
# Try to get user info from Sora API
|
||||
user_info = await self.get_user_info(token_data.token, token_id)
|
||||
|
||||
# Get subscription info from Sora API
|
||||
plan_type = None
|
||||
plan_title = None
|
||||
subscription_end = None
|
||||
try:
|
||||
sub_info = await self.get_subscription_info(token_data.token, token_id)
|
||||
plan_type = sub_info.get("plan_type")
|
||||
plan_title = sub_info.get("plan_title")
|
||||
# Parse subscription end time
|
||||
if sub_info.get("subscription_end"):
|
||||
from dateutil import parser
|
||||
subscription_end = parser.parse(sub_info["subscription_end"])
|
||||
except Exception as e:
|
||||
print(f"Failed to get subscription info: {e}")
|
||||
|
||||
# Refresh Sora2 invite code and counts
|
||||
sora2_info = await self.get_sora2_invite_code(token_data.token, token_id)
|
||||
sora2_supported = sora2_info.get("supported", False)
|
||||
@@ -950,6 +1001,14 @@ class TokenManager:
|
||||
except Exception as e:
|
||||
print(f"Failed to get Sora2 remaining count: {e}")
|
||||
|
||||
# Update token subscription info in database
|
||||
await self.db.update_token(
|
||||
token_id,
|
||||
plan_type=plan_type,
|
||||
plan_title=plan_title,
|
||||
subscription_end=subscription_end
|
||||
)
|
||||
|
||||
# Update token Sora2 info in database
|
||||
await self.db.update_token_sora2(
|
||||
token_id,
|
||||
@@ -960,11 +1019,17 @@ class TokenManager:
|
||||
remaining_count=sora2_remaining_count
|
||||
)
|
||||
|
||||
# Clear expired flag if token is valid
|
||||
await self.db.clear_token_expired(token_id)
|
||||
|
||||
return {
|
||||
"valid": True,
|
||||
"message": "Token is valid",
|
||||
"message": "Token is valid and account info updated",
|
||||
"email": user_info.get("email"),
|
||||
"username": user_info.get("username"),
|
||||
"plan_type": plan_type,
|
||||
"plan_title": plan_title,
|
||||
"subscription_end": subscription_end.isoformat() if subscription_end else None,
|
||||
"sora2_supported": sora2_supported,
|
||||
"sora2_invite_code": sora2_invite_code,
|
||||
"sora2_redeemed_count": sora2_redeemed_count,
|
||||
@@ -972,9 +1037,18 @@ class TokenManager:
|
||||
"sora2_remaining_count": sora2_remaining_count
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
# Check if error is 401 with token_invalidated
|
||||
if "401" in error_msg and "token_invalidated" in error_msg.lower():
|
||||
# Mark token as expired
|
||||
await self.db.mark_token_expired(token_id)
|
||||
return {
|
||||
"valid": False,
|
||||
"message": "Token已过期(token_invalidated)"
|
||||
}
|
||||
return {
|
||||
"valid": False,
|
||||
"message": f"Token is invalid: {str(e)}"
|
||||
"message": f"Token is invalid: {error_msg}"
|
||||
}
|
||||
|
||||
async def record_usage(self, token_id: int, is_video: bool = False):
|
||||
|
||||
@@ -331,6 +331,17 @@
|
||||
</label>
|
||||
<p class="text-xs text-muted-foreground mt-2">开启后,详细的上游API请求和响应日志将写入 <code class="bg-muted px-1 py-0.5 rounded">logs.txt</code> 文件,立即生效</p>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="downloadDebugLogs()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 text-sm font-medium transition-colors">
|
||||
<svg class="h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
下载日志文件
|
||||
</button>
|
||||
<p class="text-xs text-muted-foreground mt-2">下载完整的调试日志文件 (logs.txt)</p>
|
||||
</div>
|
||||
<div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-3 border border-yellow-200 dark:border-yellow-800">
|
||||
<p class="text-xs text-yellow-800 dark:text-yellow-200">
|
||||
⚠️ <strong>注意:</strong>调试模式会产生非常非常大量的日志,仅限Debug时候开启,否则磁盘boom
|
||||
@@ -666,6 +677,40 @@
|
||||
<input type="file" id="importFile" accept=".json" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<p class="text-xs text-muted-foreground mt-1">选择导出的 Token JSON 文件进行导入</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-2 block">选择导入模式</label>
|
||||
<select id="importMode" onchange="updateImportModeHint()" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<option value="at">优先使用AT导入(推荐)</option>
|
||||
<option value="offline">离线导入(不更新账号状态)</option>
|
||||
<option value="st">优先使用ST导入</option>
|
||||
<option value="rt">优先使用RT导入</option>
|
||||
</select>
|
||||
<p id="importModeHint" class="text-xs text-muted-foreground mt-1">使用AT更新账号状态(订阅信息、Sora2次数等)</p>
|
||||
</div>
|
||||
<div class="rounded-md bg-gray-50 dark:bg-gray-900/20 p-3 border border-gray-200 dark:border-gray-800">
|
||||
<p class="text-xs font-semibold text-gray-900 dark:text-gray-100 mb-2">📋 导入模式说明</p>
|
||||
<div class="space-y-1.5 text-xs text-gray-700 dark:text-gray-300">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium min-w-[100px]">AT导入:</span>
|
||||
<span>完整更新所有账号信息(推荐)</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium min-w-[100px]">离线导入:</span>
|
||||
<span>直接插入数据库,快速导入不调用API,账号信息需要单独获取 -</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium min-w-[100px]">ST导入:</span>
|
||||
<span>适用于只有ST没有AT,自动转换为AT</span>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="font-medium min-w-[100px]">RT导入:</span>
|
||||
<span>适用于只有RT没有AT,自动转换为AT</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-2 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
💡 提示:离线导入后可使用"测试"按钮更新账号信息,功能不稳定有bug问猫猫
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
|
||||
<p class="text-xs text-blue-800 dark:text-blue-200">
|
||||
<strong>说明:</strong>如果邮箱存在则会覆盖更新,不存在则会新增
|
||||
@@ -719,7 +764,7 @@
|
||||
formatPlanTypeWithTooltip=(t)=>{const tooltipText=t.subscription_end?`套餐到期: ${new Date(t.subscription_end).toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'}).replace(/\//g,'-')} ${new Date(t.subscription_end).toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',hour12:false})}`:'';return`<span class="inline-flex items-center rounded px-2 py-0.5 text-xs bg-blue-50 text-blue-700 cursor-pointer" title="${tooltipText||t.plan_title||'-'}">${formatPlanType(t.plan_type)}</span>`},
|
||||
formatSora2Remaining=(t)=>{if(t.sora2_supported===true){const remaining=t.sora2_remaining_count||0;return`<span class="text-xs">${remaining}</span>`}else{return'-'}},
|
||||
formatClientId=(clientId)=>{if(!clientId)return'-';const short=clientId.substring(0,8)+'...';return`<span class="text-xs font-mono cursor-pointer hover:text-primary" title="${clientId}" onclick="navigator.clipboard.writeText('${clientId}').then(()=>showToast('已复制','success'))">${short}</span>`},
|
||||
renderTokens=()=>{const tb=$('tokenTableBody');tb.innerHTML=allTokens.map(t=>{const imageDisplay=t.image_enabled?`${t.image_count||0}`:'-';const videoDisplay=(t.video_enabled&&t.sora2_supported)?`${t.video_count||0}`:'-';return`<tr><td class="py-2.5 px-3">${t.email}</td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${t.is_active?'bg-green-50 text-green-700':'bg-gray-100 text-gray-700'}">${t.is_active?'活跃':'禁用'}</span></td><td class="py-2.5 px-3">${formatClientId(t.client_id)}</td><td class="py-2.5 px-3 text-xs">${formatExpiry(t.expiry_time)}</td><td class="py-2.5 px-3 text-xs">${formatPlanTypeWithTooltip(t)}</td><td class="py-2.5 px-3 text-xs">${formatSora2(t)}</td><td class="py-2.5 px-3">${formatSora2Remaining(t)}</td><td class="py-2.5 px-3">${imageDisplay}</td><td class="py-2.5 px-3">${videoDisplay}</td><td class="py-2.5 px-3">${t.error_count||0}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${t.remark||'-'}</td><td class="py-2.5 px-3 text-right"><button onclick="testToken(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs mr-1">测试</button><button onclick="openEditModal(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-green-50 hover:text-green-700 h-7 px-2 text-xs mr-1">编辑</button><button onclick="toggleToken(${t.id},${t.is_active})" class="inline-flex items-center justify-center rounded-md hover:bg-accent h-7 px-2 text-xs mr-1">${t.is_active?'禁用':'启用'}</button><button onclick="deleteToken(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-destructive/10 hover:text-destructive h-7 px-2 text-xs">删除</button></td></tr>`}).join('')},
|
||||
renderTokens=()=>{const tb=$('tokenTableBody');tb.innerHTML=allTokens.map(t=>{const imageDisplay=t.image_enabled?`${t.image_count||0}`:'-';const videoDisplay=(t.video_enabled&&t.sora2_supported)?`${t.video_count||0}`:'-';const statusText=t.is_expired?'已过期':(t.is_active?'活跃':'禁用');const statusClass=t.is_expired?'bg-gray-100 text-gray-700':(t.is_active?'bg-green-50 text-green-700':'bg-gray-100 text-gray-700');return`<tr><td class=\"py-2.5 px-3\">${t.email}</td><td class=\"py-2.5 px-3\"><span class=\"inline-flex items-center rounded px-2 py-0.5 text-xs ${statusClass}\">${statusText}</span></td><td class="py-2.5 px-3">${formatClientId(t.client_id)}</td><td class="py-2.5 px-3 text-xs">${formatExpiry(t.expiry_time)}</td><td class="py-2.5 px-3 text-xs">${formatPlanTypeWithTooltip(t)}</td><td class="py-2.5 px-3 text-xs">${formatSora2(t)}</td><td class="py-2.5 px-3">${formatSora2Remaining(t)}</td><td class="py-2.5 px-3">${imageDisplay}</td><td class="py-2.5 px-3">${videoDisplay}</td><td class="py-2.5 px-3">${t.error_count||0}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${t.remark||'-'}</td><td class="py-2.5 px-3 text-right"><button onclick="testToken(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs mr-1">测试</button><button onclick="openEditModal(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-green-50 hover:text-green-700 h-7 px-2 text-xs mr-1">编辑</button><button onclick="toggleToken(${t.id},${t.is_active})" class="inline-flex items-center justify-center rounded-md hover:bg-accent h-7 px-2 text-xs mr-1">${t.is_active?'禁用':'启用'}</button><button onclick="deleteToken(${t.id})" class="inline-flex items-center justify-center rounded-md hover:bg-destructive/10 hover:text-destructive h-7 px-2 text-xs">删除</button></td></tr>`}).join('')},
|
||||
refreshTokens=async()=>{await loadTokens();await loadStats()},
|
||||
openAddModal=()=>$('addModal').classList.remove('hidden'),
|
||||
closeAddModal=()=>{$('addModal').classList.add('hidden');$('addTokenAT').value='';$('addTokenST').value='';$('addTokenRT').value='';$('addTokenClientId').value='';$('addTokenProxyUrl').value='';$('addTokenRemark').value='';$('addTokenImageEnabled').checked=true;$('addTokenVideoEnabled').checked=true;$('addTokenImageConcurrency').value='-1';$('addTokenVideoConcurrency').value='-1';$('addRTRefreshHint').classList.add('hidden')},
|
||||
@@ -743,14 +788,16 @@
|
||||
openImportProgressModal=()=>{$('importProgressModal').classList.remove('hidden')},
|
||||
closeImportProgressModal=()=>{$('importProgressModal').classList.add('hidden')},
|
||||
showImportProgress=(results,added,updated,failed)=>{const summary=$('importProgressSummary'),list=$('importProgressList');summary.innerHTML=`<div class="text-sm"><strong>导入完成</strong><br/>新增: ${added} | 更新: ${updated} | 失败: ${failed}</div>`;list.innerHTML=results.map(r=>{const statusColor=r.success?(r.status==='added'?'text-green-600':'text-blue-600'):'text-red-600';const statusText=r.status==='added'?'新增':r.status==='updated'?'更新':'失败';return`<div class="p-3 rounded-md border ${r.success?'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800':'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800'}"><div class="flex items-center justify-between"><span class="font-medium">${r.email}</span><span class="${statusColor} text-sm font-medium">${statusText}</span></div>${r.error?`<div class="text-xs text-red-600 dark:text-red-400 mt-1">${r.error}</div>`:''}</div>`}).join('');openImportProgressModal()},
|
||||
exportTokens=()=>{if(allTokens.length===0){showToast('没有Token可导出','error');return}const exportData=allTokens.map(t=>({email:t.email,access_token:t.token,session_token:t.st||null,refresh_token:t.rt||null,proxy_url:t.proxy_url||null,remark:t.remark||null,is_active:t.is_active,image_enabled:t.image_enabled!==false,video_enabled:t.video_enabled!==false,image_concurrency:t.image_concurrency||(-1),video_concurrency:t.video_concurrency||(-1)}));const dataStr=JSON.stringify(exportData,null,2);const dataBlob=new Blob([dataStr],{type:'application/json'});const url=URL.createObjectURL(dataBlob);const link=document.createElement('a');link.href=url;link.download=`tokens_${new Date().toISOString().split('T')[0]}.json`;document.body.appendChild(link);link.click();document.body.removeChild(link);URL.revokeObjectURL(url);showToast(`已导出 ${allTokens.length} 个Token`,'success')},
|
||||
submitImportTokens=async()=>{const fileInput=$('importFile');if(!fileInput.files||fileInput.files.length===0){showToast('请选择文件','error');return}const file=fileInput.files[0];if(!file.name.endsWith('.json')){showToast('请选择JSON文件','error');return}try{const fileContent=await file.text();const importData=JSON.parse(fileContent);if(!Array.isArray(importData)){showToast('JSON格式错误:应为数组','error');return}if(importData.length===0){showToast('JSON文件为空','error');return}const btn=$('importBtn'),btnText=$('importBtnText'),btnSpinner=$('importBtnSpinner');btn.disabled=true;btnText.textContent='导入中...';btnSpinner.classList.remove('hidden');try{const r=await apiRequest('/api/tokens/import',{method:'POST',body:JSON.stringify({tokens:importData})});if(!r){btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden');return}const d=await r.json();if(d.success){closeImportModal();await refreshTokens();showImportProgress(d.results||[],d.added||0,d.updated||0,d.failed||0)}else{showToast('导入失败: '+(d.detail||d.message||'未知错误'),'error')}}catch(e){showToast('导入失败: '+e.message,'error')}finally{btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden')}}catch(e){showToast('文件解析失败: '+e.message,'error')}},
|
||||
exportTokens=()=>{if(allTokens.length===0){showToast('没有Token可导出','error');return}const exportData=allTokens.map(t=>({email:t.email,access_token:t.token,session_token:t.st||null,refresh_token:t.rt||null,client_id:t.client_id||null,proxy_url:t.proxy_url||null,remark:t.remark||null,is_active:t.is_active,image_enabled:t.image_enabled!==false,video_enabled:t.video_enabled!==false,image_concurrency:t.image_concurrency||(-1),video_concurrency:t.video_concurrency||(-1)}));const dataStr=JSON.stringify(exportData,null,2);const dataBlob=new Blob([dataStr],{type:'application/json'});const url=URL.createObjectURL(dataBlob);const link=document.createElement('a');link.href=url;link.download=`tokens_${new Date().toISOString().split('T')[0]}.json`;document.body.appendChild(link);link.click();document.body.removeChild(link);URL.revokeObjectURL(url);showToast(`已导出 ${allTokens.length} 个Token`,'success')},
|
||||
updateImportModeHint=()=>{const mode=$('importMode').value,hint=$('importModeHint'),hints={at:'使用AT更新账号状态(订阅信息、Sora2次数等)',offline:'离线导入,不更新账号状态,动态字段显示为-',st:'自动将ST转换为AT,然后更新账号状态',rt:'自动将RT转换为AT(并刷新RT),然后更新账号状态'};hint.textContent=hints[mode]||''},
|
||||
submitImportTokens=async()=>{const fileInput=$('importFile');if(!fileInput.files||fileInput.files.length===0){showToast('请选择文件','error');return}const file=fileInput.files[0];if(!file.name.endsWith('.json')){showToast('请选择JSON文件','error');return}const mode=$('importMode').value;try{const fileContent=await file.text();const importData=JSON.parse(fileContent);if(!Array.isArray(importData)){showToast('JSON格式错误:应为数组','error');return}if(importData.length===0){showToast('JSON文件为空','error');return}for(let item of importData){if(!item.email){showToast('导入数据缺少必填字段: email','error');return}if(mode==='offline'||mode==='at'){if(!item.access_token){showToast(`${item.email} 缺少必填字段: access_token`,'error');return}}else if(mode==='st'){if(!item.session_token){showToast(`${item.email} 缺少必填字段: session_token`,'error');return}}else if(mode==='rt'){if(!item.refresh_token){showToast(`${item.email} 缺少必填字段: refresh_token`,'error');return}}}const btn=$('importBtn'),btnText=$('importBtnText'),btnSpinner=$('importBtnSpinner');btn.disabled=true;btnText.textContent='导入中...';btnSpinner.classList.remove('hidden');try{const r=await apiRequest('/api/tokens/import',{method:'POST',body:JSON.stringify({tokens:importData,mode:mode})});if(!r){btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden');return}const d=await r.json();if(d.success){closeImportModal();await refreshTokens();showImportProgress(d.results||[],d.added||0,d.updated||0,d.failed||0)}else{showToast('导入失败: '+(d.detail||d.message||'未知错误'),'error')}}catch(e){showToast('导入失败: '+e.message,'error')}finally{btn.disabled=false;btnText.textContent='导入';btnSpinner.classList.add('hidden')}}catch(e){showToast('文件解析失败: '+e.message,'error')}},
|
||||
submitSora2Activate=async()=>{const tokenId=parseInt($('sora2TokenId').value),inviteCode=$('sora2InviteCode').value.trim();if(!tokenId)return showToast('Token ID无效','error');if(!inviteCode)return showToast('请输入邀请码','error');if(inviteCode.length!==6)return showToast('邀请码必须是6位','error');const btn=$('sora2ActivateBtn'),btnText=$('sora2ActivateBtnText'),btnSpinner=$('sora2ActivateBtnSpinner');btn.disabled=true;btnText.textContent='激活中...';btnSpinner.classList.remove('hidden');try{showToast('正在激活Sora2...','info');const r=await apiRequest(`/api/tokens/${tokenId}/sora2/activate?invite_code=${inviteCode}`,{method:'POST'});if(!r){btn.disabled=false;btnText.textContent='激活';btnSpinner.classList.add('hidden');return}const d=await r.json();if(d.success){closeSora2Modal();await refreshTokens();if(d.already_accepted){showToast('Sora2已激活(之前已接受)','success')}else{showToast(`Sora2激活成功!邀请码: ${d.invite_code||'无'}`,'success')}}else{showToast('激活失败: '+(d.message||'未知错误'),'error')}}catch(e){showToast('激活失败: '+e.message,'error')}finally{btn.disabled=false;btnText.textContent='激活';btnSpinner.classList.add('hidden')}},
|
||||
loadAdminConfig=async()=>{try{const r=await apiRequest('/api/admin/config');if(!r)return;const d=await r.json();$('cfgErrorBan').value=d.error_ban_threshold||3;$('cfgAdminUsername').value=d.admin_username||'admin';$('cfgCurrentAPIKey').value=d.api_key||'';$('cfgDebugEnabled').checked=d.debug_enabled||false}catch(e){console.error('加载配置失败:',e)}},
|
||||
saveAdminConfig=async()=>{try{const r=await apiRequest('/api/admin/config',{method:'POST',body:JSON.stringify({error_ban_threshold:parseInt($('cfgErrorBan').value)||3})});if(!r)return;const d=await r.json();d.success?showToast('配置保存成功','success'):showToast('保存失败','error')}catch(e){showToast('保存失败: '+e.message,'error')}},
|
||||
updateAdminPassword=async()=>{const username=$('cfgAdminUsername').value.trim(),oldPwd=$('cfgOldPassword').value.trim(),newPwd=$('cfgNewPassword').value.trim();if(!oldPwd||!newPwd)return showToast('请输入旧密码和新密码','error');if(newPwd.length<4)return showToast('新密码至少4个字符','error');try{const r=await apiRequest('/api/admin/password',{method:'POST',body:JSON.stringify({username:username||undefined,old_password:oldPwd,new_password:newPwd})});if(!r)return;const d=await r.json();if(d.success){showToast('密码修改成功,请重新登录','success');setTimeout(()=>{localStorage.removeItem('adminToken');location.href='/login'},2000)}else{showToast('修改失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('修改失败: '+e.message,'error')}},
|
||||
updateAPIKey=async()=>{const newKey=$('cfgNewAPIKey').value.trim();if(!newKey)return showToast('请输入新的 API Key','error');if(newKey.length<6)return showToast('API Key 至少6个字符','error');if(!confirm('确定要更新 API Key 吗?更新后需要通知所有客户端使用新密钥。'))return;try{const r=await apiRequest('/api/admin/apikey',{method:'POST',body:JSON.stringify({new_api_key:newKey})});if(!r)return;const d=await r.json();if(d.success){showToast('API Key 更新成功','success');$('cfgCurrentAPIKey').value=newKey;$('cfgNewAPIKey').value=''}else{showToast('更新失败: '+(d.detail||'未知错误'),'error')}}catch(e){showToast('更新失败: '+e.message,'error')}},
|
||||
toggleDebugMode=async()=>{const enabled=$('cfgDebugEnabled').checked;try{const r=await apiRequest('/api/admin/debug',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r)return;const d=await r.json();if(d.success){showToast(enabled?'调试模式已开启':'调试模式已关闭','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('cfgDebugEnabled').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('cfgDebugEnabled').checked=!enabled}},
|
||||
downloadDebugLogs=async()=>{try{const token=localStorage.getItem('adminToken');if(!token){showToast('未登录','error');return}const r=await fetch('/api/admin/logs/download',{headers:{Authorization:`Bearer ${token}`}});if(!r.ok){if(r.status===404){showToast('日志文件不存在','error')}else{showToast('下载失败','error')}return}const blob=await r.blob();const url=URL.createObjectURL(blob);const link=document.createElement('a');link.href=url;link.download=`logs_${new Date().toISOString().split('T')[0]}.txt`;document.body.appendChild(link);link.click();document.body.removeChild(link);URL.revokeObjectURL(url);showToast('日志文件下载成功','success')}catch(e){showToast('下载失败: '+e.message,'error')}},
|
||||
loadProxyConfig=async()=>{try{const r=await apiRequest('/api/proxy/config');if(!r)return;const d=await r.json();$('cfgProxyEnabled').checked=d.proxy_enabled||false;$('cfgProxyUrl').value=d.proxy_url||''}catch(e){console.error('加载代理配置失败:',e)}},
|
||||
saveProxyConfig=async()=>{try{const r=await apiRequest('/api/proxy/config',{method:'POST',body:JSON.stringify({proxy_enabled:$('cfgProxyEnabled').checked,proxy_url:$('cfgProxyUrl').value.trim()})});if(!r)return;const d=await r.json();d.success?showToast('代理配置保存成功','success'):showToast('保存失败','error')}catch(e){showToast('保存失败: '+e.message,'error')}},
|
||||
loadWatermarkFreeConfig=async()=>{try{const r=await apiRequest('/api/watermark-free/config');if(!r)return;const d=await r.json();$('cfgWatermarkFreeEnabled').checked=d.watermark_free_enabled||false;$('cfgParseMethod').value=d.parse_method||'third_party';$('cfgCustomParseUrl').value=d.custom_parse_url||'';$('cfgCustomParseToken').value=d.custom_parse_token||'';toggleWatermarkFreeOptions();toggleCustomParseOptions()}catch(e){console.error('加载无水印模式配置失败:',e)}},
|
||||
|
||||
Reference in New Issue
Block a user