mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-02-22 00:34:42 +08:00
feat: 请求日志页面记录处理中任务状态、记录角色卡生成任务
新增生成面板 token导出新增代理、备注等字段 token导入支持使用自定义代理获取信息 完善错误提示 close #41
This commit is contained in:
@@ -97,6 +97,8 @@ class ImportTokenItem(BaseModel):
|
|||||||
access_token: str # Access Token (AT)
|
access_token: str # Access Token (AT)
|
||||||
session_token: Optional[str] = None # Session Token (ST)
|
session_token: Optional[str] = None # Session Token (ST)
|
||||||
refresh_token: Optional[str] = None # Refresh Token (RT)
|
refresh_token: Optional[str] = None # Refresh Token (RT)
|
||||||
|
proxy_url: Optional[str] = None # Proxy URL (optional, for compatibility)
|
||||||
|
remark: Optional[str] = None # Remark (optional, for compatibility)
|
||||||
is_active: bool = True # Active status
|
is_active: bool = True # Active status
|
||||||
image_enabled: bool = True # Enable image generation
|
image_enabled: bool = True # Enable image generation
|
||||||
video_enabled: bool = True # Enable video generation
|
video_enabled: bool = True # Enable video generation
|
||||||
@@ -345,11 +347,13 @@ async def delete_token(token_id: int, token: str = Depends(verify_admin_token)):
|
|||||||
@router.post("/api/tokens/import")
|
@router.post("/api/tokens/import")
|
||||||
async def import_tokens(request: ImportTokensRequest, token: str = Depends(verify_admin_token)):
|
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 in append mode (update if exists, add if not)"""
|
||||||
try:
|
added_count = 0
|
||||||
added_count = 0
|
updated_count = 0
|
||||||
updated_count = 0
|
failed_count = 0
|
||||||
|
results = []
|
||||||
|
|
||||||
for import_item in request.tokens:
|
for import_item in request.tokens:
|
||||||
|
try:
|
||||||
# Check if token with this email already exists
|
# Check if token with this email already exists
|
||||||
existing_token = await db.get_token_by_email(import_item.email)
|
existing_token = await db.get_token_by_email(import_item.email)
|
||||||
|
|
||||||
@@ -360,6 +364,8 @@ async def import_tokens(request: ImportTokensRequest, token: str = Depends(verif
|
|||||||
token=import_item.access_token,
|
token=import_item.access_token,
|
||||||
st=import_item.session_token,
|
st=import_item.session_token,
|
||||||
rt=import_item.refresh_token,
|
rt=import_item.refresh_token,
|
||||||
|
proxy_url=import_item.proxy_url,
|
||||||
|
remark=import_item.remark,
|
||||||
image_enabled=import_item.image_enabled,
|
image_enabled=import_item.image_enabled,
|
||||||
video_enabled=import_item.video_enabled,
|
video_enabled=import_item.video_enabled,
|
||||||
image_concurrency=import_item.image_concurrency,
|
image_concurrency=import_item.image_concurrency,
|
||||||
@@ -375,12 +381,19 @@ async def import_tokens(request: ImportTokensRequest, token: str = Depends(verif
|
|||||||
video_concurrency=import_item.video_concurrency
|
video_concurrency=import_item.video_concurrency
|
||||||
)
|
)
|
||||||
updated_count += 1
|
updated_count += 1
|
||||||
|
results.append({
|
||||||
|
"email": import_item.email,
|
||||||
|
"status": "updated",
|
||||||
|
"success": True
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
# Add new token
|
# Add new token
|
||||||
new_token = await token_manager.add_token(
|
new_token = await token_manager.add_token(
|
||||||
token_value=import_item.access_token,
|
token_value=import_item.access_token,
|
||||||
st=import_item.session_token,
|
st=import_item.session_token,
|
||||||
rt=import_item.refresh_token,
|
rt=import_item.refresh_token,
|
||||||
|
proxy_url=import_item.proxy_url,
|
||||||
|
remark=import_item.remark,
|
||||||
update_if_exists=False,
|
update_if_exists=False,
|
||||||
image_enabled=import_item.image_enabled,
|
image_enabled=import_item.image_enabled,
|
||||||
video_enabled=import_item.video_enabled,
|
video_enabled=import_item.video_enabled,
|
||||||
@@ -398,15 +411,28 @@ async def import_tokens(request: ImportTokensRequest, token: str = Depends(verif
|
|||||||
video_concurrency=import_item.video_concurrency
|
video_concurrency=import_item.video_concurrency
|
||||||
)
|
)
|
||||||
added_count += 1
|
added_count += 1
|
||||||
|
results.append({
|
||||||
|
"email": import_item.email,
|
||||||
|
"status": "added",
|
||||||
|
"success": True
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
failed_count += 1
|
||||||
|
results.append({
|
||||||
|
"email": import_item.email,
|
||||||
|
"status": "failed",
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Import completed: {added_count} added, {updated_count} updated",
|
"message": f"Import completed: {added_count} added, {updated_count} updated, {failed_count} failed",
|
||||||
"added": added_count,
|
"added": added_count,
|
||||||
"updated": updated_count
|
"updated": updated_count,
|
||||||
}
|
"failed": failed_count,
|
||||||
except Exception as e:
|
"results": results
|
||||||
raise HTTPException(status_code=400, detail=f"Import failed: {str(e)}")
|
}
|
||||||
|
|
||||||
@router.put("/api/tokens/{token_id}")
|
@router.put("/api/tokens/{token_id}")
|
||||||
async def update_token(
|
async def update_token(
|
||||||
@@ -654,12 +680,12 @@ async def activate_sora2(
|
|||||||
|
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
# Get new invite code after activation
|
# Get new invite code after activation
|
||||||
sora2_info = await token_manager.get_sora2_invite_code(token_obj.token)
|
sora2_info = await token_manager.get_sora2_invite_code(token_obj.token, token_id)
|
||||||
|
|
||||||
# Get remaining count
|
# Get remaining count
|
||||||
sora2_remaining_count = 0
|
sora2_remaining_count = 0
|
||||||
try:
|
try:
|
||||||
remaining_info = await token_manager.get_sora2_remaining_count(token_obj.token)
|
remaining_info = await token_manager.get_sora2_remaining_count(token_obj.token, token_id)
|
||||||
if remaining_info.get("success"):
|
if remaining_info.get("success"):
|
||||||
sora2_remaining_count = remaining_info.get("remaining_count", 0)
|
sora2_remaining_count = remaining_info.get("remaining_count", 0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -697,20 +723,34 @@ async def activate_sora2(
|
|||||||
# Logs endpoints
|
# Logs endpoints
|
||||||
@router.get("/api/logs")
|
@router.get("/api/logs")
|
||||||
async def get_logs(limit: int = 100, token: str = Depends(verify_admin_token)):
|
async def get_logs(limit: int = 100, token: str = Depends(verify_admin_token)):
|
||||||
"""Get recent logs with token email"""
|
"""Get recent logs with token email and task progress"""
|
||||||
logs = await db.get_recent_logs(limit)
|
logs = await db.get_recent_logs(limit)
|
||||||
return [{
|
result = []
|
||||||
"id": log.get("id"),
|
for log in logs:
|
||||||
"token_id": log.get("token_id"),
|
log_data = {
|
||||||
"token_email": log.get("token_email"),
|
"id": log.get("id"),
|
||||||
"token_username": log.get("token_username"),
|
"token_id": log.get("token_id"),
|
||||||
"operation": log.get("operation"),
|
"token_email": log.get("token_email"),
|
||||||
"status_code": log.get("status_code"),
|
"token_username": log.get("token_username"),
|
||||||
"duration": log.get("duration"),
|
"operation": log.get("operation"),
|
||||||
"created_at": log.get("created_at"),
|
"status_code": log.get("status_code"),
|
||||||
"request_body": log.get("request_body"),
|
"duration": log.get("duration"),
|
||||||
"response_body": log.get("response_body")
|
"created_at": log.get("created_at"),
|
||||||
} for log in logs]
|
"request_body": log.get("request_body"),
|
||||||
|
"response_body": log.get("response_body"),
|
||||||
|
"task_id": log.get("task_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
# If task_id exists and status is in-progress, get task progress
|
||||||
|
if log.get("task_id") and log.get("status_code") == -1:
|
||||||
|
task = await db.get_task(log.get("task_id"))
|
||||||
|
if task:
|
||||||
|
log_data["progress"] = task.progress
|
||||||
|
log_data["task_status"] = task.status
|
||||||
|
|
||||||
|
result.append(log_data)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@router.delete("/api/logs")
|
@router.delete("/api/logs")
|
||||||
async def clear_logs(token: str = Depends(verify_admin_token)):
|
async def clear_logs(token: str = Depends(verify_admin_token)):
|
||||||
|
|||||||
@@ -181,15 +181,27 @@ async def create_chat_completion(
|
|||||||
):
|
):
|
||||||
yield chunk
|
yield chunk
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Try to parse structured error (JSON format)
|
||||||
|
error_data = None
|
||||||
|
try:
|
||||||
|
error_data = json_module.loads(str(e))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# Return OpenAI-compatible error format
|
# Return OpenAI-compatible error format
|
||||||
error_response = {
|
if error_data and isinstance(error_data, dict) and "error" in error_data:
|
||||||
"error": {
|
# Structured error (e.g., unsupported_country_code)
|
||||||
"message": str(e),
|
error_response = error_data
|
||||||
"type": "server_error",
|
else:
|
||||||
"param": None,
|
# Generic error
|
||||||
"code": None
|
error_response = {
|
||||||
|
"error": {
|
||||||
|
"message": str(e),
|
||||||
|
"type": "server_error",
|
||||||
|
"param": None,
|
||||||
|
"code": None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
error_chunk = f'data: {json_module.dumps(error_response)}\n\n'
|
error_chunk = f'data: {json_module.dumps(error_response)}\n\n'
|
||||||
yield error_chunk
|
yield error_chunk
|
||||||
yield 'data: [DONE]\n\n'
|
yield 'data: [DONE]\n\n'
|
||||||
|
|||||||
@@ -254,6 +254,21 @@ class Database:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ✗ Failed to add column '{col_name}': {e}")
|
print(f" ✗ Failed to add column '{col_name}': {e}")
|
||||||
|
|
||||||
|
# Check and add missing columns to request_logs table
|
||||||
|
if await self._table_exists(db, "request_logs"):
|
||||||
|
columns_to_add = [
|
||||||
|
("task_id", "TEXT"),
|
||||||
|
("updated_at", "TIMESTAMP"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for col_name, col_type in columns_to_add:
|
||||||
|
if not await self._column_exists(db, "request_logs", col_name):
|
||||||
|
try:
|
||||||
|
await db.execute(f"ALTER TABLE request_logs ADD COLUMN {col_name} {col_type}")
|
||||||
|
print(f" ✓ Added column '{col_name}' to request_logs table")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Failed to add column '{col_name}': {e}")
|
||||||
|
|
||||||
# Ensure all config tables have their default rows
|
# Ensure all config tables have their default rows
|
||||||
# Pass config_dict if available to initialize from setting.toml
|
# Pass config_dict if available to initialize from setting.toml
|
||||||
await self._ensure_config_rows(db, config_dict)
|
await self._ensure_config_rows(db, config_dict)
|
||||||
@@ -340,12 +355,14 @@ class Database:
|
|||||||
CREATE TABLE IF NOT EXISTS request_logs (
|
CREATE TABLE IF NOT EXISTS request_logs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
token_id INTEGER,
|
token_id INTEGER,
|
||||||
|
task_id TEXT,
|
||||||
operation TEXT NOT NULL,
|
operation TEXT NOT NULL,
|
||||||
request_body TEXT,
|
request_body TEXT,
|
||||||
response_body TEXT,
|
response_body TEXT,
|
||||||
status_code INTEGER NOT NULL,
|
status_code INTEGER NOT NULL,
|
||||||
duration FLOAT NOT NULL,
|
duration FLOAT NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
FOREIGN KEY (token_id) REFERENCES tokens(id)
|
FOREIGN KEY (token_id) REFERENCES tokens(id)
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
@@ -848,15 +865,40 @@ class Database:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Request log operations
|
# Request log operations
|
||||||
async def log_request(self, log: RequestLog):
|
async def log_request(self, log: RequestLog) -> int:
|
||||||
"""Log a request"""
|
"""Log a request and return log ID"""
|
||||||
async with aiosqlite.connect(self.db_path) as db:
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
await db.execute("""
|
cursor = await db.execute("""
|
||||||
INSERT INTO request_logs (token_id, operation, request_body, response_body, status_code, duration)
|
INSERT INTO request_logs (token_id, task_id, operation, request_body, response_body, status_code, duration)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (log.token_id, log.operation, log.request_body, log.response_body,
|
""", (log.token_id, log.task_id, log.operation, log.request_body, log.response_body,
|
||||||
log.status_code, log.duration))
|
log.status_code, log.duration))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
return cursor.lastrowid
|
||||||
|
|
||||||
|
async def update_request_log(self, log_id: int, response_body: Optional[str] = None,
|
||||||
|
status_code: Optional[int] = None, duration: Optional[float] = None):
|
||||||
|
"""Update request log with completion data"""
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
updates = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if response_body is not None:
|
||||||
|
updates.append("response_body = ?")
|
||||||
|
params.append(response_body)
|
||||||
|
if status_code is not None:
|
||||||
|
updates.append("status_code = ?")
|
||||||
|
params.append(status_code)
|
||||||
|
if duration is not None:
|
||||||
|
updates.append("duration = ?")
|
||||||
|
params.append(duration)
|
||||||
|
|
||||||
|
if updates:
|
||||||
|
updates.append("updated_at = CURRENT_TIMESTAMP")
|
||||||
|
params.append(log_id)
|
||||||
|
query = f"UPDATE request_logs SET {', '.join(updates)} WHERE id = ?"
|
||||||
|
await db.execute(query, params)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
async def get_recent_logs(self, limit: int = 100) -> List[dict]:
|
async def get_recent_logs(self, limit: int = 100) -> List[dict]:
|
||||||
"""Get recent logs with token email"""
|
"""Get recent logs with token email"""
|
||||||
@@ -866,13 +908,15 @@ class Database:
|
|||||||
SELECT
|
SELECT
|
||||||
rl.id,
|
rl.id,
|
||||||
rl.token_id,
|
rl.token_id,
|
||||||
|
rl.task_id,
|
||||||
rl.operation,
|
rl.operation,
|
||||||
rl.request_body,
|
rl.request_body,
|
||||||
rl.response_body,
|
rl.response_body,
|
||||||
rl.status_code,
|
rl.status_code,
|
||||||
rl.duration,
|
rl.duration,
|
||||||
rl.created_at,
|
rl.created_at,
|
||||||
t.email as token_email
|
t.email as token_email,
|
||||||
|
t.username as token_username
|
||||||
FROM request_logs rl
|
FROM request_logs rl
|
||||||
LEFT JOIN tokens t ON rl.token_id = t.id
|
LEFT JOIN tokens t ON rl.token_id = t.id
|
||||||
ORDER BY rl.created_at DESC
|
ORDER BY rl.created_at DESC
|
||||||
|
|||||||
@@ -71,12 +71,14 @@ class RequestLog(BaseModel):
|
|||||||
"""Request log model"""
|
"""Request log model"""
|
||||||
id: Optional[int] = None
|
id: Optional[int] = None
|
||||||
token_id: Optional[int] = None
|
token_id: Optional[int] = None
|
||||||
|
task_id: Optional[str] = None # Link to task for progress tracking
|
||||||
operation: str
|
operation: str
|
||||||
request_body: Optional[str] = None
|
request_body: Optional[str] = None
|
||||||
response_body: Optional[str] = None
|
response_body: Optional[str] = None
|
||||||
status_code: int
|
status_code: int # -1 for in-progress
|
||||||
duration: float
|
duration: float # -1.0 for in-progress
|
||||||
created_at: Optional[datetime] = None
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
class AdminConfig(BaseModel):
|
class AdminConfig(BaseModel):
|
||||||
"""Admin configuration"""
|
"""Admin configuration"""
|
||||||
|
|||||||
@@ -332,6 +332,8 @@ class GenerationHandler:
|
|||||||
stream: Whether to stream response
|
stream: Whether to stream response
|
||||||
"""
|
"""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
log_id = None # Initialize log_id to avoid reference before assignment
|
||||||
|
token_obj = None # Initialize token_obj to avoid reference before assignment
|
||||||
|
|
||||||
# Validate model
|
# Validate model
|
||||||
if model not in MODEL_CONFIG:
|
if model not in MODEL_CONFIG:
|
||||||
@@ -515,7 +517,18 @@ class GenerationHandler:
|
|||||||
progress=0.0
|
progress=0.0
|
||||||
)
|
)
|
||||||
await self.db.create_task(task)
|
await self.db.create_task(task)
|
||||||
|
|
||||||
|
# Create initial log entry (status_code=-1, duration=-1.0 means in-progress)
|
||||||
|
log_id = await self._log_request(
|
||||||
|
token_obj.id,
|
||||||
|
f"generate_{model_config['type']}",
|
||||||
|
{"model": model, "prompt": prompt, "has_image": image is not None},
|
||||||
|
{}, # Empty response initially
|
||||||
|
-1, # -1 means in-progress
|
||||||
|
-1.0, # -1.0 means in-progress
|
||||||
|
task_id=task_id
|
||||||
|
)
|
||||||
|
|
||||||
# Record usage
|
# Record usage
|
||||||
await self.token_manager.record_usage(token_obj.id, is_video=is_video)
|
await self.token_manager.record_usage(token_obj.id, is_video=is_video)
|
||||||
|
|
||||||
@@ -557,14 +570,14 @@ class GenerationHandler:
|
|||||||
except:
|
except:
|
||||||
response_data["result_urls"] = task_info.result_urls
|
response_data["result_urls"] = task_info.result_urls
|
||||||
|
|
||||||
await self._log_request(
|
# Update log entry with completion data
|
||||||
token_obj.id,
|
if log_id:
|
||||||
f"generate_{model_config['type']}",
|
await self.db.update_request_log(
|
||||||
{"model": model, "prompt": prompt, "has_image": image is not None},
|
log_id,
|
||||||
response_data,
|
response_body=json.dumps(response_data),
|
||||||
200,
|
status_code=200,
|
||||||
duration
|
duration=duration
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Release lock for image generation on error
|
# Release lock for image generation on error
|
||||||
@@ -584,16 +597,33 @@ class GenerationHandler:
|
|||||||
is_overload = "heavy_load" in error_str or "under heavy load" in error_str
|
is_overload = "heavy_load" in error_str or "under heavy load" in error_str
|
||||||
await self.token_manager.record_error(token_obj.id, is_overload=is_overload)
|
await self.token_manager.record_error(token_obj.id, is_overload=is_overload)
|
||||||
|
|
||||||
# Log failed request
|
# Parse error message to check if it's a structured error (JSON)
|
||||||
|
error_response = None
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
error_response = json.loads(str(e))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Update log entry with error data
|
||||||
duration = time.time() - start_time
|
duration = time.time() - start_time
|
||||||
await self._log_request(
|
if log_id:
|
||||||
token_obj.id if token_obj else None,
|
if error_response:
|
||||||
f"generate_{model_config['type'] if model_config else 'unknown'}",
|
# Structured error (e.g., unsupported_country_code)
|
||||||
{"model": model, "prompt": prompt, "has_image": image is not None},
|
await self.db.update_request_log(
|
||||||
{"error": str(e)},
|
log_id,
|
||||||
500,
|
response_body=json.dumps(error_response),
|
||||||
duration
|
status_code=400,
|
||||||
)
|
duration=duration
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Generic error
|
||||||
|
await self.db.update_request_log(
|
||||||
|
log_id,
|
||||||
|
response_body=json.dumps({"error": str(e)}),
|
||||||
|
status_code=500,
|
||||||
|
duration=duration
|
||||||
|
)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
async def _poll_task_result(self, task_id: str, token: str, is_video: bool,
|
async def _poll_task_result(self, task_id: str, token: str, is_video: bool,
|
||||||
@@ -1136,21 +1166,23 @@ class GenerationHandler:
|
|||||||
|
|
||||||
async def _log_request(self, token_id: Optional[int], operation: str,
|
async def _log_request(self, token_id: Optional[int], operation: str,
|
||||||
request_data: Dict[str, Any], response_data: Dict[str, Any],
|
request_data: Dict[str, Any], response_data: Dict[str, Any],
|
||||||
status_code: int, duration: float):
|
status_code: int, duration: float, task_id: Optional[str] = None) -> Optional[int]:
|
||||||
"""Log request to database"""
|
"""Log request to database and return log ID"""
|
||||||
try:
|
try:
|
||||||
log = RequestLog(
|
log = RequestLog(
|
||||||
token_id=token_id,
|
token_id=token_id,
|
||||||
|
task_id=task_id,
|
||||||
operation=operation,
|
operation=operation,
|
||||||
request_body=json.dumps(request_data),
|
request_body=json.dumps(request_data),
|
||||||
response_body=json.dumps(response_data),
|
response_body=json.dumps(response_data),
|
||||||
status_code=status_code,
|
status_code=status_code,
|
||||||
duration=duration
|
duration=duration
|
||||||
)
|
)
|
||||||
await self.db.log_request(log)
|
return await self.db.log_request(log)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Don't fail the request if logging fails
|
# Don't fail the request if logging fails
|
||||||
print(f"Failed to log request: {e}")
|
print(f"Failed to log request: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
# ==================== Character Creation and Remix Handlers ====================
|
# ==================== Character Creation and Remix Handlers ====================
|
||||||
|
|
||||||
@@ -1171,6 +1203,7 @@ class GenerationHandler:
|
|||||||
if not token_obj:
|
if not token_obj:
|
||||||
raise Exception("No available tokens for character creation")
|
raise Exception("No available tokens for character creation")
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
try:
|
try:
|
||||||
yield self._format_stream_chunk(
|
yield self._format_stream_chunk(
|
||||||
reasoning_content="**Character Creation Begins**\n\nInitializing character creation...\n",
|
reasoning_content="**Character Creation Begins**\n\nInitializing character creation...\n",
|
||||||
@@ -1255,6 +1288,26 @@ class GenerationHandler:
|
|||||||
await self.sora_client.set_character_public(cameo_id, token_obj.token)
|
await self.sora_client.set_character_public(cameo_id, token_obj.token)
|
||||||
debug_logger.log_info(f"Character set as public")
|
debug_logger.log_info(f"Character set as public")
|
||||||
|
|
||||||
|
# Log successful character creation
|
||||||
|
duration = time.time() - start_time
|
||||||
|
await self._log_request(
|
||||||
|
token_id=token_obj.id,
|
||||||
|
operation="character_only",
|
||||||
|
request_data={
|
||||||
|
"type": "character_creation",
|
||||||
|
"has_video": True
|
||||||
|
},
|
||||||
|
response_data={
|
||||||
|
"success": True,
|
||||||
|
"username": username,
|
||||||
|
"display_name": display_name,
|
||||||
|
"character_id": character_id,
|
||||||
|
"cameo_id": cameo_id
|
||||||
|
},
|
||||||
|
status_code=200,
|
||||||
|
duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
# Step 7: Return success message
|
# Step 7: Return success message
|
||||||
yield self._format_stream_chunk(
|
yield self._format_stream_chunk(
|
||||||
content=f"角色创建成功,角色名@{username}",
|
content=f"角色创建成功,角色名@{username}",
|
||||||
@@ -1263,6 +1316,23 @@ class GenerationHandler:
|
|||||||
yield "data: [DONE]\n\n"
|
yield "data: [DONE]\n\n"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Log failed character creation
|
||||||
|
duration = time.time() - start_time
|
||||||
|
await self._log_request(
|
||||||
|
token_id=token_obj.id if token_obj else None,
|
||||||
|
operation="character_only",
|
||||||
|
request_data={
|
||||||
|
"type": "character_creation",
|
||||||
|
"has_video": True
|
||||||
|
},
|
||||||
|
response_data={
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
},
|
||||||
|
status_code=500,
|
||||||
|
duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
debug_logger.log_error(
|
debug_logger.log_error(
|
||||||
error_message=f"Character creation failed: {str(e)}",
|
error_message=f"Character creation failed: {str(e)}",
|
||||||
status_code=500,
|
status_code=500,
|
||||||
@@ -1289,6 +1359,10 @@ class GenerationHandler:
|
|||||||
raise Exception("No available tokens for video generation")
|
raise Exception("No available tokens for video generation")
|
||||||
|
|
||||||
character_id = None
|
character_id = None
|
||||||
|
start_time = time.time()
|
||||||
|
username = None
|
||||||
|
display_name = None
|
||||||
|
cameo_id = None
|
||||||
try:
|
try:
|
||||||
yield self._format_stream_chunk(
|
yield self._format_stream_chunk(
|
||||||
reasoning_content="**Character Creation and Video Generation Begins**\n\nInitializing...\n",
|
reasoning_content="**Character Creation and Video Generation Begins**\n\nInitializing...\n",
|
||||||
@@ -1366,6 +1440,28 @@ class GenerationHandler:
|
|||||||
)
|
)
|
||||||
debug_logger.log_info(f"Character finalized, character_id: {character_id}")
|
debug_logger.log_info(f"Character finalized, character_id: {character_id}")
|
||||||
|
|
||||||
|
# Log successful character creation (before video generation)
|
||||||
|
character_creation_duration = time.time() - start_time
|
||||||
|
await self._log_request(
|
||||||
|
token_id=token_obj.id,
|
||||||
|
operation="character_with_video",
|
||||||
|
request_data={
|
||||||
|
"type": "character_creation_with_video",
|
||||||
|
"has_video": True,
|
||||||
|
"prompt": prompt
|
||||||
|
},
|
||||||
|
response_data={
|
||||||
|
"success": True,
|
||||||
|
"username": username,
|
||||||
|
"display_name": display_name,
|
||||||
|
"character_id": character_id,
|
||||||
|
"cameo_id": cameo_id,
|
||||||
|
"stage": "character_created"
|
||||||
|
},
|
||||||
|
status_code=200,
|
||||||
|
duration=character_creation_duration
|
||||||
|
)
|
||||||
|
|
||||||
# Step 6: Generate video with character
|
# Step 6: Generate video with character
|
||||||
yield self._format_stream_chunk(
|
yield self._format_stream_chunk(
|
||||||
reasoning_content="**Video Generation Process Begins**\n\nGenerating video with character...\n"
|
reasoning_content="**Video Generation Process Begins**\n\nGenerating video with character...\n"
|
||||||
@@ -1414,6 +1510,28 @@ class GenerationHandler:
|
|||||||
await self.token_manager.record_success(token_obj.id, is_video=True)
|
await self.token_manager.record_success(token_obj.id, is_video=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Log failed character creation
|
||||||
|
duration = time.time() - start_time
|
||||||
|
await self._log_request(
|
||||||
|
token_id=token_obj.id if token_obj else None,
|
||||||
|
operation="character_with_video",
|
||||||
|
request_data={
|
||||||
|
"type": "character_creation_with_video",
|
||||||
|
"has_video": True,
|
||||||
|
"prompt": prompt
|
||||||
|
},
|
||||||
|
response_data={
|
||||||
|
"success": False,
|
||||||
|
"username": username,
|
||||||
|
"display_name": display_name,
|
||||||
|
"character_id": character_id,
|
||||||
|
"cameo_id": cameo_id,
|
||||||
|
"error": str(e)
|
||||||
|
},
|
||||||
|
status_code=500,
|
||||||
|
duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
# Record error (check if it's an overload error)
|
# Record error (check if it's an overload error)
|
||||||
if token_obj:
|
if token_obj:
|
||||||
error_str = str(e).lower()
|
error_str = str(e).lower()
|
||||||
@@ -1553,6 +1671,16 @@ class GenerationHandler:
|
|||||||
|
|
||||||
debug_logger.log_info(f"Cameo status: {current_status} (message: {status_message}) (attempt {attempt + 1}/{max_attempts})")
|
debug_logger.log_info(f"Cameo status: {current_status} (message: {status_message}) (attempt {attempt + 1}/{max_attempts})")
|
||||||
|
|
||||||
|
# Check if processing failed
|
||||||
|
if current_status == "failed":
|
||||||
|
error_message = status_message or "Character creation failed"
|
||||||
|
debug_logger.log_error(
|
||||||
|
error_message=f"Cameo processing failed: {error_message}",
|
||||||
|
status_code=500,
|
||||||
|
response_text=error_message
|
||||||
|
)
|
||||||
|
raise Exception(f"角色创建失败: {error_message}")
|
||||||
|
|
||||||
# Check if processing is complete
|
# Check if processing is complete
|
||||||
# Primary condition: status_message == "Completed" means processing is done
|
# Primary condition: status_message == "Completed" means processing is done
|
||||||
if status_message == "Completed":
|
if status_message == "Completed":
|
||||||
@@ -1568,6 +1696,11 @@ class GenerationHandler:
|
|||||||
consecutive_errors += 1
|
consecutive_errors += 1
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
|
|
||||||
|
# Check if it's a character creation failure (not a network error)
|
||||||
|
# These should be raised immediately, not retried
|
||||||
|
if "角色创建失败" in error_msg:
|
||||||
|
raise
|
||||||
|
|
||||||
# Log error with context
|
# Log error with context
|
||||||
debug_logger.log_error(
|
debug_logger.log_error(
|
||||||
error_message=f"Failed to get cameo status (attempt {attempt + 1}/{max_attempts}, consecutive errors: {consecutive_errors}): {error_msg}",
|
error_message=f"Failed to get cameo status (attempt {attempt + 1}/{max_attempts}, consecutive errors: {consecutive_errors}): {error_msg}",
|
||||||
|
|||||||
@@ -9,16 +9,21 @@ class ProxyManager:
|
|||||||
def __init__(self, db: Database):
|
def __init__(self, db: Database):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
async def get_proxy_url(self, token_id: Optional[int] = None) -> Optional[str]:
|
async def get_proxy_url(self, token_id: Optional[int] = None, proxy_url: Optional[str] = None) -> Optional[str]:
|
||||||
"""Get proxy URL for a token, with fallback to global proxy
|
"""Get proxy URL for a token, with fallback to global proxy
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
token_id: Token ID (optional). If provided, returns token-specific proxy if set,
|
token_id: Token ID (optional). If provided, returns token-specific proxy if set,
|
||||||
otherwise falls back to global proxy.
|
otherwise falls back to global proxy.
|
||||||
|
proxy_url: Direct proxy URL (optional). If provided, returns this proxy URL directly.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Proxy URL string or None
|
Proxy URL string or None
|
||||||
"""
|
"""
|
||||||
|
# If proxy_url is directly provided, use it
|
||||||
|
if proxy_url:
|
||||||
|
return proxy_url
|
||||||
|
|
||||||
# If token_id is provided, try to get token-specific proxy first
|
# If token_id is provided, try to get token-specific proxy first
|
||||||
if token_id is not None:
|
if token_id is not None:
|
||||||
token = await self.db.get_token(token_id)
|
token = await self.db.get_token(token_id)
|
||||||
|
|||||||
@@ -180,6 +180,29 @@ class SoraClient:
|
|||||||
|
|
||||||
# Check status
|
# Check status
|
||||||
if response.status_code not in [200, 201]:
|
if response.status_code not in [200, 201]:
|
||||||
|
# Parse error response
|
||||||
|
error_data = None
|
||||||
|
try:
|
||||||
|
error_data = response.json()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check for unsupported_country_code error
|
||||||
|
if error_data and isinstance(error_data, dict):
|
||||||
|
error_info = error_data.get("error", {})
|
||||||
|
if error_info.get("code") == "unsupported_country_code":
|
||||||
|
# Create structured error with full error data
|
||||||
|
import json
|
||||||
|
error_msg = json.dumps(error_data)
|
||||||
|
debug_logger.log_error(
|
||||||
|
error_message=f"Unsupported country: {error_msg}",
|
||||||
|
status_code=response.status_code,
|
||||||
|
response_text=error_msg
|
||||||
|
)
|
||||||
|
# Raise exception with structured error data
|
||||||
|
raise Exception(error_msg)
|
||||||
|
|
||||||
|
# Generic error handling
|
||||||
error_msg = f"API request failed: {response.status_code} - {response.text}"
|
error_msg = f"API request failed: {response.status_code} - {response.text}"
|
||||||
debug_logger.log_error(
|
debug_logger.log_error(
|
||||||
error_message=error_msg,
|
error_message=error_msg,
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ class TokenManager:
|
|||||||
# 转换为小写
|
# 转换为小写
|
||||||
return format_choice.lower()
|
return format_choice.lower()
|
||||||
|
|
||||||
async def get_user_info(self, access_token: str) -> dict:
|
async def get_user_info(self, access_token: str, token_id: Optional[int] = None, proxy_url: Optional[str] = None) -> dict:
|
||||||
"""Get user info from Sora API"""
|
"""Get user info from Sora API"""
|
||||||
proxy_url = await self.proxy_manager.get_proxy_url()
|
proxy_url = await self.proxy_manager.get_proxy_url(token_id, proxy_url)
|
||||||
|
|
||||||
async with AsyncSession() as session:
|
async with AsyncSession() as session:
|
||||||
headers = {
|
headers = {
|
||||||
@@ -90,7 +90,7 @@ class TokenManager:
|
|||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
async def get_subscription_info(self, token: str) -> Dict[str, Any]:
|
async def get_subscription_info(self, token: str, token_id: Optional[int] = None, proxy_url: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""Get subscription information from Sora API
|
"""Get subscription information from Sora API
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -101,7 +101,7 @@ class TokenManager:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
print(f"🔍 开始获取订阅信息...")
|
print(f"🔍 开始获取订阅信息...")
|
||||||
proxy_url = await self.proxy_manager.get_proxy_url()
|
proxy_url = await self.proxy_manager.get_proxy_url(token_id, proxy_url)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {token}"
|
"Authorization": f"Bearer {token}"
|
||||||
@@ -163,9 +163,9 @@ class TokenManager:
|
|||||||
|
|
||||||
raise Exception(f"Failed to get subscription info: {response.status_code}")
|
raise Exception(f"Failed to get subscription info: {response.status_code}")
|
||||||
|
|
||||||
async def get_sora2_invite_code(self, access_token: str) -> dict:
|
async def get_sora2_invite_code(self, access_token: str, token_id: Optional[int] = None, proxy_url: Optional[str] = None) -> dict:
|
||||||
"""Get Sora2 invite code"""
|
"""Get Sora2 invite code"""
|
||||||
proxy_url = await self.proxy_manager.get_proxy_url()
|
proxy_url = await self.proxy_manager.get_proxy_url(token_id, proxy_url)
|
||||||
|
|
||||||
print(f"🔍 开始获取Sora2邀请码...")
|
print(f"🔍 开始获取Sora2邀请码...")
|
||||||
|
|
||||||
@@ -263,7 +263,7 @@ class TokenManager:
|
|||||||
"invite_code": None
|
"invite_code": None
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_sora2_remaining_count(self, access_token: str) -> dict:
|
async def get_sora2_remaining_count(self, access_token: str, token_id: Optional[int] = None, proxy_url: Optional[str] = None) -> dict:
|
||||||
"""Get Sora2 remaining video count
|
"""Get Sora2 remaining video count
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -273,7 +273,7 @@ class TokenManager:
|
|||||||
"access_resets_in_seconds": 46833
|
"access_resets_in_seconds": 46833
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
proxy_url = await self.proxy_manager.get_proxy_url()
|
proxy_url = await self.proxy_manager.get_proxy_url(token_id, proxy_url)
|
||||||
|
|
||||||
print(f"🔍 开始获取Sora2剩余次数...")
|
print(f"🔍 开始获取Sora2剩余次数...")
|
||||||
|
|
||||||
@@ -692,7 +692,7 @@ class TokenManager:
|
|||||||
|
|
||||||
# Get user info from Sora API
|
# Get user info from Sora API
|
||||||
try:
|
try:
|
||||||
user_info = await self.get_user_info(token_value)
|
user_info = await self.get_user_info(token_value, proxy_url=proxy_url)
|
||||||
email = user_info.get("email", jwt_email or "")
|
email = user_info.get("email", jwt_email or "")
|
||||||
name = user_info.get("name") or ""
|
name = user_info.get("name") or ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -705,7 +705,7 @@ class TokenManager:
|
|||||||
plan_title = None
|
plan_title = None
|
||||||
subscription_end = None
|
subscription_end = None
|
||||||
try:
|
try:
|
||||||
sub_info = await self.get_subscription_info(token_value)
|
sub_info = await self.get_subscription_info(token_value, proxy_url=proxy_url)
|
||||||
plan_type = sub_info.get("plan_type")
|
plan_type = sub_info.get("plan_type")
|
||||||
plan_title = sub_info.get("plan_title")
|
plan_title = sub_info.get("plan_title")
|
||||||
# Parse subscription end time
|
# Parse subscription end time
|
||||||
@@ -727,7 +727,7 @@ class TokenManager:
|
|||||||
sora2_total_count = 0
|
sora2_total_count = 0
|
||||||
sora2_remaining_count = 0
|
sora2_remaining_count = 0
|
||||||
try:
|
try:
|
||||||
sora2_info = await self.get_sora2_invite_code(token_value)
|
sora2_info = await self.get_sora2_invite_code(token_value, proxy_url=proxy_url)
|
||||||
sora2_supported = sora2_info.get("supported", False)
|
sora2_supported = sora2_info.get("supported", False)
|
||||||
sora2_invite_code = sora2_info.get("invite_code")
|
sora2_invite_code = sora2_info.get("invite_code")
|
||||||
sora2_redeemed_count = sora2_info.get("redeemed_count", 0)
|
sora2_redeemed_count = sora2_info.get("redeemed_count", 0)
|
||||||
@@ -736,7 +736,7 @@ class TokenManager:
|
|||||||
# If Sora2 is supported, get remaining count
|
# If Sora2 is supported, get remaining count
|
||||||
if sora2_supported:
|
if sora2_supported:
|
||||||
try:
|
try:
|
||||||
remaining_info = await self.get_sora2_remaining_count(token_value)
|
remaining_info = await self.get_sora2_remaining_count(token_value, proxy_url=proxy_url)
|
||||||
if remaining_info.get("success"):
|
if remaining_info.get("success"):
|
||||||
sora2_remaining_count = remaining_info.get("remaining_count", 0)
|
sora2_remaining_count = remaining_info.get("remaining_count", 0)
|
||||||
print(f"✅ Sora2剩余次数: {sora2_remaining_count}")
|
print(f"✅ Sora2剩余次数: {sora2_remaining_count}")
|
||||||
@@ -753,7 +753,7 @@ class TokenManager:
|
|||||||
# Check and set username if needed
|
# Check and set username if needed
|
||||||
try:
|
try:
|
||||||
# Get fresh user info to check username
|
# Get fresh user info to check username
|
||||||
user_info = await self.get_user_info(token_value)
|
user_info = await self.get_user_info(token_value, proxy_url=proxy_url)
|
||||||
username = user_info.get("username")
|
username = user_info.get("username")
|
||||||
|
|
||||||
# If username is null, need to set one
|
# If username is null, need to set one
|
||||||
@@ -931,10 +931,10 @@ class TokenManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Try to get user info from Sora API
|
# Try to get user info from Sora API
|
||||||
user_info = await self.get_user_info(token_data.token)
|
user_info = await self.get_user_info(token_data.token, token_id)
|
||||||
|
|
||||||
# Refresh Sora2 invite code and counts
|
# Refresh Sora2 invite code and counts
|
||||||
sora2_info = await self.get_sora2_invite_code(token_data.token)
|
sora2_info = await self.get_sora2_invite_code(token_data.token, token_id)
|
||||||
sora2_supported = sora2_info.get("supported", False)
|
sora2_supported = sora2_info.get("supported", False)
|
||||||
sora2_invite_code = sora2_info.get("invite_code")
|
sora2_invite_code = sora2_info.get("invite_code")
|
||||||
sora2_redeemed_count = sora2_info.get("redeemed_count", 0)
|
sora2_redeemed_count = sora2_info.get("redeemed_count", 0)
|
||||||
@@ -944,7 +944,7 @@ class TokenManager:
|
|||||||
# If Sora2 is supported, get remaining count
|
# If Sora2 is supported, get remaining count
|
||||||
if sora2_supported:
|
if sora2_supported:
|
||||||
try:
|
try:
|
||||||
remaining_info = await self.get_sora2_remaining_count(token_data.token)
|
remaining_info = await self.get_sora2_remaining_count(token_data.token, token_id)
|
||||||
if remaining_info.get("success"):
|
if remaining_info.get("success"):
|
||||||
sora2_remaining_count = remaining_info.get("remaining_count", 0)
|
sora2_remaining_count = remaining_info.get("remaining_count", 0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1012,19 +1012,22 @@ class TokenManager:
|
|||||||
try:
|
try:
|
||||||
token_data = await self.db.get_token(token_id)
|
token_data = await self.db.get_token(token_id)
|
||||||
if token_data and token_data.sora2_supported:
|
if token_data and token_data.sora2_supported:
|
||||||
remaining_info = await self.get_sora2_remaining_count(token_data.token)
|
remaining_info = await self.get_sora2_remaining_count(token_data.token, token_id)
|
||||||
if remaining_info.get("success"):
|
if remaining_info.get("success"):
|
||||||
remaining_count = remaining_info.get("remaining_count", 0)
|
remaining_count = remaining_info.get("remaining_count", 0)
|
||||||
await self.db.update_token_sora2_remaining(token_id, remaining_count)
|
await self.db.update_token_sora2_remaining(token_id, remaining_count)
|
||||||
print(f"✅ 更新Token {token_id} 的Sora2剩余次数: {remaining_count}")
|
print(f"✅ 更新Token {token_id} 的Sora2剩余次数: {remaining_count}")
|
||||||
|
|
||||||
# If remaining count is 0, set cooldown
|
# If remaining count is 1 or less, disable token and set cooldown
|
||||||
if remaining_count == 0:
|
if remaining_count <= 1:
|
||||||
reset_seconds = remaining_info.get("access_resets_in_seconds", 0)
|
reset_seconds = remaining_info.get("access_resets_in_seconds", 0)
|
||||||
if reset_seconds > 0:
|
if reset_seconds > 0:
|
||||||
cooldown_until = datetime.now() + timedelta(seconds=reset_seconds)
|
cooldown_until = datetime.now() + timedelta(seconds=reset_seconds)
|
||||||
await self.db.update_token_sora2_cooldown(token_id, cooldown_until)
|
await self.db.update_token_sora2_cooldown(token_id, cooldown_until)
|
||||||
print(f"⏱️ Token {token_id} 剩余次数为0,设置冷却时间至: {cooldown_until}")
|
print(f"⏱️ Token {token_id} 剩余次数为{remaining_count},设置冷却时间至: {cooldown_until}")
|
||||||
|
# Disable token
|
||||||
|
await self.disable_token(token_id)
|
||||||
|
print(f"🚫 Token {token_id} 剩余次数为{remaining_count},已自动禁用")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to update Sora2 remaining count: {e}")
|
print(f"Failed to update Sora2 remaining count: {e}")
|
||||||
|
|
||||||
@@ -1040,7 +1043,7 @@ class TokenManager:
|
|||||||
print(f"🔄 Token {token_id} Sora2冷却已过期,正在刷新剩余次数...")
|
print(f"🔄 Token {token_id} Sora2冷却已过期,正在刷新剩余次数...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
remaining_info = await self.get_sora2_remaining_count(token_data.token)
|
remaining_info = await self.get_sora2_remaining_count(token_data.token, token_id)
|
||||||
if remaining_info.get("success"):
|
if remaining_info.get("success"):
|
||||||
remaining_count = remaining_info.get("remaining_count", 0)
|
remaining_count = remaining_info.get("remaining_count", 0)
|
||||||
await self.db.update_token_sora2_remaining(token_id, remaining_count)
|
await self.db.update_token_sora2_remaining(token_id, remaining_count)
|
||||||
|
|||||||
1867
static/generate.html
Normal file
1867
static/generate.html
Normal file
File diff suppressed because it is too large
Load Diff
6831
static/js/generate.js
Normal file
6831
static/js/generate.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,7 @@
|
|||||||
<button onclick="switchTab('tokens')" id="tabTokens" class="tab-btn border-b-2 border-primary text-sm font-medium py-3 px-1">Token 管理</button>
|
<button onclick="switchTab('tokens')" id="tabTokens" class="tab-btn border-b-2 border-primary text-sm font-medium py-3 px-1">Token 管理</button>
|
||||||
<button onclick="switchTab('settings')" id="tabSettings" class="tab-btn border-b-2 border-transparent text-sm font-medium py-3 px-1">系统配置</button>
|
<button onclick="switchTab('settings')" id="tabSettings" class="tab-btn border-b-2 border-transparent text-sm font-medium py-3 px-1">系统配置</button>
|
||||||
<button onclick="switchTab('logs')" id="tabLogs" class="tab-btn border-b-2 border-transparent text-sm font-medium py-3 px-1">请求日志</button>
|
<button onclick="switchTab('logs')" id="tabLogs" class="tab-btn border-b-2 border-transparent text-sm font-medium py-3 px-1">请求日志</button>
|
||||||
|
<button onclick="switchTab('generate')" id="tabGenerate" class="tab-btn border-b-2 border-transparent text-sm font-medium py-3 px-1">生成面板</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -379,6 +380,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成面板(单层嵌入,铺满主内容宽度) -->
|
||||||
|
<div id="panelGenerate" class="hidden" style="background: #ffffff;">
|
||||||
|
<iframe id="generateFrame" src="/static/generate.html" class="w-full" style="border: 0; background: #ffffff; display:block; width:100%; height: 800px;" loading="lazy"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 日志详情模态框 -->
|
<!-- 日志详情模态框 -->
|
||||||
<div id="logDetailModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
|
<div id="logDetailModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
|
||||||
<div class="bg-background rounded-lg border border-border w-full max-w-3xl shadow-xl max-h-[80vh] flex flex-col">
|
<div class="bg-background rounded-lg border border-border w-full max-w-3xl shadow-xl max-h-[80vh] flex flex-col">
|
||||||
@@ -679,6 +685,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="importProgressModal" class="hidden fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4">
|
||||||
|
<div class="bg-background rounded-lg border border-border w-full max-w-2xl shadow-xl max-h-[80vh] flex flex-col">
|
||||||
|
<div class="flex items-center justify-between p-5 border-b border-border">
|
||||||
|
<h3 class="text-lg font-semibold">导入进度</h3>
|
||||||
|
<button onclick="closeImportProgressModal()" class="text-muted-foreground hover:text-foreground">
|
||||||
|
<svg class="h-3.5 w-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 overflow-y-auto flex-1">
|
||||||
|
<div id="importProgressSummary" class="mb-4 p-3 rounded-md bg-muted"></div>
|
||||||
|
<div id="importProgressList" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end gap-3 p-5 border-t border-border">
|
||||||
|
<button onclick="closeImportProgressModal()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-5">关闭</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let allTokens=[];
|
let allTokens=[];
|
||||||
const $=(id)=>document.getElementById(id),
|
const $=(id)=>document.getElementById(id),
|
||||||
@@ -713,8 +740,11 @@
|
|||||||
closeSora2Modal=()=>{$('sora2Modal').classList.add('hidden');$('sora2TokenId').value='';$('sora2InviteCode').value=''},
|
closeSora2Modal=()=>{$('sora2Modal').classList.add('hidden');$('sora2TokenId').value='';$('sora2InviteCode').value=''},
|
||||||
openImportModal=()=>{$('importModal').classList.remove('hidden');$('importFile').value=''},
|
openImportModal=()=>{$('importModal').classList.remove('hidden');$('importFile').value=''},
|
||||||
closeImportModal=()=>{$('importModal').classList.add('hidden');$('importFile').value=''},
|
closeImportModal=()=>{$('importModal').classList.add('hidden');$('importFile').value=''},
|
||||||
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,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')},
|
openImportProgressModal=()=>{$('importProgressModal').classList.remove('hidden')},
|
||||||
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();const msg=`导入成功!新增: ${d.added||0}, 更新: ${d.updated||0}`;showToast(msg,'success')}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')}},
|
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')}},
|
||||||
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')}},
|
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)}},
|
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')}},
|
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')}},
|
||||||
@@ -734,14 +764,27 @@
|
|||||||
saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
saveGenerationTimeout=async()=>{const imageTimeout=parseInt($('cfgImageTimeout').value)||300,videoTimeout=parseInt($('cfgVideoTimeout').value)||1500;console.log('保存生成超时配置:',{imageTimeout,videoTimeout});if(imageTimeout<60||imageTimeout>3600)return showToast('图片超时时间必须在 60-3600 秒之间','error');if(videoTimeout<60||videoTimeout>7200)return showToast('视频超时时间必须在 60-7200 秒之间','error');try{const r=await apiRequest('/api/generation/timeout',{method:'POST',body:JSON.stringify({image_timeout:imageTimeout,video_timeout:videoTimeout})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('生成超时配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadGenerationTimeout()}else{console.error('保存失败:',d);showToast('保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
||||||
toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
|
toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
|
||||||
loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
|
loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
|
||||||
loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();window.allLogs=logs;const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td><td class="py-2.5 px-3"><button onclick="showLogDetail(${l.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs">查看</button></td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
|
loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();window.allLogs=logs;const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===-1?'bg-blue-50 text-blue-700':l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code===-1?'生成中':l.status_code}</span></td><td class="py-2.5 px-3">${l.duration===-1?'生成中':l.duration.toFixed(2)+'秒'}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td><td class="py-2.5 px-3"><button onclick="showLogDetail(${l.id})" class="inline-flex items-center justify-center rounded-md hover:bg-blue-50 hover:text-blue-700 h-7 px-2 text-xs">查看</button></td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
|
||||||
refreshLogs=async()=>{await loadLogs()},
|
refreshLogs=async()=>{await loadLogs()},
|
||||||
showLogDetail=(logId)=>{const log=window.allLogs.find(l=>l.id===logId);if(!log){showToast('日志不存在','error');return}const content=$('logDetailContent');let detailHtml='';if(log.status_code===200){try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody){if(responseBody.data&&responseBody.data.length>0){const item=responseBody.data[0];if(item.url){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">生成结果</h4><div class="rounded-md border border-border p-3 bg-muted/30"><p class="text-sm mb-2"><span class="font-medium">文件URL:</span></p><a href="${item.url}" target="_blank" class="text-blue-600 hover:underline text-xs break-all">${item.url}</a></div></div>`}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${JSON.stringify(responseBody,null,2)}</pre></div>`}}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${JSON.stringify(responseBody,null,2)}</pre></div>`}}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应信息</h4><p class="text-sm text-muted-foreground">无响应数据</p></div>`}}catch(e){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${log.response_body||'无'}</pre></div>`}}else{try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody&&responseBody.error){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误原因</h4><div class="rounded-md border border-red-200 p-3 bg-red-50"><p class="text-sm text-red-700">${responseBody.error.message||responseBody.error||'未知错误'}</p></div></div>`}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误信息</h4><pre class="rounded-md border border-red-200 p-3 bg-red-50 text-xs overflow-x-auto">${log.response_body||'无错误信息'}</pre></div>`}}catch(e){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误信息</h4><pre class="rounded-md border border-red-200 p-3 bg-red-50 text-xs overflow-x-auto">${log.response_body||'无错误信息'}</pre></div>`}}detailHtml+=`<div class="space-y-2 pt-4 border-t border-border"><h4 class="font-medium text-sm">基本信息</h4><div class="grid grid-cols-2 gap-2 text-sm"><div><span class="text-muted-foreground">操作:</span> ${log.operation}</div><div><span class="text-muted-foreground">状态码:</span> <span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${log.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${log.status_code}</span></div><div><span class="text-muted-foreground">耗时:</span> ${log.duration.toFixed(2)}秒</div><div><span class="text-muted-foreground">时间:</span> ${log.created_at?new Date(log.created_at).toLocaleString('zh-CN'):'-'}</div></div></div>`;content.innerHTML=detailHtml;$('logDetailModal').classList.remove('hidden')},
|
showLogDetail=(logId)=>{const log=window.allLogs.find(l=>l.id===logId);if(!log){showToast('日志不存在','error');return}const content=$('logDetailContent');let detailHtml='';if(log.status_code===-1){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-blue-600">生成进度</h4><div class="rounded-md border border-blue-200 p-3 bg-blue-50"><p class="text-sm text-blue-700">任务正在生成中...</p>${log.task_status?`<p class="text-xs text-blue-600 mt-1">状态: ${log.task_status}</p>`:''}</div></div>`}else if(log.status_code===200){try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody){if(responseBody.data&&responseBody.data.length>0){const item=responseBody.data[0];if(item.url){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">生成结果</h4><div class="rounded-md border border-border p-3 bg-muted/30"><p class="text-sm mb-2"><span class="font-medium">文件URL:</span></p><a href="${item.url}" target="_blank" class="text-blue-600 hover:underline text-xs break-all">${item.url}</a></div></div>`}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${JSON.stringify(responseBody,null,2)}</pre></div>`}}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${JSON.stringify(responseBody,null,2)}</pre></div>`}}else{detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应信息</h4><p class="text-sm text-muted-foreground">无响应数据</p></div>`}}catch(e){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm">响应数据</h4><pre class="rounded-md border border-border p-3 bg-muted/30 text-xs overflow-x-auto">${log.response_body||'无'}</pre></div>`}}else{try{const responseBody=log.response_body?JSON.parse(log.response_body):null;if(responseBody&&responseBody.error){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误原因</h4><div class="rounded-md border border-red-200 p-3 bg-red-50"><p class="text-sm text-red-700">${responseBody.error.message||responseBody.error||'未知错误'}</p></div></div>`}else if(log.response_body&&log.response_body!=='{}'){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误信息</h4><pre class="rounded-md border border-red-200 p-3 bg-red-50 text-xs overflow-x-auto">${log.response_body}</pre></div>`}}catch(e){if(log.response_body&&log.response_body!=='{}'){detailHtml+=`<div class="space-y-2"><h4 class="font-medium text-sm text-red-600">错误信息</h4><pre class="rounded-md border border-red-200 p-3 bg-red-50 text-xs overflow-x-auto">${log.response_body}</pre></div>`}}}detailHtml+=`<div class="space-y-2 pt-4 border-t border-border"><h4 class="font-medium text-sm">基本信息</h4><div class="grid grid-cols-2 gap-2 text-sm"><div><span class="text-muted-foreground">操作:</span> ${log.operation}</div><div><span class="text-muted-foreground">状态码:</span> <span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${log.status_code===-1?'bg-blue-50 text-blue-700':log.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${log.status_code===-1?'生成中':log.status_code}</span></div><div><span class="text-muted-foreground">耗时:</span> ${log.duration===-1?'生成中':log.duration.toFixed(2)+'秒'}</div><div><span class="text-muted-foreground">时间:</span> ${log.created_at?new Date(log.created_at).toLocaleString('zh-CN'):'-'}</div></div></div>`;content.innerHTML=detailHtml;$('logDetailModal').classList.remove('hidden')},
|
||||||
closeLogDetailModal=()=>{$('logDetailModal').classList.add('hidden')},
|
closeLogDetailModal=()=>{$('logDetailModal').classList.add('hidden')},
|
||||||
clearAllLogs=async()=>{if(!confirm('确定要清空所有日志吗?此操作不可恢复!'))return;try{const r=await apiRequest('/api/logs',{method:'DELETE'});if(!r)return;const d=await r.json();if(d.success){showToast('日志已清空','success');await loadLogs()}else{showToast('清空失败: '+(d.message||'未知错误'),'error')}}catch(e){showToast('清空失败: '+e.message,'error')}},
|
clearAllLogs=async()=>{if(!confirm('确定要清空所有日志吗?此操作不可恢复!'))return;try{const r=await apiRequest('/api/logs',{method:'DELETE'});if(!r)return;const d=await r.json();if(d.success){showToast('日志已清空','success');await loadLogs()}else{showToast('清空失败: '+(d.message||'未知错误'),'error')}}catch(e){showToast('清空失败: '+e.message,'error')}},
|
||||||
showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)},
|
showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)},
|
||||||
logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
|
logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
|
||||||
switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});if(t==='settings'){loadAdminConfig();loadProxyConfig();loadWatermarkFreeConfig();loadCacheConfig();loadGenerationTimeout();loadATAutoRefreshConfig()}else if(t==='logs'){loadLogs()}};
|
loadCharacters=async()=>{try{const r=await apiRequest('/api/characters');if(!r)return;const d=await r.json();const g=$('charactersGrid');if(!d||d.length===0){g.innerHTML='<div class="col-span-full text-center py-8 text-muted-foreground">暂无角色卡</div>';return}g.innerHTML=d.map(c=>`<div class="rounded-lg border border-border bg-background p-4"><div class="flex items-start gap-3"><img src="${c.avatar_path||'/static/favicon.ico'}" class="h-14 w-14 rounded-lg object-cover" onerror="this.src='/static/favicon.ico'"/><div class="flex-1 min-w-0"><div class="font-semibold truncate">${c.display_name||c.username}</div><div class="text-xs text-muted-foreground truncate">@${c.username}</div>${c.description?`<div class="text-xs text-muted-foreground mt-1 line-clamp-2">${c.description}</div>`:''}</div></div><div class="mt-3 flex gap-2"><button onclick="deleteCharacter(${c.id})" class="flex-1 inline-flex items-center justify-center rounded-md border border-destructive text-destructive hover:bg-destructive hover:text-white h-8 px-3 text-sm transition-colors">删除</button></div></div>`).join('')}catch(e){showToast('加载失败: '+e.message,'error')}},
|
||||||
|
deleteCharacter=async(id)=>{if(!confirm('确定要删除这个角色卡吗?'))return;try{const r=await apiRequest(`/api/characters/${id}`,{method:'DELETE'});if(!r)return;const d=await r.json();if(d.success){showToast('删除成功','success');await loadCharacters()}else{showToast('删除失败','error')}}catch(e){showToast('删除失败: '+e.message,'error')}},
|
||||||
|
switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs','generate'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});if(t==='settings'){loadAdminConfig();loadProxyConfig();loadWatermarkFreeConfig();loadCacheConfig();loadGenerationTimeout();loadATAutoRefreshConfig()}else if(t==='logs'){loadLogs()}};
|
||||||
|
// 自适应生成面板 iframe 高度
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
const data = event.data || {};
|
||||||
|
if (data.type === 'sora-generate-height' && typeof data.height === 'number') {
|
||||||
|
const iframe = document.getElementById('generateFrame');
|
||||||
|
if (iframe) {
|
||||||
|
const nextH = Math.max(560, Math.ceil(data.height) + 8);
|
||||||
|
iframe.style.height = `${nextH}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
window.addEventListener('DOMContentLoaded',()=>{checkAuth();refreshTokens();loadATAutoRefreshConfig()});
|
window.addEventListener('DOMContentLoaded',()=>{checkAuth();refreshTokens();loadATAutoRefreshConfig()});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user