Files
DadaoTask/backend/alembic/versions/0013_wechat_bot_login.py
T
Steven 8436c0edbf
CI / backend (push) Canceled after 0s
feat(integrations): 微信要先扫码配对,改成 iLink 扫码登录 + 长轮询
上一版按"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(拿的是微信真二维码)

教训:接入第三方协议先去已有实现里找协议,别从直觉倒推。
2026-11-05 23:03:18 +08:00

64 lines
2.5 KiB
Python
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.
"""微信 clawbot 改成"扫码登录 + 长轮询"
Revision ID: 0013
Revises: 0012
Create Date: 2026-11-05
上一版(0012)做成了"clawbot 往我们这 POST webhook"。真实协议是腾讯 iLink Bot:
我们先扫码登录拿 `bot_token`,再由我们长轮询 `getupdates` 拉消息(见
`app/services/wechat_ilink.py` 文件头的出处说明)。所以这张表要补登录态与游标:
- `bot_token_enc`:加密后的登录态(密钥由 JWT_SECRET 派生),换 secret 就要重新扫码;
- `ilink_user_id`:扫码的微信号,界面上显示"谁扫的";
- `base_url`:服务端可能下发新基址;
- `sync_buf`:长轮询游标,丢了会重复收消息;
- `context_token`:回复必须带对方上一条消息的 context_token。
`bot_channel.status` 增加 `pending`(等扫码)与 `expired`(登录态失效)两个取值;
`token_hash` 是上一版遗留列,改成可空、不再使用。
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
from app.roles import APP_ROLE
revision: str = "0013"
down_revision: str = "0012"
branch_labels: Sequence[str] | None = None
depends_on: Sequence[str] | None = None
STATEMENTS = (
"ALTER TABLE bot_channel ADD COLUMN IF NOT EXISTS bot_token_enc text",
"ALTER TABLE bot_channel ADD COLUMN IF NOT EXISTS ilink_user_id varchar(64)",
"ALTER TABLE bot_channel ADD COLUMN IF NOT EXISTS base_url text",
"ALTER TABLE bot_channel ADD COLUMN IF NOT EXISTS sync_buf text",
"ALTER TABLE bot_channel ADD COLUMN IF NOT EXISTS context_token text",
"ALTER TABLE bot_channel ALTER COLUMN token_hash DROP NOT NULL",
"ALTER TABLE bot_channel ALTER COLUMN status SET DEFAULT 'pending'",
"COMMENT ON COLUMN bot_channel.bot_token_enc IS 'iLink 登录态,Fernet 加密(密钥由 JWT_SECRET 派生)'",
"COMMENT ON COLUMN bot_channel.sync_buf IS 'getupdates 游标,丢了会重复收消息'",
f"GRANT SELECT, INSERT, UPDATE, DELETE ON bot_channel TO {APP_ROLE}",
)
DOWN_STATEMENTS = (
"ALTER TABLE bot_channel DROP COLUMN IF EXISTS context_token",
"ALTER TABLE bot_channel DROP COLUMN IF EXISTS sync_buf",
"ALTER TABLE bot_channel DROP COLUMN IF EXISTS base_url",
"ALTER TABLE bot_channel DROP COLUMN IF EXISTS ilink_user_id",
"ALTER TABLE bot_channel DROP COLUMN IF EXISTS bot_token_enc",
)
def upgrade() -> None:
for statement in STATEMENTS:
op.execute(statement)
def downgrade() -> None:
for statement in DOWN_STATEMENTS:
op.execute(statement)