/** # 保活脚本状态机测试(不走浏览器;模拟 ctx.page/gTask/adminFetch) * 复刻节点 script.run 的管线:包函数 → Transpiler → new Function 实例化。 */ import { readFileSync } from "node:fs"; const src = readFileSync(new URL("./admin/scripts/keepalive-flow.ts", import.meta.url), "utf8"); const wrapped = `async function __task__(ctx) {\n${src}\n}`; const js = new Bun.Transpiler({ loader: "ts" }).transformSync(wrapped); const factory = new Function(`${js}\nreturn __task__;`)() as (ctx: any) => Promise; /** # 函数 模拟页面(导航 URL 由脚本自己 goto,我们记录每次 goto 的 url) */ function mockPage(urlsOnGoto: (url: string) => string) { let url = "about:blank"; return { url: () => url, viewportSize: async () => ({ width: 1280, height: 720 }), goto: async (u: string) => { url = urlsOnGoto(u); return { url }; }, waitForLoadState: async () => { }, evaluate: async (code: string) => { if (code.includes("sorry")) return '{"sorry":false,"captchaForm":false,"unusual":false}'; if (code.includes("closest")) return null; if (code.includes("getBoundingClientRect")) return null; return '{"host":"www.google.com","act":""}'; }, $$eval: async () => [], locator: (_sel: string) => ({ first: () => ({ count: async () => 0, isVisible: async () => false, click: async () => { }, fill: async () => { } }) }), keyboard: { type: async () => { }, press: async () => { } }, mouse: { move: async () => { }, down: async () => { }, up: async () => { }, wheel: async () => { } }, }; } /** # 函数 构造一组可被任意改的假数据页 */ function makeCtx(opts: { data?: any; captchaOn?: boolean; adminOk?: boolean; reloadExprOn?: boolean; }) { return makeCtxArg(opts); } function makeCtxArg(opts: { data?: any; captchaOn?: boolean; adminOk?: boolean }) { const calls: string[] = []; let cur = JSON.parse(JSON.stringify(opts.data ?? {})); const page = mockPage((u: string) => (u.startsWith("https://www.google.com/search") && opts.captchaOn ? "https://www.google.com/sorry/index?id=xx" : u.startsWith("https://www.google") ? "https://www.google.com/" : u)); const page2: any = page; page2.evaluate = async (code: string) => { if (code.includes("unusual")) return opts.captchaOn ? '{"sorry":true,"captchaForm":true,"unusual":true}' : '{"sorry":false,"captchaForm":false,"unusual":false}'; if (code.includes("closest")) return null; return '{"host":"www.google.com","act":""}'; }; const ctx = { profileId: "21111111", page: page2, sleep: async (ms: number) => { /* 不真的睡,秒进 */ }, log: (...a: any[]) => calls.push("L " + a.join(" ")), getTaskState: async () => ({ exists: !!Object.keys(cur).length, state: null, data: cur }), setTaskState: async (state: string, data?: any) => { cur = { ...cur, ...(data ?? {}) }; calls.push(`S ${state} next=${cur.nextCheckAt ?? "-"}`); return true; }, adminFetch: async (path: string, init?: any) => { if (opts.adminOk === false) return null; if (path === `/api/profiles/${ctx.profileId}` && init?.method === "GET") return { ok: true, status: 200, json: { data: { name: "keepalive-8655-US", browserKind: "veil", browserTheme: "light", node: "OVH-FR", tags: ["keepalive"], notes: "ka slot=8655", proxy: { id: "68b1...uuid" }, browser: { locale: "en-US", timezone: "America/New_York" } } } }; if (path === "/api/profiles" && init?.method === "POST") return { ok: true, status: 201, json: { data: { id: "99999999" } } }; if (path === `/api/profiles/${ctx.profileId}` && init?.method === "DELETE") return { ok: true, status: 200, json: { data: null } }; return { ok: false, status: 404, json: null }; }, }; ctx.setTaskState = async (state: string, data?: any) => { cur = { ...cur, ...(data ?? {}) }; calls.push(`S ${state} next=${cur.nextCheckAt ?? "-"}`); return true; }; (ctx as any).calls = calls; (ctx as any).cur = cur; return ctx as any; } /** # 用例 1:全新档案 → v1 → 等待-v2 */ { const c = makeCtxArg({ data: {} }); const r = await factory(c); const r1 = await factory(c); console.log("case1 fresh:", r, "| 再跑:", r1); console.log(" 状态轨迹:", c.calls.filter(s => s.startsWith("S ")).join(" / ")); } /** # 用例 2:v1 已达 2h → v2 完成 */ { const c = makeCtxArg({ data: { v1At: new Date(Date.now() - 2.2 * 3600e3).toISOString() } }); const r = await factory(c); const r2 = await factory(c); // v2 刚完成,2h 未到 console.log("case2 v2:", r, "| 紧接:", r2); } /** # 用例 3:s1 全链(x/amazon/google)完成后 s2 立即跑,次日收敛 */ { const c = makeCtxArg({ data: { v1At: new Date(Date.now() - 5 * 3600e3).toISOString(), v2At: new Date(Date.now() - 2.1 * 3600e3).toISOString(), s1At: undefined } }); // 修正:s1At 未定,先跑 s1 c.cur.s1At = undefined; const r = await factory(c); const r2 = await factory(c); // s2 const r3 = await factory(c); // 当日再跑 → idle-today console.log("case3 s1:", r, "| s2:", r2, "| 同日再跑:", r3); } /** # 用例 4:s2 命中 CAPTCHA → 放行删建路径(admin 正常) */ { const c = makeCtxArg({ data: { v1At: new Date(Date.now() - 3 * 3600e3).toISOString(), v2At: new Date(Date.now() - 2.2 * 3600e3).toISOString() }, captchaOn: true }); // 没写 s1At:会走 s1(cap 命中即重建) c.cur.s1At = new Date(Date.now() - 3600e3).toISOString(); const r = await factory(c); console.log("case4 captcha:", r, "| 状态尾:", c.calls.filter(s => s.startsWith("S ")).slice(-2).join(" / ")); } /** # 用例 5:admin 关闭 → CAPTCHA 挂起 30 分钟 */ { const c = makeCtxArg({ data: { v1At: new Date(Date.now() - 3 * 3600e3).toISOString(), v2At: new Date(Date.now() - 2.2 * 3600e3).toISOString(), s1At: new Date(Date.now() - 3600e3).toISOString() }, captchaOn: true, adminOk: false }); const r = await factory(c); console.log("case5 admin-null:", r); }