dsh-browser-playwright-codex 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/NOTICE.md +26 -5
- package/README.md +27 -4
- package/lib/browser-lifecycle.js +27 -1
- package/lib/client.js +1219 -647
- package/lib/config.d.ts +26 -0
- package/lib/config.js +24 -1
- package/lib/page-actions.js +32 -1
- package/lib/provider.js +8 -0
- package/lib/read-deadline.d.ts +22 -0
- package/lib/read-deadline.js +77 -0
- package/lib/runtime-state.d.ts +26 -0
- package/lib/runtime-state.js +100 -0
- package/lib/session.d.ts +22 -0
- package/lib/session.js +69 -8
- package/lib/settings-card.js +123 -0
- package/lib/swarm-contract.d.ts +226 -0
- package/lib/swarm-contract.js +209 -0
- package/lib/swarm-curtain.d.ts +22 -0
- package/lib/swarm-curtain.js +115 -0
- package/lib/swarm-intro.d.ts +66 -0
- package/lib/swarm-intro.js +827 -0
- package/lib/tool.js +19 -4
- package/package.json +4 -2
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「同一个引擎」的提前启动:从打开网页的第一帧起,画面就是**原版特效自己**的
|
|
3
|
+
* 白底 + 18 个光标在转,全程只有这一套,不再有「自造白幕 → 切到原版」的交接断层。
|
|
4
|
+
*
|
|
5
|
+
* 做法:把随包的 `assets/swarm-rebuild.js`(原版引擎)整段塞进 `addInitScript`,
|
|
6
|
+
* 它在页面自己的脚本之前、<html> 一出现就启动(脚本里的"延后到 <html>"补丁见
|
|
7
|
+
* scripts/swarm-patch.mjs)。所以它比"打开页面→加载完→再注入"早得多,原页面
|
|
8
|
+
* 来不及露脸(用户 2026-11-09 最在意的那"一瞬间原画面"就此压住)。
|
|
9
|
+
*
|
|
10
|
+
* 只在真的要播的那次启动:导航前用 `window.name` 打标记(实测**跨域导航会保留**),
|
|
11
|
+
* addInitScript 读到标记才启动、并立刻清掉。三重保险防"残留标记误启动":
|
|
12
|
+
* · 只在顶层文档启动(`window.top === window`),iframe 不另起一套;
|
|
13
|
+
* · 标记带时间戳,超过 30 秒的旧标记不认(导航失败后残留的那枚会自己过期);
|
|
14
|
+
* · 任何"不播"路径都调 dropCurtain,把 name 和引擎一起清干净。
|
|
15
|
+
* 万一标记没落到最终文档(少见的重定向),playSwarm 会检测到"引擎不在"并回退到
|
|
16
|
+
* 旧的"加载后注入",功能仍可用,只是那一页会先露一下。
|
|
17
|
+
*
|
|
18
|
+
* 为什么不再自画 canvas 假光标:那是第二套引擎,和原版的光标参数再像也是两套,
|
|
19
|
+
* 交接必然"断一下、突然重新开始"(用户反复指出的缝)。用原版自己从第一帧登场,
|
|
20
|
+
* 缝就没了 —— 这正是用户 16:38 选过的「删掉自造的、用原版开场」,这次真正落地。
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
/** 导航前写进 window.name 的标记:新文档第一帧读到它才启动引擎。 */
|
|
25
|
+
const TOKEN = '__dsh_swarm__:';
|
|
26
|
+
/**
|
|
27
|
+
* 生成注册进 `addInitScript` 的脚本全文。
|
|
28
|
+
*
|
|
29
|
+
* 必须用**字符串拼接**把资产塞进去,不能用模板字符串:资产里有大量反引号和
|
|
30
|
+
* `${...}`,放进外层模板字符串会被当成插值直接炸掉。
|
|
31
|
+
* 每次现读资产:和 playSwarm 里 `readScript()` 一样,改完磁盘立即用新的,不缓存。
|
|
32
|
+
*/
|
|
33
|
+
export function swarmBootScript() {
|
|
34
|
+
let asset;
|
|
35
|
+
try {
|
|
36
|
+
asset = fs.readFileSync(fileURLToPath(new URL('../assets/swarm-rebuild.js', import.meta.url)), 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// 资产不在(0.8.0 起 assets/ 不再随 npm 包分发)。这里**绝不能往外抛**:
|
|
40
|
+
// 两个调用点都是 `await ctx.addInitScript({ content: swarmBootScript() })`,
|
|
41
|
+
// 抛出去就是上下文创建失败、浏览器直接打不开 —— 把"动效没了"放大成"插件挂了"。
|
|
42
|
+
// 返回一段空注释脚本,等于这次启动不注入任何东西,行为与"没开这个功能"一致。
|
|
43
|
+
return '/* dsh: swarm asset not shipped (0.8.0+) — nothing injected */\n';
|
|
44
|
+
}
|
|
45
|
+
return ('if (window.top === window && typeof window.name === "string" &&\n' +
|
|
46
|
+
' window.name.indexOf("' +
|
|
47
|
+
TOKEN +
|
|
48
|
+
'") === 0 &&\n' +
|
|
49
|
+
' Date.now() - Number(window.name.slice(' +
|
|
50
|
+
TOKEN.length +
|
|
51
|
+
')) < 30000) {\n' +
|
|
52
|
+
' window.name = "";\n' +
|
|
53
|
+
' window.__dshSwarmEarly = true;\n' +
|
|
54
|
+
asset +
|
|
55
|
+
'\n}\n');
|
|
56
|
+
}
|
|
57
|
+
/** 导航前打标记:下一次文档创建时,addInitScript 会把原版引擎提前启动。 */
|
|
58
|
+
export async function armCurtain(page) {
|
|
59
|
+
try {
|
|
60
|
+
await page.evaluate((token) => {
|
|
61
|
+
window.name = token;
|
|
62
|
+
}, TOKEN + Date.now());
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* 页面还没就绪(about:blank 之前),下一次导航照样会带标记 */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** 清掉导航标记(导航失败时别让它留到下一次普通跳转)。 */
|
|
69
|
+
export async function clearCurtainToken(page) {
|
|
70
|
+
try {
|
|
71
|
+
await page.evaluate(() => {
|
|
72
|
+
window.name = '';
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* 页面已经走了 */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 决定"这一页不播"时:把可能已经提前启动的引擎彻底关掉。
|
|
81
|
+
*
|
|
82
|
+
* 这层原是"撤掉自造白幕",现在没有白幕了 —— 引擎是真的、会一直等放行,所以必须
|
|
83
|
+
* 调它自己的 stop(false)、把宿主立刻摘掉、把滚动还原,否则那一页会永远盖着白。
|
|
84
|
+
* 触发场景:遮罩页不播、CSP 把影子树样式表挡成半成品、oncePerSite 判定跳过、
|
|
85
|
+
* 用户关掉特效等。顺手把标记和几个开关清干净,免得同文档内 toggle/replay 被旧值影响。
|
|
86
|
+
*/
|
|
87
|
+
export async function dropCurtain(page, _waitForSwarm = false) {
|
|
88
|
+
try {
|
|
89
|
+
await page.evaluate(() => {
|
|
90
|
+
const w = window;
|
|
91
|
+
const api = w.__swarmRebuild;
|
|
92
|
+
if (api !== undefined && typeof api.stop === 'function')
|
|
93
|
+
api.stop(false);
|
|
94
|
+
// 还原滚动(提前启动的引擎在等放行、还没扫描,通常没滚过;保险起见照做)。
|
|
95
|
+
const start = w.__dshSwarmStart;
|
|
96
|
+
if (start !== undefined) {
|
|
97
|
+
const env = api?.app?.env;
|
|
98
|
+
if (env !== undefined && typeof env.scrollTo === 'function')
|
|
99
|
+
env.scrollTo(start.y, start.x);
|
|
100
|
+
else
|
|
101
|
+
window.scrollTo(start.x, start.y);
|
|
102
|
+
delete w.__dshSwarmStart;
|
|
103
|
+
}
|
|
104
|
+
// stop(false) 是延时移除,这里立刻摘,免得 AI 紧接着的点击命中测试被盖住。
|
|
105
|
+
for (const host of document.querySelectorAll('swarm-rebuild'))
|
|
106
|
+
host.remove();
|
|
107
|
+
w.name = '';
|
|
108
|
+
delete w.__dshSwarmEarly;
|
|
109
|
+
delete w.__dshSwarmGo;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* 页面已经走了 */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
/** 这个域名播过没有。跨所有活着的窗口判断。 */
|
|
3
|
+
export declare function hasPlayed(host: string): boolean;
|
|
4
|
+
/** 记下这个域名已经播过;ctx 传所属浏览器上下文,不传则用兜底。 */
|
|
5
|
+
export declare function markPlayed(host: string, ctx?: object): void;
|
|
6
|
+
/** 清空播放记录(设置卡上的「清空记录」用):内存和磁盘一起清。 */
|
|
7
|
+
export declare function resetPlayed(): void;
|
|
8
|
+
/**
|
|
9
|
+
* 「一直记住」开关切换时调用:打开就把这趟已有记录立刻落盘(别等关窗,
|
|
10
|
+
* 免得宿主崩溃丢记录);关掉就删掉磁盘记录,防止以后切回来时幽灵复活
|
|
11
|
+
* (2026-11-09 复核 find A)。这个文件是插件自己写的,删它不动用户数据。
|
|
12
|
+
*/
|
|
13
|
+
export declare function onPersistPlayedChanged(enabled: boolean): void;
|
|
14
|
+
/**
|
|
15
|
+
* 某个浏览器上下文关掉时调用:只忘掉**它自己**那趟播过哪些站。
|
|
16
|
+
* 还有别的窗口活着时,别的窗口的记录一律保留(复核 find B)。
|
|
17
|
+
* persist 打开时把这趟记录落盘,下次接着记。
|
|
18
|
+
* 关单个标签不会走到这里 —— 那是 closeTab,不是 context 关闭。
|
|
19
|
+
*/
|
|
20
|
+
export declare function forgetOnBrowserClose(ctx?: object): void;
|
|
21
|
+
/** 这个页面正在播吗。 */
|
|
22
|
+
export declare function isPlaying(page: Page): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* 关掉正在播的特效。任何工具动作开始前都调用它 —— 覆盖层本身不接管鼠标
|
|
25
|
+
* (pointer-events: none),所以拦不住点击;但 AI 动手时它该让开,画面才
|
|
26
|
+
* 不会被人当成页面坏了。
|
|
27
|
+
*
|
|
28
|
+
* 失败一律吞掉:页面可能已经关掉、正在跳转,或者根本没播过。
|
|
29
|
+
*/
|
|
30
|
+
export declare function stopSwarm(page: Page): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* 启动页。原来浏览器刚打开时停在 about:blank —— 一张纯白页,除了占位没有
|
|
33
|
+
* 任何用处,用户看见只会问「这是干嘛的」。这里换成一张说明卡:告诉人它已经
|
|
34
|
+
* 就绪、地址栏为什么是 about:blank、以及怎么用。没有外部资源,纯内联样式,
|
|
35
|
+
* 断网也是秒开。
|
|
36
|
+
*/
|
|
37
|
+
export declare const WELCOME_HTML = "<!doctype html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\">\n<title>\u6D4F\u89C8\u5668\u5DF2\u5C31\u7EEA</title>\n<style>\n :root { color-scheme: light; }\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center;\n background: #f5f6f8; color: #1d1d1f;\n font: 15px/1.7 system-ui, -apple-system, \"Segoe UI\", \"Microsoft YaHei\", sans-serif;\n }\n .card {\n width: min(560px, 86vw); padding: 38px 40px 34px; background: #fff; border-radius: 18px;\n box-shadow: 0 1px 2px rgba(0,0,0,.04), 0 12px 40px rgba(0,0,0,.08);\n }\n .dot { width: 9px; height: 9px; border-radius: 50%; background: #34c759; display: inline-block; margin-right: 9px; vertical-align: middle; }\n h1 { font-size: 23px; margin: 0 0 6px; letter-spacing: -.01em; }\n .sub { color: #6b6b70; margin: 0 0 26px; }\n .row { display: flex; gap: 12px; padding: 13px 0; border-top: 1px solid #ececf0; }\n .k { color: #8a8a8f; min-width: 96px; font-size: 13px; }\n .v { flex: 1; font-size: 14px; }\n code { font-family: ui-monospace, Consolas, monospace; background: #f2f2f5; padding: 1px 6px; border-radius: 5px; font-size: 13px; }\n .foot { margin: 24px 0 0; color: #9a9aa0; font-size: 12.5px; }\n</style></head><body>\n<div class=\"card\">\n <h1><span class=\"dot\"></span>\u6D4F\u89C8\u5668\u5DF2\u5C31\u7EEA</h1>\n <p class=\"sub\">\u60F3\u8BA9\u6211\u6253\u5F00\u54EA\u4E2A\u7F51\u7AD9\uFF0C\u8BF4\u4E00\u53E5\u5C31\u884C\u3002</p>\n <div class=\"row\"><div class=\"k\">\u5730\u5740\u680F</div><div class=\"v\"><code>about:blank</code> \u2014\u2014 \u6D4F\u89C8\u5668\u5185\u7F6E\u7684\u7A7A\u767D\u9875\uFF0C\u8868\u793A\u8FD8\u6CA1\u53BB\u4EFB\u4F55\u7F51\u7AD9</div></div>\n <div class=\"row\"><div class=\"k\">\u8FD9\u4E00\u9875</div><div class=\"v\">\u662F\u5149\u6807\u4E00\u7B14\u4E00\u7B14\u753B\u51FA\u6765\u7684\uFF0C\u6309 <code>Esc</code> \u53EF\u4EE5\u7ACB\u5373\u8DF3\u8FC7</div></div>\n <div class=\"row\"><div class=\"k\">\u767B\u5F55\u72B6\u6001</div><div class=\"v\">\u4FDD\u5B58\u5728\u8FD9\u4E2A\u6D4F\u89C8\u5668\u91CC\uFF0C\u5173\u6389\u7A97\u53E3\u4E5F\u4E0D\u4F1A\u4E22</div></div>\n <p class=\"foot\">\u7B2C\u4E00\u6B21\u6253\u5F00\u67D0\u4E2A\u7F51\u7AD9\u65F6\uFF0C\u90A3\u9875\u4E5F\u4F1A\u8FD9\u6837\u81EA\u5DF1\u957F\u51FA\u6765\u4E00\u6B21\u3002</p>\n</div></body></html>";
|
|
38
|
+
/**
|
|
39
|
+
* 把空白页换成启动卡片,并在上面放一次特效。特效不等它播完(启动不能被
|
|
40
|
+
* 拖住 9 秒),放出去就返回;真有工具动作要执行时 stopSwarm 会先把它关掉。
|
|
41
|
+
*/
|
|
42
|
+
export declare function showWelcome(page: Page, config: {
|
|
43
|
+
swarm?: {
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
speed?: number;
|
|
46
|
+
maxMs?: number;
|
|
47
|
+
};
|
|
48
|
+
} | undefined): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* 这次导航要不要播(只看不费时的条件,用来决定"开场白幕"要不要提前挂)。
|
|
51
|
+
*
|
|
52
|
+
* 注意传的是**目标地址**:导航前的 `page.url()` 还是旧页面,拿它判断会永远不播。
|
|
53
|
+
* 遮罩页那种需要读页面的判断留到注入之后做,那时会顺手把白幕撤掉。
|
|
54
|
+
*/
|
|
55
|
+
export declare function willPlaySwarm(targetUrl: string, config: Parameters<typeof maybePlaySwarm>[1], headless?: boolean): boolean;
|
|
56
|
+
/** 导航之后调用:够条件就播一次,不等它播完(导航照常立刻返回快照)。 */
|
|
57
|
+
export declare function maybePlaySwarm(page: Page, config: {
|
|
58
|
+
swarm?: {
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
oncePerSite?: boolean;
|
|
61
|
+
speed?: number;
|
|
62
|
+
maxMs?: number;
|
|
63
|
+
playOnVeiled?: boolean;
|
|
64
|
+
range?: 'viewport' | 'page';
|
|
65
|
+
};
|
|
66
|
+
} | undefined, headless?: boolean): void;
|