import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { browserServiceFor } from "../../src/modules/browser/index.ts"; import { type BrowserService, registerChallengeVision } from "../../src/modules/browser/service.ts"; import { type BrowserHarness, createBrowserHarness, type Loose, ORIGIN, pgAvailable } from "./harness.ts"; // 站点自己的页面验证码(字幕库云锁)代码识别没过 → 智能体视觉接管: // 过了带回站点 Cookie;没过会话转等待、开待处理事项;自动化关闭时不接管。 const up = await pgAvailable(); const MARKER = "网站访问认证页面"; describe.skipIf(!up)("页面验证码视觉接管", () => { let h: BrowserHarness; let svc: BrowserService; let captchaUp = true; let visionSolves = true; const visionCalls: string[] = []; const url = `${ORIGIN}/search?q=dune`; const req = { url, marker: MARKER, goal: "输入验证码" }; const sessions = async () => (await h.req("/browser/sessions")).body.data.items as Loose[]; beforeAll(async () => { h = await createBrowserHarness("pagecap", { pageText: (u) => (captchaUp && u.includes("/search") ? `网站防火墙\n${MARKER}\n请输入验证码后继续访问:` : null), browserOptions: { challengePollMs: 5, challengeAutoWaitMs: 200, settleMs: 0 }, }); await sessions(); svc = browserServiceFor(h.ctx)!; // 视觉子流程(假):输对验证码 → 站点种下验证 Cookie、页面放行 registerChallengeVision(h.ctx, async (r) => { visionCalls.push(r.sessionId); if (visionSolves) { captchaUp = false; h.mock.cookieJars.set(`umt-site-${h.siteId}-acct-anon`, [ { name: "security_session_verify", value: "ok-from-browser", domain: new URL(ORIGIN).hostname, path: "/", expires: Math.floor(Date.now() / 1000) + 3600, httpOnly: true, secure: true, }, ]); } return { status: visionSolves ? "solved" : "needs_human", reason: visionSolves ? "" : "看不清", childRunId: null, }; }); }, 120_000); afterAll(async () => { await h?.close(); }); beforeEach(async () => { for (const s of await sessions()) await h.req(`/browser/sessions/${s.id}`, { method: "DELETE" }); h.mock.cookieJars.clear(); captchaUp = true; visionSolves = true; visionCalls.length = 0; await h.db .insertInto("settings") .values({ key: "automation", value: JSON.stringify({ enabled: true }) }) .onConflict((oc) => oc.column("key").doUpdateSet({ value: JSON.stringify({ enabled: true }) })) .execute(); }); it("视觉接管通过:匿名会话,带回站点 Cookie", async () => { const r = await svc.solvePageCaptcha({ siteId: h.siteId, ...req }); expect(r).toEqual({ status: "passed", by: "vision", cookies: [{ name: "security_session_verify", value: "ok-from-browser" }], }); expect(visionCalls).toHaveLength(1); const [s] = await sessions(); expect(s.accountId ?? null).toBeNull(); expect(s.controller).toBe("automation"); }); it("浏览器里已经没有验证码页(人工过过):不派子流程,直接带回 Cookie", async () => { captchaUp = false; const r = await svc.solvePageCaptcha({ siteId: h.siteId, ...req }); expect(r).toMatchObject({ status: "passed", by: "browser" }); expect(visionCalls).toEqual([]); }); it("视觉接管没过:会话转等待,待处理事项写明去原站视图输验证码;再来一次不打扰", async () => { visionSolves = false; const r = await svc.solvePageCaptcha({ siteId: h.siteId, ...req }); expect(r).toMatchObject({ status: "needs_human", reason: "看不清" }); const d = await h.db .selectFrom("decision") .selectAll() .where("id", "=", (r as Loose).decisionId) .executeTakeFirstOrThrow(); expect(d.title).toContain("需要你在原站视图里过一次验证码"); expect(d.status).toBe("awaiting_user"); const [s] = await sessions(); expect(s.controller).toBe("waiting"); const again = await svc.solvePageCaptcha({ siteId: h.siteId, ...req }); expect(again).toMatchObject({ status: "needs_human", decisionId: (r as Loose).decisionId }); expect(visionCalls).toHaveLength(1); }); it("原站视图当前页:人工打开的页面地址与浏览器里的站点 Cookie(字幕站从当前页下载)", async () => { captchaUp = false; await svc.solvePageCaptcha({ siteId: h.siteId, ...req }); h.mock.cookieJars.set(`umt-site-${h.siteId}-acct-anon`, [ { name: "security_session_verify", value: "from-human", domain: new URL(ORIGIN).hostname, path: "/", expires: Math.floor(Date.now() / 1000) + 3600, httpOnly: true, secure: true, }, ]); const [s] = await sessions(); const p = await svc.currentPage(s.id, { kind: "human", holder: "user:tester" }); expect(p).toEqual({ siteId: h.siteId, url, cookies: [{ name: "security_session_verify", value: "from-human" }], }); }); it("自动化总开关关闭:不接管", async () => { await h.db .updateTable("settings") .set({ value: JSON.stringify({ enabled: false }) }) .where("key", "=", "automation") .execute(); const r = await svc.solvePageCaptcha({ siteId: h.siteId, ...req }); expect(r).toMatchObject({ status: "unavailable" }); expect(visionCalls).toEqual([]); }); });