上一版按"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:
+36
-54
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* 微信接入的端到端验证:页面上建通道 → 把"微信"发来的那条 POST 打到回调地址
|
||||
* → 任务真出现在任务列表里。
|
||||
* 微信接入的端到端验证:页面上点「扫码连接微信」→ 真拿一张二维码回来 → 画在页面上。
|
||||
*
|
||||
* 这里不发真正的微信消息(那得有人拿手机发),而是照着 clawbot 的形状
|
||||
* 直接 POST 一次——它就是这个接口唯一的调用方。
|
||||
* 不扫这张码(那得有人拿手机扫),所以只验到"二维码出来了、通道在等扫码"为止;
|
||||
* 扫码之后的落库与轮询由 `backend/tests/test_wechat_bot.py` 覆盖。
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { chromium } from "playwright-core";
|
||||
@@ -64,67 +63,50 @@ async function main() {
|
||||
localStorage.setItem("dadao.access", access);
|
||||
localStorage.setItem("dadao.refresh", refresh);
|
||||
},
|
||||
[token, session.tokens.refresh_token],
|
||||
[session.tokens.access_token, session.tokens.refresh_token],
|
||||
);
|
||||
|
||||
await page.goto(`${WEB}/w/${ws.id}/integrations`, { waitUntil: "networkidle" });
|
||||
check("接入页能打开", await page.getByRole("heading", { name: "接入" }).isVisible());
|
||||
check("接入页打开", await page.getByText("怎么用").isVisible());
|
||||
|
||||
// 建一条新通道:令牌只在创建后那一屏出现
|
||||
await page.getByRole("button", { name: "新建通道" }).click();
|
||||
const urlBox = page.getByTestId("webhook-url");
|
||||
await urlBox.waitFor({ state: "visible" });
|
||||
const url = (await urlBox.innerText()).trim();
|
||||
check("创建后拿到回调地址", /\/integrations\/clawbot\/[\w-]{20,}/.test(url), url.slice(0, 48) + "…");
|
||||
const button = page.getByRole("button", { name: "扫码连接微信" });
|
||||
if (await button.isVisible()) {
|
||||
await button.click();
|
||||
} else {
|
||||
// 之前连过:卡片上给的是「重新扫码」
|
||||
await page.getByRole("button", { name: "重新扫码" }).click();
|
||||
}
|
||||
|
||||
// 冒充 clawbot 推一条消息
|
||||
const push = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
msg_id: `e2e-${Date.now()}`,
|
||||
from: "e2e",
|
||||
text: `微信来的任务 ${Date.now()}\n从手机发的,带正文`,
|
||||
}),
|
||||
});
|
||||
const pushed = await push.json();
|
||||
check("回调地址收到消息并建了任务", pushed.code === 0 && pushed.data.action === "task");
|
||||
const title = pushed.data.reply.match(/《(.+?)》/)?.[1] || "";
|
||||
const card = page.getByTestId("wechat-qr");
|
||||
await card.waitFor({ state: "visible", timeout: 20_000 });
|
||||
const img = card.locator("img");
|
||||
const src = await img.getAttribute("src");
|
||||
check(
|
||||
"二维码是页面里画出来的 SVG",
|
||||
Boolean(src?.startsWith("data:image/svg+xml;base64,")),
|
||||
src?.slice(0, 32),
|
||||
);
|
||||
check(
|
||||
"提示用微信扫码",
|
||||
(await card.innerText()).includes("用微信扫这张码"),
|
||||
(await card.innerText()).split("\n")[0],
|
||||
);
|
||||
|
||||
// 再推一次同一条:不该多出第二条
|
||||
const again = await (
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ msg_id: "dup", text: "重复消息" }),
|
||||
})
|
||||
).json();
|
||||
const dup = await (
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ msg_id: "dup", text: "重复消息" }),
|
||||
})
|
||||
).json();
|
||||
check("同一条消息不会建两条", again.code === 0 && dup.data.duplicate === true);
|
||||
const channels = await (await fetch(`${API}/workspaces/${ws.id}/integrations`, { headers })).json();
|
||||
const channel = channels.data.items[0];
|
||||
check("后端记下了这条通道", Boolean(channel), JSON.stringify(channel?.status));
|
||||
check("还没扫码,状态是 pending", channel?.status === "pending", channel?.status);
|
||||
check("没有登录态就不该在轮询", channel?.polling === false, String(channel?.polling));
|
||||
|
||||
// 页面上的「最近消息」要能看到刚才那几条
|
||||
await page.getByRole("button", { name: "最近消息" }).first().click();
|
||||
await page.getByText("建了任务").first().waitFor({ state: "visible" });
|
||||
check("页面能看到收到的消息", await page.getByText("建了任务").first().isVisible());
|
||||
await page.screenshot({ path: "e2e/artifacts/integrations.png", fullPage: false });
|
||||
check("控制台没有报错", consoleErrors.length === 0, consoleErrors.slice(0, 2).join(" | "));
|
||||
|
||||
// 任务真的进了任务列表
|
||||
const items = await (await fetch(`${API}/workspaces/${ws.id}/items`, { headers })).json();
|
||||
const found = items.data.items.find((i) => i.title === title);
|
||||
check("任务出现在工作区里", Boolean(found), title);
|
||||
|
||||
check("控制台零报错", consoleErrors.length === 0, consoleErrors.slice(0, 2).join(" | "));
|
||||
|
||||
await page.screenshot({ path: "e2e/artifacts/integrations.png", fullPage: true });
|
||||
await browser.close();
|
||||
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
console.log(`\n${failed.length === 0 ? "ALL PASS" : "FAILED"} — pass ${results.length - failed.length}, fail ${failed.length}`);
|
||||
console.log(
|
||||
`\n${failed.length === 0 ? "ALL PASS" : "FAILED"} — pass ${results.length - failed.length}, fail ${failed.length}`,
|
||||
);
|
||||
if (failed.length > 0) process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user