diff --git a/Dockerfile b/Dockerfile index 950d0f0..e83a13b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,25 @@ COPY --from=build /app/dist ./dist COPY --from=build /app/public ./public COPY backend/requirements.txt ./backend/requirements.txt -RUN pip3 install --no-cache-dir --break-system-packages -r backend/requirements.txt +ARG PIP_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple" +ARG PIP_TRUSTED_HOST="pypi.tuna.tsinghua.edu.cn" +RUN pip3 install --no-cache-dir --break-system-packages \ + -i "${PIP_INDEX}" --trusted-host "${PIP_TRUSTED_HOST}" \ + fastapi==0.115.0 && \ + pip3 install --no-cache-dir --break-system-packages \ + -i "${PIP_INDEX}" --trusted-host "${PIP_TRUSTED_HOST}" \ + "uvicorn[standard]==0.30.0" && \ + pip3 install --no-cache-dir --break-system-packages \ + -i "${PIP_INDEX}" --trusted-host "${PIP_TRUSTED_HOST}" \ + httpx==0.27.0 && \ + pip3 install --no-cache-dir --break-system-packages \ + -i "${PIP_INDEX}" --trusted-host "${PIP_TRUSTED_HOST}" \ + "websockets>=12.0" && \ + pip3 install --no-cache-dir --break-system-packages \ + -i "${PIP_INDEX}" --trusted-host "${PIP_TRUSTED_HOST}" \ + "openai>=1.0.0" && \ + pip3 check && \ + python3 -c "import fastapi; import uvicorn; import httpx; import websockets; import openai; print('all deps ok')" COPY backend ./backend COPY .env.example ./ diff --git a/backend/data/sync_messages.json b/backend/data/sync_messages.json index 3faaacc..827c545 100644 --- a/backend/data/sync_messages.json +++ b/backend/data/sync_messages.json @@ -1483,5 +1483,104 @@ "new_msg_id": 6612157681502055018 }, "type": "message" + }, + { + "key": "HBpEnbtj9BJZ", + "message": { + "msg_id": 1649765637, + "from_user_name": { + "str": "wxid_f2q8xscgg31322" + }, + "to_user_name": { + "str": "wxid_f2q8xscgg31322" + }, + "msg_type": 51, + "content": { + "str": "\n\nwxid_f2q8xscgg31322\nlastMessage\n{\"messageSvrId\":\"8354732942085133458\",\"MsgCreateTime\":\"1773163308\"}\n\n" + }, + "status": 3, + "img_status": 1, + "img_buf": { + "len": 0 + }, + "create_time": 1773192394, + "msg_source": "\n\tv1_bhcJTcNo\n\t\n\t\t\n\t\n\n", + "new_msg_id": 3249105399159457776 + }, + "type": "message" + }, + { + "key": "HBpEnbtj9BJZ", + "message": { + "msg_id": 453179271, + "from_user_name": { + "str": "wxid_f2q8xscgg31322" + }, + "to_user_name": { + "str": "newsapp" + }, + "msg_type": 51, + "content": { + "str": "\n\nnewsapp\nlastMessage\n{\"messageSvrId\":\"1453521873\",\"MsgCreateTime\":\"1773192299\"}\n\n" + }, + "status": 3, + "img_status": 1, + "img_buf": { + "len": 0 + }, + "create_time": 1773192400, + "msg_source": "\n\tv1_olpnsmGt\n\t\n\t\t\n\t\n\n", + "new_msg_id": 5288934263040164256 + }, + "type": "message" + }, + { + "key": "HBpEnbtj9BJZ", + "message": { + "msg_id": 5670552, + "from_user_name": { + "str": "wxid_f2q8xscgg31322" + }, + "to_user_name": { + "str": "zhang499142409" + }, + "msg_type": 51, + "content": { + "str": "\n\nzhang499142409\nlastMessage\n{\"messageSvrId\":\"6612157681502055018\",\"MsgCreateTime\":\"1773163339\"}\n\n" + }, + "status": 3, + "img_status": 1, + "img_buf": { + "len": 0 + }, + "create_time": 1773192408, + "msg_source": "\n\tv1_aMGpPuti\n\t\n\t\t\n\t\n\n", + "new_msg_id": 950905004367894098 + }, + "type": "message" + }, + { + "key": "HBpEnbtj9BJZ", + "message": { + "msg_id": 1773181860, + "from_user_name": { + "str": "newsapp" + }, + "to_user_name": { + "str": "wxid_f2q8xscgg31322" + }, + "msg_type": 10002, + "content": { + "str": "\n\n\t\n\t\t/cgi-bin/micromsg-bin/addtxnewsmsg\n\t\t825\n\t\t50001\n\t\t2026031100\n\t\t0\n\t\t1773181804\n\t\t150\n\t\t63162\n\t\t0\n\t\t1\n\t\t2\n\t\tCAAQ\nAzjttsLNBkDstsLNBkikt8LNBlAAwAEB\n\t\t3\n\t\n\n" + }, + "status": 3, + "img_status": 1, + "img_buf": { + "len": 0 + }, + "create_time": 1773193510, + "new_msg_id": 1773181860 + }, + "type": "message" } ] \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 964671a..bbd126b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -301,14 +301,16 @@ async def get_login_qrcode(body: QrCodeRequest): body_text, ) data = resp.json() - # 第一步:记录完整返回并保存 Data62,供第二步滑块自动填充参数 + # 保存 Data62(顶层 "Data62"),以 d000 标识移除尾部乱码 try: - data62 = data.get("Data62") or (data.get("Data") or {}).get("data62") or "" + data62 = (data.get("Data62") or "").strip() + if not data62 and isinstance(data.get("Data"), dict): + data62 = (data.get("Data").get("Data62") or data.get("Data").get("data62") or "").strip() + data62 = _clean_data62(data62) qrcode_store[key] = {"data62": data62, "response": data} - # 在返回中拼接已存储标记,便于后续步骤使用同一 key 取 data62 data["_data62_stored"] = True data["_data62_length"] = len(data62) - logger.info("Stored Data62 for key=%s (len=%s)", key, len(data62)) + logger.info("Stored Data62 for key=%s (len=%s) from GetLoginQrCodeNewDirect top-level", key, len(data62)) except Exception as e: logger.warning("Store qrcode data for key=%s failed: %s", key, e) return data @@ -364,6 +366,23 @@ def _extract_clean_ticket(obj: dict) -> Optional[str]: return "".join(clean) if clean else None +def _clean_data62(s: str) -> str: + """去掉 Data62 尾部乱码:以 d000 标识乱码起始,截断保留此前有效内容。""" + if not s or not isinstance(s, str): + return "" + s = s.strip() + idx = s.find("d0000000000000101000000000000000d0000000000000000000000000000007f") + if idx != -1: + return s[:idx].strip() + idx = s.find("d0000000000000101") + if idx != -1: + return s[:idx].strip() + idx = s.find("d00000000") + if idx != -1: + return s[:idx].strip() + return s + + @app.get("/auth/scan-status") async def check_scan_status( key: str = Query(..., description="账号唯一标识"), @@ -389,9 +408,11 @@ async def check_scan_status( data = resp.json() ticket = _extract_clean_ticket(data) if ticket: - # 不调用滑块服务;返回自带预填表单的页面 path,iframe 加载后自动填充 Key/Data62/Original Ticket,用户点「开始验证」提交到第三方 7765 + # data62 必须来自 GetLoginQrCodeNewDirect 返回的顶层 "Data62",不能使用 CheckLoginStatus 里的 data62(常为空);并去掉尾部乱码 stored = qrcode_store.get(key) or {} - data62 = stored.get("data62") or "" + data62 = _clean_data62(stored.get("data62") or "") + if not data62: + data62 = _clean_data62(data.get("Data62") or (data.get("Data") or {}).get("Data62") or (data.get("Data") or {}).get("data62") or "") params = {"key": SLIDER_VERIFY_KEY, "ticket": ticket} if data62: params["data62"] = data62 @@ -453,9 +474,65 @@ async def slider_form( ticket: str = Query(..., description="Original Ticket"), ): """返回带 Key/Data62/Original Ticket 预填的表单页,提交到第三方 7765,供 iframe 加载并自动填充。""" + data62 = _clean_data62(data62) return HTMLResponse(content=_slider_form_html(key, data62, ticket)) +# ---------- 滑块验证提交接口(代理 7765) ---------- +# 7765 页面提交为 GET:action=SLIDER_VERIFY_BASE_URL,参数 key、data62、original_ticket +@app.get("/api/slider-verify") +async def api_slider_verify_get( + key: str = Query(..., description="Key"), + data62: str = Query("", description="Data62"), + original_ticket: str = Query(..., description="Original Ticket(与 ticket 二选一)"), + ticket: str = Query("", description="Original Ticket(与 original_ticket 二选一)"), +): + """代理 7765 滑块提交:GET 转发到 http://113.44.162.180:7765/?key=&data62=&original_ticket=,返回上游响应。""" + ticket_val = original_ticket or ticket + if not ticket_val: + raise HTTPException(status_code=400, detail="original_ticket or ticket required") + url = SLIDER_VERIFY_BASE_URL.rstrip("/") + "/" + params = {"key": key, "data62": _clean_data62(data62), "original_ticket": ticket_val} + try: + async with httpx.AsyncClient(timeout=30.0) as client: + resp = await client.get(url, params=params) + # 返回上游的 body;若为 JSON 则解析后返回 + try: + return resp.json() + except Exception: + return {"ok": resp.status_code == 200, "status_code": resp.status_code, "text": resp.text[:500]} + except Exception as e: + logger.warning("Slider verify upstream error: %s", e) + raise HTTPException(status_code=502, detail=f"slider_upstream_error: {e}") from e + + +class SliderVerifyBody(BaseModel): + key: str + data62: Optional[str] = "" + original_ticket: Optional[str] = None + ticket: Optional[str] = None + + +@app.post("/api/slider-verify") +async def api_slider_verify_post(body: SliderVerifyBody): + """代理 7765 滑块提交:POST body 转成 GET 请求转发到 7765,返回上游响应。""" + ticket_val = body.original_ticket or body.ticket + if not ticket_val: + raise HTTPException(status_code=400, detail="original_ticket or ticket required") + url = SLIDER_VERIFY_BASE_URL.rstrip("/") + "/" + params = {"key": body.key, "data62": _clean_data62(body.data62 or ""), "original_ticket": ticket_val} + try: + async with httpx.AsyncClient(timeout=30.0) as client: + resp = await client.get(url, params=params) + try: + return resp.json() + except Exception: + return {"ok": resp.status_code == 200, "status_code": resp.status_code, "text": resp.text[:500]} + except Exception as e: + logger.warning("Slider verify upstream error: %s", e) + raise HTTPException(status_code=502, detail=f"slider_upstream_error: {e}") from e + + # ---------- R1-2 客户画像 / R1-3 定时问候 / R1-4 分群推送 / 消息与发送 ---------- class CustomerCreate(BaseModel):