dsh-browser-playwright-codex 0.7.8 → 0.8.1
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 +92 -0
- package/NOTICE.md +26 -5
- package/README.md +33 -10
- package/assets/swarm-rebuild.js +3864 -0
- package/lib/browser-lifecycle.js +27 -1
- package/lib/challenge.d.ts +7 -4
- package/lib/challenge.js +28 -6
- package/lib/client.js +1245 -647
- package/lib/config.d.ts +26 -0
- package/lib/config.js +19 -0
- package/lib/errors.d.ts +17 -0
- package/lib/errors.js +28 -0
- package/lib/gates-patch.d.ts +41 -1
- package/lib/gates-patch.js +103 -5
- package/lib/injected.js +45 -0
- package/lib/page-actions.d.ts +24 -0
- package/lib/page-actions.js +73 -11
- package/lib/provider.js +8 -0
- package/lib/read-deadline.d.ts +5 -1
- package/lib/read-deadline.js +10 -3
- package/lib/runtime-state.d.ts +26 -0
- package/lib/runtime-state.js +100 -0
- package/lib/session.d.ts +11 -0
- package/lib/session.js +80 -11
- package/lib/settings-card.js +123 -0
- package/lib/snapshot-render.js +52 -7
- package/lib/swarm-contract.d.ts +232 -0
- package/lib/swarm-contract.js +215 -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 +829 -0
- package/lib/tool-types.d.ts +10 -0
- package/lib/tool.js +20 -6
- package/lib/types.d.ts +6 -0
- package/package.json +18 -13
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* swarm-contract — 这段第三方资产的**接口契约**:插件到底捏着上游脚本的哪几个把手。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有这个文件(2026-11-10):
|
|
5
|
+
*
|
|
6
|
+
* 起因是"上游改版,我这边会不会突然失效"。查完事实是:**不会**。上游那份脚本是
|
|
7
|
+
* vendored 进来的(`assets/swarm-rebuild.upstream.js`),SHA-256 在 `scripts/swarm-patch.mjs`
|
|
8
|
+
* 里锁死,上游在 GitHub 上改成什么样,本地这份一个字节都不会动。真正会失效的是另一件事 ——
|
|
9
|
+
* 插件对上游内部结构的依赖**散落在三个文件的字符串里**(`swarm-intro.ts` 里 20 余处
|
|
10
|
+
* `page.evaluate` 的字面量、`swarm-curtain.ts` 的桥、`swarm-patch.mjs` 的匹配片段),
|
|
11
|
+
* 没有任何一处声明"这是我们要的接口"。于是:
|
|
12
|
+
*
|
|
13
|
+
* - 加一处依赖,没人知道又多了个耦合点;
|
|
14
|
+
* - 补丁改一处,插件侧不知道自己的前提已经变了;
|
|
15
|
+
* - `docs/SWARM-CODE.md` 写着"改动 4 处",实际已经是 7 处 —— 文档对不上代码,
|
|
16
|
+
* 而这份文档正是"上游改版时该查哪里"的唯一依据。
|
|
17
|
+
*
|
|
18
|
+
* 所以这里把三个层次钉死,并交给 `tests/swarm-contract.test.ts` 机械校验:
|
|
19
|
+
*
|
|
20
|
+
* Tier A(硬接口) 上游自己挂在 window 上的句柄树。补丁一处都没动,插件主路径全走这里。
|
|
21
|
+
* 上游改了 → 插件立刻坏 → 这是**必须盯住**的部分。
|
|
22
|
+
* Tier B(兜底口) `.hud` / `[data-a=*]`。上游原生,但**已被我们的补丁删掉**。
|
|
23
|
+
* 插件只在"万一有人塞回未打补丁的原版"时才走这些分支。
|
|
24
|
+
* Tier C(自造桥) `__dshSwarm*`。我们自己注入的,上游永远不会提供,也不该指望。
|
|
25
|
+
*
|
|
26
|
+
* 契约的用法:**改任何一处依赖,先改这个文件**,再改实现,再看测试红绿。测试会去
|
|
27
|
+
* `assets/swarm-rebuild.upstream.js` 里逐条核对下面的每个名字是否真的存在 —— 上游换人、
|
|
28
|
+
* 改名、删节点,这里第一个报错,而不是等某天浏览器里莫名其妙不播了。
|
|
29
|
+
*
|
|
30
|
+
* 法律面(0.8.1 起):上游仓库根目录**仍然没有 LICENSE**(默认保留所有权利),
|
|
31
|
+
* 但 Dhvanil Patel 于 2026-11-11 邮件书面许可本项目**打包与修改**,Joel Califa 于
|
|
32
|
+
* 2026-11-08 书面许可并仅要求署名。所以资产可以随包分发,**但授权不是 MIT**:
|
|
33
|
+
* 它是邮件许可,只覆盖本项目,不给第三方通用许可。别把这份资产 fork 成
|
|
34
|
+
* "我们自己的版本" —— fork 不改变任何人拿到的权利,只会多出一份没有许可的副本。
|
|
35
|
+
*/
|
|
36
|
+
/** 上游出处与入仓指纹。这三项之外的任何东西,都不该成为插件的依赖。 */
|
|
37
|
+
export declare const SWARM_UPSTREAM: {
|
|
38
|
+
/** 上游仓库。注意:根目录没有 LICENSE 文件。 */
|
|
39
|
+
readonly repo: "DhvanilPatel/swarm-cursors";
|
|
40
|
+
/** 入仓的那份原始文件(**未经补丁**)。diff 的基准。 */
|
|
41
|
+
readonly vendoredFile: "assets/swarm-rebuild.upstream.js";
|
|
42
|
+
/**
|
|
43
|
+
* 入仓时那份上游文件的 SHA-256。
|
|
44
|
+
* `scripts/swarm-patch.mjs` 的 `UPSTREAM_SHA256` 必须与它逐字符相同(测试会比对),
|
|
45
|
+
* 否则"补丁按哪一版写的"就说不清了。
|
|
46
|
+
*/
|
|
47
|
+
readonly sha256: "6a30f19daaa182d52673035feaeae775c398fa426655cc71a87a451fe00c7bfe";
|
|
48
|
+
/** 补丁后的产出(**随包分发的那份**)。 */
|
|
49
|
+
readonly patchedFile: "assets/swarm-rebuild.js";
|
|
50
|
+
/**
|
|
51
|
+
* 授权状态:`none` = 仓库没有许可证;`permission` = 无许可证文本,但有作者的书面
|
|
52
|
+
* 邮件许可(本项目:Dhvanil 2026-11-11 打包 + 修改;Joel 2026-11-08 效果 + 署名)。
|
|
53
|
+
* 它不是"宽松许可",第三方不能据此复用。
|
|
54
|
+
*/
|
|
55
|
+
readonly license: "permission";
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Tier A —— 硬接口。上游官方句柄树,补丁完全没碰,插件主路径全走这里。
|
|
59
|
+
*
|
|
60
|
+
* 事实依据(2026-11-10 在 `assets/swarm-rebuild.upstream.js` 里核对过):
|
|
61
|
+
* 上游用 `const KEY = "__swarmRebuild"; window[KEY] = { toggle() {...}, stop(...) }` 暴露自己,
|
|
62
|
+
* 并把 `app`(engine / env / view / tour)挂在同一个对象上。这一段补丁一字未改。
|
|
63
|
+
*/
|
|
64
|
+
export declare const SWARM_API: {
|
|
65
|
+
/** 上游暴露自己的全局名。脚本第二次注入时会自己 `toggle()`。 */
|
|
66
|
+
readonly global: "__swarmRebuild";
|
|
67
|
+
/** 句柄上的方法。 */
|
|
68
|
+
readonly methods: {
|
|
69
|
+
readonly toggle: "toggle";
|
|
70
|
+
/**
|
|
71
|
+
* 停止。补丁后的脚本里 `stop(false)` = 立刻 destroy(取消 RAF、移除宿主),
|
|
72
|
+
* `stop(true)` = 淡出回真页面(原来 "Go live" 按钮走的路)。
|
|
73
|
+
* **插件一律用 `false`**:要的是立刻让路,不是让用户看淡出。
|
|
74
|
+
*/
|
|
75
|
+
readonly stop: "stop";
|
|
76
|
+
};
|
|
77
|
+
/** `stop(reveal)` 的两种语义。 */
|
|
78
|
+
readonly stopArgs: {
|
|
79
|
+
/** 立即销毁,不留残余覆盖层。插件用这个。 */
|
|
80
|
+
readonly immediate: false;
|
|
81
|
+
/** 淡出到真页面。 */
|
|
82
|
+
readonly reveal: true;
|
|
83
|
+
};
|
|
84
|
+
/** `window.__swarmRebuild.app` 上插件用到的字段。 */
|
|
85
|
+
readonly app: {
|
|
86
|
+
/** 引擎:速度/启动都在这里。 */
|
|
87
|
+
readonly engine: {
|
|
88
|
+
readonly path: "engine";
|
|
89
|
+
/** 速度,上游初值 `1`;上游 UI 的 `speeds = [1, 2, 3]`,按钮点 N 次即 1+N 倍。**可直接写。** */
|
|
90
|
+
readonly speed: "speed";
|
|
91
|
+
/** 启动引擎(`app.tour = true` 之后调它才真正开始往下滚)。 */
|
|
92
|
+
readonly start: "start";
|
|
93
|
+
/** 是否已启动。已是 `true` 就别再 `start()` 一遍。 */
|
|
94
|
+
readonly started: "started";
|
|
95
|
+
};
|
|
96
|
+
/** 环境探针:整页滚动 / 局部滚动(`overflow:auto` 容器)都靠它。 */
|
|
97
|
+
readonly env: {
|
|
98
|
+
readonly path: "env";
|
|
99
|
+
/** `() => { ox, oy, sx, sy, vw, vh }`。局部滚动页里 `window.scrollY` 恒为 0,只有它知道真实位置。 */
|
|
100
|
+
readonly geom: "geom";
|
|
101
|
+
/** `(y, x) => void`,`behavior: 'instant'`。还原滚动位置用。 */
|
|
102
|
+
readonly scrollTo: "scrollTo";
|
|
103
|
+
};
|
|
104
|
+
/** 视图:影子树的入口。 */
|
|
105
|
+
readonly view: {
|
|
106
|
+
readonly path: "view";
|
|
107
|
+
/** 影子树(上游以 `mode: 'open'` 创建)。 */
|
|
108
|
+
readonly root: "root";
|
|
109
|
+
};
|
|
110
|
+
/** 整页模式开关。上游 Tour 按钮只做两件事之一就是这个;补丁删了按钮,所以直接写字段。 */
|
|
111
|
+
readonly tour: "tour";
|
|
112
|
+
};
|
|
113
|
+
/** 句柄自身的影子树兜底(`app.view.root` 拿不到时用它)。 */
|
|
114
|
+
readonly rootFallback: "root";
|
|
115
|
+
/** 覆盖层的宿主元素:自定义元素标签,`document.querySelector` 可直接选到。 */
|
|
116
|
+
readonly hostTag: "swarm-rebuild";
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Tier B —— 兜底口。上游原生,但**已被补丁删除**,插件只在"塞回了未打补丁的原版"时才用到。
|
|
120
|
+
*
|
|
121
|
+
* 2026-11-10 核对 `assets/swarm-rebuild.js`:`data-a=` 从上游的 11 处降到 2 处 ——
|
|
122
|
+
* 控制条整个被删(`scripts/swarm-patch.mjs` 第 1 处改动),剩下两处是
|
|
123
|
+
* `[data-a="sound"]`(有 `if (!b || !this.sfx)` 防护)和一处 `name` 拼接的通用选择器。
|
|
124
|
+
*
|
|
125
|
+
* **所以这一层不是主路径。** 主路径全在 Tier A。这里留着是为了:有人把未打补丁的原版
|
|
126
|
+
* 塞回 `assets/`,插件仍然能用(点按钮),而不是当场白屏。
|
|
127
|
+
*/
|
|
128
|
+
export declare const SWARM_DOM: {
|
|
129
|
+
/** 控制条容器。CSS 里定义:`.hud` / `.hud.on` / `.hud .dots` / `.hud .txt` / `.hud .status`。 */
|
|
130
|
+
readonly hud: ".hud";
|
|
131
|
+
/** 头像组(控制条上的小圆点)。 */
|
|
132
|
+
readonly dots: ".hud .dots";
|
|
133
|
+
/** 状态文字。 */
|
|
134
|
+
readonly status: ".hud .status";
|
|
135
|
+
/** 进度条:`<div class="bar"><i></i></div>`。 */
|
|
136
|
+
readonly barFill: ".hud .bar i";
|
|
137
|
+
/** 插件往影子树里插的隐藏规则所带的标记(重试时据此判断"已经插过了")。 */
|
|
138
|
+
readonly hideStyleAttr: "data-dsh-swarm-hide";
|
|
139
|
+
/** 隐藏规则本体。用 CSSOM/内联 + 这条规则双层压住,重建的节点也一并压。 */
|
|
140
|
+
readonly hideStyleText: ".hud{display:none!important}";
|
|
141
|
+
/**
|
|
142
|
+
* 控制条按钮。`selector` 是插件实际会用 `querySelector` 去点的那个字符串。
|
|
143
|
+
*
|
|
144
|
+
* `upstreamQuery` 标的是**另一件事**:上游自己的代码里有没有出现过这个选择器。
|
|
145
|
+
* 两者不等价,`close` 就是反例 —— 上游 HTML 里确实渲染了 `<button data-a="close" class="x">✕</button>`,
|
|
146
|
+
* 但它的代码从不 `querySelector('[data-a="close"]')`,点击走事件委托。
|
|
147
|
+
* 也就是说这一条是**我们自己声称有效**的选择器,靠的是 DOM 属性确实存在,而不是上游验证过。
|
|
148
|
+
* 上游哪天把 `data-a` 换成别的属性名、HTML 模板改了而代码没改(反之亦然),差别就在这里。
|
|
149
|
+
*/
|
|
150
|
+
readonly buttons: {
|
|
151
|
+
/** 整页自动滚动。插件不点它(改 `app.tour = true`),留着仅为完整性。上游有查询。 */
|
|
152
|
+
readonly tour: {
|
|
153
|
+
readonly action: "tour";
|
|
154
|
+
readonly selector: "[data-a=\"tour\"]";
|
|
155
|
+
readonly upstreamQuery: true;
|
|
156
|
+
};
|
|
157
|
+
/** 帧速切换(1× → 2× → 3×)。插件直接写 `engine.speed`,不点它。上游有查询。 */
|
|
158
|
+
readonly speed: {
|
|
159
|
+
readonly action: "speed";
|
|
160
|
+
readonly selector: "[data-a=\"speed\"]";
|
|
161
|
+
readonly upstreamQuery: true;
|
|
162
|
+
};
|
|
163
|
+
/** 声音开关(快捷键 M)。插件从不碰。上游有查询(带 `if (!b || !this.sfx)` 空值防护)。 */
|
|
164
|
+
readonly sound: {
|
|
165
|
+
readonly action: "sound";
|
|
166
|
+
readonly selector: "[data-a=\"sound\"]";
|
|
167
|
+
readonly upstreamQuery: true;
|
|
168
|
+
};
|
|
169
|
+
/** Go live = `stop(true)`。兜底退出路径之一。上游有查询。 */
|
|
170
|
+
readonly live: {
|
|
171
|
+
readonly action: "live";
|
|
172
|
+
readonly selector: "[data-a=\"live\"]";
|
|
173
|
+
readonly upstreamQuery: true;
|
|
174
|
+
};
|
|
175
|
+
/** 重播。插件从不碰。上游有查询。 */
|
|
176
|
+
readonly replay: {
|
|
177
|
+
readonly action: "replay";
|
|
178
|
+
readonly selector: "[data-a=\"replay\"]";
|
|
179
|
+
readonly upstreamQuery: true;
|
|
180
|
+
};
|
|
181
|
+
/** ✕ = `stop(false)`。兜底退出路径之二。**上游不查询它**(事件委托),仅 HTML 属性存在。 */
|
|
182
|
+
readonly close: {
|
|
183
|
+
readonly action: "close";
|
|
184
|
+
readonly selector: "[data-a=\"close\"]";
|
|
185
|
+
readonly upstreamQuery: false;
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
/** 退出时的兜底顺序:先 live 再 close(对应 `stop(true)` / `stop(false)`)。 */
|
|
189
|
+
readonly exitFallbackOrder: readonly ["[data-a=\"live\"]", "[data-a=\"close\"]"];
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Tier C —— 自造桥。插件注入页面、上游永远不会提供的那些全局名。
|
|
193
|
+
*
|
|
194
|
+
* 上游改版跟这一层**无关**(它根本不认识这些名字)。这一层坏的原因通常是插件自己改了
|
|
195
|
+
* 注入时机、或者有人重构时漏了某个赋值 —— 所以它更需要测试,而不是文档。
|
|
196
|
+
*/
|
|
197
|
+
export declare const SWARM_BRIDGE: {
|
|
198
|
+
/** `() => void`,插件注入的"立刻退出"入口。**Tier A 的 `stop(false)` 走不通时才用。** */
|
|
199
|
+
readonly stop: "__dshSwarmStop";
|
|
200
|
+
/** `{ x, y }`,播放前的滚动位置。局部滚动页由 `env.geom()` 覆盖掉 window 那份。 */
|
|
201
|
+
readonly start: "__dshSwarmStart";
|
|
202
|
+
/** `true` = 引擎已在 `addInitScript` 阶段提前起来,**绝不能再注入第二遍**(否则白底和光标全部重来)。 */
|
|
203
|
+
readonly early: "__dshSwarmEarly";
|
|
204
|
+
/** `true` = AI 已经在用这个页面,整页滚动循环看到它就停。 */
|
|
205
|
+
readonly aiWorking: "__dshSwarmAiWorking";
|
|
206
|
+
/** `true` = 快照已经拿到手,可以放行扫描。`swarm-patch.mjs` 第 5 处改动等它。 */
|
|
207
|
+
readonly go: "__dshSwarmGo";
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* 上游自带的诊断钩子。插件**不依赖**它们,列在这里是为了两件事:
|
|
211
|
+
* ① 调试时知道这些名字是谁的,别误当成我们的桥;
|
|
212
|
+
* ② 上游改版核对时,这些名字若消失,说明改动范围比预期大,值得警觉。
|
|
213
|
+
*/
|
|
214
|
+
export declare const SWARM_UPSTREAM_DEBUG_HOOKS: readonly ["__swarmShot", "__swarmSfxLog", "__swarmHidden", "__swarmUnhide"];
|
|
215
|
+
/**
|
|
216
|
+
* 上游改版应急流程。**这是"有据可依"的落点** —— 不是祈祷上游别改,而是改了好核对。
|
|
217
|
+
*
|
|
218
|
+
* 触发条件:把新的上游文件换进 `assets/swarm-rebuild.upstream.js`(这一步必须是人主动做的)。
|
|
219
|
+
* 本地日常开发**不需要**做任何事:vendored 文件不会自己变。
|
|
220
|
+
*
|
|
221
|
+
* 核对顺序(严格按此序,否则会追错方向):
|
|
222
|
+
* 1. `npm run swarm:patch` —— SHA 不符会当场拒绝,并报出新旧两个哈希。**先看这一条。**
|
|
223
|
+
* 2. 按新哈希更新 `SWARM_UPSTREAM.sha256` 和 `scripts/swarm-patch.mjs` 的 `UPSTREAM_SHA256`
|
|
224
|
+
* (两处必须一致,测试会比对)。
|
|
225
|
+
* 3. `node --test tests/swarm-contract.test.ts` —— 它会逐条到新文件里找 Tier A/B 的每个名字。
|
|
226
|
+
* 红的每一条,就是上游这次真动过的地方,一条一条处理。
|
|
227
|
+
* 4. 只有 Tier A 变红才是**必须改代码**的。Tier B 变红通常只需更新选择器(它只是兜底)。
|
|
228
|
+
* Tier C 变红说明插件自己的桥出了问题,与上游无关。
|
|
229
|
+
* 5. 改完实现后,跑 `npm test` 全量 —— `swarm-intro.test.ts` 会用假 page 校验调用序列。
|
|
230
|
+
* 6. 最后更新 `docs/SWARM-CODE.md`(改了几处、为什么)和 `docs/SWARM-CONTRACT.md` 的指纹段。
|
|
231
|
+
*/
|
|
232
|
+
export declare const SWARM_UPGRADE_CHECKLIST: readonly ["npm run swarm:patch", "sync SWARM_UPSTREAM.sha256 <-> scripts/swarm-patch.mjs UPSTREAM_SHA256", "node --test tests/swarm-contract.test.ts", "triage red: Tier A = code change, Tier B = selector update, Tier C = our own bug", "npm test", "update docs/SWARM-CODE.md + docs/SWARM-CONTRACT.md fingerprints"];
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* swarm-contract — 这段第三方资产的**接口契约**:插件到底捏着上游脚本的哪几个把手。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有这个文件(2026-11-10):
|
|
5
|
+
*
|
|
6
|
+
* 起因是"上游改版,我这边会不会突然失效"。查完事实是:**不会**。上游那份脚本是
|
|
7
|
+
* vendored 进来的(`assets/swarm-rebuild.upstream.js`),SHA-256 在 `scripts/swarm-patch.mjs`
|
|
8
|
+
* 里锁死,上游在 GitHub 上改成什么样,本地这份一个字节都不会动。真正会失效的是另一件事 ——
|
|
9
|
+
* 插件对上游内部结构的依赖**散落在三个文件的字符串里**(`swarm-intro.ts` 里 20 余处
|
|
10
|
+
* `page.evaluate` 的字面量、`swarm-curtain.ts` 的桥、`swarm-patch.mjs` 的匹配片段),
|
|
11
|
+
* 没有任何一处声明"这是我们要的接口"。于是:
|
|
12
|
+
*
|
|
13
|
+
* - 加一处依赖,没人知道又多了个耦合点;
|
|
14
|
+
* - 补丁改一处,插件侧不知道自己的前提已经变了;
|
|
15
|
+
* - `docs/SWARM-CODE.md` 写着"改动 4 处",实际已经是 7 处 —— 文档对不上代码,
|
|
16
|
+
* 而这份文档正是"上游改版时该查哪里"的唯一依据。
|
|
17
|
+
*
|
|
18
|
+
* 所以这里把三个层次钉死,并交给 `tests/swarm-contract.test.ts` 机械校验:
|
|
19
|
+
*
|
|
20
|
+
* Tier A(硬接口) 上游自己挂在 window 上的句柄树。补丁一处都没动,插件主路径全走这里。
|
|
21
|
+
* 上游改了 → 插件立刻坏 → 这是**必须盯住**的部分。
|
|
22
|
+
* Tier B(兜底口) `.hud` / `[data-a=*]`。上游原生,但**已被我们的补丁删掉**。
|
|
23
|
+
* 插件只在"万一有人塞回未打补丁的原版"时才走这些分支。
|
|
24
|
+
* Tier C(自造桥) `__dshSwarm*`。我们自己注入的,上游永远不会提供,也不该指望。
|
|
25
|
+
*
|
|
26
|
+
* 契约的用法:**改任何一处依赖,先改这个文件**,再改实现,再看测试红绿。测试会去
|
|
27
|
+
* `assets/swarm-rebuild.upstream.js` 里逐条核对下面的每个名字是否真的存在 —— 上游换人、
|
|
28
|
+
* 改名、删节点,这里第一个报错,而不是等某天浏览器里莫名其妙不播了。
|
|
29
|
+
*
|
|
30
|
+
* 法律面(0.8.1 起):上游仓库根目录**仍然没有 LICENSE**(默认保留所有权利),
|
|
31
|
+
* 但 Dhvanil Patel 于 2026-11-11 邮件书面许可本项目**打包与修改**,Joel Califa 于
|
|
32
|
+
* 2026-11-08 书面许可并仅要求署名。所以资产可以随包分发,**但授权不是 MIT**:
|
|
33
|
+
* 它是邮件许可,只覆盖本项目,不给第三方通用许可。别把这份资产 fork 成
|
|
34
|
+
* "我们自己的版本" —— fork 不改变任何人拿到的权利,只会多出一份没有许可的副本。
|
|
35
|
+
*/
|
|
36
|
+
/** 上游出处与入仓指纹。这三项之外的任何东西,都不该成为插件的依赖。 */
|
|
37
|
+
export const SWARM_UPSTREAM = {
|
|
38
|
+
/** 上游仓库。注意:根目录没有 LICENSE 文件。 */
|
|
39
|
+
repo: 'DhvanilPatel/swarm-cursors',
|
|
40
|
+
/** 入仓的那份原始文件(**未经补丁**)。diff 的基准。 */
|
|
41
|
+
vendoredFile: 'assets/swarm-rebuild.upstream.js',
|
|
42
|
+
/**
|
|
43
|
+
* 入仓时那份上游文件的 SHA-256。
|
|
44
|
+
* `scripts/swarm-patch.mjs` 的 `UPSTREAM_SHA256` 必须与它逐字符相同(测试会比对),
|
|
45
|
+
* 否则"补丁按哪一版写的"就说不清了。
|
|
46
|
+
*/
|
|
47
|
+
sha256: '6a30f19daaa182d52673035feaeae775c398fa426655cc71a87a451fe00c7bfe',
|
|
48
|
+
/** 补丁后的产出(**随包分发的那份**)。 */
|
|
49
|
+
patchedFile: 'assets/swarm-rebuild.js',
|
|
50
|
+
/**
|
|
51
|
+
* 授权状态:`none` = 仓库没有许可证;`permission` = 无许可证文本,但有作者的书面
|
|
52
|
+
* 邮件许可(本项目:Dhvanil 2026-11-11 打包 + 修改;Joel 2026-11-08 效果 + 署名)。
|
|
53
|
+
* 它不是"宽松许可",第三方不能据此复用。
|
|
54
|
+
*/
|
|
55
|
+
license: 'permission',
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Tier A —— 硬接口。上游官方句柄树,补丁完全没碰,插件主路径全走这里。
|
|
59
|
+
*
|
|
60
|
+
* 事实依据(2026-11-10 在 `assets/swarm-rebuild.upstream.js` 里核对过):
|
|
61
|
+
* 上游用 `const KEY = "__swarmRebuild"; window[KEY] = { toggle() {...}, stop(...) }` 暴露自己,
|
|
62
|
+
* 并把 `app`(engine / env / view / tour)挂在同一个对象上。这一段补丁一字未改。
|
|
63
|
+
*/
|
|
64
|
+
export const SWARM_API = {
|
|
65
|
+
/** 上游暴露自己的全局名。脚本第二次注入时会自己 `toggle()`。 */
|
|
66
|
+
global: '__swarmRebuild',
|
|
67
|
+
/** 句柄上的方法。 */
|
|
68
|
+
methods: {
|
|
69
|
+
toggle: 'toggle',
|
|
70
|
+
/**
|
|
71
|
+
* 停止。补丁后的脚本里 `stop(false)` = 立刻 destroy(取消 RAF、移除宿主),
|
|
72
|
+
* `stop(true)` = 淡出回真页面(原来 "Go live" 按钮走的路)。
|
|
73
|
+
* **插件一律用 `false`**:要的是立刻让路,不是让用户看淡出。
|
|
74
|
+
*/
|
|
75
|
+
stop: 'stop',
|
|
76
|
+
},
|
|
77
|
+
/** `stop(reveal)` 的两种语义。 */
|
|
78
|
+
stopArgs: {
|
|
79
|
+
/** 立即销毁,不留残余覆盖层。插件用这个。 */
|
|
80
|
+
immediate: false,
|
|
81
|
+
/** 淡出到真页面。 */
|
|
82
|
+
reveal: true,
|
|
83
|
+
},
|
|
84
|
+
/** `window.__swarmRebuild.app` 上插件用到的字段。 */
|
|
85
|
+
app: {
|
|
86
|
+
/** 引擎:速度/启动都在这里。 */
|
|
87
|
+
engine: {
|
|
88
|
+
path: 'engine',
|
|
89
|
+
/** 速度,上游初值 `1`;上游 UI 的 `speeds = [1, 2, 3]`,按钮点 N 次即 1+N 倍。**可直接写。** */
|
|
90
|
+
speed: 'speed',
|
|
91
|
+
/** 启动引擎(`app.tour = true` 之后调它才真正开始往下滚)。 */
|
|
92
|
+
start: 'start',
|
|
93
|
+
/** 是否已启动。已是 `true` 就别再 `start()` 一遍。 */
|
|
94
|
+
started: 'started',
|
|
95
|
+
},
|
|
96
|
+
/** 环境探针:整页滚动 / 局部滚动(`overflow:auto` 容器)都靠它。 */
|
|
97
|
+
env: {
|
|
98
|
+
path: 'env',
|
|
99
|
+
/** `() => { ox, oy, sx, sy, vw, vh }`。局部滚动页里 `window.scrollY` 恒为 0,只有它知道真实位置。 */
|
|
100
|
+
geom: 'geom',
|
|
101
|
+
/** `(y, x) => void`,`behavior: 'instant'`。还原滚动位置用。 */
|
|
102
|
+
scrollTo: 'scrollTo',
|
|
103
|
+
},
|
|
104
|
+
/** 视图:影子树的入口。 */
|
|
105
|
+
view: {
|
|
106
|
+
path: 'view',
|
|
107
|
+
/** 影子树(上游以 `mode: 'open'` 创建)。 */
|
|
108
|
+
root: 'root',
|
|
109
|
+
},
|
|
110
|
+
/** 整页模式开关。上游 Tour 按钮只做两件事之一就是这个;补丁删了按钮,所以直接写字段。 */
|
|
111
|
+
tour: 'tour',
|
|
112
|
+
},
|
|
113
|
+
/** 句柄自身的影子树兜底(`app.view.root` 拿不到时用它)。 */
|
|
114
|
+
rootFallback: 'root',
|
|
115
|
+
/** 覆盖层的宿主元素:自定义元素标签,`document.querySelector` 可直接选到。 */
|
|
116
|
+
hostTag: 'swarm-rebuild',
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Tier B —— 兜底口。上游原生,但**已被补丁删除**,插件只在"塞回了未打补丁的原版"时才用到。
|
|
120
|
+
*
|
|
121
|
+
* 2026-11-10 核对 `assets/swarm-rebuild.js`:`data-a=` 从上游的 11 处降到 2 处 ——
|
|
122
|
+
* 控制条整个被删(`scripts/swarm-patch.mjs` 第 1 处改动),剩下两处是
|
|
123
|
+
* `[data-a="sound"]`(有 `if (!b || !this.sfx)` 防护)和一处 `name` 拼接的通用选择器。
|
|
124
|
+
*
|
|
125
|
+
* **所以这一层不是主路径。** 主路径全在 Tier A。这里留着是为了:有人把未打补丁的原版
|
|
126
|
+
* 塞回 `assets/`,插件仍然能用(点按钮),而不是当场白屏。
|
|
127
|
+
*/
|
|
128
|
+
export const SWARM_DOM = {
|
|
129
|
+
/** 控制条容器。CSS 里定义:`.hud` / `.hud.on` / `.hud .dots` / `.hud .txt` / `.hud .status`。 */
|
|
130
|
+
hud: '.hud',
|
|
131
|
+
/** 头像组(控制条上的小圆点)。 */
|
|
132
|
+
dots: '.hud .dots',
|
|
133
|
+
/** 状态文字。 */
|
|
134
|
+
status: '.hud .status',
|
|
135
|
+
/** 进度条:`<div class="bar"><i></i></div>`。 */
|
|
136
|
+
barFill: '.hud .bar i',
|
|
137
|
+
/** 插件往影子树里插的隐藏规则所带的标记(重试时据此判断"已经插过了")。 */
|
|
138
|
+
hideStyleAttr: 'data-dsh-swarm-hide',
|
|
139
|
+
/** 隐藏规则本体。用 CSSOM/内联 + 这条规则双层压住,重建的节点也一并压。 */
|
|
140
|
+
hideStyleText: '.hud{display:none!important}',
|
|
141
|
+
/**
|
|
142
|
+
* 控制条按钮。`selector` 是插件实际会用 `querySelector` 去点的那个字符串。
|
|
143
|
+
*
|
|
144
|
+
* `upstreamQuery` 标的是**另一件事**:上游自己的代码里有没有出现过这个选择器。
|
|
145
|
+
* 两者不等价,`close` 就是反例 —— 上游 HTML 里确实渲染了 `<button data-a="close" class="x">✕</button>`,
|
|
146
|
+
* 但它的代码从不 `querySelector('[data-a="close"]')`,点击走事件委托。
|
|
147
|
+
* 也就是说这一条是**我们自己声称有效**的选择器,靠的是 DOM 属性确实存在,而不是上游验证过。
|
|
148
|
+
* 上游哪天把 `data-a` 换成别的属性名、HTML 模板改了而代码没改(反之亦然),差别就在这里。
|
|
149
|
+
*/
|
|
150
|
+
buttons: {
|
|
151
|
+
/** 整页自动滚动。插件不点它(改 `app.tour = true`),留着仅为完整性。上游有查询。 */
|
|
152
|
+
tour: { action: 'tour', selector: '[data-a="tour"]', upstreamQuery: true },
|
|
153
|
+
/** 帧速切换(1× → 2× → 3×)。插件直接写 `engine.speed`,不点它。上游有查询。 */
|
|
154
|
+
speed: { action: 'speed', selector: '[data-a="speed"]', upstreamQuery: true },
|
|
155
|
+
/** 声音开关(快捷键 M)。插件从不碰。上游有查询(带 `if (!b || !this.sfx)` 空值防护)。 */
|
|
156
|
+
sound: { action: 'sound', selector: '[data-a="sound"]', upstreamQuery: true },
|
|
157
|
+
/** Go live = `stop(true)`。兜底退出路径之一。上游有查询。 */
|
|
158
|
+
live: { action: 'live', selector: '[data-a="live"]', upstreamQuery: true },
|
|
159
|
+
/** 重播。插件从不碰。上游有查询。 */
|
|
160
|
+
replay: { action: 'replay', selector: '[data-a="replay"]', upstreamQuery: true },
|
|
161
|
+
/** ✕ = `stop(false)`。兜底退出路径之二。**上游不查询它**(事件委托),仅 HTML 属性存在。 */
|
|
162
|
+
close: { action: 'close', selector: '[data-a="close"]', upstreamQuery: false },
|
|
163
|
+
},
|
|
164
|
+
/** 退出时的兜底顺序:先 live 再 close(对应 `stop(true)` / `stop(false)`)。 */
|
|
165
|
+
exitFallbackOrder: ['[data-a="live"]', '[data-a="close"]'],
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Tier C —— 自造桥。插件注入页面、上游永远不会提供的那些全局名。
|
|
169
|
+
*
|
|
170
|
+
* 上游改版跟这一层**无关**(它根本不认识这些名字)。这一层坏的原因通常是插件自己改了
|
|
171
|
+
* 注入时机、或者有人重构时漏了某个赋值 —— 所以它更需要测试,而不是文档。
|
|
172
|
+
*/
|
|
173
|
+
export const SWARM_BRIDGE = {
|
|
174
|
+
/** `() => void`,插件注入的"立刻退出"入口。**Tier A 的 `stop(false)` 走不通时才用。** */
|
|
175
|
+
stop: '__dshSwarmStop',
|
|
176
|
+
/** `{ x, y }`,播放前的滚动位置。局部滚动页由 `env.geom()` 覆盖掉 window 那份。 */
|
|
177
|
+
start: '__dshSwarmStart',
|
|
178
|
+
/** `true` = 引擎已在 `addInitScript` 阶段提前起来,**绝不能再注入第二遍**(否则白底和光标全部重来)。 */
|
|
179
|
+
early: '__dshSwarmEarly',
|
|
180
|
+
/** `true` = AI 已经在用这个页面,整页滚动循环看到它就停。 */
|
|
181
|
+
aiWorking: '__dshSwarmAiWorking',
|
|
182
|
+
/** `true` = 快照已经拿到手,可以放行扫描。`swarm-patch.mjs` 第 5 处改动等它。 */
|
|
183
|
+
go: '__dshSwarmGo',
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* 上游自带的诊断钩子。插件**不依赖**它们,列在这里是为了两件事:
|
|
187
|
+
* ① 调试时知道这些名字是谁的,别误当成我们的桥;
|
|
188
|
+
* ② 上游改版核对时,这些名字若消失,说明改动范围比预期大,值得警觉。
|
|
189
|
+
*/
|
|
190
|
+
export const SWARM_UPSTREAM_DEBUG_HOOKS = ['__swarmShot', '__swarmSfxLog', '__swarmHidden', '__swarmUnhide'];
|
|
191
|
+
/**
|
|
192
|
+
* 上游改版应急流程。**这是"有据可依"的落点** —— 不是祈祷上游别改,而是改了好核对。
|
|
193
|
+
*
|
|
194
|
+
* 触发条件:把新的上游文件换进 `assets/swarm-rebuild.upstream.js`(这一步必须是人主动做的)。
|
|
195
|
+
* 本地日常开发**不需要**做任何事:vendored 文件不会自己变。
|
|
196
|
+
*
|
|
197
|
+
* 核对顺序(严格按此序,否则会追错方向):
|
|
198
|
+
* 1. `npm run swarm:patch` —— SHA 不符会当场拒绝,并报出新旧两个哈希。**先看这一条。**
|
|
199
|
+
* 2. 按新哈希更新 `SWARM_UPSTREAM.sha256` 和 `scripts/swarm-patch.mjs` 的 `UPSTREAM_SHA256`
|
|
200
|
+
* (两处必须一致,测试会比对)。
|
|
201
|
+
* 3. `node --test tests/swarm-contract.test.ts` —— 它会逐条到新文件里找 Tier A/B 的每个名字。
|
|
202
|
+
* 红的每一条,就是上游这次真动过的地方,一条一条处理。
|
|
203
|
+
* 4. 只有 Tier A 变红才是**必须改代码**的。Tier B 变红通常只需更新选择器(它只是兜底)。
|
|
204
|
+
* Tier C 变红说明插件自己的桥出了问题,与上游无关。
|
|
205
|
+
* 5. 改完实现后,跑 `npm test` 全量 —— `swarm-intro.test.ts` 会用假 page 校验调用序列。
|
|
206
|
+
* 6. 最后更新 `docs/SWARM-CODE.md`(改了几处、为什么)和 `docs/SWARM-CONTRACT.md` 的指纹段。
|
|
207
|
+
*/
|
|
208
|
+
export const SWARM_UPGRADE_CHECKLIST = [
|
|
209
|
+
'npm run swarm:patch',
|
|
210
|
+
'sync SWARM_UPSTREAM.sha256 <-> scripts/swarm-patch.mjs UPSTREAM_SHA256',
|
|
211
|
+
'node --test tests/swarm-contract.test.ts',
|
|
212
|
+
'triage red: Tier A = code change, Tier B = selector update, Tier C = our own bug',
|
|
213
|
+
'npm test',
|
|
214
|
+
'update docs/SWARM-CODE.md + docs/SWARM-CONTRACT.md fingerprints',
|
|
215
|
+
];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
/**
|
|
3
|
+
* 生成注册进 `addInitScript` 的脚本全文。
|
|
4
|
+
*
|
|
5
|
+
* 必须用**字符串拼接**把资产塞进去,不能用模板字符串:资产里有大量反引号和
|
|
6
|
+
* `${...}`,放进外层模板字符串会被当成插值直接炸掉。
|
|
7
|
+
* 每次现读资产:和 playSwarm 里 `readScript()` 一样,改完磁盘立即用新的,不缓存。
|
|
8
|
+
*/
|
|
9
|
+
export declare function swarmBootScript(): string;
|
|
10
|
+
/** 导航前打标记:下一次文档创建时,addInitScript 会把原版引擎提前启动。 */
|
|
11
|
+
export declare function armCurtain(page: Page): Promise<void>;
|
|
12
|
+
/** 清掉导航标记(导航失败时别让它留到下一次普通跳转)。 */
|
|
13
|
+
export declare function clearCurtainToken(page: Page): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* 决定"这一页不播"时:把可能已经提前启动的引擎彻底关掉。
|
|
16
|
+
*
|
|
17
|
+
* 这层原是"撤掉自造白幕",现在没有白幕了 —— 引擎是真的、会一直等放行,所以必须
|
|
18
|
+
* 调它自己的 stop(false)、把宿主立刻摘掉、把滚动还原,否则那一页会永远盖着白。
|
|
19
|
+
* 触发场景:遮罩页不播、CSP 把影子树样式表挡成半成品、oncePerSite 判定跳过、
|
|
20
|
+
* 用户关掉特效等。顺手把标记和几个开关清干净,免得同文档内 toggle/replay 被旧值影响。
|
|
21
|
+
*/
|
|
22
|
+
export declare function dropCurtain(page: Page, _waitForSwarm?: boolean): Promise<void>;
|
|
@@ -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.1 起随包分发,理论上不会发生;手工删包时会)。这里**绝不能往外抛**:
|
|
40
|
+
// 两个调用点都是 `await ctx.addInitScript({ content: swarmBootScript() })`,
|
|
41
|
+
// 抛出去就是上下文创建失败、浏览器直接打不开 —— 把"动效没了"放大成"插件挂了"。
|
|
42
|
+
// 返回一段空注释脚本,等于这次启动不注入任何东西,行为与"没开这个功能"一致。
|
|
43
|
+
return '/* dsh: swarm asset missing — 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
|
+
}
|