Files
grok2api/app/api/v1/admin.py
T

1369 lines
47 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, Depends, HTTPException, Request, Query
from fastapi.responses import HTMLResponse, StreamingResponse
from pydantic import BaseModel
from app.core.auth import verify_api_key, verify_app_key, get_admin_api_key
from app.core.config import config, get_config
from app.core.batch_tasks import create_task, get_task, expire_task
from app.core.storage import get_storage, LocalStorage, RedisStorage, SQLStorage
from app.core.exceptions import AppException
import os
from pathlib import Path
import aiofiles
import asyncio
import orjson
from app.core.logger import logger
from app.services.grok.services.voice import VoiceService
from app.services.token import get_token_manager
router = APIRouter()
TEMPLATE_DIR = Path(__file__).parent.parent.parent / "static"
async def render_template(filename: str):
"""渲染指定模板"""
template_path = TEMPLATE_DIR / filename
if not template_path.exists():
return HTMLResponse(f"Template {filename} not found.", status_code=404)
async with aiofiles.open(template_path, "r", encoding="utf-8") as f:
content = await f.read()
return HTMLResponse(content)
def _sse_event(payload: dict) -> str:
return f"data: {orjson.dumps(payload).decode()}\n\n"
def _verify_stream_api_key(request: Request) -> None:
api_key = get_admin_api_key()
if not api_key:
return
key = request.query_params.get("api_key")
if key != api_key:
raise HTTPException(status_code=401, detail="Invalid authentication token")
@router.get("/api/v1/admin/batch/{task_id}/stream")
async def stream_batch(task_id: str, request: Request):
_verify_stream_api_key(request)
task = get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
async def event_stream():
queue = task.attach()
try:
yield _sse_event({"type": "snapshot", **task.snapshot()})
final = task.final_event()
if final:
yield _sse_event(final)
return
while True:
try:
event = await asyncio.wait_for(queue.get(), timeout=15)
except asyncio.TimeoutError:
yield ": ping\n\n"
final = task.final_event()
if final:
yield _sse_event(final)
return
continue
yield _sse_event(event)
if event.get("type") in ("done", "error", "cancelled"):
return
finally:
task.detach(queue)
return StreamingResponse(event_stream(), media_type="text/event-stream")
@router.post(
"/api/v1/admin/batch/{task_id}/cancel", dependencies=[Depends(verify_api_key)]
)
async def cancel_batch(task_id: str):
task = get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
task.cancel()
return {"status": "success"}
@router.get("/admin", response_class=HTMLResponse, include_in_schema=False)
async def admin_login_page():
"""管理后台登录页"""
return await render_template("login/login.html")
@router.get("/admin/config", response_class=HTMLResponse, include_in_schema=False)
async def admin_config_page():
"""配置管理页"""
return await render_template("config/config.html")
@router.get("/admin/token", response_class=HTMLResponse, include_in_schema=False)
async def admin_token_page():
"""Token 管理页"""
return await render_template("token/token.html")
@router.get("/admin/voice", response_class=HTMLResponse, include_in_schema=False)
async def admin_voice_page():
"""Voice Live 调试页"""
return await render_template("voice/voice.html")
class VoiceTokenResponse(BaseModel):
token: str
url: str
participant_name: str = ""
room_name: str = ""
@router.get(
"/api/v1/admin/voice/token",
dependencies=[Depends(verify_api_key)],
response_model=VoiceTokenResponse,
)
async def admin_voice_token(
voice: str = "ara",
personality: str = "assistant",
speed: float = 1.0,
):
"""获取 Grok Voice Mode (LiveKit) Token"""
token_mgr = await get_token_manager()
sso_token = None
for pool_name in ("ssoBasic", "ssoSuper"):
sso_token = token_mgr.get_token(pool_name)
if sso_token:
break
if not sso_token:
raise AppException(
"No available tokens for voice mode",
code="no_token",
status_code=503,
)
service = VoiceService()
try:
data = await service.get_token(
token=sso_token,
voice=voice,
personality=personality,
speed=speed,
)
token = data.get("token")
if not token:
raise AppException(
"Upstream returned no voice token",
code="upstream_error",
status_code=502,
)
return VoiceTokenResponse(
token=token,
url="wss://livekit.grok.com",
participant_name="",
room_name="",
)
except Exception as e:
if isinstance(e, AppException):
raise
raise AppException(
f"Voice token error: {str(e)}",
code="voice_error",
status_code=500,
)
@router.post("/api/v1/admin/login", dependencies=[Depends(verify_app_key)])
async def admin_login_api():
"""管理后台登录验证(使用 app_key)"""
return {"status": "success", "api_key": get_admin_api_key()}
@router.get("/api/v1/admin/config", dependencies=[Depends(verify_api_key)])
async def get_config_api():
"""获取当前配置"""
# 暴露原始配置字典
return config._config
@router.post("/api/v1/admin/config", dependencies=[Depends(verify_api_key)])
async def update_config_api(data: dict):
"""更新配置"""
try:
await config.update(data)
return {"status": "success", "message": "配置已更新"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/v1/admin/storage", dependencies=[Depends(verify_api_key)])
async def get_storage_info():
"""获取当前存储模式"""
storage_type = os.getenv("SERVER_STORAGE_TYPE", "local").lower()
logger.info(f"Storage type: {storage_type}")
if not storage_type:
storage_type = str(get_config("storage.type", "")).lower()
if not storage_type:
storage = get_storage()
if isinstance(storage, LocalStorage):
storage_type = "local"
elif isinstance(storage, RedisStorage):
storage_type = "redis"
elif isinstance(storage, SQLStorage):
if storage.dialect in ("mysql", "mariadb"):
storage_type = "mysql"
elif storage.dialect in ("postgres", "postgresql", "pgsql"):
storage_type = "pgsql"
else:
storage_type = storage.dialect
return {"type": storage_type or "local"}
@router.get("/api/v1/admin/tokens", dependencies=[Depends(verify_api_key)])
async def get_tokens_api():
"""获取所有 Token"""
storage = get_storage()
tokens = await storage.load_tokens()
return tokens or {}
@router.post("/api/v1/admin/tokens", dependencies=[Depends(verify_api_key)])
async def update_tokens_api(data: dict):
"""更新 Token 信息"""
storage = get_storage()
try:
from app.services.token.manager import get_token_manager
from app.services.token.models import TokenInfo
async with storage.acquire_lock("tokens_save", timeout=10):
existing = await storage.load_tokens() or {}
normalized = {}
allowed_fields = set(TokenInfo.model_fields.keys())
existing_map = {}
for pool_name, tokens in existing.items():
if not isinstance(tokens, list):
continue
pool_map = {}
for item in tokens:
if isinstance(item, str):
token_data = {"token": item}
elif isinstance(item, dict):
token_data = dict(item)
else:
continue
raw_token = token_data.get("token")
if isinstance(raw_token, str) and raw_token.startswith("sso="):
token_data["token"] = raw_token[4:]
token_key = token_data.get("token")
if isinstance(token_key, str):
pool_map[token_key] = token_data
existing_map[pool_name] = pool_map
for pool_name, tokens in (data or {}).items():
if not isinstance(tokens, list):
continue
pool_list = []
for item in tokens:
if isinstance(item, str):
token_data = {"token": item}
elif isinstance(item, dict):
token_data = dict(item)
else:
continue
raw_token = token_data.get("token")
if isinstance(raw_token, str) and raw_token.startswith("sso="):
token_data["token"] = raw_token[4:]
base = existing_map.get(pool_name, {}).get(token_data.get("token"), {})
merged = dict(base)
merged.update(token_data)
if merged.get("tags") is None:
merged["tags"] = []
filtered = {k: v for k, v in merged.items() if k in allowed_fields}
try:
info = TokenInfo(**filtered)
pool_list.append(info.model_dump())
except Exception as e:
logger.warning(f"Skip invalid token in pool '{pool_name}': {e}")
continue
normalized[pool_name] = pool_list
await storage.save_tokens(normalized)
mgr = await get_token_manager()
await mgr.reload()
return {"status": "success", "message": "Token 已更新"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/v1/admin/tokens/refresh", dependencies=[Depends(verify_api_key)])
async def refresh_tokens_api(data: dict):
"""刷新 Token 状态"""
from app.services.token.manager import get_token_manager
from app.services.grok.utils.batch import run_in_batches
try:
mgr = await get_token_manager()
tokens = []
if "token" in data:
tokens.append(data["token"])
if "tokens" in data and isinstance(data["tokens"], list):
tokens.extend(data["tokens"])
if not tokens:
raise HTTPException(status_code=400, detail="No tokens provided")
# 去重并保持顺序
unique_tokens = list(dict.fromkeys(tokens))
# 最大数量限制
max_tokens = get_config("performance.usage_max_tokens", 1000)
try:
max_tokens = int(max_tokens)
except Exception:
max_tokens = 1000
truncated = False
original_count = len(unique_tokens)
if len(unique_tokens) > max_tokens:
unique_tokens = unique_tokens[:max_tokens]
truncated = True
logger.warning(
f"Usage refresh: truncated from {original_count} to {max_tokens} tokens"
)
# 批量执行配置
max_concurrent = get_config("performance.usage_max_concurrent", 25)
batch_size = get_config("performance.usage_batch_size", 50)
async def _refresh_one(t):
return await mgr.sync_usage(
t, "grok-3", consume_on_fail=False, is_usage=False
)
raw_results = await run_in_batches(
unique_tokens,
_refresh_one,
max_concurrent=max_concurrent,
batch_size=batch_size,
)
results = {}
for token, res in raw_results.items():
if res.get("ok"):
results[token] = res.get("data", False)
else:
results[token] = False
response = {"status": "success", "results": results}
if truncated:
response["warning"] = (
f"数量超出限制,仅处理前 {max_tokens} 个(共 {original_count} 个)"
)
return response
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/api/v1/admin/tokens/refresh/async", dependencies=[Depends(verify_api_key)]
)
async def refresh_tokens_api_async(data: dict):
"""刷新 Token 状态(异步批量 + SSE 进度)"""
from app.services.token.manager import get_token_manager
from app.services.grok.utils.batch import run_in_batches
mgr = await get_token_manager()
tokens: list[str] = []
if isinstance(data.get("token"), str) and data["token"].strip():
tokens.append(data["token"].strip())
if isinstance(data.get("tokens"), list):
tokens.extend([str(t).strip() for t in data["tokens"] if str(t).strip()])
if not tokens:
raise HTTPException(status_code=400, detail="No tokens provided")
unique_tokens = list(dict.fromkeys(tokens))
max_tokens = get_config("performance.usage_max_tokens", 1000)
try:
max_tokens = int(max_tokens)
except Exception:
max_tokens = 1000
truncated = False
original_count = len(unique_tokens)
if len(unique_tokens) > max_tokens:
unique_tokens = unique_tokens[:max_tokens]
truncated = True
logger.warning(
f"Usage refresh: truncated from {original_count} to {max_tokens} tokens"
)
max_concurrent = get_config("performance.usage_max_concurrent", 25)
batch_size = get_config("performance.usage_batch_size", 50)
task = create_task(len(unique_tokens))
async def _run():
try:
async def _refresh_one(t: str):
return await mgr.sync_usage(
t, "grok-3", consume_on_fail=False, is_usage=False
)
async def _on_item(item: str, res: dict):
task.record(bool(res.get("ok")))
raw_results = await run_in_batches(
unique_tokens,
_refresh_one,
max_concurrent=max_concurrent,
batch_size=batch_size,
on_item=_on_item,
should_cancel=lambda: task.cancelled,
)
if task.cancelled:
task.finish_cancelled()
return
results: dict[str, bool] = {}
ok_count = 0
fail_count = 0
for token, res in raw_results.items():
if res.get("ok") and res.get("data") is True:
ok_count += 1
results[token] = True
else:
fail_count += 1
results[token] = False
await mgr._save()
result = {
"status": "success",
"summary": {
"total": len(unique_tokens),
"ok": ok_count,
"fail": fail_count,
},
"results": results,
}
warning = None
if truncated:
warning = (
f"数量超出限制,仅处理前 {max_tokens} 个(共 {original_count} 个)"
)
task.finish(result, warning=warning)
except Exception as e:
task.fail_task(str(e))
finally:
asyncio.create_task(expire_task(task.id, 300))
asyncio.create_task(_run())
return {
"status": "success",
"task_id": task.id,
"total": len(unique_tokens),
}
@router.post("/api/v1/admin/tokens/nsfw/enable", dependencies=[Depends(verify_api_key)])
async def enable_nsfw_api(data: dict):
"""批量开启 NSFW (Unhinged) 模式"""
from app.services.grok.services.nsfw import NSFWService
from app.services.grok.utils.batch import run_in_batches
from app.services.token.manager import get_token_manager
try:
mgr = await get_token_manager()
nsfw_service = NSFWService()
# 收集 token 列表
tokens: list[str] = []
if isinstance(data.get("token"), str) and data["token"].strip():
tokens.append(data["token"].strip())
if isinstance(data.get("tokens"), list):
tokens.extend([str(t).strip() for t in data["tokens"] if str(t).strip()])
# 若未指定,则使用所有 pool 中的 token
if not tokens:
for pool_name, pool in mgr.pools.items():
for info in pool.list():
raw = (
info.token[4:] if info.token.startswith("sso=") else info.token
)
tokens.append(raw)
if not tokens:
raise HTTPException(status_code=400, detail="No tokens available")
# 去重并保持顺序
unique_tokens = list(dict.fromkeys(tokens))
# 限制最大数量(超出时截取前 N 个)
max_tokens = get_config("performance.nsfw_max_tokens", 1000)
try:
max_tokens = int(max_tokens)
except Exception:
max_tokens = 1000
truncated = False
original_count = len(unique_tokens)
if len(unique_tokens) > max_tokens:
unique_tokens = unique_tokens[:max_tokens]
truncated = True
logger.warning(
f"NSFW enable: truncated from {original_count} to {max_tokens} tokens"
)
# 批量执行配置
max_concurrent = get_config("performance.nsfw_max_concurrent", 10)
batch_size = get_config("performance.nsfw_batch_size", 50)
# 定义 worker
async def _enable(token: str):
result = await nsfw_service.enable(token)
# 成功后添加 nsfw tag
if result.success:
await mgr.add_tag(token, "nsfw")
return {
"success": result.success,
"http_status": result.http_status,
"grpc_status": result.grpc_status,
"grpc_message": result.grpc_message,
"error": result.error,
}
# 执行批量操作
raw_results = await run_in_batches(
unique_tokens, _enable, max_concurrent=max_concurrent, batch_size=batch_size
)
# 构造返回结果(mask token)
results = {}
ok_count = 0
fail_count = 0
for token, res in raw_results.items():
masked = f"{token[:8]}...{token[-8:]}" if len(token) > 20 else token
if res.get("ok") and res.get("data", {}).get("success"):
ok_count += 1
results[masked] = res.get("data", {})
else:
fail_count += 1
results[masked] = res.get("data") or {"error": res.get("error")}
response = {
"status": "success",
"summary": {
"total": len(unique_tokens),
"ok": ok_count,
"fail": fail_count,
},
"results": results,
}
# 添加截断提示
if truncated:
response["warning"] = (
f"数量超出限制,仅处理前 {max_tokens} 个(共 {original_count} 个)"
)
return response
except HTTPException:
raise
except Exception as e:
logger.error(f"Enable NSFW failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/api/v1/admin/tokens/nsfw/enable/async", dependencies=[Depends(verify_api_key)]
)
async def enable_nsfw_api_async(data: dict):
"""批量开启 NSFW (Unhinged) 模式(异步批量 + SSE 进度)"""
from app.services.grok.services.nsfw import NSFWService
from app.services.grok.utils.batch import run_in_batches
from app.services.token.manager import get_token_manager
mgr = await get_token_manager()
nsfw_service = NSFWService()
tokens: list[str] = []
if isinstance(data.get("token"), str) and data["token"].strip():
tokens.append(data["token"].strip())
if isinstance(data.get("tokens"), list):
tokens.extend([str(t).strip() for t in data["tokens"] if str(t).strip()])
if not tokens:
for pool_name, pool in mgr.pools.items():
for info in pool.list():
raw = info.token[4:] if info.token.startswith("sso=") else info.token
tokens.append(raw)
if not tokens:
raise HTTPException(status_code=400, detail="No tokens available")
unique_tokens = list(dict.fromkeys(tokens))
max_tokens = get_config("performance.nsfw_max_tokens", 1000)
try:
max_tokens = int(max_tokens)
except Exception:
max_tokens = 1000
truncated = False
original_count = len(unique_tokens)
if len(unique_tokens) > max_tokens:
unique_tokens = unique_tokens[:max_tokens]
truncated = True
logger.warning(
f"NSFW enable: truncated from {original_count} to {max_tokens} tokens"
)
max_concurrent = get_config("performance.nsfw_max_concurrent", 10)
batch_size = get_config("performance.nsfw_batch_size", 50)
task = create_task(len(unique_tokens))
async def _run():
try:
async def _enable(token: str):
result = await nsfw_service.enable(token)
if result.success:
await mgr.add_tag(token, "nsfw")
return {
"success": result.success,
"http_status": result.http_status,
"grpc_status": result.grpc_status,
"grpc_message": result.grpc_message,
"error": result.error,
}
async def _on_item(item: str, res: dict):
ok = bool(res.get("ok") and res.get("data", {}).get("success"))
task.record(ok)
raw_results = await run_in_batches(
unique_tokens,
_enable,
max_concurrent=max_concurrent,
batch_size=batch_size,
on_item=_on_item,
should_cancel=lambda: task.cancelled,
)
if task.cancelled:
task.finish_cancelled()
return
results = {}
ok_count = 0
fail_count = 0
for token, res in raw_results.items():
masked = f"{token[:8]}...{token[-8:]}" if len(token) > 20 else token
if res.get("ok") and res.get("data", {}).get("success"):
ok_count += 1
results[masked] = res.get("data", {})
else:
fail_count += 1
results[masked] = res.get("data") or {"error": res.get("error")}
await mgr._save()
result = {
"status": "success",
"summary": {
"total": len(unique_tokens),
"ok": ok_count,
"fail": fail_count,
},
"results": results,
}
warning = None
if truncated:
warning = (
f"数量超出限制,仅处理前 {max_tokens} 个(共 {original_count} 个)"
)
task.finish(result, warning=warning)
except Exception as e:
task.fail_task(str(e))
finally:
asyncio.create_task(expire_task(task.id, 300))
asyncio.create_task(_run())
return {
"status": "success",
"task_id": task.id,
"total": len(unique_tokens),
}
@router.get("/admin/cache", response_class=HTMLResponse, include_in_schema=False)
async def admin_cache_page():
"""缓存管理页"""
return await render_template("cache/cache.html")
@router.get("/api/v1/admin/cache", dependencies=[Depends(verify_api_key)])
async def get_cache_stats_api(request: Request):
"""获取缓存统计"""
from app.services.grok.services.assets import DownloadService, ListService
from app.services.token.manager import get_token_manager
from app.services.grok.utils.batch import run_in_batches
try:
dl_service = DownloadService()
image_stats = dl_service.get_stats("image")
video_stats = dl_service.get_stats("video")
mgr = await get_token_manager()
pools = mgr.pools
accounts = []
for pool_name, pool in pools.items():
for info in pool.list():
raw_token = (
info.token[4:] if info.token.startswith("sso=") else info.token
)
masked = (
f"{raw_token[:8]}...{raw_token[-16:]}"
if len(raw_token) > 24
else raw_token
)
accounts.append(
{
"token": raw_token,
"token_masked": masked,
"pool": pool_name,
"status": info.status,
"last_asset_clear_at": info.last_asset_clear_at,
}
)
scope = request.query_params.get("scope")
selected_token = request.query_params.get("token")
tokens_param = request.query_params.get("tokens")
selected_tokens = []
if tokens_param:
selected_tokens = [t.strip() for t in tokens_param.split(",") if t.strip()]
online_stats = {
"count": 0,
"status": "unknown",
"token": None,
"last_asset_clear_at": None,
}
online_details = []
account_map = {a["token"]: a for a in accounts}
max_concurrent = get_config("performance.assets_max_concurrent", 25)
batch_size = get_config("performance.assets_batch_size", 10)
try:
max_concurrent = int(max_concurrent)
except Exception:
max_concurrent = 25
try:
batch_size = int(batch_size)
except Exception:
batch_size = 10
max_concurrent = max(1, max_concurrent)
batch_size = max(1, batch_size)
max_tokens = get_config("performance.assets_max_tokens", 1000)
try:
max_tokens = int(max_tokens)
except Exception:
max_tokens = 1000
truncated = False
original_count = 0
async def _fetch_assets(token: str):
list_service = ListService()
try:
return await list_service.count(token)
finally:
await list_service.close()
async def _fetch_detail(token: str):
account = account_map.get(token)
try:
count = await _fetch_assets(token)
return {
"detail": {
"token": token,
"token_masked": account["token_masked"] if account else token,
"count": count,
"status": "ok",
"last_asset_clear_at": account["last_asset_clear_at"]
if account
else None,
},
"count": count,
}
except Exception as e:
return {
"detail": {
"token": token,
"token_masked": account["token_masked"] if account else token,
"count": 0,
"status": f"error: {str(e)}",
"last_asset_clear_at": account["last_asset_clear_at"]
if account
else None,
},
"count": 0,
}
if selected_tokens:
selected_tokens = list(dict.fromkeys(selected_tokens))
original_count = len(selected_tokens)
if len(selected_tokens) > max_tokens:
selected_tokens = selected_tokens[:max_tokens]
truncated = True
total = 0
raw_results = await run_in_batches(
selected_tokens,
_fetch_detail,
max_concurrent=max_concurrent,
batch_size=batch_size,
)
for token, res in raw_results.items():
if res.get("ok"):
data = res.get("data", {})
detail = data.get("detail")
total += data.get("count", 0)
else:
account = account_map.get(token)
detail = {
"token": token,
"token_masked": account["token_masked"] if account else token,
"count": 0,
"status": f"error: {res.get('error')}",
"last_asset_clear_at": account["last_asset_clear_at"]
if account
else None,
}
if detail:
online_details.append(detail)
online_stats = {
"count": total,
"status": "ok" if selected_tokens else "no_token",
"token": None,
"last_asset_clear_at": None,
}
scope = "selected"
elif scope == "all":
total = 0
tokens = list(dict.fromkeys([account["token"] for account in accounts]))
original_count = len(tokens)
if len(tokens) > max_tokens:
tokens = tokens[:max_tokens]
truncated = True
raw_results = await run_in_batches(
tokens,
_fetch_detail,
max_concurrent=max_concurrent,
batch_size=batch_size,
)
for token, res in raw_results.items():
if res.get("ok"):
data = res.get("data", {})
detail = data.get("detail")
total += data.get("count", 0)
else:
account = account_map.get(token)
detail = {
"token": token,
"token_masked": account["token_masked"] if account else token,
"count": 0,
"status": f"error: {res.get('error')}",
"last_asset_clear_at": account["last_asset_clear_at"]
if account
else None,
}
if detail:
online_details.append(detail)
online_stats = {
"count": total,
"status": "ok" if accounts else "no_token",
"token": None,
"last_asset_clear_at": None,
}
else:
token = selected_token
if token:
try:
count = await _fetch_assets(token)
match = next((a for a in accounts if a["token"] == token), None)
online_stats = {
"count": count,
"status": "ok",
"token": token,
"token_masked": match["token_masked"] if match else token,
"last_asset_clear_at": match["last_asset_clear_at"]
if match
else None,
}
except Exception as e:
match = next((a for a in accounts if a["token"] == token), None)
online_stats = {
"count": 0,
"status": f"error: {str(e)}",
"token": token,
"token_masked": match["token_masked"] if match else token,
"last_asset_clear_at": match["last_asset_clear_at"]
if match
else None,
}
else:
online_stats = {
"count": 0,
"status": "not_loaded",
"token": None,
"last_asset_clear_at": None,
}
response = {
"local_image": image_stats,
"local_video": video_stats,
"online": online_stats,
"online_accounts": accounts,
"online_scope": scope or "none",
"online_details": online_details,
}
if truncated:
response["warning"] = (
f"数量超出限制,仅处理前 {max_tokens} 个(共 {original_count} 个)"
)
return response
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post(
"/api/v1/admin/cache/online/load/async", dependencies=[Depends(verify_api_key)]
)
async def load_online_cache_api_async(data: dict):
"""在线资产统计(异步批量 + SSE 进度)"""
from app.services.grok.services.assets import DownloadService, ListService
from app.services.token.manager import get_token_manager
from app.services.grok.utils.batch import run_in_batches
mgr = await get_token_manager()
# 账号列表
accounts = []
for pool_name, pool in mgr.pools.items():
for info in pool.list():
raw_token = info.token[4:] if info.token.startswith("sso=") else info.token
masked = (
f"{raw_token[:8]}...{raw_token[-16:]}"
if len(raw_token) > 24
else raw_token
)
accounts.append(
{
"token": raw_token,
"token_masked": masked,
"pool": pool_name,
"status": info.status,
"last_asset_clear_at": info.last_asset_clear_at,
}
)
account_map = {a["token"]: a for a in accounts}
tokens = data.get("tokens")
scope = data.get("scope")
selected_tokens: list[str] = []
if isinstance(tokens, list):
selected_tokens = [str(t).strip() for t in tokens if str(t).strip()]
if not selected_tokens and scope == "all":
selected_tokens = [account["token"] for account in accounts]
scope = "all"
elif selected_tokens:
scope = "selected"
else:
raise HTTPException(status_code=400, detail="No tokens provided")
selected_tokens = list(dict.fromkeys(selected_tokens))
max_tokens = get_config("performance.assets_max_tokens", 1000)
try:
max_tokens = int(max_tokens)
except Exception:
max_tokens = 1000
truncated = False
original_count = len(selected_tokens)
if len(selected_tokens) > max_tokens:
selected_tokens = selected_tokens[:max_tokens]
truncated = True
max_concurrent = get_config("performance.assets_max_concurrent", 25)
batch_size = get_config("performance.assets_batch_size", 10)
task = create_task(len(selected_tokens))
async def _run():
try:
dl_service = DownloadService()
image_stats = dl_service.get_stats("image")
video_stats = dl_service.get_stats("video")
async def _fetch_detail(token: str):
account = account_map.get(token)
list_service = ListService()
try:
count = await list_service.count(token)
detail = {
"token": token,
"token_masked": account["token_masked"] if account else token,
"count": count,
"status": "ok",
"last_asset_clear_at": account["last_asset_clear_at"]
if account
else None,
}
return {"ok": True, "detail": detail, "count": count}
except Exception as e:
detail = {
"token": token,
"token_masked": account["token_masked"] if account else token,
"count": 0,
"status": f"error: {str(e)}",
"last_asset_clear_at": account["last_asset_clear_at"]
if account
else None,
}
return {"ok": False, "detail": detail, "count": 0}
finally:
await list_service.close()
async def _on_item(item: str, res: dict):
ok = bool(res.get("data", {}).get("ok"))
task.record(ok)
raw_results = await run_in_batches(
selected_tokens,
_fetch_detail,
max_concurrent=max_concurrent,
batch_size=batch_size,
on_item=_on_item,
should_cancel=lambda: task.cancelled,
)
if task.cancelled:
task.finish_cancelled()
return
online_details = []
total = 0
for token, res in raw_results.items():
data = res.get("data", {})
detail = data.get("detail")
if detail:
online_details.append(detail)
total += data.get("count", 0)
online_stats = {
"count": total,
"status": "ok" if selected_tokens else "no_token",
"token": None,
"last_asset_clear_at": None,
}
result = {
"local_image": image_stats,
"local_video": video_stats,
"online": online_stats,
"online_accounts": accounts,
"online_scope": scope or "none",
"online_details": online_details,
}
warning = None
if truncated:
warning = (
f"数量超出限制,仅处理前 {max_tokens} 个(共 {original_count} 个)"
)
task.finish(result, warning=warning)
except Exception as e:
task.fail_task(str(e))
finally:
asyncio.create_task(expire_task(task.id, 300))
asyncio.create_task(_run())
return {
"status": "success",
"task_id": task.id,
"total": len(selected_tokens),
}
@router.post("/api/v1/admin/cache/clear", dependencies=[Depends(verify_api_key)])
async def clear_local_cache_api(data: dict):
"""清理本地缓存"""
from app.services.grok.services.assets import DownloadService
cache_type = data.get("type", "image")
try:
dl_service = DownloadService()
result = dl_service.clear(cache_type)
return {"status": "success", "result": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/v1/admin/cache/list", dependencies=[Depends(verify_api_key)])
async def list_local_cache_api(
cache_type: str = "image",
type_: str = Query(default=None, alias="type"),
page: int = 1,
page_size: int = 1000,
):
"""列出本地缓存文件"""
from app.services.grok.services.assets import DownloadService
try:
if type_:
cache_type = type_
dl_service = DownloadService()
result = dl_service.list_files(cache_type, page, page_size)
return {"status": "success", **result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/v1/admin/cache/item/delete", dependencies=[Depends(verify_api_key)])
async def delete_local_cache_item_api(data: dict):
"""删除单个本地缓存文件"""
from app.services.grok.services.assets import DownloadService
cache_type = data.get("type", "image")
name = data.get("name")
if not name:
raise HTTPException(status_code=400, detail="Missing file name")
try:
dl_service = DownloadService()
result = dl_service.delete_file(cache_type, name)
return {"status": "success", "result": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/v1/admin/cache/online/clear", dependencies=[Depends(verify_api_key)])
async def clear_online_cache_api(data: dict):
"""清理在线缓存"""
from app.services.grok.services.assets import DeleteService
from app.services.token.manager import get_token_manager
from app.services.grok.utils.batch import run_in_batches
delete_service = None
try:
mgr = await get_token_manager()
tokens = data.get("tokens")
delete_service = DeleteService()
if isinstance(tokens, list):
token_list = [t.strip() for t in tokens if isinstance(t, str) and t.strip()]
if not token_list:
raise HTTPException(status_code=400, detail="No tokens provided")
# 去重并保持顺序
token_list = list(dict.fromkeys(token_list))
# 最大数量限制
max_tokens = get_config("performance.assets_max_tokens", 1000)
try:
max_tokens = int(max_tokens)
except Exception:
max_tokens = 1000
truncated = False
original_count = len(token_list)
if len(token_list) > max_tokens:
token_list = token_list[:max_tokens]
truncated = True
results = {}
max_concurrent = get_config("performance.assets_max_concurrent", 25)
batch_size = get_config("performance.assets_batch_size", 10)
try:
max_concurrent = int(max_concurrent)
except Exception:
max_concurrent = 25
try:
batch_size = int(batch_size)
except Exception:
batch_size = 10
max_concurrent = max(1, max_concurrent)
batch_size = max(1, batch_size)
async def _clear_one(t: str):
try:
result = await delete_service.delete_all(t)
await mgr.mark_asset_clear(t)
return {"status": "success", "result": result}
except Exception as e:
return {"status": "error", "error": str(e)}
raw_results = await run_in_batches(
token_list,
_clear_one,
max_concurrent=max_concurrent,
batch_size=batch_size,
)
for token, res in raw_results.items():
if res.get("ok"):
results[token] = res.get("data", {})
else:
results[token] = {"status": "error", "error": res.get("error")}
response = {"status": "success", "results": results}
if truncated:
response["warning"] = (
f"数量超出限制,仅处理前 {max_tokens} 个(共 {original_count} 个)"
)
return response
token = data.get("token") or mgr.get_token()
if not token:
raise HTTPException(
status_code=400, detail="No available token to perform cleanup"
)
result = await delete_service.delete_all(token)
await mgr.mark_asset_clear(token)
return {"status": "success", "result": result}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
if delete_service:
await delete_service.close()
@router.post(
"/api/v1/admin/cache/online/clear/async", dependencies=[Depends(verify_api_key)]
)
async def clear_online_cache_api_async(data: dict):
"""清理在线缓存(异步批量 + SSE 进度)"""
from app.services.grok.services.assets import DeleteService
from app.services.token.manager import get_token_manager
from app.services.grok.utils.batch import run_in_batches
mgr = await get_token_manager()
tokens = data.get("tokens")
if not isinstance(tokens, list):
raise HTTPException(status_code=400, detail="No tokens provided")
token_list = [t.strip() for t in tokens if isinstance(t, str) and t.strip()]
if not token_list:
raise HTTPException(status_code=400, detail="No tokens provided")
token_list = list(dict.fromkeys(token_list))
max_tokens = get_config("performance.assets_max_tokens", 1000)
try:
max_tokens = int(max_tokens)
except Exception:
max_tokens = 1000
truncated = False
original_count = len(token_list)
if len(token_list) > max_tokens:
token_list = token_list[:max_tokens]
truncated = True
max_concurrent = get_config("performance.assets_max_concurrent", 25)
batch_size = get_config("performance.assets_batch_size", 10)
task = create_task(len(token_list))
async def _run():
delete_service = DeleteService()
try:
async def _clear_one(t: str):
try:
result = await delete_service.delete_all(t)
await mgr.mark_asset_clear(t)
return {"ok": True, "result": result}
except Exception as e:
return {"ok": False, "error": str(e)}
async def _on_item(item: str, res: dict):
ok = bool(res.get("data", {}).get("ok"))
task.record(ok)
raw_results = await run_in_batches(
token_list,
_clear_one,
max_concurrent=max_concurrent,
batch_size=batch_size,
on_item=_on_item,
should_cancel=lambda: task.cancelled,
)
if task.cancelled:
task.finish_cancelled()
return
results = {}
ok_count = 0
fail_count = 0
for token, res in raw_results.items():
data = res.get("data", {})
if data.get("ok"):
ok_count += 1
results[token] = {"status": "success", "result": data.get("result")}
else:
fail_count += 1
results[token] = {"status": "error", "error": data.get("error")}
result = {
"status": "success",
"summary": {
"total": len(token_list),
"ok": ok_count,
"fail": fail_count,
},
"results": results,
}
warning = None
if truncated:
warning = (
f"数量超出限制,仅处理前 {max_tokens} 个(共 {original_count} 个)"
)
task.finish(result, warning=warning)
except Exception as e:
task.fail_task(str(e))
finally:
await delete_service.close()
asyncio.create_task(expire_task(task.id, 300))
asyncio.create_task(_run())
return {
"status": "success",
"task_id": task.id,
"total": len(token_list),
}