1.77.6: SERP sorry-重定向按验证码语义(不换号+15min冷却);浏览池保温释放跳过+60min持有上限;速率-门壳实验与监控工具
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/** # 保活脚本状态机测试(不走浏览器;模拟 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<unknown>;
|
||||
|
||||
/** # 函数 模拟页面(导航 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);
|
||||
}
|
||||
Reference in New Issue
Block a user