feat(subtitles): 字幕库验证码由智能体视觉接管;字幕站原站视图下载当前字幕
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
023063edf4
commit
c30a758a74
24 files changed
+466
-41
No files matched your search
@@ -69,6 +69,7 @@ export async function createBrowserHarness(
|
||||
/** CF-2:mock 站点的 Cloudflare 挑战与页面 HTML */
|
||||
cloudflare?: MockCamofoxSite["cloudflare"];
|
||||
html?: MockCamofoxSite["html"];
|
||||
pageText?: MockCamofoxSite["pageText"];
|
||||
/** 浏览器模块的额外选项(挑战轮询间隔等) */
|
||||
browserOptions?: Partial<BrowserModuleOptions>;
|
||||
} = {},
|
||||
@@ -82,6 +83,7 @@ export async function createBrowserHarness(
|
||||
...(opts.sessionCookie ? { sessionCookie: opts.sessionCookie } : {}),
|
||||
...(opts.cloudflare ? { cloudflare: opts.cloudflare } : {}),
|
||||
...(opts.html ? { html: opts.html } : {}),
|
||||
...(opts.pageText ? { pageText: opts.pageText } : {}),
|
||||
origin: ORIGIN,
|
||||
username: "fixture-user",
|
||||
password: PASSWORD,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user