From 28b70ca26ca4fcde77708a5525d8bbf3ce8fb9cf Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Tue, 3 Feb 2026 13:31:04 +0800 Subject: [PATCH 01/19] Update README.MD --- README.MD | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.MD b/README.MD index 300d956..159baed 100644 --- a/README.MD +++ b/README.MD @@ -7,6 +7,15 @@ 将 DeepSeek 免费对话版转换为 **OpenAI & Claude 兼容 API**,支持多账号轮询、自动 Token 刷新、可视化管理界面。 +
+ p1 + p2 + p3 + p4 + p5 +
+ + ## ✨ 特性 - 🔄 **双协议兼容** - 同时支持 OpenAI 和 Claude (Anthropic) API 格式 From 06ae417dad21f5350a70232ea5a8d08bc28025a9 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Tue, 3 Feb 2026 13:31:49 +0800 Subject: [PATCH 02/19] Update README.MD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新webui展示图片 --- README.MD | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.MD b/README.MD index 159baed..27424f2 100644 --- a/README.MD +++ b/README.MD @@ -7,13 +7,12 @@ 将 DeepSeek 免费对话版转换为 **OpenAI & Claude 兼容 API**,支持多账号轮询、自动 Token 刷新、可视化管理界面。 -
- p1 - p2 - p3 - p4 - p5 -
+![p1](https://github.com/user-attachments/assets/07296a50-50d4-4f05-a9e5-280df14e9532) +![p2](https://github.com/user-attachments/assets/03b4a763-766f-4050-aea8-1a183e70ae6a) +![p3](https://github.com/user-attachments/assets/beb9e41d-4c12-45d1-a26c-154280211185) +![p4](https://github.com/user-attachments/assets/fc8b9836-11e3-4c38-a684-eb2c79b80fe9) +![p5](https://github.com/user-attachments/assets/513e9ca7-aa9e-45a6-8f7e-f362b1650675) + ## ✨ 特性 From 43cb68cc1df1ed97e37215fcb4aa590ad54820bd Mon Sep 17 00:00:00 2001 From: "cto-new[bot]" <140088366+cto-new[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:07:22 +0000 Subject: [PATCH 03/19] Add decoupled Docker support with zero-intrusion design --- .dockerignore | 69 ++++++++++++++++++ DEPLOY.md | 158 +++++++++++++++++++++++++++++------------ Dockerfile | 20 ++++++ README.MD | 24 +++++-- docker-compose.dev.yml | 32 +++++++++ docker-compose.yml | 29 ++++++++ 6 files changed, 279 insertions(+), 53 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a33226a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,69 @@ +# Git +.git +.gitignore + +# Python +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 虚拟环境 +venv/ +env/ +ENV/ +.venv + +# 环境配置(通过 docker-compose 挂载或环境变量传递) +.env +.env.local +.env.*.local +config.json + +# 开发工具 +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 测试 +tests/ +.pytest_cache/ +.coverage +htmlcov/ + +# Node.js / WebUI 开发依赖 +node_modules/ +webui/node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# 文档 +*.md +!README*.md + +# CI/CD +.github/ +.releaserc.json + +# 其他 +.DS_Store +Thumbs.db diff --git a/DEPLOY.md b/DEPLOY.md index ff13005..b1c47d9 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -7,6 +7,7 @@ ## 目录 - [Vercel 部署(推荐)](#vercel-部署推荐) +- [Docker 部署(推荐)](#docker-部署推荐) - [本地开发](#本地开发) - [生产环境部署](#生产环境部署) - [常见问题](#常见问题) @@ -148,6 +149,108 @@ npm run build --- +## Docker 部署(推荐) + +Docker 部署采用**零侵入、解耦设计**: +- Dockerfile 仅执行标准 Python 项目操作,不硬编码任何项目特定配置 +- 所有配置通过环境变量和 `.env` 文件管理 +- **主代码更新时只需重新构建镜像,无需修改 Docker 配置** + +### 快速开始(Docker Compose) + +```bash +# 1. 复制环境变量模板 +cp .env.example .env +# 编辑 .env,填写 DS2API_ADMIN_KEY 和 DS2API_CONFIG_JSON + +# 2. 启动服务 +docker-compose up -d + +# 3. 查看日志 +docker-compose logs -f + +# 4. 主代码更新后重新构建 +docker-compose up -d --build +``` + +### 配置文件挂载方式 + +如需使用 `config.json` 而非环境变量: + +```yaml +# docker-compose.yml +services: + ds2api: + build: . + ports: + - "5001:5001" + environment: + - DS2API_ADMIN_KEY=your-admin-key + volumes: + - ./config.json:/app/config.json:ro + restart: unless-stopped +``` + +### Docker 命令行部署 + +```bash +# 构建镜像 +docker build -t ds2api:latest . + +# 使用环境变量运行 +docker run -d \ + --name ds2api \ + -p 5001:5001 \ + -e DS2API_ADMIN_KEY=your-admin-key \ + -e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \ + --restart unless-stopped \ + ds2api:latest + +# 或使用配置文件挂载 +docker run -d \ + --name ds2api \ + -p 5001:5001 \ + -e DS2API_ADMIN_KEY=your-admin-key \ + -v $(pwd)/config.json:/app/config.json:ro \ + --restart unless-stopped \ + ds2api:latest +``` + +### 开发模式(热重载) + +```bash +# 使用开发配置启动,代码修改实时生效 +docker-compose -f docker-compose.dev.yml up +``` + +开发模式特性: +- 源代码挂载到容器,修改即时生效 +- 日志级别设为 DEBUG +- 自动读取本地 `config.json` + +### 维护命令 + +```bash +# 查看容器状态 +docker-compose ps + +# 查看日志 +docker-compose logs -f ds2api + +# 重启服务 +docker-compose restart + +# 停止服务 +docker-compose down + +# 完全重建(清除缓存) +docker-compose down +docker-compose build --no-cache +docker-compose up -d +``` + +--- + ## 生产环境部署 ### 使用 systemd (Linux) @@ -231,52 +334,6 @@ server { } ``` -### Docker 部署(可选) - -```dockerfile -# Dockerfile -FROM python:3.11-slim - -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY . . - -EXPOSE 5001 -CMD ["python", "app.py"] -``` - -```bash -# 构建镜像 -docker build -t ds2api . - -# 运行容器 -docker run -d \ - --name ds2api \ - -p 5001:5001 \ - -e DS2API_ADMIN_KEY=your-admin-key \ - -e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \ - ds2api -``` - -### Docker Compose - -```yaml -# docker-compose.yml -version: '3.8' - -services: - ds2api: - build: . - ports: - - "5001:5001" - environment: - - DS2API_ADMIN_KEY=${DS2API_ADMIN_KEY} - - DS2API_CONFIG_JSON=${DS2API_CONFIG_JSON} - restart: unless-stopped -``` - --- ## 常见问题 @@ -312,6 +369,15 @@ pip install -r requirements.txt # 重启服务 ``` +**Docker 部署**: +```bash +# 拉取最新代码 +git pull origin main + +# 重新构建并启动(无需修改 Docker 配置) +docker-compose up -d --build +``` + **Vercel 部署**: - 项目会自动从 GitHub 同步更新 - 或在 Vercel 控制台手动触发重新部署 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7245e3a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +# DS2API Docker 镜像 +# 采用极简、零侵入设计,所有配置通过环境变量传递 +# 主代码更新时只需重新构建镜像,无需修改 Dockerfile + +FROM python:3.11-slim + +WORKDIR /app + +# 安装依赖(利用 Docker 缓存层) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 复制整个项目(保留原始目录结构) +COPY . . + +# 暴露服务端口 +EXPOSE 5001 + +# 启动命令(依赖项目自身的启动逻辑) +CMD ["python", "app.py"] diff --git a/README.MD b/README.MD index 27424f2..12660ee 100644 --- a/README.MD +++ b/README.MD @@ -4,6 +4,7 @@ ![Stars](https://img.shields.io/github/stars/CJackHwang/ds2api.svg) ![Forks](https://img.shields.io/github/forks/CJackHwang/ds2api.svg) [![Version](https://img.shields.io/badge/version-1.6.11-blue.svg)](version.txt) +[![Docker](https://img.shields.io/badge/docker-ready-blue.svg)](DEPLOY.md#docker-部署推荐) 将 DeepSeek 免费对话版转换为 **OpenAI & Claude 兼容 API**,支持多账号轮询、自动 Token 刷新、可视化管理界面。 @@ -192,17 +193,26 @@ location / { } ``` -### Docker 部署(可选) +### 方式三:Docker 部署 ```bash -# 使用环境变量配置 -docker run -d \ - -p 5001:5001 \ - -e DS2API_ADMIN_KEY=your-admin-key \ - -e DS2API_CONFIG_JSON='{"keys":["api-key"],"accounts":[...]}' \ - ds2api +# 1. 克隆仓库并进入目录 +git clone https://github.com/CJackHwang/ds2api.git +cd ds2api + +# 2. 配置环境变量 +cp .env.example .env +# 编辑 .env,填写 DS2API_ADMIN_KEY 和 DS2API_CONFIG_JSON + +# 3. 启动服务 +docker-compose up -d + +# 4. 查看日志 +docker-compose logs -f ``` +> **Docker 优势**:零侵入设计,主代码更新只需 `docker-compose up -d --build`,无需修改 Docker 配置。详见 [DEPLOY.md](DEPLOY.md#docker-部署推荐)。 + ## ⚠️ 免责声明 **本项目基于逆向工程实现,服务稳定性无法保证。** diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..19a42ba --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,32 @@ +# DS2API 开发环境配置 +# 特性: +# - 源代码挂载(热重载) +# - 调试日志级别 +# - 自动重启 +# +# 使用说明: +# docker-compose -f docker-compose.dev.yml up + +services: + ds2api: + build: . + image: ds2api:dev + container_name: ds2api-dev + ports: + - "${PORT:-5001}:5001" + env_file: + - .env + environment: + - HOST=0.0.0.0 + - LOG_LEVEL=DEBUG + volumes: + # 源代码挂载(开发时实时生效) + - ./app.py:/app/app.py:ro + - ./core:/app/core:ro + - ./routes:/app/routes:ro + - ./static:/app/static:ro + # 配置文件挂载(便于本地修改) + - ./config.json:/app/config.json:ro + restart: "no" + stdin_open: true + tty: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3842060 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +# DS2API 生产环境配置 +# 使用说明: +# 1. 复制 .env.example 为 .env 并填写配置 +# 2. docker-compose up -d +# 3. 主代码更新后:docker-compose up -d --build +# +# 设计原则: +# - 零侵入:所有项目配置通过 .env 文件传递 +# - 易维护:主代码更新只需重新构建镜像 + +services: + ds2api: + build: . + image: ds2api:latest + container_name: ds2api + ports: + - "${PORT:-5001}:5001" + env_file: + - .env + environment: + # 确保容器内使用正确的主机绑定 + - HOST=0.0.0.0 + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:5001/v1/models')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s From d67b64633b5d2bde9b1011a0aeca3d17618db22b Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Wed, 4 Feb 2026 13:25:43 +0800 Subject: [PATCH 04/19] Fix docker dev reload and token sync --- docker-compose.dev.yml | 15 ++- routes/admin/accounts.py | 144 ++++++++++++++++++++++------ webui/src/components/VercelSync.jsx | 2 +- 3 files changed, 128 insertions(+), 33 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 19a42ba..a329cf8 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,6 +12,19 @@ services: build: . image: ds2api:dev container_name: ds2api-dev + command: [ + "uvicorn", + "app:app", + "--host", + "0.0.0.0", + "--port", + "5001", + "--reload", + "--reload-dir", + "/app", + "--log-level", + "debug" + ] ports: - "${PORT:-5001}:5001" env_file: @@ -26,7 +39,7 @@ services: - ./routes:/app/routes:ro - ./static:/app/static:ro # 配置文件挂载(便于本地修改) - - ./config.json:/app/config.json:ro + - ./config.json:/app/config.json restart: "no" stdin_open: true tty: true diff --git a/routes/admin/accounts.py b/routes/admin/accounts.py index f5b8b43..f53bb55 100644 --- a/routes/admin/accounts.py +++ b/routes/admin/accounts.py @@ -36,23 +36,75 @@ async def validate_single_account(account: dict) -> dict: "has_token": bool(account.get("token", "").strip()), "message": "", } - + + def _is_token_invalid(status_code: int, data: dict) -> bool: + msg = (data.get("msg") or data.get("message") or "").lower() + code = data.get("code") + return status_code in {401, 403} or code in {40001, 40002, 40003} or "token" in msg or "unauthorized" in msg + + def _create_session(token: str) -> dict: + headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"} + try: + session_resp = cffi_requests.post( + DEEPSEEK_CREATE_SESSION_URL, + headers=headers, + json={"agent": "chat"}, + impersonate="safari15_3", + timeout=15, + ) + except Exception as e: + return {"success": False, "message": f"请求异常: {e}", "status_code": 0, "data": {}} + + try: + data = session_resp.json() + except Exception: + data = {} + finally: + session_resp.close() + if session_resp.status_code == 200 and data.get("code") == 0: + return { + "success": True, + "session_id": data.get("data", {}).get("biz_data", {}).get("id"), + "status_code": session_resp.status_code, + "data": data, + } + return { + "success": False, + "message": data.get("msg") or f"HTTP {session_resp.status_code}", + "status_code": session_resp.status_code, + "data": data, + } + try: - if result["has_token"]: - result["valid"] = True - result["message"] = "已有有效 token" - else: + token = account.get("token", "").strip() + if token: + session_result = _create_session(token) + if session_result["success"]: + result["valid"] = True + result["message"] = "Token 有效" + return result + + if _is_token_invalid(session_result["status_code"], session_result["data"]): + token = "" + account["token"] = "" + + if not token: try: login_deepseek_via_account(account) - result["valid"] = True - result["has_token"] = True - result["message"] = "登录成功" + token = account.get("token", "").strip() + session_result = _create_session(token) + if session_result["success"]: + result["valid"] = True + result["has_token"] = True + result["message"] = "登录成功并验证通过" + else: + result["message"] = f"登录成功但验证失败: {session_result['message']}" except Exception as e: result["valid"] = False result["message"] = f"登录失败: {str(e)}" except Exception as e: result["message"] = f"验证出错: {str(e)}" - + return result @@ -134,38 +186,68 @@ async def test_account_api(account: dict, model: str = "deepseek-chat", message: start_time = time.time() + def _is_token_invalid(status_code: int, data: dict) -> bool: + msg = (data.get("msg") or data.get("message") or "").lower() + code = data.get("code") + return status_code in {401, 403} or code in {40001, 40002, 40003} or "token" in msg or "unauthorized" in msg + + def _create_session(token: str) -> dict: + headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"} + try: + session_resp = cffi_requests.post( + DEEPSEEK_CREATE_SESSION_URL, + headers=headers, + json={"agent": "chat"}, + impersonate="safari15_3", + timeout=15, + ) + except Exception as e: + return {"success": False, "message": f"请求异常: {e}", "status_code": 0, "data": {}} + + try: + session_data = session_resp.json() + except Exception: + session_data = {} + finally: + session_resp.close() + + if session_resp.status_code == 200 and session_data.get("code") == 0: + return { + "success": True, + "session_id": session_data.get("data", {}).get("biz_data", {}).get("id"), + "status_code": session_resp.status_code, + "data": session_data, + } + return { + "success": False, + "message": session_data.get("msg") or f"HTTP {session_resp.status_code}", + "status_code": session_resp.status_code, + "data": session_data, + } + try: token = account.get("token", "").strip() - if not token: + session_result = None + if token: + session_result = _create_session(token) + + if not token or (session_result and not session_result["success"] and _is_token_invalid(session_result["status_code"], session_result["data"])): try: + account["token"] = "" login_deepseek_via_account(account) token = account.get("token", "") + session_result = _create_session(token) except Exception as e: result["message"] = f"登录失败: {str(e)}" return result - + + if not session_result or not session_result["success"]: + result["message"] = f"创建会话失败: {session_result['message'] if session_result else 'Unknown error'}" + return result + + session_id = session_result["session_id"] headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"} - session_resp = cffi_requests.post( - DEEPSEEK_CREATE_SESSION_URL, - headers=headers, - json={"agent": "chat"}, - impersonate="safari15_3", - timeout=15, - ) - - if session_resp.status_code != 200: - result["message"] = f"创建会话失败: HTTP {session_resp.status_code}" - return result - - session_data = session_resp.json() - if session_data.get("code") != 0: - result["message"] = f"创建会话失败: {session_data.get('msg', 'Unknown error')}" - account["token"] = "" - return result - - session_id = session_data.get("data", {}).get("biz_data", {}).get("id") - if not message.strip(): result["success"] = True result["message"] = "API 测试成功(仅会话创建)" diff --git a/webui/src/components/VercelSync.jsx b/webui/src/components/VercelSync.jsx index 841411a..59e516a 100644 --- a/webui/src/components/VercelSync.jsx +++ b/webui/src/components/VercelSync.jsx @@ -47,7 +47,7 @@ export default function VercelSync({ onMessage, authFetch }) { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - vercel_token: vercelToken, + vercel_token: tokenToUse, project_id: projectId, team_id: teamId || undefined, }), From 840042d301500cc139ffff2a24cfe138353c127b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Feb 2026 05:35:55 +0000 Subject: [PATCH 05/19] chore: auto-build WebUI [skip ci] --- static/admin/assets/{index-C7aw1GYL.js => index-cwfNH6yZ.js} | 2 +- static/admin/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename static/admin/assets/{index-C7aw1GYL.js => index-cwfNH6yZ.js} (92%) diff --git a/static/admin/assets/index-C7aw1GYL.js b/static/admin/assets/index-cwfNH6yZ.js similarity index 92% rename from static/admin/assets/index-C7aw1GYL.js rename to static/admin/assets/index-cwfNH6yZ.js index 9b576a6..e915f3f 100644 --- a/static/admin/assets/index-C7aw1GYL.js +++ b/static/admin/assets/index-cwfNH6yZ.js @@ -269,4 +269,4 @@ Please change the parent to {(async()=>{try{const f=await x("/admin/vercel/config");if(f.ok){const d=await f.json();v(d),d.project_id&&o(d.project_id),d.team_id&&i(d.team_id)}}catch(f){console.error("Failed to load preconfig:",f)}})()},[]);const w=async()=>{if(!(h!=null&&h.has_token&&!n?"__USE_PRECONFIG__":n)&&!(h!=null&&h.has_token)){e("error","需要 Vercel 访问令牌");return}if(!l){e("error","需要项目 ID");return}c(!0),p(null);try{const f=await x("/admin/vercel/sync",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({vercel_token:n,project_id:l,team_id:s||void 0})}),d=await f.json();f.ok?(p({...d,success:!0}),e("success",d.message)):(p({...d,success:!1}),e("error",d.detail||"同步失败"))}catch{e("error","网络错误")}finally{c(!1)}};return a.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-5xl mx-auto h-[calc(100vh-140px)]",children:[a.jsxs("div",{className:"bg-card border border-border rounded-xl shadow-sm p-6 space-y-6",children:[a.jsxs("div",{className:"border-b border-border pb-6",children:[a.jsxs("h2",{className:"text-xl font-semibold flex items-center gap-2",children:[a.jsx(zd,{className:"w-6 h-6 text-primary"}),"Vercel 部署"]}),a.jsx("p",{className:"text-muted-foreground text-sm mt-1",children:"将当前密钥和账号配置直接同步到 Vercel 环境变量中。"})]}),a.jsxs("div",{className:"space-y-4",children:[a.jsxs("div",{className:"space-y-2",children:[a.jsxs("label",{className:"text-sm font-medium flex items-center justify-between",children:["Vercel 访问令牌",a.jsxs("a",{href:"https://vercel.com/account/tokens",target:"_blank",rel:"noopener noreferrer",className:"text-xs text-primary hover:underline flex items-center gap-1",children:["获取令牌 ",a.jsx(Ja,{className:"w-3 h-3"})]})]}),a.jsxs("div",{className:"relative",children:[a.jsx("input",{type:"password",className:"w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all pr-10",placeholder:h!=null&&h.has_token?"正在使用预配置的令牌":"输入 Vercel 访问令牌",value:n,onChange:N=>r(N.target.value)}),(h==null?void 0:h.has_token)&&!n&&a.jsx("div",{className:"absolute right-3 top-2.5 text-emerald-500",children:a.jsx(Dl,{className:"w-5 h-5"})})]})]}),a.jsxs("div",{className:"space-y-2",children:[a.jsx("label",{className:"text-sm font-medium",children:"项目 ID"}),a.jsx("input",{type:"text",className:"w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all",placeholder:"prj_xxxxxxxxxxxx or Project Name",value:l,onChange:N=>o(N.target.value)}),a.jsx("p",{className:"text-xs text-muted-foreground",children:"可在项目设置 (Project Settings) → 常规 (General) 中找到"})]}),a.jsxs("div",{className:"space-y-2",children:[a.jsxs("label",{className:"text-sm font-medium flex items-center gap-2",children:["团队 ID ",a.jsx("span",{className:"text-xs text-muted-foreground font-normal",children:"(可选)"})]}),a.jsx("input",{type:"text",className:"w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all",placeholder:"team_xxxxxxxxxxxx",value:s,onChange:N=>i(N.target.value)})]})]}),a.jsxs("div",{className:"pt-4",children:[a.jsx("button",{onClick:w,disabled:u,className:"w-full flex items-center justify-center gap-2 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-all font-medium text-sm shadow-sm hover:shadow-md disabled:opacity-50 disabled:shadow-none",children:u?a.jsxs("span",{className:"flex items-center gap-2",children:[a.jsx("span",{className:"w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"}),"正在同步..."]}):a.jsxs("span",{className:"flex items-center gap-2",children:["同步并重新部署 ",a.jsx(Td,{className:"w-4 h-4"})]})}),a.jsx("p",{className:"text-xs text-center text-muted-foreground mt-4",children:"这将触发 Vercel 的重新部署,大约需要 30-60 秒。"})]})]}),a.jsxs("div",{className:"space-y-6",children:[y&&a.jsx("div",{className:`p-6 rounded-xl border ${y.success?"bg-emerald-500/10 border-emerald-500/20":"bg-destructive/10 border-destructive/20"} animate-in fade-in slide-in-from-right-4`,children:a.jsxs("div",{className:"flex items-start gap-4",children:[y.success?a.jsx("div",{className:"p-2 bg-emerald-500 text-white rounded-full shadow-lg shadow-emerald-500/30",children:a.jsx(Dl,{className:"w-6 h-6"})}):a.jsx("div",{className:"p-2 bg-destructive text-white rounded-full shadow-lg shadow-destructive/30",children:a.jsx(ny,{className:"w-6 h-6"})}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("h3",{className:`font-semibold text-lg ${y.success?"text-emerald-500":"text-destructive"}`,children:y.success?"同步成功":"同步失败"}),a.jsx("p",{className:"text-sm opacity-90",children:y.message}),y.deployment_url&&a.jsx("div",{className:"pt-3 mt-3 border-t border-emerald-500/20",children:a.jsxs("a",{href:`https://${y.deployment_url}`,target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-1 text-sm font-medium hover:underline",children:["访问部署地址 ",a.jsx(Ja,{className:"w-3 h-3"})]})})]})]})}),a.jsxs("div",{className:"bg-secondary/20 border border-border rounded-xl p-6",children:[a.jsxs("h3",{className:"font-semibold flex items-center gap-2 mb-4",children:[a.jsx(py,{className:"w-5 h-5 text-primary"}),"工作原理"]}),a.jsxs("ul",{className:"space-y-4",children:[a.jsxs("li",{className:"flex gap-3",children:[a.jsx("span",{className:"shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground",children:"1"}),a.jsx("p",{className:"text-sm text-muted-foreground",children:"当前配置 (密钥和账号) 被导出为 JSON 字符串。"})]}),a.jsxs("li",{className:"flex gap-3",children:[a.jsx("span",{className:"shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground",children:"2"}),a.jsx("p",{className:"text-sm text-muted-foreground",children:"JSON 被编码为 Base64 以确保格式兼容性。"})]}),a.jsxs("li",{className:"flex gap-3",children:[a.jsx("span",{className:"shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground",children:"3"}),a.jsxs("p",{className:"text-sm text-muted-foreground",children:["更新 Vercel 项目中的 ",a.jsx("code",{className:"bg-background px-1 py-0.5 rounded border border-border text-xs",children:"DS2API_CONFIG_JSON"})," 环境变量。"]})]}),a.jsxs("li",{className:"flex gap-3",children:[a.jsx("span",{className:"shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground",children:"4"}),a.jsx("p",{className:"text-sm text-muted-foreground",children:"触发重新部署以应用新的环境变量。"})]})]})]})]})]})}function tg({onLogin:e,onMessage:t}){const[n,r]=g.useState(""),[l,o]=g.useState(!1),[s,i]=g.useState(!0),u=async c=>{if(c.preventDefault(),!!n.trim()){o(!0);try{const y=await fetch("/admin/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({admin_key:n})}),p=await y.json();if(y.ok&&p.success){const h=s?localStorage:sessionStorage;h.setItem("ds2api_token",p.token),h.setItem("ds2api_token_expires",Date.now()+p.expires_in*1e3),e(p.token),p.message&&t("warning",p.message)}else t("error",p.detail||"登录失败")}catch(y){t("error","网络错误: "+y.message)}finally{o(!1)}}};return a.jsx("div",{className:"min-h-screen w-full flex flex-col items-center justify-center p-4 bg-background text-foreground",children:a.jsxs("div",{className:"w-full max-w-[400px] relative z-10 animate-in fade-in zoom-in-95 duration-200",children:[a.jsxs("div",{className:"w-full bg-card border border-border rounded-xl p-8 shadow-sm",children:[a.jsxs("div",{className:"text-center space-y-2 mb-8 animate-in fade-in slide-in-from-top-4 duration-500",children:[a.jsx("div",{className:"inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-2",children:a.jsx(wy,{className:"w-6 h-6"})}),a.jsx("h1",{className:"text-3xl font-bold tracking-tight text-foreground",children:"欢迎回来"}),a.jsx("p",{className:"text-sm text-muted-foreground/80",children:"请输入管理员密钥以继续"})]}),a.jsxs("form",{onSubmit:u,className:"space-y-5 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-150",children:[a.jsxs("div",{className:"space-y-2",children:[a.jsx("label",{className:"text-xs font-semibold text-muted-foreground uppercase tracking-widest ml-1",children:"管理员密钥"}),a.jsxs("div",{className:"relative group",children:[a.jsx("div",{className:"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors",children:a.jsx(hy,{className:"w-4 h-4"})}),a.jsx("input",{type:"password",className:"w-full bg-[#09090b] border border-border rounded-xl pl-10 pr-4 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all placeholder:text-muted-foreground/30 text-foreground",placeholder:"输入您的管理员密钥...",value:n,onChange:c=>r(c.target.value),autoFocus:!0})]})]}),a.jsx("div",{className:"flex items-center justify-between px-1",children:a.jsxs("label",{className:"flex items-center gap-2.5 cursor-pointer group",children:[a.jsxs("div",{className:"relative flex items-center",children:[a.jsx("input",{type:"checkbox",className:"peer sr-only",checked:s,onChange:c=>i(c.target.checked)}),a.jsx("div",{className:"w-4.5 h-4.5 bg-secondary border border-border rounded-md peer-checked:bg-primary peer-checked:border-primary transition-all shadow-sm"}),a.jsx(Il,{className:"absolute w-3 h-3 text-primary-foreground opacity-0 peer-checked:opacity-100 left-0.5 transition-opacity"})]}),a.jsx("span",{className:"text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors",children:"记住登录状态"})]})}),a.jsx("button",{type:"submit",disabled:l,className:"w-full h-12 flex items-center justify-center gap-2 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 transition-all font-semibold text-sm shadow-lg shadow-primary/20 hover:shadow-primary/30 disabled:opacity-50 disabled:shadow-none",children:l?a.jsx("div",{className:"w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin"}):a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("span",{children:"登录"}),a.jsx(Td,{className:"w-4 h-4"})]})})]}),a.jsx("div",{className:"mt-6 pt-6 border-t border-border flex justify-center",children:a.jsxs("div",{className:"flex items-center gap-1.5 text-[10px] text-muted-foreground/60 font-medium tracking-wide uppercase",children:[a.jsx(Dd,{className:"w-3 h-3"}),a.jsx("span",{children:"安全连接"})]})})]}),a.jsx("div",{className:"mt-8 text-center",children:a.jsx("p",{className:"text-[10px] text-muted-foreground/30 font-mono text-center",children:"DS2API 管理员门户"})})]})})}const To=[{id:"accounts",label:"账号管理",icon:Qy,description:"管理 DeepSeek 账号池"},{id:"test",label:"API 测试",icon:Id,description:"测试 API 连接与响应"},{id:"import",label:"批量导入",icon:bd,description:"批量导入账号配置"},{id:"vercel",label:"Vercel 同步",icon:zd,description:"同步配置到 Vercel"}];function ng({token:e,onLogout:t,config:n,fetchConfig:r,showMessage:l,message:o}){var x,w,N,f;const[s,i]=g.useState("accounts"),[u,c]=g.useState(!1),[y,p]=g.useState(!1),h=async(d,m={})=>{const k={...m.headers,Authorization:`Bearer ${e}`},S=await fetch(d,{...m,headers:k});if(S.status===401)throw t(),new Error("认证已过期,请重新登录");return S},v=()=>{switch(s){case"accounts":return a.jsx(Gy,{config:n,onRefresh:r,onMessage:l,authFetch:h});case"test":return a.jsx(Zy,{config:n,onMessage:l,authFetch:h});case"import":return a.jsx(qy,{onRefresh:r,onMessage:l,authFetch:h});case"vercel":return a.jsx(eg,{onMessage:l,authFetch:h});default:return null}};return a.jsxs("div",{className:"flex h-screen bg-background overflow-hidden text-foreground",children:[u&&a.jsx("div",{className:"fixed inset-0 bg-background/80 backdrop-blur-sm z-40 lg:hidden",onClick:()=>c(!1)}),a.jsxs("aside",{className:ee("fixed lg:static inset-y-0 left-0 z-50 w-64 bg-card border-r border-border transition-transform duration-300 ease-in-out lg:transform-none flex flex-col shadow-2xl lg:shadow-none",u?"translate-x-0":"-translate-x-full"),children:[a.jsxs("div",{className:"p-6",children:[a.jsxs("div",{className:"flex items-center gap-2.5 font-bold text-xl text-foreground tracking-tight",children:[a.jsx("div",{className:"w-8 h-8 rounded-lg bg-primary flex items-center justify-center text-primary-foreground shadow-lg shadow-primary/20",children:a.jsx(Ya,{className:"w-5 h-5"})}),a.jsx("span",{children:"DS2API"})]}),a.jsx("p",{className:"text-[10px] text-muted-foreground mt-2 font-semibold tracking-[0.1em] uppercase opacity-60 px-1",children:"在线管理面板"})]}),a.jsx("nav",{className:"flex-1 px-3 space-y-1 overflow-y-auto pt-2",children:To.map(d=>{const m=d.icon,k=s===d.id;return a.jsxs("button",{onClick:()=>{i(d.id),c(!1)},className:ee("w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 group border",k?"bg-secondary text-primary border-border shadow-sm":"text-muted-foreground border-transparent hover:bg-secondary/80 hover:text-foreground"),children:[a.jsx(m,{className:ee("w-4 h-4 transition-colors",k?"text-primary":"text-muted-foreground group-hover:text-foreground")}),a.jsx("span",{className:"flex-1 text-left",children:d.label}),k&&a.jsx("div",{className:"w-1.5 h-1.5 rounded-full bg-primary"})]},d.id)})}),a.jsx("div",{className:"p-4 border-t border-border bg-card",children:a.jsxs("div",{className:"space-y-4",children:[a.jsxs("div",{className:"flex items-center justify-between text-sm px-1",children:[a.jsx("span",{className:"text-muted-foreground font-semibold text-[10px] uppercase tracking-wider",children:"系统状态"}),a.jsxs("span",{className:"flex items-center gap-1.5 text-[10px] font-bold text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20",children:[a.jsx("span",{className:"w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"}),"在线"]})]}),a.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[a.jsxs("div",{className:"bg-background rounded-lg p-3 border border-border shadow-sm",children:[a.jsx("div",{className:"text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70",children:"账号"}),a.jsx("div",{className:"text-lg font-bold text-foreground leading-tight",children:((x=n.accounts)==null?void 0:x.length)||0})]}),a.jsxs("div",{className:"bg-background rounded-lg p-3 border border-border shadow-sm",children:[a.jsx("div",{className:"text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70",children:"密钥"}),a.jsx("div",{className:"text-lg font-bold text-foreground",children:((w=n.keys)==null?void 0:w.length)||0})]})]}),a.jsxs("button",{onClick:t,className:"w-full h-10 flex items-center justify-center gap-2 rounded-lg border border-border text-xs font-medium text-muted-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/20 transition-all",children:[a.jsx(Ny,{className:"w-3.5 h-3.5"}),"退出登录"]})]})})]}),a.jsxs("main",{className:"flex-1 flex flex-col min-w-0 overflow-hidden relative",children:[a.jsxs("header",{className:"lg:hidden h-14 flex items-center justify-between px-4 border-b border-border bg-card",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("div",{className:"w-6 h-6 rounded bg-primary flex items-center justify-center text-primary-foreground text-[10px]",children:a.jsx(Ya,{className:"w-3.5 h-3.5"})}),a.jsx("span",{className:"font-semibold text-sm",children:"DS2API"})]}),a.jsx("button",{onClick:()=>c(!0),className:"p-2 -mr-2 text-muted-foreground hover:text-foreground",children:a.jsx(jy,{className:"w-5 h-5"})})]}),a.jsx("div",{className:"flex-1 overflow-auto bg-background p-4 lg:p-10",children:a.jsxs("div",{className:"max-w-6xl mx-auto space-y-4 lg:space-y-6",children:[a.jsxs("div",{className:"hidden lg:block mb-8",children:[a.jsx("h1",{className:"text-3xl font-bold tracking-tight mb-2",children:(N=To.find(d=>d.id===s))==null?void 0:N.label}),a.jsx("p",{className:"text-muted-foreground",children:(f=To.find(d=>d.id===s))==null?void 0:f.description})]}),o&&a.jsxs("div",{className:ee("p-4 rounded-lg border flex items-center gap-3 animate-in fade-in slide-in-from-top-2",o.type==="error"?"bg-destructive/10 border-destructive/20 text-destructive":"bg-emerald-500/10 border-emerald-500/20 text-emerald-500"),children:[o.type==="error"?a.jsx(Ps,{className:"w-5 h-5"}):a.jsx("div",{className:"w-5 h-5 rounded-full border-2 border-emerald-500 flex items-center justify-center text-[10px]",children:"✓"}),o.text]}),a.jsx("div",{className:"animate-in fade-in duration-500",children:v()})]})})]})]})}function rg(){Ci();const e=ht(),[t,n]=g.useState({keys:[],accounts:[]}),[r,l]=g.useState(!0),[o,s]=g.useState(null),[i,u]=g.useState(null),[c,y]=g.useState(!0),p=!0,h=e.pathname.startsWith("/admin")||p;g.useEffect(()=>{(async()=>{const d=localStorage.getItem("ds2api_token")||sessionStorage.getItem("ds2api_token"),m=parseInt(localStorage.getItem("ds2api_token_expires")||sessionStorage.getItem("ds2api_token_expires")||"0");if(d&&m>Date.now())try{(await fetch("/admin/verify",{headers:{Authorization:`Bearer ${d}`}})).ok?u(d):N()}catch{u(d)}y(!1)})()},[h]);const v=async()=>{if(i)try{l(!0);const f=await fetch("/admin/config",{headers:{Authorization:`Bearer ${i}`}});if(f.ok){const d=await f.json();n(d)}}catch(f){console.error("获取配置失败:",f),x("error",f.message)}finally{l(!1)}};g.useEffect(()=>{i&&v()},[i]);const x=(f,d)=>{s({type:f,text:d}),setTimeout(()=>s(null),5e3)},w=f=>{u(f)},N=()=>{u(null),localStorage.removeItem("ds2api_token"),localStorage.removeItem("ds2api_token_expires"),sessionStorage.removeItem("ds2api_token"),sessionStorage.removeItem("ds2api_token_expires")};return c?a.jsx("div",{className:"min-h-screen flex items-center justify-center bg-background",children:a.jsxs("div",{className:"flex flex-col items-center gap-4",children:[a.jsx("div",{className:"w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"}),a.jsx("p",{className:"text-muted-foreground animate-pulse",children:"正在检查登录状态..."})]})}):a.jsxs(ch,{children:[!p,a.jsx(Cs,{path:"/",element:i?a.jsx(ng,{token:i,onLogout:N,config:t,fetchConfig:v,showMessage:x,message:o}):a.jsxs("div",{className:"min-h-screen flex flex-col bg-background relative overflow-hidden",children:[a.jsxs("div",{className:"absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none z-0",children:[a.jsx("div",{className:"absolute top-[-10%] right-[-10%] w-[50%] h-[50%] bg-primary/5 rounded-full blur-[120px]"}),a.jsx("div",{className:"absolute bottom-[-10%] left-[-10%] w-[50%] h-[50%] bg-accent/5 rounded-full blur-[120px]"})]}),o&&a.jsx("div",{className:ee("fixed top-4 right-4 z-50 px-4 py-3 rounded-lg shadow-lg border animate-in slide-in-from-top-2 fade-in",o.type==="error"?"bg-destructive/10 border-destructive/20 text-destructive":"bg-primary/10 border-primary/20 text-primary"),children:o.text}),a.jsx(tg,{onLogin:w,onMessage:x})]})}),a.jsx(Cs,{path:"*",element:a.jsx(ah,{to:"/",replace:!0})})]})}const lg="/admin";zo.createRoot(document.getElementById("root")).render(a.jsx(ef.StrictMode,{children:a.jsx(Ih,{basename:lg,children:a.jsx(rg,{})})})); +}`,spellCheck:!1})}),i&&a.jsx("div",{className:ee("p-4 border-t",i.imported_keys||i.imported_accounts?"bg-emerald-500/10 border-emerald-500/20":"bg-destructive/10 border-destructive/20"),children:a.jsxs("div",{className:"flex items-start gap-3",children:[i.imported_keys||i.imported_accounts?a.jsx(Il,{className:"w-5 h-5 text-emerald-500 mt-0.5"}):a.jsx(Uy,{className:"w-5 h-5 text-destructive mt-0.5"}),a.jsxs("div",{children:[a.jsx("h4",{className:ee("font-medium",i.imported_keys||i.imported_accounts?"text-emerald-500":"text-destructive"),children:"导入操作已完成"}),a.jsxs("p",{className:"text-sm opacity-80 mt-1",children:["成功导入了 ",i.imported_keys," 个 API 密钥,并更新了 ",i.imported_accounts," 个账号。"]})]})]})})]})]})}function eg({onMessage:e,authFetch:t}){const[n,r]=g.useState(""),[l,o]=g.useState(""),[s,i]=g.useState(""),[u,c]=g.useState(!1),[y,p]=g.useState(null),[h,v]=g.useState(null),x=t||fetch;g.useEffect(()=>{(async()=>{try{const f=await x("/admin/vercel/config");if(f.ok){const d=await f.json();v(d),d.project_id&&o(d.project_id),d.team_id&&i(d.team_id)}}catch(f){console.error("Failed to load preconfig:",f)}})()},[]);const w=async()=>{const N=h!=null&&h.has_token&&!n?"__USE_PRECONFIG__":n;if(!N&&!(h!=null&&h.has_token)){e("error","需要 Vercel 访问令牌");return}if(!l){e("error","需要项目 ID");return}c(!0),p(null);try{const f=await x("/admin/vercel/sync",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({vercel_token:N,project_id:l,team_id:s||void 0})}),d=await f.json();f.ok?(p({...d,success:!0}),e("success",d.message)):(p({...d,success:!1}),e("error",d.detail||"同步失败"))}catch{e("error","网络错误")}finally{c(!1)}};return a.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-5xl mx-auto h-[calc(100vh-140px)]",children:[a.jsxs("div",{className:"bg-card border border-border rounded-xl shadow-sm p-6 space-y-6",children:[a.jsxs("div",{className:"border-b border-border pb-6",children:[a.jsxs("h2",{className:"text-xl font-semibold flex items-center gap-2",children:[a.jsx(zd,{className:"w-6 h-6 text-primary"}),"Vercel 部署"]}),a.jsx("p",{className:"text-muted-foreground text-sm mt-1",children:"将当前密钥和账号配置直接同步到 Vercel 环境变量中。"})]}),a.jsxs("div",{className:"space-y-4",children:[a.jsxs("div",{className:"space-y-2",children:[a.jsxs("label",{className:"text-sm font-medium flex items-center justify-between",children:["Vercel 访问令牌",a.jsxs("a",{href:"https://vercel.com/account/tokens",target:"_blank",rel:"noopener noreferrer",className:"text-xs text-primary hover:underline flex items-center gap-1",children:["获取令牌 ",a.jsx(Ja,{className:"w-3 h-3"})]})]}),a.jsxs("div",{className:"relative",children:[a.jsx("input",{type:"password",className:"w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all pr-10",placeholder:h!=null&&h.has_token?"正在使用预配置的令牌":"输入 Vercel 访问令牌",value:n,onChange:N=>r(N.target.value)}),(h==null?void 0:h.has_token)&&!n&&a.jsx("div",{className:"absolute right-3 top-2.5 text-emerald-500",children:a.jsx(Dl,{className:"w-5 h-5"})})]})]}),a.jsxs("div",{className:"space-y-2",children:[a.jsx("label",{className:"text-sm font-medium",children:"项目 ID"}),a.jsx("input",{type:"text",className:"w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all",placeholder:"prj_xxxxxxxxxxxx or Project Name",value:l,onChange:N=>o(N.target.value)}),a.jsx("p",{className:"text-xs text-muted-foreground",children:"可在项目设置 (Project Settings) → 常规 (General) 中找到"})]}),a.jsxs("div",{className:"space-y-2",children:[a.jsxs("label",{className:"text-sm font-medium flex items-center gap-2",children:["团队 ID ",a.jsx("span",{className:"text-xs text-muted-foreground font-normal",children:"(可选)"})]}),a.jsx("input",{type:"text",className:"w-full h-10 px-3 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-all",placeholder:"team_xxxxxxxxxxxx",value:s,onChange:N=>i(N.target.value)})]})]}),a.jsxs("div",{className:"pt-4",children:[a.jsx("button",{onClick:w,disabled:u,className:"w-full flex items-center justify-center gap-2 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-all font-medium text-sm shadow-sm hover:shadow-md disabled:opacity-50 disabled:shadow-none",children:u?a.jsxs("span",{className:"flex items-center gap-2",children:[a.jsx("span",{className:"w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"}),"正在同步..."]}):a.jsxs("span",{className:"flex items-center gap-2",children:["同步并重新部署 ",a.jsx(Td,{className:"w-4 h-4"})]})}),a.jsx("p",{className:"text-xs text-center text-muted-foreground mt-4",children:"这将触发 Vercel 的重新部署,大约需要 30-60 秒。"})]})]}),a.jsxs("div",{className:"space-y-6",children:[y&&a.jsx("div",{className:`p-6 rounded-xl border ${y.success?"bg-emerald-500/10 border-emerald-500/20":"bg-destructive/10 border-destructive/20"} animate-in fade-in slide-in-from-right-4`,children:a.jsxs("div",{className:"flex items-start gap-4",children:[y.success?a.jsx("div",{className:"p-2 bg-emerald-500 text-white rounded-full shadow-lg shadow-emerald-500/30",children:a.jsx(Dl,{className:"w-6 h-6"})}):a.jsx("div",{className:"p-2 bg-destructive text-white rounded-full shadow-lg shadow-destructive/30",children:a.jsx(ny,{className:"w-6 h-6"})}),a.jsxs("div",{className:"space-y-1",children:[a.jsx("h3",{className:`font-semibold text-lg ${y.success?"text-emerald-500":"text-destructive"}`,children:y.success?"同步成功":"同步失败"}),a.jsx("p",{className:"text-sm opacity-90",children:y.message}),y.deployment_url&&a.jsx("div",{className:"pt-3 mt-3 border-t border-emerald-500/20",children:a.jsxs("a",{href:`https://${y.deployment_url}`,target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center gap-1 text-sm font-medium hover:underline",children:["访问部署地址 ",a.jsx(Ja,{className:"w-3 h-3"})]})})]})]})}),a.jsxs("div",{className:"bg-secondary/20 border border-border rounded-xl p-6",children:[a.jsxs("h3",{className:"font-semibold flex items-center gap-2 mb-4",children:[a.jsx(py,{className:"w-5 h-5 text-primary"}),"工作原理"]}),a.jsxs("ul",{className:"space-y-4",children:[a.jsxs("li",{className:"flex gap-3",children:[a.jsx("span",{className:"shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground",children:"1"}),a.jsx("p",{className:"text-sm text-muted-foreground",children:"当前配置 (密钥和账号) 被导出为 JSON 字符串。"})]}),a.jsxs("li",{className:"flex gap-3",children:[a.jsx("span",{className:"shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground",children:"2"}),a.jsx("p",{className:"text-sm text-muted-foreground",children:"JSON 被编码为 Base64 以确保格式兼容性。"})]}),a.jsxs("li",{className:"flex gap-3",children:[a.jsx("span",{className:"shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground",children:"3"}),a.jsxs("p",{className:"text-sm text-muted-foreground",children:["更新 Vercel 项目中的 ",a.jsx("code",{className:"bg-background px-1 py-0.5 rounded border border-border text-xs",children:"DS2API_CONFIG_JSON"})," 环境变量。"]})]}),a.jsxs("li",{className:"flex gap-3",children:[a.jsx("span",{className:"shrink-0 w-6 h-6 rounded-full bg-background border border-border flex items-center justify-center text-xs font-bold text-muted-foreground",children:"4"}),a.jsx("p",{className:"text-sm text-muted-foreground",children:"触发重新部署以应用新的环境变量。"})]})]})]})]})]})}function tg({onLogin:e,onMessage:t}){const[n,r]=g.useState(""),[l,o]=g.useState(!1),[s,i]=g.useState(!0),u=async c=>{if(c.preventDefault(),!!n.trim()){o(!0);try{const y=await fetch("/admin/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({admin_key:n})}),p=await y.json();if(y.ok&&p.success){const h=s?localStorage:sessionStorage;h.setItem("ds2api_token",p.token),h.setItem("ds2api_token_expires",Date.now()+p.expires_in*1e3),e(p.token),p.message&&t("warning",p.message)}else t("error",p.detail||"登录失败")}catch(y){t("error","网络错误: "+y.message)}finally{o(!1)}}};return a.jsx("div",{className:"min-h-screen w-full flex flex-col items-center justify-center p-4 bg-background text-foreground",children:a.jsxs("div",{className:"w-full max-w-[400px] relative z-10 animate-in fade-in zoom-in-95 duration-200",children:[a.jsxs("div",{className:"w-full bg-card border border-border rounded-xl p-8 shadow-sm",children:[a.jsxs("div",{className:"text-center space-y-2 mb-8 animate-in fade-in slide-in-from-top-4 duration-500",children:[a.jsx("div",{className:"inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-2",children:a.jsx(wy,{className:"w-6 h-6"})}),a.jsx("h1",{className:"text-3xl font-bold tracking-tight text-foreground",children:"欢迎回来"}),a.jsx("p",{className:"text-sm text-muted-foreground/80",children:"请输入管理员密钥以继续"})]}),a.jsxs("form",{onSubmit:u,className:"space-y-5 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-150",children:[a.jsxs("div",{className:"space-y-2",children:[a.jsx("label",{className:"text-xs font-semibold text-muted-foreground uppercase tracking-widest ml-1",children:"管理员密钥"}),a.jsxs("div",{className:"relative group",children:[a.jsx("div",{className:"absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-muted-foreground group-focus-within:text-primary transition-colors",children:a.jsx(hy,{className:"w-4 h-4"})}),a.jsx("input",{type:"password",className:"w-full bg-[#09090b] border border-border rounded-xl pl-10 pr-4 py-3 text-sm focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all placeholder:text-muted-foreground/30 text-foreground",placeholder:"输入您的管理员密钥...",value:n,onChange:c=>r(c.target.value),autoFocus:!0})]})]}),a.jsx("div",{className:"flex items-center justify-between px-1",children:a.jsxs("label",{className:"flex items-center gap-2.5 cursor-pointer group",children:[a.jsxs("div",{className:"relative flex items-center",children:[a.jsx("input",{type:"checkbox",className:"peer sr-only",checked:s,onChange:c=>i(c.target.checked)}),a.jsx("div",{className:"w-4.5 h-4.5 bg-secondary border border-border rounded-md peer-checked:bg-primary peer-checked:border-primary transition-all shadow-sm"}),a.jsx(Il,{className:"absolute w-3 h-3 text-primary-foreground opacity-0 peer-checked:opacity-100 left-0.5 transition-opacity"})]}),a.jsx("span",{className:"text-xs font-medium text-muted-foreground group-hover:text-foreground transition-colors",children:"记住登录状态"})]})}),a.jsx("button",{type:"submit",disabled:l,className:"w-full h-12 flex items-center justify-center gap-2 bg-primary text-primary-foreground rounded-xl hover:bg-primary/90 transition-all font-semibold text-sm shadow-lg shadow-primary/20 hover:shadow-primary/30 disabled:opacity-50 disabled:shadow-none",children:l?a.jsx("div",{className:"w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin"}):a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("span",{children:"登录"}),a.jsx(Td,{className:"w-4 h-4"})]})})]}),a.jsx("div",{className:"mt-6 pt-6 border-t border-border flex justify-center",children:a.jsxs("div",{className:"flex items-center gap-1.5 text-[10px] text-muted-foreground/60 font-medium tracking-wide uppercase",children:[a.jsx(Dd,{className:"w-3 h-3"}),a.jsx("span",{children:"安全连接"})]})})]}),a.jsx("div",{className:"mt-8 text-center",children:a.jsx("p",{className:"text-[10px] text-muted-foreground/30 font-mono text-center",children:"DS2API 管理员门户"})})]})})}const To=[{id:"accounts",label:"账号管理",icon:Qy,description:"管理 DeepSeek 账号池"},{id:"test",label:"API 测试",icon:Id,description:"测试 API 连接与响应"},{id:"import",label:"批量导入",icon:bd,description:"批量导入账号配置"},{id:"vercel",label:"Vercel 同步",icon:zd,description:"同步配置到 Vercel"}];function ng({token:e,onLogout:t,config:n,fetchConfig:r,showMessage:l,message:o}){var x,w,N,f;const[s,i]=g.useState("accounts"),[u,c]=g.useState(!1),[y,p]=g.useState(!1),h=async(d,m={})=>{const k={...m.headers,Authorization:`Bearer ${e}`},S=await fetch(d,{...m,headers:k});if(S.status===401)throw t(),new Error("认证已过期,请重新登录");return S},v=()=>{switch(s){case"accounts":return a.jsx(Gy,{config:n,onRefresh:r,onMessage:l,authFetch:h});case"test":return a.jsx(Zy,{config:n,onMessage:l,authFetch:h});case"import":return a.jsx(qy,{onRefresh:r,onMessage:l,authFetch:h});case"vercel":return a.jsx(eg,{onMessage:l,authFetch:h});default:return null}};return a.jsxs("div",{className:"flex h-screen bg-background overflow-hidden text-foreground",children:[u&&a.jsx("div",{className:"fixed inset-0 bg-background/80 backdrop-blur-sm z-40 lg:hidden",onClick:()=>c(!1)}),a.jsxs("aside",{className:ee("fixed lg:static inset-y-0 left-0 z-50 w-64 bg-card border-r border-border transition-transform duration-300 ease-in-out lg:transform-none flex flex-col shadow-2xl lg:shadow-none",u?"translate-x-0":"-translate-x-full"),children:[a.jsxs("div",{className:"p-6",children:[a.jsxs("div",{className:"flex items-center gap-2.5 font-bold text-xl text-foreground tracking-tight",children:[a.jsx("div",{className:"w-8 h-8 rounded-lg bg-primary flex items-center justify-center text-primary-foreground shadow-lg shadow-primary/20",children:a.jsx(Ya,{className:"w-5 h-5"})}),a.jsx("span",{children:"DS2API"})]}),a.jsx("p",{className:"text-[10px] text-muted-foreground mt-2 font-semibold tracking-[0.1em] uppercase opacity-60 px-1",children:"在线管理面板"})]}),a.jsx("nav",{className:"flex-1 px-3 space-y-1 overflow-y-auto pt-2",children:To.map(d=>{const m=d.icon,k=s===d.id;return a.jsxs("button",{onClick:()=>{i(d.id),c(!1)},className:ee("w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 group border",k?"bg-secondary text-primary border-border shadow-sm":"text-muted-foreground border-transparent hover:bg-secondary/80 hover:text-foreground"),children:[a.jsx(m,{className:ee("w-4 h-4 transition-colors",k?"text-primary":"text-muted-foreground group-hover:text-foreground")}),a.jsx("span",{className:"flex-1 text-left",children:d.label}),k&&a.jsx("div",{className:"w-1.5 h-1.5 rounded-full bg-primary"})]},d.id)})}),a.jsx("div",{className:"p-4 border-t border-border bg-card",children:a.jsxs("div",{className:"space-y-4",children:[a.jsxs("div",{className:"flex items-center justify-between text-sm px-1",children:[a.jsx("span",{className:"text-muted-foreground font-semibold text-[10px] uppercase tracking-wider",children:"系统状态"}),a.jsxs("span",{className:"flex items-center gap-1.5 text-[10px] font-bold text-emerald-500 bg-emerald-500/10 px-2 py-0.5 rounded-full border border-emerald-500/20",children:[a.jsx("span",{className:"w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"}),"在线"]})]}),a.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[a.jsxs("div",{className:"bg-background rounded-lg p-3 border border-border shadow-sm",children:[a.jsx("div",{className:"text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70",children:"账号"}),a.jsx("div",{className:"text-lg font-bold text-foreground leading-tight",children:((x=n.accounts)==null?void 0:x.length)||0})]}),a.jsxs("div",{className:"bg-background rounded-lg p-3 border border-border shadow-sm",children:[a.jsx("div",{className:"text-[9px] text-muted-foreground font-bold uppercase tracking-wider mb-0.5 opacity-70",children:"密钥"}),a.jsx("div",{className:"text-lg font-bold text-foreground",children:((w=n.keys)==null?void 0:w.length)||0})]})]}),a.jsxs("button",{onClick:t,className:"w-full h-10 flex items-center justify-center gap-2 rounded-lg border border-border text-xs font-medium text-muted-foreground hover:bg-destructive/10 hover:text-destructive hover:border-destructive/20 transition-all",children:[a.jsx(Ny,{className:"w-3.5 h-3.5"}),"退出登录"]})]})})]}),a.jsxs("main",{className:"flex-1 flex flex-col min-w-0 overflow-hidden relative",children:[a.jsxs("header",{className:"lg:hidden h-14 flex items-center justify-between px-4 border-b border-border bg-card",children:[a.jsxs("div",{className:"flex items-center gap-2",children:[a.jsx("div",{className:"w-6 h-6 rounded bg-primary flex items-center justify-center text-primary-foreground text-[10px]",children:a.jsx(Ya,{className:"w-3.5 h-3.5"})}),a.jsx("span",{className:"font-semibold text-sm",children:"DS2API"})]}),a.jsx("button",{onClick:()=>c(!0),className:"p-2 -mr-2 text-muted-foreground hover:text-foreground",children:a.jsx(jy,{className:"w-5 h-5"})})]}),a.jsx("div",{className:"flex-1 overflow-auto bg-background p-4 lg:p-10",children:a.jsxs("div",{className:"max-w-6xl mx-auto space-y-4 lg:space-y-6",children:[a.jsxs("div",{className:"hidden lg:block mb-8",children:[a.jsx("h1",{className:"text-3xl font-bold tracking-tight mb-2",children:(N=To.find(d=>d.id===s))==null?void 0:N.label}),a.jsx("p",{className:"text-muted-foreground",children:(f=To.find(d=>d.id===s))==null?void 0:f.description})]}),o&&a.jsxs("div",{className:ee("p-4 rounded-lg border flex items-center gap-3 animate-in fade-in slide-in-from-top-2",o.type==="error"?"bg-destructive/10 border-destructive/20 text-destructive":"bg-emerald-500/10 border-emerald-500/20 text-emerald-500"),children:[o.type==="error"?a.jsx(Ps,{className:"w-5 h-5"}):a.jsx("div",{className:"w-5 h-5 rounded-full border-2 border-emerald-500 flex items-center justify-center text-[10px]",children:"✓"}),o.text]}),a.jsx("div",{className:"animate-in fade-in duration-500",children:v()})]})})]})]})}function rg(){Ci();const e=ht(),[t,n]=g.useState({keys:[],accounts:[]}),[r,l]=g.useState(!0),[o,s]=g.useState(null),[i,u]=g.useState(null),[c,y]=g.useState(!0),p=!0,h=e.pathname.startsWith("/admin")||p;g.useEffect(()=>{(async()=>{const d=localStorage.getItem("ds2api_token")||sessionStorage.getItem("ds2api_token"),m=parseInt(localStorage.getItem("ds2api_token_expires")||sessionStorage.getItem("ds2api_token_expires")||"0");if(d&&m>Date.now())try{(await fetch("/admin/verify",{headers:{Authorization:`Bearer ${d}`}})).ok?u(d):N()}catch{u(d)}y(!1)})()},[h]);const v=async()=>{if(i)try{l(!0);const f=await fetch("/admin/config",{headers:{Authorization:`Bearer ${i}`}});if(f.ok){const d=await f.json();n(d)}}catch(f){console.error("获取配置失败:",f),x("error",f.message)}finally{l(!1)}};g.useEffect(()=>{i&&v()},[i]);const x=(f,d)=>{s({type:f,text:d}),setTimeout(()=>s(null),5e3)},w=f=>{u(f)},N=()=>{u(null),localStorage.removeItem("ds2api_token"),localStorage.removeItem("ds2api_token_expires"),sessionStorage.removeItem("ds2api_token"),sessionStorage.removeItem("ds2api_token_expires")};return c?a.jsx("div",{className:"min-h-screen flex items-center justify-center bg-background",children:a.jsxs("div",{className:"flex flex-col items-center gap-4",children:[a.jsx("div",{className:"w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"}),a.jsx("p",{className:"text-muted-foreground animate-pulse",children:"正在检查登录状态..."})]})}):a.jsxs(ch,{children:[!p,a.jsx(Cs,{path:"/",element:i?a.jsx(ng,{token:i,onLogout:N,config:t,fetchConfig:v,showMessage:x,message:o}):a.jsxs("div",{className:"min-h-screen flex flex-col bg-background relative overflow-hidden",children:[a.jsxs("div",{className:"absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none z-0",children:[a.jsx("div",{className:"absolute top-[-10%] right-[-10%] w-[50%] h-[50%] bg-primary/5 rounded-full blur-[120px]"}),a.jsx("div",{className:"absolute bottom-[-10%] left-[-10%] w-[50%] h-[50%] bg-accent/5 rounded-full blur-[120px]"})]}),o&&a.jsx("div",{className:ee("fixed top-4 right-4 z-50 px-4 py-3 rounded-lg shadow-lg border animate-in slide-in-from-top-2 fade-in",o.type==="error"?"bg-destructive/10 border-destructive/20 text-destructive":"bg-primary/10 border-primary/20 text-primary"),children:o.text}),a.jsx(tg,{onLogin:w,onMessage:x})]})}),a.jsx(Cs,{path:"*",element:a.jsx(ah,{to:"/",replace:!0})})]})}const lg="/admin";zo.createRoot(document.getElementById("root")).render(a.jsx(ef.StrictMode,{children:a.jsx(Ih,{basename:lg,children:a.jsx(rg,{})})})); diff --git a/static/admin/index.html b/static/admin/index.html index 76e4e9d..16993f2 100644 --- a/static/admin/index.html +++ b/static/admin/index.html @@ -32,7 +32,7 @@ - + From f65cf0c51047de5eb9c423deee9e0f4aecad3df5 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Wed, 4 Feb 2026 13:57:34 +0800 Subject: [PATCH 06/19] Build webui during Vercel deployment --- routes/home.py | 11 ++++++++--- vercel.json | 23 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/routes/home.py b/routes/home.py index dad0fb8..7199f33 100644 --- a/routes/home.py +++ b/routes/home.py @@ -290,14 +290,19 @@ async def webui(request: Request, path: str = ""): if path and "." in path: file_path = os.path.join(STATIC_ADMIN_DIR, path) if os.path.isfile(file_path): - return FileResponse(file_path) + cache_control = "public, max-age=31536000, immutable" + if path.startswith("assets/"): + headers = {"Cache-Control": cache_control} + else: + headers = {"Cache-Control": "no-store, must-revalidate"} + return FileResponse(file_path, headers=headers) return HTMLResponse(content="Not Found", status_code=404) # 否则返回 index.html(SPA 路由) index_path = os.path.join(STATIC_ADMIN_DIR, "index.html") if os.path.isfile(index_path): - return FileResponse(index_path) + headers = {"Cache-Control": "no-store, must-revalidate"} + return FileResponse(index_path, headers=headers) return HTMLResponse(content="index.html not found", status_code=404) - diff --git a/vercel.json b/vercel.json index 98e2637..277e079 100644 --- a/vercel.json +++ b/vercel.json @@ -6,10 +6,31 @@ "use": "@vercel/python" } ], + "buildCommand": "bash scripts/build-webui.sh", "routes": [ { "src": "/(.*)", "dest": "app.py" } + ], + "headers": [ + { + "source": "/admin/assets/(.*)", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=31536000, immutable" + } + ] + }, + { + "source": "/admin/(.*)", + "headers": [ + { + "key": "Cache-Control", + "value": "no-store, must-revalidate" + } + ] + } ] -} \ No newline at end of file +} From a917e19e9d249c62bf313c06eb969a02bdfd4be4 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Wed, 4 Feb 2026 14:00:19 +0800 Subject: [PATCH 07/19] Fix Vercel config routing properties --- vercel.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vercel.json b/vercel.json index 277e079..a34b297 100644 --- a/vercel.json +++ b/vercel.json @@ -7,10 +7,10 @@ } ], "buildCommand": "bash scripts/build-webui.sh", - "routes": [ + "rewrites": [ { - "src": "/(.*)", - "dest": "app.py" + "source": "/(.*)", + "destination": "/app.py" } ], "headers": [ From 94238070d8b4a01d83c5b439facd751cc380df8a Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Wed, 4 Feb 2026 14:12:42 +0800 Subject: [PATCH 08/19] Update accounts module description --- routes/admin/accounts.py | 141 +----------------------- webui/src/components/AccountManager.jsx | 81 +------------- 2 files changed, 4 insertions(+), 218 deletions(-) diff --git a/routes/admin/accounts.py b/routes/admin/accounts.py index f53bb55..6603acd 100644 --- a/routes/admin/accounts.py +++ b/routes/admin/accounts.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Admin 账号管理模块 - 账号验证和测试""" +"""Admin 账号管理模块 - 账号测试与导入""" import asyncio import json import base64 @@ -24,145 +24,6 @@ from .auth import verify_admin router = APIRouter() -# ---------------------------------------------------------------------- -# 账号验证 -# ---------------------------------------------------------------------- -async def validate_single_account(account: dict) -> dict: - """验证单个账号的有效性""" - acc_id = get_account_identifier(account) - result = { - "account": acc_id, - "valid": False, - "has_token": bool(account.get("token", "").strip()), - "message": "", - } - - def _is_token_invalid(status_code: int, data: dict) -> bool: - msg = (data.get("msg") or data.get("message") or "").lower() - code = data.get("code") - return status_code in {401, 403} or code in {40001, 40002, 40003} or "token" in msg or "unauthorized" in msg - - def _create_session(token: str) -> dict: - headers = {**BASE_HEADERS, "authorization": f"Bearer {token}"} - try: - session_resp = cffi_requests.post( - DEEPSEEK_CREATE_SESSION_URL, - headers=headers, - json={"agent": "chat"}, - impersonate="safari15_3", - timeout=15, - ) - except Exception as e: - return {"success": False, "message": f"请求异常: {e}", "status_code": 0, "data": {}} - - try: - data = session_resp.json() - except Exception: - data = {} - finally: - session_resp.close() - if session_resp.status_code == 200 and data.get("code") == 0: - return { - "success": True, - "session_id": data.get("data", {}).get("biz_data", {}).get("id"), - "status_code": session_resp.status_code, - "data": data, - } - return { - "success": False, - "message": data.get("msg") or f"HTTP {session_resp.status_code}", - "status_code": session_resp.status_code, - "data": data, - } - - try: - token = account.get("token", "").strip() - if token: - session_result = _create_session(token) - if session_result["success"]: - result["valid"] = True - result["message"] = "Token 有效" - return result - - if _is_token_invalid(session_result["status_code"], session_result["data"]): - token = "" - account["token"] = "" - - if not token: - try: - login_deepseek_via_account(account) - token = account.get("token", "").strip() - session_result = _create_session(token) - if session_result["success"]: - result["valid"] = True - result["has_token"] = True - result["message"] = "登录成功并验证通过" - else: - result["message"] = f"登录成功但验证失败: {session_result['message']}" - except Exception as e: - result["valid"] = False - result["message"] = f"登录失败: {str(e)}" - except Exception as e: - result["message"] = f"验证出错: {str(e)}" - - return result - - -@router.post("/accounts/validate") -async def validate_account(request: Request, _: bool = Depends(verify_admin)): - """验证单个账号""" - data = await request.json() - identifier = data.get("identifier", "").strip() - - if not identifier: - raise HTTPException(status_code=400, detail="需要账号标识(email 或 mobile)") - - account = None - for acc in CONFIG.get("accounts", []): - if acc.get("email") == identifier or acc.get("mobile") == identifier: - account = acc - break - - if not account: - raise HTTPException(status_code=404, detail="账号不存在") - - result = await validate_single_account(account) - - if result["valid"] and result["has_token"]: - save_config(CONFIG) - - return JSONResponse(content=result) - - -@router.post("/accounts/validate-all") -async def validate_all_accounts(_: bool = Depends(verify_admin)): - """批量验证所有账号""" - accounts = CONFIG.get("accounts", []) - if not accounts: - return JSONResponse(content={ - "total": 0, "valid": 0, "invalid": 0, "results": [], - }) - - results = [] - valid_count = 0 - - for acc in accounts: - result = await validate_single_account(acc) - results.append(result) - if result["valid"]: - valid_count += 1 - await asyncio.sleep(0.5) - - save_config(CONFIG) - - return JSONResponse(content={ - "total": len(accounts), - "valid": valid_count, - "invalid": len(accounts) - valid_count, - "results": results, - }) - - # ---------------------------------------------------------------------- # 账号 API 测试 # ---------------------------------------------------------------------- diff --git a/webui/src/components/AccountManager.jsx b/webui/src/components/AccountManager.jsx index 66658a0..dd0d48d 100644 --- a/webui/src/components/AccountManager.jsx +++ b/webui/src/components/AccountManager.jsx @@ -2,12 +2,8 @@ import { useState, useEffect } from 'react' import { Plus, Trash2, - RefreshCw, CheckCircle2, - AlertCircle, - Search, Play, - MoreHorizontal, X, Server, ShieldCheck, @@ -23,8 +19,6 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch const [copiedKey, setCopiedKey] = useState(null) const [newAccount, setNewAccount] = useState({ email: '', mobile: '', password: '' }) const [loading, setLoading] = useState(false) - const [validating, setValidating] = useState({}) - const [validatingAll, setValidatingAll] = useState(false) const [testing, setTesting] = useState({}) const [testingAll, setTestingAll] = useState(false) const [batchProgress, setBatchProgress] = useState({ current: 0, total: 0, results: [] }) @@ -133,60 +127,6 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch } } - const validateAccount = async (identifier) => { - setValidating(prev => ({ ...prev, [identifier]: true })) - try { - const res = await apiFetch('/admin/accounts/validate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ identifier }), - }) - const data = await res.json() - onMessage(data.valid ? 'success' : 'error', `${identifier}: ${data.message}`) - onRefresh() - } catch (e) { - onMessage('error', 'Validation failed: ' + e.message) - } finally { - setValidating(prev => ({ ...prev, [identifier]: false })) - } - } - - const validateAllAccounts = async () => { - if (!confirm('校验所有账号?这可能需要一些时间。')) return - const accounts = config.accounts || [] - if (accounts.length === 0) return - - setValidatingAll(true) - setBatchProgress({ current: 0, total: accounts.length, results: [] }) - - let validCount = 0 - const results = [] - - for (let i = 0; i < accounts.length; i++) { - const acc = accounts[i] - const id = acc.email || acc.mobile - - try { - const res = await apiFetch('/admin/accounts/validate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ identifier: id }), - }) - const data = await res.json() - results.push({ id, success: data.valid, message: data.message }) - if (data.valid) validCount++ - } catch (e) { - results.push({ id, success: false, message: e.message }) - } - - setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] }) - } - - onMessage('success', `Completed: ${validCount}/${accounts.length} valid`) - onRefresh() - setValidatingAll(false) - } - const testAccount = async (identifier) => { setTesting(prev => ({ ...prev, [identifier]: true })) try { @@ -347,20 +287,12 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
-
{/* Batch Progress */} - {(testingAll || validatingAll) && batchProgress.total > 0 && ( + {testingAll && batchProgress.total > 0 && (
- {testingAll ? '正在测试所有账号...' : '正在校验所有账号...'} + 正在测试所有账号... {batchProgress.current} / {batchProgress.total}
@@ -430,13 +362,6 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch > {testing[id] ? '正在测试...' : '测试'} - -
{/* Batch Progress */} - {(testingAll || validatingAll) && batchProgress.total > 0 && ( + {testingAll && batchProgress.total > 0 && (
- {testingAll ? '正在测试所有账号...' : '正在校验所有账号...'} + 正在测试所有账号... {batchProgress.current} / {batchProgress.total}
@@ -430,13 +362,6 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch > {testing[id] ? '正在测试...' : '测试'} - +
{/* Batch Progress */} - {testingAll && batchProgress.total > 0 && ( + {(testingAll || validatingAll) && batchProgress.total > 0 && (
- 正在测试所有账号... + {testingAll ? '正在测试所有账号...' : '正在校验所有账号...'} {batchProgress.current} / {batchProgress.total}
@@ -362,6 +430,13 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch > {testing[id] ? '正在测试...' : '测试'} +
DS2API
-

在线管理面板

+
+

{t('sidebar.onlineAdminConsole')}

+ +
DS2API - +
+ + +

- {NAV_ITEMS.find(n => n.id === activeTab)?.label} + {navItems.find(n => n.id === activeTab)?.label}

- {NAV_ITEMS.find(n => n.id === activeTab)?.description} + {navItems.find(n => n.id === activeTab)?.description}

@@ -195,6 +204,7 @@ function Dashboard({ token, onLogout, config, fetchConfig, showMessage, message } export default function App() { + const { t } = useI18n() const navigate = useNavigate() const location = useLocation() const [config, setConfig] = useState({ keys: [], accounts: [] }) @@ -207,7 +217,7 @@ export default function App() { const isAdminRoute = location.pathname.startsWith('/admin') || isProduction useEffect(() => { - // 只在 admin 路由时检查登录状态 + // Only check auth status on admin routes. if (!isAdminRoute) { setAuthChecking(false) return @@ -248,8 +258,8 @@ export default function App() { setConfig(data) } } catch (e) { - console.error('获取配置失败:', e) - showMessage('error', e.message) + console.error('Failed to fetch config:', e) + showMessage('error', t('errors.fetchConfig', { error: e.message })) } finally { setLoading(false) } @@ -278,13 +288,13 @@ export default function App() { sessionStorage.removeItem('ds2api_token_expires') } - // 在 admin 路由时,等待认证检查完成 + // Wait for auth checks on admin routes. if (isAdminRoute && authChecking) { return (
-

正在检查登录状态...

+

{t('auth.checking')}

) diff --git a/webui/src/components/AccountManager.jsx b/webui/src/components/AccountManager.jsx index dd0d48d..25aa8c1 100644 --- a/webui/src/components/AccountManager.jsx +++ b/webui/src/components/AccountManager.jsx @@ -11,8 +11,10 @@ import { Check } from 'lucide-react' import clsx from 'clsx' +import { useI18n } from '../i18n' export default function AccountManager({ config, onRefresh, onMessage, authFetch }) { + const { t } = useI18n() const [showAddKey, setShowAddKey] = useState(false) const [showAddAccount, setShowAddAccount] = useState(false) const [newKey, setNewKey] = useState('') @@ -54,39 +56,39 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch body: JSON.stringify({ key: newKey.trim() }), }) if (res.ok) { - onMessage('success', 'API 密钥添加成功') + onMessage('success', t('accountManager.addKeySuccess')) setNewKey('') setShowAddKey(false) onRefresh() } else { const data = await res.json() - onMessage('error', data.detail || 'Failed to add') + onMessage('error', data.detail || t('messages.failedToAdd')) } } catch (e) { - onMessage('error', '网络错误') + onMessage('error', t('messages.networkError')) } finally { setLoading(false) } } const deleteKey = async (key) => { - if (!confirm('确定要删除此 API 密钥吗?')) return + if (!confirm(t('accountManager.deleteKeyConfirm'))) return try { const res = await apiFetch(`/admin/keys/${encodeURIComponent(key)}`, { method: 'DELETE' }) if (res.ok) { - onMessage('success', 'Deleted successfully') + onMessage('success', t('messages.deleted')) onRefresh() } else { - onMessage('error', 'Delete failed') + onMessage('error', t('messages.deleteFailed')) } } catch (e) { - onMessage('error', 'Network error') + onMessage('error', t('messages.networkError')) } } const addAccount = async () => { if (!newAccount.password || (!newAccount.email && !newAccount.mobile)) { - onMessage('error', 'Password and Email/Mobile are required') + onMessage('error', t('accountManager.requiredFields')) return } setLoading(true) @@ -97,33 +99,33 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch body: JSON.stringify(newAccount), }) if (res.ok) { - onMessage('success', '账号添加成功') + onMessage('success', t('accountManager.addAccountSuccess')) setNewAccount({ email: '', mobile: '', password: '' }) setShowAddAccount(false) onRefresh() } else { const data = await res.json() - onMessage('error', data.detail || 'Failed to add') + onMessage('error', data.detail || t('messages.failedToAdd')) } } catch (e) { - onMessage('error', '网络错误') + onMessage('error', t('messages.networkError')) } finally { setLoading(false) } } const deleteAccount = async (id) => { - if (!confirm('确定要删除此账号吗?')) return + if (!confirm(t('accountManager.deleteAccountConfirm'))) return try { const res = await apiFetch(`/admin/accounts/${encodeURIComponent(id)}`, { method: 'DELETE' }) if (res.ok) { - onMessage('success', 'Deleted successfully') + onMessage('success', t('messages.deleted')) onRefresh() } else { - onMessage('error', 'Delete failed') + onMessage('error', t('messages.deleteFailed')) } } catch (e) { - onMessage('error', 'Network error') + onMessage('error', t('messages.networkError')) } } @@ -136,17 +138,20 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch body: JSON.stringify({ identifier }), }) const data = await res.json() - onMessage(data.success ? 'success' : 'error', `${identifier}: ${data.success ? `Success (${data.response_time}ms)` : data.message}`) + const statusMessage = data.success + ? t('apiTester.testSuccess', { account: identifier, time: data.response_time }) + : `${identifier}: ${data.message}` + onMessage(data.success ? 'success' : 'error', statusMessage) onRefresh() } catch (e) { - onMessage('error', 'Test failed: ' + e.message) + onMessage('error', t('accountManager.testFailed', { error: e.message })) } finally { setTesting(prev => ({ ...prev, [identifier]: false })) } } const testAllAccounts = async () => { - if (!confirm('测试所有账号的 API 连通性?')) return + if (!confirm(t('accountManager.testAllConfirm'))) return const accounts = config.accounts || [] if (accounts.length === 0) return @@ -176,7 +181,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch setBatchProgress({ current: i + 1, total: accounts.length, results: [...results] }) } - onMessage('success', `Completed: ${successCount}/${accounts.length} available`) + onMessage('success', t('accountManager.testAllCompleted', { success: successCount, total: accounts.length })) onRefresh() setTestingAll(false) } @@ -191,30 +196,30 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
-

可用

+

{t('accountManager.available')}

{queueStatus.available} - 个账号 + {t('accountManager.accountsUnit')}
-

正在使用

+

{t('accountManager.inUse')}

{queueStatus.in_use} - 线程 + {t('accountManager.threadsUnit')}
-

账号池总数

+

{t('accountManager.totalPool')}

{queueStatus.total} - 个账号 + {t('accountManager.accountsUnit')}
@@ -225,15 +230,15 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
-

API 密钥

-

管理 API 访问密钥池

+

{t('accountManager.apiKeysTitle')}

+

{t('accountManager.apiKeysDesc')}

@@ -246,7 +251,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch {key.slice(0, 16)}****
{copiedKey === key && ( - 已复制 + {t('accountManager.copied')} )}
@@ -257,14 +262,14 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch setTimeout(() => setCopiedKey(null), 2000) }} className="p-2 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-md transition-colors opacity-0 group-hover:opacity-100" - title="复制密钥" + title={t('accountManager.copyKeyTitle')} > {copiedKey === key ? : } @@ -272,7 +277,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
)) ) : ( -
未找到 API 密钥
+
{t('accountManager.noApiKeys')}
)} @@ -281,8 +286,8 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
-

