feat(integrations): 微信要先扫码配对,改成 iLink 扫码登录 + 长轮询
CI / backend (push) Canceled after 0s

上一版按"webhook + URL 里带令牌"自己编了一套,用户当场指出微信必须先扫码配对。
现在照腾讯 iLink Bot 协议自己做客户端:取二维码 → 手机扫码确认 → 存加密登录态 →
后端长轮询收消息 → 用 context_token 回话。独立实现,不依赖 DSH 的运行环境。

后端
- wechat_ilink.py:get_bot_qrcode / get_qrcode_status / getupdates / sendmessage,
  bot_token 用 Fernet(密钥由 JWT_SECRET 派生)加密入库,日志与接口都不回明文
- wechat_intake.py:一句话 → 任务或知识草稿,前缀识别、#标签、首行标题
- wechat_bot.py:每条通道一个轮询任务,启动时按 status='active' 恢复;
  message_id 去重;errcode -14 转 expired 让用户重新扫
- api/integrations.py 重写成扫码三接口 + 停用/断开/最近消息
- 迁移 0013(登录态列)、0014(轮询入口按 app.bot_channel 主键的 RLS 口子)

Web
- 接入页改成扫码流程:页面里画出二维码、每 2 秒问一次状态、连上显示微信用户与最近消息

验证
- pytest 149 passed(新增 test_wechat_bot 9 例)
- ruff check / ruff format --check 干净;tsc 0;vite build ✓
- web/e2e/integrations.mjs 7 项 ALL PASS(拿的是微信真二维码)

教训:接入第三方协议先去已有实现里找协议,别从直觉倒推。
This commit is contained in:
Steven
2026-11-05 23:03:18 +08:00
parent d21394a5ac
commit 8436c0edbf
18 changed files with 1836 additions and 1060 deletions
+4 -1
View File
@@ -32,6 +32,7 @@ from .db import SessionFactory, current_app_role, engine
from .errors import AppError, ErrorCode, http_status_for
from .logging_setup import get_logger, request_id_ctx, setup_logging
from .middleware import IdempotencyMiddleware, RequestContextMiddleware, error_body
from .services.wechat_bot import manager as wechat_manager
settings = get_settings()
setup_logging("DEBUG" if settings.env == "dev" else "INFO")
@@ -50,7 +51,10 @@ async def lifespan(app: FastAPI):
"服务启动",
extra={"extra_fields": {"env": settings.env, "commit": settings.build_commit}},
)
# 已登录的微信通道在进程启动时接上,不然重启之后消息就静默收不到了。
await wechat_manager.start_all()
yield
await wechat_manager.shutdown()
await engine.dispose()
@@ -128,7 +132,6 @@ app.include_router(attachments.router, prefix=settings.api_prefix)
app.include_router(app_release.router, prefix=settings.api_prefix)
app.include_router(settings_api.router, prefix=settings.api_prefix)
app.include_router(integrations.admin_router, prefix=settings.api_prefix)
app.include_router(integrations.webhook_router, prefix=settings.api_prefix)
@app.get("/health", tags=["运维"], summary="存活探针")