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(拿的是微信真二维码) 教训:接入第三方协议先去已有实现里找协议,别从直觉倒推。
64 lines
2.5 KiB
Python
64 lines
2.5 KiB
Python
"""微信 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)
|