|
1 | 1 | from fastapi import APIRouter, Depends, HTTPException, Request, Query, Body |
2 | 2 | from fastapi.responses import HTMLResponse, RedirectResponse |
3 | 3 | from pydantic import BaseModel |
| 4 | +from typing import Any |
4 | 5 |
|
5 | 6 | from app.core.auth import verify_api_key |
6 | 7 | from app.core.config import config, get_config |
|
12 | 13 | import json |
13 | 14 | from app.core.logger import logger |
14 | 15 | from app.services.register import get_auto_register_manager |
| 16 | +from app.services.api_keys import api_key_manager |
15 | 17 |
|
16 | 18 |
|
17 | 19 | router = APIRouter() |
@@ -65,6 +67,21 @@ async def admin_datacenter_page(): |
65 | 67 | """数据中心页""" |
66 | 68 | return await render_template("datacenter/datacenter.html") |
67 | 69 |
|
| 70 | +@router.get("/admin/keys", response_class=HTMLResponse, include_in_schema=False) |
| 71 | +async def admin_keys_page(): |
| 72 | + """API Key 管理页""" |
| 73 | + return await render_template("keys/keys.html") |
| 74 | + |
| 75 | +@router.get("/chat", response_class=HTMLResponse, include_in_schema=False) |
| 76 | +async def chat_page(): |
| 77 | + """在线聊天页(公开入口)""" |
| 78 | + return await render_template("chat/chat.html") |
| 79 | + |
| 80 | +@router.get("/admin/chat", response_class=HTMLResponse, include_in_schema=False) |
| 81 | +async def admin_chat_page(): |
| 82 | + """在线聊天页(后台入口)""" |
| 83 | + return await render_template("chat/chat_admin.html") |
| 84 | + |
68 | 85 | @router.post("/api/v1/admin/login") |
69 | 86 | async def admin_login_api(request: Request, body: AdminLoginBody | None = Body(default=None)): |
70 | 87 | """管理后台登录验证(用户名+密码) |
@@ -110,6 +127,143 @@ async def update_config_api(data: dict): |
110 | 127 | except Exception as e: |
111 | 128 | raise HTTPException(status_code=500, detail=str(e)) |
112 | 129 |
|
| 130 | + |
| 131 | +def _display_key(key: str) -> str: |
| 132 | + k = str(key or "") |
| 133 | + if len(k) <= 12: |
| 134 | + return k |
| 135 | + return f"{k[:6]}...{k[-4:]}" |
| 136 | + |
| 137 | + |
| 138 | +def _normalize_limit(v: Any) -> int: |
| 139 | + if v is None or v == "": |
| 140 | + return -1 |
| 141 | + try: |
| 142 | + return max(-1, int(v)) |
| 143 | + except Exception: |
| 144 | + return -1 |
| 145 | + |
| 146 | + |
| 147 | +@router.get("/api/v1/admin/keys", dependencies=[Depends(verify_api_key)]) |
| 148 | +async def list_api_keys(): |
| 149 | + """List API keys + daily usage/remaining (for admin UI).""" |
| 150 | + await api_key_manager.init() |
| 151 | + day, usage_map = await api_key_manager.usage_today() |
| 152 | + |
| 153 | + out = [] |
| 154 | + for row in api_key_manager.get_all_keys(): |
| 155 | + key = str(row.get("key") or "") |
| 156 | + used = usage_map.get(key) or {} |
| 157 | + chat_used = int(used.get("chat_used", 0) or 0) |
| 158 | + heavy_used = int(used.get("heavy_used", 0) or 0) |
| 159 | + image_used = int(used.get("image_used", 0) or 0) |
| 160 | + video_used = int(used.get("video_used", 0) or 0) |
| 161 | + |
| 162 | + chat_limit = _normalize_limit(row.get("chat_limit", -1)) |
| 163 | + heavy_limit = _normalize_limit(row.get("heavy_limit", -1)) |
| 164 | + image_limit = _normalize_limit(row.get("image_limit", -1)) |
| 165 | + video_limit = _normalize_limit(row.get("video_limit", -1)) |
| 166 | + |
| 167 | + remaining = { |
| 168 | + "chat": None if chat_limit < 0 else max(0, chat_limit - chat_used), |
| 169 | + "heavy": None if heavy_limit < 0 else max(0, heavy_limit - heavy_used), |
| 170 | + "image": None if image_limit < 0 else max(0, image_limit - image_used), |
| 171 | + "video": None if video_limit < 0 else max(0, video_limit - video_used), |
| 172 | + } |
| 173 | + |
| 174 | + out.append({ |
| 175 | + **row, |
| 176 | + "is_active": bool(row.get("is_active", True)), |
| 177 | + "display_key": _display_key(key), |
| 178 | + "usage_today": { |
| 179 | + "chat_used": chat_used, |
| 180 | + "heavy_used": heavy_used, |
| 181 | + "image_used": image_used, |
| 182 | + "video_used": video_used, |
| 183 | + }, |
| 184 | + "remaining_today": remaining, |
| 185 | + "day": day, |
| 186 | + }) |
| 187 | + |
| 188 | + # New UI expects { success: true, data: [...] } |
| 189 | + return {"success": True, "data": out} |
| 190 | + |
| 191 | + |
| 192 | +@router.post("/api/v1/admin/keys", dependencies=[Depends(verify_api_key)]) |
| 193 | +async def create_api_key(data: dict): |
| 194 | + """Create a new API key (optional name/key/limits).""" |
| 195 | + await api_key_manager.init() |
| 196 | + data = data or {} |
| 197 | + |
| 198 | + name = str(data.get("name") or "").strip() or api_key_manager.generate_name() |
| 199 | + key_val = str(data.get("key") or "").strip() or None |
| 200 | + is_active = bool(data.get("is_active", True)) |
| 201 | + |
| 202 | + limits = data.get("limits") if isinstance(data.get("limits"), dict) else {} |
| 203 | + try: |
| 204 | + row = await api_key_manager.add_key( |
| 205 | + name=name, |
| 206 | + key=key_val, |
| 207 | + is_active=is_active, |
| 208 | + limits={ |
| 209 | + "chat_per_day": limits.get("chat_per_day"), |
| 210 | + "heavy_per_day": limits.get("heavy_per_day"), |
| 211 | + "image_per_day": limits.get("image_per_day"), |
| 212 | + "video_per_day": limits.get("video_per_day"), |
| 213 | + }, |
| 214 | + ) |
| 215 | + except ValueError as e: |
| 216 | + raise HTTPException(status_code=400, detail=str(e)) |
| 217 | + |
| 218 | + return {"success": True, "data": {**row, "display_key": _display_key(row.get("key", ""))}} |
| 219 | + |
| 220 | + |
| 221 | +@router.post("/api/v1/admin/keys/update", dependencies=[Depends(verify_api_key)]) |
| 222 | +async def update_api_key(data: dict): |
| 223 | + """Update name/status/limits for an API key.""" |
| 224 | + await api_key_manager.init() |
| 225 | + data = data or {} |
| 226 | + key = str(data.get("key") or "").strip() |
| 227 | + if not key: |
| 228 | + raise HTTPException(status_code=400, detail="Missing key") |
| 229 | + |
| 230 | + if "name" in data and data.get("name") is not None: |
| 231 | + name = str(data.get("name") or "").strip() |
| 232 | + if name: |
| 233 | + await api_key_manager.update_key_name(key, name) |
| 234 | + |
| 235 | + if "is_active" in data: |
| 236 | + await api_key_manager.update_key_status(key, bool(data.get("is_active"))) |
| 237 | + |
| 238 | + limits = data.get("limits") if isinstance(data.get("limits"), dict) else None |
| 239 | + if limits is not None: |
| 240 | + await api_key_manager.update_key_limits( |
| 241 | + key, |
| 242 | + { |
| 243 | + "chat_per_day": limits.get("chat_per_day"), |
| 244 | + "heavy_per_day": limits.get("heavy_per_day"), |
| 245 | + "image_per_day": limits.get("image_per_day"), |
| 246 | + "video_per_day": limits.get("video_per_day"), |
| 247 | + }, |
| 248 | + ) |
| 249 | + |
| 250 | + return {"success": True} |
| 251 | + |
| 252 | + |
| 253 | +@router.post("/api/v1/admin/keys/delete", dependencies=[Depends(verify_api_key)]) |
| 254 | +async def delete_api_key(data: dict): |
| 255 | + """Delete an API key.""" |
| 256 | + await api_key_manager.init() |
| 257 | + data = data or {} |
| 258 | + key = str(data.get("key") or "").strip() |
| 259 | + if not key: |
| 260 | + raise HTTPException(status_code=400, detail="Missing key") |
| 261 | + |
| 262 | + ok = await api_key_manager.delete_key(key) |
| 263 | + if not ok: |
| 264 | + raise HTTPException(status_code=404, detail="Key not found") |
| 265 | + return {"success": True} |
| 266 | + |
113 | 267 | @router.get("/api/v1/admin/storage", dependencies=[Depends(verify_api_key)]) |
114 | 268 | async def get_storage_info(): |
115 | 269 | """获取当前存储模式""" |
|
0 commit comments