/** * 微信接入的端到端验证:页面上建通道 → 把"微信"发来的那条 POST 打到回调地址 * → 任务真出现在任务列表里。 * * 这里不发真正的微信消息(那得有人拿手机发),而是照着 clawbot 的形状 * 直接 POST 一次——它就是这个接口唯一的调用方。 */ import { readFileSync } from "node:fs"; import { chromium } from "playwright-core"; // 上一轮 e2e 建的密码就在仓库根的 .local 里(web/e2e 往上两级) function devPassword() { if (process.env.PASSWORD) return process.env.PASSWORD; try { return readFileSync(new URL("../../.local/e2e-account-password.txt", import.meta.url), "utf8").trim(); } catch { return "dadao-e2e-2026"; } } const WEB = process.env.WEB || "http://localhost:5180"; const API = process.env.API || "http://127.0.0.1:8100/api/v1"; const PHONE = process.env.PHONE || "13900000001"; const PASSWORD = devPassword(); const CHROME = process.env.CHROME || `${process.env.LOCALAPPDATA}\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe`; const results = []; function check(name, ok, detail = "") { results.push({ name, ok, detail }); console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? ` — ${detail}` : ""}`); } async function login() { const resp = await fetch(`${API}/auth/login/password`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ phone: PHONE, password: PASSWORD }), }); const body = await resp.json(); if (body.code !== 0) throw new Error(`登录失败 ${JSON.stringify(body)}`); return body.data; } async function main() { const session = await login(); const token = session.tokens.access_token; const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" }; const wsList = await (await fetch(`${API}/workspaces`, { headers })).json(); const ws = wsList.data.items[0]; if (!ws) throw new Error("这个账号没有工作区"); const browser = await chromium.launch({ executablePath: CHROME, headless: true }); const context = await browser.newContext({ viewport: { width: 1440, height: 960 } }); const page = await context.newPage(); const consoleErrors = []; page.on("console", (m) => m.type() === "error" && consoleErrors.push(m.text())); page.on("pageerror", (e) => consoleErrors.push(String(e))); await page.addInitScript( ([access, refresh]) => { localStorage.setItem("dadao.access", access); localStorage.setItem("dadao.refresh", refresh); }, [token, session.tokens.refresh_token], ); await page.goto(`${WEB}/w/${ws.id}/integrations`, { waitUntil: "networkidle" }); check("接入页能打开", await page.getByRole("heading", { name: "接入" }).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) + "…"); // 冒充 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 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); // 页面上的「最近消息」要能看到刚才那几条 await page.getByRole("button", { name: "最近消息" }).first().click(); await page.getByText("建了任务").first().waitFor({ state: "visible" }); check("页面能看到收到的消息", await page.getByText("建了任务").first().isVisible()); // 任务真的进了任务列表 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}`); if (failed.length > 0) process.exit(1); } main().catch((err) => { console.error("FAIL 脚本异常 —", err.message); process.exit(1); });