feat(browser): 插件真实坐标点击与页面内容读取(CF-2)

- umt 插件 POST /umt/tabs/{id}/mouse-click:page.mouse.click,跨域 iframe 的 Turnstile 也点得到;click 坐标优先走它,旧镜像退回 elementFromPoint
- 适配器 pageContent(地址 / 标题 / outerHTML / 正文 / navigator.userAgent);setEgress 返回插件出口模式
- mock camofox 增加 Cloudflare 模式(auto / interactive / captcha)与插件鼠标点击

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 21:26:53 +08:00
1 parent a11ef16e40
commit 6b14b0da45
7 files changed
+402 -10

No files matched your search

@@ -163,12 +163,67 @@ describe("camofox 适配器:身份与出口", () => {
mock = await createMockCamofox({ accessKey: "k" });
const p = provider(mock);
expect(await p.identity!()).toMatchObject({ userAgent: MOCK_CAMOFOX_IDENTITY.userAgent, os: "windows" });
expect(await p.setEgress!("site-a", { server: "http://127.0.0.1:7891" })).toEqual({ restarted: false });
expect(await p.setEgress!("site-b", null)).toEqual({ restarted: false });
expect(await p.setEgress!("site-a", { server: "http://127.0.0.1:7891" })).toEqual({
restarted: false,
mode: "site",
});
expect(await p.setEgress!("site-b", null)).toEqual({ restarted: false, mode: "site" });
expect(mock.egress.get("site-a")).toEqual({ server: "http://127.0.0.1:7891" });
expect(mock.egress.get("site-b")).toBeNull();
});
const budget = { domainAllowlist: ["audiences.example"], timeoutMs: 5_000, maxRetries: 0, maxBytes: 1_000_000 };
const openCf = async (m: MockCamofox) => {
const p = provider(m);
const { session } = await p.openSession({
providerId: "11111111-1111-4111-8111-111111111111",
siteId: "s",
accountId: "a",
profileKey: "umt-site-s-acct-a",
initialUrl: "https://audiences.example/torrents.php",
budget,
});
return { p, ref: { sessionId: session.id } };
};
const cfSite = (mode: "auto" | "interactive" | "captcha") => ({
origin: "https://audiences.example",
username: "u",
password: "p",
protectedPaths: /^$/,
cloudflare: { mode, autoPolls: 2, clearance: "clr-1" },
});
it("CF-2 pageContent():地址 / 标题 / HTML / UA;挑战页分类由调用方做", async () => {
mock = await createMockCamofox({ accessKey: "k", site: cfSite("auto") });
const { p, ref } = await openCf(mock);
const c1 = await p.pageContent!(ref);
expect(c1).toMatchObject({ title: "Just a moment...", userAgent: MOCK_CAMOFOX_IDENTITY.userAgent });
expect(c1?.html).toContain("_cf_chl_opt");
// 第 2 次读页面时 mock 自动放行,并写 cf_clearance
const c2 = await p.pageContent!(ref);
expect(c2?.title).not.toBe("Just a moment...");
const cookies = await p.getCookies!("umt-site-s-acct-a", ["https://audiences.example/"]);
expect(cookies.find((c) => c.name === "cf_clearance")?.value).toBe("clr-1");
});
it("CF-2 坐标点击走插件真实鼠标事件(跨域 iframe 的 Turnstile 也点得到),不再 elementFromPoint", async () => {
mock = await createMockCamofox({ accessKey: "k", site: cfSite("interactive") });
const { p, ref } = await openCf(mock);
// 第 2 行(y = 20 + 40 + 10)是复选框
await p.click(ref, { x: 100, y: 70 });
expect(mock.actions.some((a) => a.action === "mouse-click" && a.body.x === 100 && a.body.y === 70)).toBe(true);
expect(mock.actions.some((a) => a.action === "evaluate")).toBe(false);
expect(mock.cfPassed.has("umt-site-s-acct-a")).toBe(true);
});
it("CF-2 插件没有 mouse-click(旧镜像)→ 退回 elementFromPoint + 选择器点击", async () => {
mock = await createMockCamofox({ accessKey: "k", cookieApi: "none", site: cfSite("interactive") });
const { p, ref } = await openCf(mock);
await p.click(ref, { x: 100, y: 70 });
expect(mock.actions.some((a) => a.action === "evaluate")).toBe(true);
expect(mock.actions.some((a) => a.action === "click")).toBe(true);
});
it("旧插件(没有接口):identity() 为 null,setEgress() 报 not_implemented", async () => {
mock = await createMockCamofox({ accessKey: "k", cookieApi: "none" });
const p = provider(mock);