DeepSeek 账号

-

管理 DeepSeek 账号池

+

{t('accountManager.accountsTitle')}

+

{t('accountManager.accountsDesc')}

@@ -307,7 +312,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch {testingAll && batchProgress.total > 0 && (
- 正在测试所有账号... + {t('accountManager.testingAllAccounts')} {batchProgress.current} / {batchProgress.total}
@@ -345,7 +350,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
{id}
- {acc.has_token ? '已建立会话' : '需重新登录'} + {acc.has_token ? t('accountManager.sessionActive') : t('accountManager.reauthRequired')} {acc.token_preview && ( {acc.token_preview} @@ -360,7 +365,7 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch disabled={testing[id]} className="px-2 lg:px-3 py-1 lg:py-1.5 text-[10px] lg:text-xs font-medium border border-border rounded-md hover:bg-secondary transition-colors disabled:opacity-50" > - {testing[id] ? '正在测试...' : '测试'} + {testing[id] ? t('actions.testing') : t('actions.test')}
@@ -384,19 +389,19 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
-

添加 API 密钥

+

{t('accountManager.modalAddKeyTitle')}

- +
setNewKey(e.target.value)} autoFocus @@ -406,15 +411,15 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch onClick={() => setNewKey('sk-' + crypto.randomUUID().replace(/-/g, ''))} className="px-3 py-2 bg-secondary text-secondary-foreground rounded-lg hover:bg-secondary/80 transition-colors text-sm font-medium border border-border whitespace-nowrap" > - 生成 + {t('accountManager.generate')}
-

点击「生成」自动创建随机密钥

+

{t('accountManager.generateHint')}

- +
@@ -428,14 +433,14 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
-

添加 DeepSeek 账号

+

{t('accountManager.modalAddAccountTitle')}

- +
- +
- + setNewAccount({ ...newAccount, password: e.target.value })} />
- +
diff --git a/webui/src/components/ApiTester.jsx b/webui/src/components/ApiTester.jsx index ba7ced7..6b413d2 100644 --- a/webui/src/components/ApiTester.jsx +++ b/webui/src/components/ApiTester.jsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' import { Send, Square, @@ -17,17 +17,13 @@ import { Zap } from 'lucide-react' import clsx from 'clsx' - -const MODELS = [ - { id: "deepseek-chat", name: "deepseek-chat", icon: MessageSquare, desc: "非思考模型", color: "text-amber-500" }, - { id: "deepseek-reasoner", name: "deepseek-reasoner", icon: Cpu, desc: "思考模型", color: "text-amber-600" }, - { id: "deepseek-chat-search", name: "deepseek-chat-search", icon: SearchIcon, desc: "非思考模型 (带搜索)", color: "text-cyan-500" }, - { id: "deepseek-reasoner-search", name: "deepseek-reasoner-search", icon: SearchIcon, desc: "思考模型 (带搜索)", color: "text-cyan-600" }, -]; +import { useI18n } from '../i18n' export default function ApiTester({ config, onMessage, authFetch }) { + const { t } = useI18n() const [model, setModel] = useState('deepseek-chat') - const [message, setMessage] = useState('Hello, please introduce yourself in one sentence.') + const defaultMessage = t('apiTester.defaultMessage') + const [message, setMessage] = useState(defaultMessage) const [apiKey, setApiKey] = useState('') const [selectedAccount, setSelectedAccount] = useState('') const [response, setResponse] = useState(null) @@ -36,12 +32,19 @@ export default function ApiTester({ config, onMessage, authFetch }) { const [streamingThinking, setStreamingThinking] = useState('') const [isStreaming, setIsStreaming] = useState(false) const abortControllerRef = useRef(null) + const defaultMessageRef = useRef(defaultMessage) const [sidebarOpen, setSidebarOpen] = useState(false) const [configExpanded, setConfigExpanded] = useState(false) const apiFetch = authFetch || fetch const accounts = config.accounts || [] + const models = [ + { id: "deepseek-chat", name: "deepseek-chat", icon: MessageSquare, desc: t('apiTester.models.chat'), color: "text-amber-500" }, + { id: "deepseek-reasoner", name: "deepseek-reasoner", icon: Cpu, desc: t('apiTester.models.reasoner'), color: "text-amber-600" }, + { id: "deepseek-chat-search", name: "deepseek-chat-search", icon: SearchIcon, desc: t('apiTester.models.chatSearch'), color: "text-cyan-500" }, + { id: "deepseek-reasoner-search", name: "deepseek-reasoner-search", icon: SearchIcon, desc: t('apiTester.models.reasonerSearch'), color: "text-cyan-600" }, + ] const stopGeneration = () => { if (abortControllerRef.current) { @@ -66,7 +69,7 @@ export default function ApiTester({ config, onMessage, authFetch }) { try { const key = apiKey || (config.keys?.[0] || '') if (!key) { - onMessage('error', '请提供 API 密钥') + onMessage('error', t('apiTester.missingApiKey')) setLoading(false) setIsStreaming(false) return @@ -88,8 +91,8 @@ export default function ApiTester({ config, onMessage, authFetch }) { if (!res.ok) { const data = await res.json() - setResponse({ success: false, error: data.error?.message || '请求失败' }) - onMessage('error', data.error?.message || '请求失败') + setResponse({ success: false, error: data.error?.message || t('apiTester.requestFailed') }) + onMessage('error', data.error?.message || t('apiTester.requestFailed')) setLoading(false) setIsStreaming(false) return @@ -138,9 +141,9 @@ export default function ApiTester({ config, onMessage, authFetch }) { } } catch (e) { if (e.name === 'AbortError') { - onMessage('info', '已停止生成') + onMessage('info', t('messages.generationStopped')) } else { - onMessage('error', '网络错误: ' + e.message) + onMessage('error', t('apiTester.networkError', { error: e.message })) setResponse({ error: e.message, success: false }) } } finally { @@ -172,12 +175,12 @@ export default function ApiTester({ config, onMessage, authFetch }) { account: selectedAccount, }) if (data.success) { - onMessage('success', `${selectedAccount}: 测试成功 (${data.response_time}ms)`) + onMessage('success', t('apiTester.testSuccess', { account: selectedAccount, time: data.response_time })) } else { onMessage('error', `${selectedAccount}: ${data.message}`) } } catch (e) { - onMessage('error', '网络错误: ' + e.message) + onMessage('error', t('apiTester.networkError', { error: e.message })) setResponse({ error: e.message }) } finally { setLoading(false) @@ -201,12 +204,12 @@ export default function ApiTester({ config, onMessage, authFetch }) { onClick={() => setConfigExpanded(!configExpanded)} className="lg:hidden flex items-center justify-between p-4 w-full bg-muted/20 hover:bg-muted/30 transition-colors" > -
-
- +
+
+ +
+ {t('apiTester.config')}
- 配置 -
@@ -217,9 +220,9 @@ export default function ApiTester({ config, onMessage, authFetch }) { !configExpanded && "hidden lg:block" )}>
- +
- {MODELS.map(m => { + {models.map(m => { const Icon = m.icon return (