Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/butteryiyi_talk.json
T

1 line
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "butteryiyi/talk", "files": {"app.py": {"a57df361159da4d2bd891669839bed7213702e99": {"t": 1785674067.2076998, "n": 14411, "h": "1f83599701dba475", "c": "# app.py\r\n\"\"\"\r\n主服务器:FastAPI + WebSocket 代理\r\n外部请求通过 HTTP/WebSocket 进入,由内部浏览器通过 DOM 交互执行。\r\n\"\"\"\r\n\r\nimport os\r\nimport sys\r\nimport json\r\nimport asyncio\r\nimport time\r\nfrom datetime import datetime\r\nfrom contextlib import asynccontextmanager\r\nfrom browser_manager import BrowserManager, HEARTBEAT_MARKER\r\n\r\nfrom fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request\r\nfrom fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse\r\nfrom fastapi.security import HTTPBearer\r\nimport uvicorn\r\n\r\nfrom browser_manager import BrowserManager, HEARTBEAT_MARKER\r\nfrom keepalive import KeepaliveService\r\n\r\n# ============================================================\r\n# 全局实例 & 配置\r\n# ============================================================\r\nbrowser_mgr: BrowserManager = None\r\nkeepalive_svc: KeepaliveService = None\r\n\r\nAPI_SECRET_KEY = os.getenv(\"API_SECRET_KEY\", \"123456\")\r\nsecurity = HTTPBearer(auto_error=False)\r\n\r\n\r\ndef verify_api_key(request: Request):\r\n auth_header = request.headers.get(\"authorization\", \"\")\r\n if auth_header.startswith(\"Bearer \"):\r\n token = auth_header[7:].strip()\r\n if token == API_SECRET_KEY:\r\n return True\r\n\r\n x_api_key = request.headers.get(\"x-api-key\", \"\").strip()\r\n if x_api_key == API_SECRET_KEY:\r\n return True\r\n\r\n api_key_param = request.query_params.get(\"api_key\", \"\").strip()\r\n if api_key_param == API_SECRET_KEY:\r\n return True\r\n\r\n raise HTTPException(\r\n status_code=401,\r\n detail={\r\n \"error\": {\r\n \"message\": \"Invalid API key.\",\r\n \"type\": \"invalid_request_error\",\r\n \"code\": \"invalid_api_key\",\r\n }\r\n },\r\n )\r\n\r\n\r\nasync def ensure_browser_ready():\r\n if browser_mgr is None:\r\n raise HTTPException(status_code=503, detail=\"服务正在启动中,请稍后重试\")\r\n\r\n if not browser_mgr.is_ready:\r\n print(\" ⏳ 请求到达,等待浏览器初始化完成...\")\r\n ok = await browser_mgr.wait_until_ready(timeout=180)\r\n if not ok:\r\n raise HTTPException(status_code=503, detail=\"浏览器初始化超时,请稍后重试\")\r\n\r\n if not await browser_mgr.is_alive():\r\n raise HTTPException(status_code=503, detail=\"浏览器会话已断开\")\r\n\r\n # ═══ 请求到达时检查登录状态 ═══\r\n if not browser_mgr.logged_in:\r\n print(\" ⚠️ 请求到达但未登录,尝试重新登录...\")\r\n success = await browser_mgr.re_login()\r\n if not success:\r\n raise HTTPException(\r\n status_code=503,\r\n detail=\"登录状态失效且重新登录失败,请更新 Cookie\"\r\n )\r\n\r\n # 每 20 个请求做一次深度检查\r\n if browser_mgr.total_requests % 20 == 0:\r\n is_ok = await browser_mgr.check_login_status()\r\n if not is_ok:\r\n print(\" ⚠️ 抽查发现登录失效,尝试重新登录...\")\r\n\r\n\r\n@asynccontextmanager\r\nasync def lifespan(app: FastAPI):\r\n global browser_mgr, keepalive_svc\r\n\r\n print(f\"\\n{'='*60}\")\r\n print(f\" 应用启动 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\r\n print(f\" 模式: DOM 交互(无需手动 PoW)\")\r\n print(f\" API_SECRET_KEY: {API_SECRET_KEY[:3]}{'*' * (len(API_SECRET_KEY) - 3)}\")\r\n print(f\"{'='*60}\\n\")\r\n\r\n browser_mgr = BrowserManager()\r\n keepalive_svc = KeepaliveService(browser_mgr)\r\n\r\n asyncio.create_task(initialize_background())\r\n\r\n print(\"🚀 服务已就绪(浏览器后台初始化中),等待请求...\\n\")\r\n yield\r\n\r\n print(\"\\n⏹️ 正在关闭服务...\")\r\n if keepalive_svc:\r\n await keepalive_svc.stop()\r\n if browser_mgr:\r\n await browser_mgr.shutdown()\r\n print(\"✅ 服务已安全关闭。\")\r\n\r\n\r\nasync def initialize_background():\r\n global browser_mgr, keepalive_svc\r\n print(\"⏳ 后台任务:开始初始化浏览器...\")\r\n try:\r\n await browser_mgr.initialize()\r\n print(\"✅ 后台任务:浏览器初始化完成。\")\r\n\r\n if keepalive_svc and not keepalive_svc.is_running:\r\n await keepalive_svc.start()\r\n except Exception as e:\r\n print(f\"❌ 后台任务:浏览器初始化失败: {e}\")\r\n import traceback\r\n traceback.print_exc()\r\n\r\n\r\napp = FastAPI(title=\"DeepSeek Proxy\", lifespan=lifespan)\r\n\r\n\r\n# ============================================================\r\n# 健康检查 & 状态页\r\n# ============================================================\r\n@app.get(\"/\")\r\nasync def index():\r\n status = await browser_mgr.get_status() if browser_mgr else {\"status\": \"initializing\"}\r\n uptime = status.get(\"uptime_seconds\", 0)\r\n hours, remainder = divmod(int(uptime), 3600)\r\n minutes, seconds = divmod(remainder, 60)\r\n\r\n ready = status.get(\"ready\", False)\r\n alive = status.get(\"browser_alive\", False)\r\n\r\n if ready and alive:\r\n status_text = \"运行中\"\r\n dot_color = \"#3fb950\"\r\n elif not ready:\r\n status_text = \"初始化中...\"\r\n dot_color = \"#f0ad4e\"\r\n else:\r\n status_text = \"离线\"\r\n dot_color = \"#f85149\"\r\n\r\n html = f\"\"\"\r\n <!DOCTYPE html>\r\n <html>\r\n <head>\r\n <title>DeepSeek Proxy</title>\r\n <meta http-equiv=\"refresh\" content=\"10\">\r\n <style>\r\n body {{ font-family: 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9;\r\n display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }}\r\n .card {{ background: #161b22; border: 1px solid #30363d; border-radius: 12px;\r\n padding: 40px; max-width: 500px; width: 90%; }}\r\n h1 {{ color: #58a6ff; margin-top: 0; }}\r\n .status {{ display: flex; align-items: center; gap: 10px; margin: 20px 0; }}\r\n .dot {{ width: 12px; height: 12px; border-radius: 50%; background: {dot_color}; }}\r\n .info {{ background: #21262d; padding: 15px; border-radius: 8px; margin: 10px 0;\r\n font-family: monospace; font-size: 14px; }}\r\n .label {{ color: #8b949e; }}\r\n </style>\r\n </head>\r\n <body>\r\n <div class=\"card\">\r\n <h1>🤖 DeepSeek Proxy</h1>\r\n <div class=\"status\">\r\n <div class=\"dot\"></div>\r\n <span>{status_text}</span>\r\n </div>\r\n <div class=\"info\">\r\n <div><span class=\"label\">运行时间:</span>{hours}h {minutes}m {seconds}s</div>\r\n <div><span class=\"label\">就绪状态:</span>{\"✅ 就绪\" if ready else \"⏳ 初始化中\"}</div>\r\n <div><span class=\"label\">登录状态:</span>{\"✅ 已登录\" if status.get(\"logged_in\") else \"❌ 未登录\"}</div>\r\n <div><span class=\"label\">引擎:</span>{status.get(\"engine\", \"N/A\")}</div>\r\n <div><span class=\"label\">模式:</span>{status.get(\"mode\", \"N/A\")}</div>\r\n <div><span class=\"label\">心跳次数:</span>{status.get(\"heartbeat_count\", 0)}</div>\r\n <div><span class=\"label\">处理请求:</span>{status.get(\"requests_handled\", 0)}</div>\r\n </div>\r\n <p style=\"color: #8b949e; font-size: 12px;\">\r\n POST /v1/chat/completions 发送聊天请求(需要 API Key)<br>\r\n 鉴权方式:Authorization: Bearer &lt;your-api-key&gt;\r\n </p>\r\n </div>\r\n </body>\r\n </html>\r\n \"\"\"\r\n return HTMLResponse(content=html)\r\n\r\n\r\n@app.get(\"/health\")\r\nasync def health():\r\n if browser_mgr and browser_mgr.is_ready:\r\n return {\"status\": \"ok\", \"ready\": True}\r\n return {\"status\": \"initializing\", \"ready\": False}\r\n\r\n\r\n@app.get(\"/status\")\r\nasync def status():\r\n if browser_mgr:\r\n return await browser_mgr.get_status()\r\n return {\"status\": \"initializing\"}\r\n\r\n\r\n@app.get(\"/screenshot\")\r\nasync def screenshot():\r\n if not browser_mgr:\r\n raise HTTPException(status_code=503, detail=\"浏览器未就绪\")\r\n img_base64 = await browser_mgr.take_screenshot_base64()\r\n if img_base64:\r\n html = f\"\"\"\r\n <html><body style=\"background:#000;display:flex;justify-content:center;padding:20px;\">\r\n <img src=\"data:image/png;base64,{img_base64}\" style=\"max-width:100%;border:1px solid #333;\"/>\r\n </body></html>\r\n \"\"\"\r\n return HTMLResponse(content=html)\r\n raise HTTPException(status_code=500, detail=\"截图失败\")\r\n\r\n\r\n@app.get(\"/v1/models\")\r\nasync def list_models(request: Request):\r\n verify_api_key(request)\r\n return {\r\n \"object\": \"list\",\r\n \"data\": [\r\n {\r\n \"id\": \"deepseek-chat\",\r\n \"object\": \"model\",\r\n \"created\": int(time.time()),\r\n \"owned_by\": \"deepseek-proxy\",\r\n }\r\n ],\r\n }\r\n\r\n\r\ndef build_prompt_from_messages(messages: list) -> str:\r\n return json.dumps({\"messages\": messages}, ensure_ascii=False)\r\n\r\n\r\n@app.post(\"/v1/chat/completions\")\r\nasync def chat_completions(request: Request):\r\n verify_api_key(request)\r\n await ensure_browser_ready()\r\n\r\n try:\r\n body = await request.json()\r\n except Exception:\r\n raise HTTPException(status_code=400, detail=\"无效的 JSON 请求体\")\r\n\r\n messages = body.get(\"messages\", [])\r\n if not messages:\r\n raise HTTPException(status_code=400, detail=\"messages 不能为空\")\r\n\r\n stream = body.get(\"stream\", False)\r\n user_prompt = build_prompt_from_messages(messages)\r\n\r\n if not user_prompt:\r\n raise HTTPException(status_code=400, detail=\"未找到有效的用户输入\")\r\n\r\n print(f\"📝 构建的 prompt 长度: {len(user_prompt)} 字符\")\r\n\r\n model = body.get(\"model\", \"deepseek-chat\")\r\n req_id = f\"chatcmpl-{int(time.time()*1000)}\"\r\n\r\n if stream:\r\n async def generate():\r\n async for chunk in browser_mgr.send_message_stream(user_prompt):\r\n # ★ 心跳标记:发 SSE 注释保活\r\n if chunk == HEARTBEAT_MARKER:\r\n yield \": heartbeat\\n\\n\"\r\n continue\r\n\r\n # 正常内容\r\n data = {\r\n \"id\": req_id,\r\n \"object\": \"chat.completion.chunk\",\r\n \"created\": int(time.time()),\r\n \"model\": model,\r\n \"choices\": [{\r\n \"index\": 0,\r\n \"delta\": {\"content\": chunk},\r\n \"finish_reason\": None\r\n }]\r\n }\r\n yield f\"data: {json.dumps(data, ensure_ascii=False)}\\n\\n\"\r\n\r\n # 结束标记\r\n end_data = {\r\n \"id\": req_id,\r\n \"object\": \"chat.completion.chunk\",\r\n \"created\": int(time.time()),\r\n \"model\": model,\r\n \"choices\": [{\r\n \"index\": 0,\r\n \"delta\": {},\r\n \"finish_reason\": \"stop\"\r\n }]\r\n }\r\n yield f\"data: {json.dumps(end_data, ensure_ascii=False)}\\n\\n\"\r\n yield \"data: [DONE]\\n\\n\"\r\n\r\n # ═══ 关键改动:加反缓冲头 ═══\r\n return StreamingResponse(\r\n generate(),\r\n media_type=\"text/event-stream\",\r\n headers={\r\n \"Cache-Control\": \"no-cache, no-store, must-revalidate\",\r\n \"X-Accel-Buffering\": \"no\", # ★ Nginx 禁止缓冲\r\n \"Connection\": \"keep-alive\",\r\n \"Transfer-Encoding\": \"chunked\",\r\n },\r\n )\r\n else:\r\n response_text = await browser_mgr.send_message(user_prompt)\r\n return {\r\n \"id\": req_id,\r\n \"object\": \"chat.completion\",\r\n \"created\": int(time.time()),\r\n \"model\": model,\r\n \"choices\": [{\r\n \"index\": 0,\r\n \"message\": {\"role\": \"assistant\", \"content\": response_text},\r\n \"finish_reason\": \"stop\"\r\n }],\r\n \"usage\": {\r\n \"prompt_tokens\": len(user_prompt),\r\n \"completion_tokens\": len(response_text),\r\n \"total_tokens\": len(user_prompt) + len(response_text)\r\n }\r\n }\r\n\r\n@app.websocket(\"/ws\")\r\nasync def websocket_endpoint(websocket: WebSocket):\r\n await websocket.accept()\r\n print(f\"📡 WebSocket 客户端已连接: {websocket.client}\")\r\n\r\n api_key_param = websocket.query_params.get(\"api_key\", \"\").strip()\r\n authenticated = (api_key_param == API_SECRET_KEY)\r\n\r\n if not authenticated:\r\n try:\r\n first_msg = await asyncio.wait_for(websocket.receive_text(), timeout=10)\r\n try:\r\n payload = json.loads(first_msg)\r\n if payload.get(\"api_key\") == API_SECRET_KEY:\r\n authenticated = True\r\n await websocket.send_json({\"type\": \"auth\", \"status\": \"ok\"})\r\n except json.JSONDecodeError:\r\n pass\r\n except asyncio.TimeoutError:\r\n pass\r\n\r\n if not authenticated:\r\n await websocket.send_json({\r\n \"type\": \"error\",\r\n \"error\": \"Authentication required.\"\r\n })\r\n await websocket.close(code=4001, reason=\"Unauthorized\")\r\n return\r\n\r\n try:\r\n while True:\r\n data = await websocket.receive_text()\r\n try:\r\n payload = json.loads(data)\r\n message = payload.get(\"message\", \"\")\r\n except json.JSONDecodeError:\r\n message = data\r\n\r\n if not message:\r\n await websocket.send_json({\"error\": \"消息不能为空\"})\r\n continue\r\n\r\n if not browser_mgr or not browser_mgr.is_ready:\r\n await websocket.send_json({\"type\": \"info\", \"message\": \"浏览器初始化中,请稍候...\"})\r\n ok = await browser_mgr.wait_until_ready(timeout=180) if browser_mgr else False\r\n if not ok:\r\n await websocket.send_json({\"error\": \"浏览器初始化超时\"})\r\n continue\r\n\r\n if not await browser_mgr.is_alive():\r\n await websocket.send_json({\"error\": \"浏览器会话已断开\"})\r\n continue\r\n\r\n await websocket.send_json({\"type\": \"start\"})\r\n full_response = \"\"\r\n async for chunk in browser_mgr.send_message_stream(message):\r\n # ★ 心跳标记\r\n if chunk == HEARTBEAT_MARKER:\r\n await websocket.send_json({\"type\": \"heartbeat\"})\r\n continue\r\n\r\n full_response += chunk\r\n await websocket.send_json({\"type\": \"chunk\", \"content\": chunk})\r\n await websocket.send_json({\"type\": \"end\", \"full_content\": full_response})\r\n\r\n except WebSocketDisconnect:\r\n print(f\"📡 WebSocket 客户端已断开: {websocket.client}\")\r\n except Exception as e:\r\n print(f\"❌ WebSocket 错误: {e}\")\r\n try:\r\n await websocket.send_json({\"error\": str(e)})\r\n except Exception:\r\n pass\r\n\r\n\r\nif __name__ == \"__main__\":\r\n port = int(os.environ.get(\"PORT\", 7860))\r\n uvicorn.run(app, host=\"0.0.0.0\", port=port, log_level=\"info\")\r\n"}}}}