diff --git a/.github/workflows/build-webui.yml b/.github/workflows/build-webui.yml new file mode 100644 index 0000000..e9fdf45 --- /dev/null +++ b/.github/workflows/build-webui.yml @@ -0,0 +1,76 @@ +# 自动构建 WebUI 并提交构建产物 +# 触发条件:webui 目录下的文件变更 + +name: Build WebUI + +on: + push: + branches: + - main + paths: + - 'webui/**' + - '.github/workflows/build-webui.yml' + pull_request: + branches: + - main + paths: + - 'webui/**' + # 允许手动触发 + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + # 只在主仓库运行,避免 fork 仓库运行 + if: github.repository == 'CJackHwang/ds2api' + + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: webui/package-lock.json + + - name: Install dependencies + working-directory: webui + run: npm ci + + - name: Build WebUI + working-directory: webui + run: npm run build + + - name: Check for changes + id: check_changes + run: | + git add static/admin + if git diff --staged --quiet; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.check_changes.outputs.changed == 'true' && github.event_name == 'push' + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git commit -m "chore: auto-build WebUI [skip ci]" + git push + + - name: Upload build artifacts (for PR review) + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: webui-build + path: static/admin + retention-days: 7 diff --git a/DEPLOY.md b/DEPLOY.md index 1da68d5..b1c47d9 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -131,8 +131,8 @@ WebUI 开发服务器会启动在 `http://localhost:5173`,并自动代理 API WebUI 构建产物位于 `static/admin/` 目录。 **自动构建(推荐)**: -- 当前由 Vercel 在部署时执行 WebUI 构建(见 `vercel.json` 的 `buildCommand`) -- GitHub Actions 的 WebUI 自动构建流程已关闭 +- 当 `webui/` 目录下的文件变更并推送到 `main` 分支时,GitHub Actions 会自动构建并提交产物 +- PR 合并时会自动触发构建 **手动构建**: ```bash @@ -145,7 +145,7 @@ npm install npm run build ``` -> **贡献者注意**:修改 WebUI 后无需手动构建,Vercel 部署会自动构建。 +> **贡献者注意**:修改 WebUI 后无需手动构建,CI 会自动处理。 --- diff --git a/routes/admin/accounts.py b/routes/admin/accounts.py index 6603acd..f53bb55 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,6 +24,145 @@ 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/routes/home.py b/routes/home.py index 7199f33..dad0fb8 100644 --- a/routes/home.py +++ b/routes/home.py @@ -290,19 +290,14 @@ 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): - 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 FileResponse(file_path) 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): - headers = {"Cache-Control": "no-store, must-revalidate"} - return FileResponse(index_path, headers=headers) + return FileResponse(index_path) return HTMLResponse(content="index.html not found", status_code=404) + diff --git a/vercel.json b/vercel.json index a34b297..98e2637 100644 --- a/vercel.json +++ b/vercel.json @@ -6,31 +6,10 @@ "use": "@vercel/python" } ], - "buildCommand": "bash scripts/build-webui.sh", - "rewrites": [ + "routes": [ { - "source": "/(.*)", - "destination": "/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" - } - ] + "src": "/(.*)", + "dest": "app.py" } ] -} +} \ No newline at end of file diff --git a/webui/src/components/AccountManager.jsx b/webui/src/components/AccountManager.jsx index dd0d48d..66658a0 100644 --- a/webui/src/components/AccountManager.jsx +++ b/webui/src/components/AccountManager.jsx @@ -2,8 +2,12 @@ import { useState, useEffect } from 'react' import { Plus, Trash2, + RefreshCw, CheckCircle2, + AlertCircle, + Search, Play, + MoreHorizontal, X, Server, ShieldCheck, @@ -19,6 +23,8 @@ 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: [] }) @@ -127,6 +133,60 @@ 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 { @@ -287,12 +347,20 @@ export default function AccountManager({ config, onRefresh, onMessage, authFetch
+
{/* 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] ? '正在测试...' : '测试'} +