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 CHANGED
@@ -1,3 +1,88 @@
1
+ ## 0.8.0 — 2026-11-10
2
+
3
+ 一句话摘要:**动效资产被移出 npm 发布包,swarm 默认关掉**——这是主动选的合规路径,不是修 bug:那套资产的授权状态不允许再分发,与其在授权上纠缠,不如不让它随包出去。代码一行没删,本地开发者把资产放回 `assets/` 打开开关就还是完整功能;代价是 npm 用户升级后默认看不到这个效果。顺带把 0.7.9 那批修复一起封进来——npm 上的 `latest` 一直停在 0.7.8,`v0.7.9` 只是本地 tag,所以从 0.7.8 直接升上来的用户会一次拿到两个版本的全部内容。
4
+
5
+ ### Breaking
6
+
7
+ - **`swarm.enabled` 默认值 `true` → `false`**:出厂不再播动效。资产不在包里的时候,默认开等于让每个新用户在"文件根本不存在"的状态下开箱即撞。
8
+ - **`assets/` 不再进入 npm 包**:`package.json` 的 `files` 移除了 `assets` 与 `!assets/swarm-rebuild.upstream.js`。仓库里两个文件都还在(`swarm-rebuild.upstream.js` 原始基线 + `swarm-rebuild.js` 补丁后产物),`npm run swarm:check` 的 SHA 校验照旧在本地生效,只是发布出去的 tarball 里没有了。
9
+ - **资产缺失现在静默降级,不是崩溃**:`swarmBootScript()` 读资产改成 `try/catch`,读不到就返回一段空注释脚本。这一点是本次改动的**安全底线**——它的两个调用点都长在 `await ctx.addInitScript({ content: swarmBootScript() })` 上,原来裸读会让资产缺失直接把上下文创建打断,浏览器连开都开不了,等于把"动效没了"放大成"插件挂了"。
10
+ - **`playSwarm` 的旧兜底继续有效**:`readScript()` 早就是 `catch → return`(资源缺失就当没这个功能),这次没有动它。
11
+
12
+ ### 迁移
13
+
14
+ | 你的情况 | 升级后表现 | 要做的事 |
15
+ | --- | --- | --- |
16
+ | 只是装包用浏览器 | 动效不再出现,其余功能不变 | 无需操作 |
17
+ | 想要动效 | 默认关,开了也看不到(包里没资产) | 把 `assets/swarm-rebuild.js` 放进包根 `assets/`,再把设置卡里 swarm 的 `enabled` 打开 |
18
+ | 本地从仓库开发 | 资产就在仓库里,开关一打开即恢复 | 无 |
19
+
20
+ 以下这批是 0.7.9 的内容:**已经写好、但从未在 npm 上发布过**(`latest` 一直是 0.7.8,`v0.7.9` 只有本地 tag),随本版首次交付。
21
+
22
+ 一句话摘要:**0.7.9 的行为对了,话没跟上**——超时之后实际做的是"把那一页关掉",报错文案却还在承诺"下一次调用会重开会话";用户会莫名其妙发现标签页少了一个,而且页面上没提交的内容跟着没了,全程没人告诉他。顺手补上真正走完整链路的回归测试:0.7.9 那三条是单元级假绿,删掉 `capture()` 里那行关页调用照样全过。
23
+
24
+ ### Fixed
25
+
26
+ - **超时文案和真实行为对齐**:`withDeadline()` 新增 `consequence` 参数,由调用方写明超时之后**真的发生了什么**。`capture()` 现在报的是:那一页已经关掉、填了没提交的内容丢失、如果那是唯一标签则从空白页开始。默认值也去掉了"下一次调用会重开会话"这句不再成立的承诺。
27
+ - **关页代价写进代码注释**:`discardStuckPage()` 明说 `runBeforeUnload:false` 意味着不弹确认、未提交内容直接丢弃——对真卡死的页这笔账划算,但必须让人知道(现在由报错文案负责告知)。
28
+
29
+ ### Tests
30
+
31
+ - **新增 `tests/read-timeout-e2e.test.ts`(真端到端)**:真浏览器打开一页 → 主线程真死循环 → 读内容真超时 → 断言 ① 报错是 `ACTION_TIMEOUT` 且文案写明页被关、未提交内容丢失;② 那张卡死的页 `isClosed()`;③ 下一次 `acquire()` 拿到的不是同一页,且读得动(活页)。变异验证:把 `capture()` 里 `await this.discardStuckPage(page)` 注释掉,这条用例在"the wedged page must actually be closed"处红;恢复后绿(约 4.5 秒)。
32
+ - **诚实标注旧用例的覆盖面**:`tests/regressions.test.ts` 里那三条(两条直调私有方法塞假页、一条正则读源码)注释里写明只是单元级、`capture()` 不调它就失效,端到端证据在 `read-timeout-e2e.test.ts`。
33
+
34
+ ### Known trade-offs(未在本次行为里改动,仅记录)
35
+
36
+ - **导航默认不等 `load`**:首屏全靠脚本渲染的页面,快照可能拍到还没长出来的空壳;后面有"等页面稳定"兜着,影响被削弱。
37
+ - **启动锁重试的正则放宽**:任何启动超时都会触发"清一次锁再重试",卡住的原因与锁无关时(杀软拦截等)会白多等一个 30 秒窗口。
38
+
39
+ ---
40
+
41
+ ## 0.7.9 — 2026-11-10
42
+
43
+ 一句话摘要:**0.7.8 的硬期限只治了"读内容"那一步,四周还留着四个口子** ——最要紧的是读内容超时之后,下一次调用拿回来的还是同一个卡死的页面,所以 0.7.8 承诺的"自动换一个干净的句柄"从来没有真的发生。本版补上这一处,另外三处是顺势加固:启动阶段原本一次超时都没传、配置锁的重试一直在等一个不会出现的错误形状、导航默认等 `load` 让长连接页面白等到超时。这四项里只有第一项对得上 issue #5 报的"自愈无效",其余三项不是报告人提的,也不是原判定的缺口。发版自查时另外揪出一处 0.7.8 自己的毛病:期限定时器被 `unref`,进程里只剩它一个句柄时事件循环直接收工,那条 `ACTION_TIMEOUT` 永远发不出来——顺带让 7 条回归测试变成"未运行",而报告仍是 `fail 0`(详见下面第二条)。
44
+
45
+ ### Fixed
46
+
47
+ - **超时之后把卡死的页面原样发回来**:`invalidateOwner()` 只把条目从表里删掉,持久化模式下 `ensurePage()` 下一次又按索引把同一个页面交回去,于是"下一次调用重开会话"这句承诺落空,页面继续烂在原地。现在超时后由 `discardStuckPage()` 真正关闭那一页——带期限、尽力而为,关不掉也不拖住报错路径——下一个 `acquire()` 从活着的标签页开始。
48
+ - **期限定时器不该 `unref`**:0.7.8 写的 `timer.unref?.()` 会让"到点必须断"变成"到点看心情"——若这是进程里最后一个句柄,事件循环判定无事可做直接退出,调用方等不到超时、也等不到别的东西。同样因为这个,`tests/read-deadline.test.ts` 那 7 条在孤立运行时全部被打成 `cancelledByParent`(`Promise resolution is still pending but the event loop has already resolved`),而 `node --test` 只报 `fail 0`、不报错,看起来是一片绿。所以 0.7.8 记的"通过 278 / 跳过 3 / 失败 0"里,实际只有 271 条真的跑过。去掉 `unref` 后 7 条全部实跑通过。
49
+
50
+ ### Changed
51
+
52
+ - **启动阶段补上限**:`launchPersistentContext` 原来没传 timeout,配置目录被占用、单例握手卡住、GPU 初始化挂死都会让 `acquire()` 无限等下去。现在传 `max(navigationTimeoutMs, 30s)`——`navigationTimeoutMs` 允许低到 1 秒,冷启动浏览器根本不够用,所以另设 30 秒地板。
53
+ - **锁重试认错了错误形状**:原逻辑只匹配锁文件名,而 Playwright 把同一种情况报成 `Timeout 30000ms exceeded`,等于这段重试从来没触发过。现在两种形状都认。
54
+ - **导航默认不再等 `load`**:`navigate` / `open_tab` 的默认 `waitUntil` 从 `load` 改成 `domcontentloaded`。长轮询、流式、下载类页面的 `load` 永远不来,导航超时全花在一个早就可用的页面上。
55
+ - **README / PLUGIN-README 的 token 表按新测量重抄**:11,283 / 15,886 字符。
56
+
57
+ ### Tests
58
+
59
+ - 新增 3 条回归:卡死页只关一次、`close()` 不返回也不能再把错误路径挂住、两个默认值钉死。
60
+ - 全套 **305 条**:通过 302、跳过 3、失败 0,**302 + 3 = 305,一条没漏**。这次特意核对总数而不是只看 `fail 0`——0.7.8 就是在这个位置看走眼的。
61
+ - 复跑方式(本机 Node 22 需要显式打开类型剥离,否则 `npm test` 会以 `Unknown file extension ".ts"` 全崩):`set NODE_OPTIONS=--experimental-strip-types && npm test`。
62
+
63
+ ---
64
+
65
+ ## 0.7.8 — 2026-11-10
66
+
67
+ 一句话摘要:**工具调用不会再无限挂着** —— 打开网页之后"读页面内容"这一步一直没有超时上限,也不接取消信号,页面自己的脚本把浏览器主线程占住时,整个工具调用就停在那里等(GitHub issue #5 报的就是这个,实测能卡到分钟级)。现在读内容有硬期限、能按取消退出,超时后自动换一个干净的会话句柄。
68
+
69
+ ### Fixed
70
+
71
+ - **读页面内容没有超时(issue #5)**:`page.evaluate` 与 `ariaSnapshot` 都没有 timeout 参数,页面脚本占住主线程时它们永远不返回。实测定位:打开网页(`goto`)只用 **13 毫秒**就返回了,卡住的是之后读内容那一步——同一个页面读内容 **8.5 秒不返回**,此时按取消(1.5 秒)也**完全不生效**。新增 `src/read-deadline.ts`:`withDeadline()` 给可能永不返回的调用加硬期限,到点抛 `ACTION_TIMEOUT`(消息里写明是哪一步、多少毫秒、下次会重开会话),按取消抛 `AbortError`;原调用晚到的失败被吞掉,不会变成未捕获异常把宿主带崩。另有软超时模式返回空值,供可降级的场景使用。
72
+ - **超时后不会一直烂在那里**:`session.capture()` 是所有快照/读内容的必经点,超时时作废当前会话句柄、下次调用自动重建。持久化模式共用同一个浏览器上下文,所以**页面和登录状态都不丢,也不需要杀进程**——页面自己恢复后浏览器照常可用。修后实测:读内容 1500ms 硬期限 → 1509ms 抛错;取消 400ms → 406ms 抛 `AbortError`。
73
+ - **"等页面稳定"这一步也会卡**:`session.waitForPageStable()` 同样是一次页内调用,改成软超时——卡住就当"页面没稳定"照常往下走,保持原有的尽力而为语义。
74
+ - **取消信号能传到读内容这一步**:新增 `session.setSignal()` / `signal()`,工具层在 `acquire()` 时把这次调用的取消信号挂到会话上,于是快照、导航以及所有动作后的读取都能被取消,不用改十几个动作函数的签名。
75
+ - **`navigationTimeoutMs` 补下限**:原来只校验"是个数字",填 `0` 在 Playwright 里等于**永不超时**,正是这个 bug 的放大器。现在要求 ≥ 1000 毫秒,默认仍是 30 秒。
76
+ - **没有照抄报告人提的两个补丁**:他建议"启动加 20 秒超时""默认等 `domcontentloaded`",实测证明都解决不了问题(`goto` 本来就 13 毫秒返回)。真正对得上的只有上面两条。
77
+
78
+ ### Added
79
+
80
+ - **`tests/read-deadline.test.ts`(7 条)**:硬期限到点抛 `ACTION_TIMEOUT` 且消息含步骤名与毫秒数、到点前取消抛 `AbortError`(不被当成超时)、已 abort 的信号立即拒绝、正常返回不受影响、**原调用的迟到失败不会变成未捕获异常**、软超时返回空值不抛错、配置下限(`0` 与 `500` 被拒、`1000` 通过、默认 `30000`)。
81
+
82
+ ### Tests
83
+
84
+ - 全套 274 → **281 条**(+7):通过 278、跳过 3、失败 0。
85
+
1
86
  ## 0.7.7 — 2026-10-28
2
87
 
3
88
  一句话摘要:**首页和 Releases 页能看见版本号了** —— README 顶部加四枚徽章(npm 版本 / 许可证 / 要求的 Node / 最新发布),并把漏掉的 v0.7.5、v0.7.6 补成 GitHub Releases;此前仓库只有 tag,Releases 页停在 v0.7.4。
package/NOTICE.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # NOTICE — licensing split, provenance, and third-party content
2
2
 
3
- This repository mixes work from three different origins. They are **not** under one licence, and the split is deliberate.
3
+ This repository mixes work from four different origins. They are **not** under one licence, and the split is deliberate.
4
4
 
5
- 本仓库的内容来自三个不同来源,**不是单一许可**,切分如下。
5
+ 本仓库的内容来自四个不同来源,**不是单一许可**,切分如下。
6
6
 
7
7
  ## 1. The plugin — MIT (`./LICENSE`)
8
8
 
@@ -24,15 +24,36 @@ Covers only the two analysis documents produced *for this repository*:
24
24
 
25
25
  Share and adapt for **non-commercial** purposes with attribution and share-alike; commercial use is not permitted. Legal code: [`LICENSE-DOCS`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/LICENSE-DOCS) (the licence text lives in the repository; it is not part of the npm tarball).
26
26
 
27
- Note the deliberate boundary: these two documents are **not shipped in the npm package** — only `README.md`, `CHANGELOG.md`, `SECURITY.md` and `NOTICE.md` travel with the plugin, and all four are MIT (§1). So everything an npm installer actually receives stays redistributable on MIT terms.
27
+ Note the deliberate boundary: these two documents are **not shipped in the npm package** — only `README.md`, `CHANGELOG.md`, `SECURITY.md` and `NOTICE.md` travel with the plugin, and all four are MIT (§1). As of 0.8.0 the third-party file described in §3 is **not** in the tarball either (see §3), so everything the package does ship is MIT.
28
28
 
29
- ## 3. Third-party content that is **not** redistributed here
29
+ ## 3. The intro effect asset — third-party code, no longer distributed in the npm package
30
+
31
+ `assets/swarm-rebuild.js` (**as of 0.8.0 no longer packed into the npm tarball** — `package.json#files` no longer lists `assets/`; it lives in the repository only) is **not** this project's work. The position, stated plainly:
32
+
33
+ 1. **We hold written permission from Joel Califa for the effect and its choreography** (2026-11-08, on file; his one condition was that he be credited). That credit is in [`README.md`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/README.md#credits--the-intro-effect) and in the settings card.
34
+ 2. **The specific script comes from a repository that declares no licence**, and its author has been asked and had not replied as of 2026-11-09 (issue #12). His permission covers his own work; it does not license someone else's code.
35
+ 3. **This project claims and grants no rights in that file.** It stopped travelling with the npm package in 0.8.0 for exactly this reason. If either author objects to it remaining in the repository, the file — and the feature that injects it — will be removed on request.
36
+
37
+ | Origin | What | Status |
38
+ |---|---|---|
39
+ | [califa/swarm-site](https://github.com/califa/swarm-site) — **Joel Califa** | The effect itself: the page wiped white, the swarm of cursors, and the choreography that draws boxes, text and images back. The published copy declares no licence. | Written permission, 2026-11-08, condition: credit. Done. |
40
+ | [DhvanilPatel/swarm-cursors](https://github.com/DhvanilPatel/swarm-cursors) — **Dhvanil Patel** | `swarm-rebuild/swarm.js` at commit `3d49e0ac` (2026-11-06). That repository declares no licence ("None yet") and its README says the original Swarm was released without one. | Asked (issue #12), no reply as of 2026-11-09. |
41
+
42
+ **Modified, and the modification is disclosed.** `assets/swarm-rebuild.js` is that upstream file **with seven changes**, applied by [`scripts/swarm-patch.mjs`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/scripts/swarm-patch.mjs): (1) the vendor's own bottom control bar is removed; (2) its four DOM fields are pointed at off-screen placeholders so the rest of the code keeps working; (3) its per-frame HUD refresh returns early; (4) the end-of-playback button updates are null-guarded — that last one is a real crash fix: without it the asset throws as soon as the animation completes; (5) scanning waits for the plugin's go-ahead (so the snapshot is taken before the page is scrolled); (6) the white background no longer fades in over 0.5 s, and on early starts the scroll environment is re-detected once after the page loads; (7) its final self-start is deferred until `<html>` exists, so the whole engine can run from `addInitScript` before the page's own scripts. The pristine upstream file ships **in the repository only** (`assets/swarm-rebuild.upstream.js`, SHA-256 `6a30f19d…c7bfe`), as does the patched `assets/swarm-rebuild.js` itself since 0.8.0 — neither reaches npm — so anyone can diff what we changed and regenerate the patched file with `npm run swarm:patch` — or check that it still matches with `npm run swarm:check`.
43
+
44
+ **运行方式也被改了**:本项目不是等网页加载完再运行这份第三方脚本,而是通过 Playwright 的 `addInitScript` 把它放到**网页自己的脚本之前**执行(只在顶层文档、且带一个 30 秒内有效的 `window.name` 标记时启动),启动时机再由 `window.__dshSwarmGo` 放行控制。也就是说,第三方作品的启动流程、注入时机和运行上下文都由本项目重新安排过;原作者的授权仍在问询中,尚未回复。
45
+
46
+ **中文**:`assets/swarm-rebuild.js` **改过**(0.8.0 起不再随 npm 包分发,只留在仓库),改动共七处 —— ①删掉原版自带的底部控制条;②把它的四个界面字段指向离屏占位元素(否则下游代码会抛错);③让每帧的界面刷新提前返回;④播完那一下设置按钮显示状态的代码加空值保护(这一处是**真崩溃修复**:不改的话动画一播完就抛错);⑤扫描改成等插件放行、开场至少停 2.8 秒(拍快照前不滚页面);⑥白底去掉 0.5 秒淡入、第一帧就盖死,提前启动时页面加载完再重认一次滚动环境;⑦结尾自启动改成「等 `<html>` 出现」,这样整台引擎能在网页脚本之前就跑起来。两个文件都只放在仓库里(原始基线 `assets/swarm-rebuild.upstream.js` 与补丁产物,SHA-256 可核对),npm 包内一个都没有,所以任何人都能 diff 出我们改了什么,用 `npm run swarm:patch` 复原生成、用 `npm run swarm:check` 校验。
47
+
48
+ What this means for you: §1 (MIT) does **not** cover `assets/swarm-rebuild.js`. If you re-use that file, ask the two authors yourself — nothing here gives you a licence to it.
49
+
50
+ ## 4. Third-party content that is **not** redistributed here
30
51
 
31
52
  - **No OpenAI code is included in this repository.** Nothing from the bundled Codex browser plugin (`extension-host.exe`, the Chrome extension, `browser-client.mjs`, its `node_modules`) is copied, linked, decompiled or redistributed here.
32
53
  - `codex-rules.md` is an **independently written Chinese summary of browser-automation behaviour**, produced by studying publicly observable behaviour and a locally installed copy of that plugin's documentation. It contains no verbatim reproduction of that documentation; what it does contain is a *source map* (file names such as `docs/browser-safety.md`) so readers can consult the originals themselves. Facts and ideas are not copyrightable; the expression here is this project's own.
33
54
  - **Rights in the original Codex documentation and browser plugin remain with their owner (OpenAI).** If you are that owner and you object to the source map or to any phrasing here, open an issue and it will be removed.
34
55
  - `openai/codex` (the CLI) is Apache-2.0. It is mentioned for context only; no code from it is included.
35
56
 
36
- ## 4. If you need a different split
57
+ ## 5. If you need a different split
37
58
 
38
59
  Want the analysis prose under a permissive licence, or the whole thing MIT? Open an issue — that licence is a choice, not a constraint. The plugin's licence is not (see §1).
package/README.md CHANGED
@@ -41,6 +41,8 @@ dsh plugin --profile <name> add github:miqian-nomad/dsh-browser-playwright-codex
41
41
  silently clicking a different element.
42
42
  - **强力工具默认关闭** — `browser_evaluate` / `browser_cdp` are **not registered at all** unless your
43
43
  deployment opts in; this package ships both off.
44
+ - **第一次打开一个网站时,页面会自己「长」出来** — 整页被抹白,然后一群光标把方框、文字、图片一笔一笔画回来,播完自动退回真实页面(可关,设置卡里可调速度/时长)。动效与编排出自 **Joel Califa** 的 [Swarm](https://github.com/califa/swarm-site) — 他 2026-11-08 书面同意本项目使用,条件是**署名**,见 [Credits](#credits--the-intro-effect)。
45
+ The page is wiped white, then a swarm of cursors draws it back — boxes, text and images, one stroke at a time, before handing the real page over. The effect and its choreography are **Joel Califa's** [Swarm](https://github.com/califa/swarm-site), used with his written permission on the one condition that he is credited ([Credits](#credits--the-intro-effect)).
44
46
 
45
47
  **安全一览 / Security at a glance** — dsh.so's scanner flags this repository's **one Critical finding**, and that finding **is** the
46
48
  implementation of `browser_evaluate` (its dynamic evaluation), which this package ships switched off.
@@ -380,9 +382,9 @@ Measured with ``npm run cost`` (per-turn, resident in the system prompt):
380
382
 
381
383
  | Surface | Tools | Schema characters | ≈ tokens/turn |
382
384
  |---|---|---|---|
383
- | Schema defaults (`Config({})`, no patch layer) | 16 | 11,095 | ≈ 3,082 |
384
- | Shipped bundle layer (this package's `cordis.patch.yml`: both power gates off) | 16 | 11,095 | ≈ 3,082 |
385
- | Everything registered (`registerDisabledTools: true`, plus `extract`) | 21 | 15,698 | ≈ 4,361 |
385
+ | Schema defaults (`Config({})`, no patch layer) | 16 | 11,283 | ≈ 3,134 |
386
+ | Shipped bundle layer (this package's `cordis.patch.yml`: both power gates off) | 16 | 11,283 | ≈ 3,134 |
387
+ | Everything registered (`registerDisabledTools: true`, plus `extract`) | 21 | 15,886 | ≈ 4,413 |
386
388
 
387
389
  This paragraph describes the 0.6.0 change; the table above is today's measurement. Three things changed
388
390
  there, and only one of them was about size. (1) The click family was rewritten so each rule is stated once
@@ -460,9 +462,22 @@ The scenario suites originally surfaced four hazards, now fixed and regression-t
460
462
  - **Idle disposal defers during operations** — a navigation slower than `idleTimeoutMs` completes normally; disposal only fires when the session is truly idle.
461
463
  - **Mid-navigation snapshots settle** — a `browser_snapshot` racing a JS navigation waits for the new document and retries instead of surfacing a raw "Execution context was destroyed" error.
462
464
 
465
+ ## Credits — the intro effect
466
+
467
+ The first-open effect (the page is wiped white, then a swarm of cursors draws the boxes, text and images back) is **not this project's work**, and its code is one third-party file — **repository-only since 0.8.0, no longer in the npm package**:
468
+
469
+ - **The effect and its choreography** — [Swarm](https://github.com/califa/swarm-site) by **Joel Califa** ([swarm.joelcalifa.com](https://swarm.joelcalifa.com)). Used with his written permission (2026-11-08), on his one condition: **credit him** — this section is that credit.
470
+ - **The code** — `assets/swarm-rebuild.js`, from [`DhvanilPatel/swarm-cursors`](https://github.com/DhvanilPatel/swarm-cursors) at commit `3d49e0ac` (2026-11-06, file `swarm-rebuild/swarm.js`), **with seven changes** (all in [`scripts/swarm-patch.mjs`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/scripts/swarm-patch.mjs): the vendor's own control bar removed, its four DOM fields retargeted at off-screen placeholders, the per-frame HUD refresh short-circuited, the end-of-playback updates null-guarded — that one fixes a crash the removal would otherwise cause — scanning gated on the plugin's go-ahead, the 0.5 s white fade-in dropped, and its self-start deferred until `<html>` exists. The pristine baseline stays in the repository as `assets/swarm-rebuild.upstream.js`, SHA-256 `6a30f19d…c7bfe`).
471
+
472
+ The exact licensing split, and what is still outstanding, is recorded in [`NOTICE.md`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/NOTICE.md).
473
+
474
+ **中文**:首次打开网页的「进场特效」——页面被抹白,再由一群光标把方框、文字、图片一笔一笔画回来——**不是本项目自己的作品**。动效与编排出自 **Joel Califa** 的 [Swarm](https://github.com/califa/swarm-site),他于 2026-11-08 书面同意本项目使用,唯一条件是**署名**,此节即为署名;代码取自 [DhvanilPatel/swarm-cursors](https://github.com/DhvanilPatel/swarm-cursors) 的 `assets/swarm-rebuild.js`(改过七处)。**从 0.8.0 起这份资产只在仓库里,不进 npm 包**,也默认关闭。许可切分与仍在等的事见 `NOTICE.md`。
475
+
463
476
  ## License
464
477
 
465
- MIT
478
+ MIT — **except `assets/swarm-rebuild.js`** (repository-only, not in the npm package as of 0.8.0), which is third-party work: see [Credits](#credits--the-intro-effect) above and [`NOTICE.md`](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/NOTICE.md) §3. This project grants no rights in that file.
479
+
480
+ **中文**:插件本体与随包文档是 MIT;**`assets/swarm-rebuild.js` 除外**(它只在仓库里,0.8.0 起不进 npm 包),它是第三方作品,本项目不对它主张也不授予任何权利,详见上文 Credits 与 `NOTICE.md` §3。
466
481
 
467
482
 
468
483
  <!-- changelog:today -->
@@ -470,6 +485,14 @@ MIT
470
485
 
471
486
  (此列表只记 **minor 里程碑**;每个补丁版本见 [CHANGELOG.md](https://github.com/miqian-nomad/dsh-browser-playwright-codex/blob/main/CHANGELOG.md)。)
472
487
 
488
+ ### 0.8.0(2026-11-10)
489
+
490
+ - **动效资产移出发布包(破坏性)**:`assets/swarm-rebuild.js` 不再随 npm 包分发,`package.json` 的 `files` 移除了 `assets` 相关两条。原因是那套资产是第三方作品、授权状态不允许再分发 —— 与其在授权上纠缠,不如不让它进 tarball。仓库里文件仍在,`npm run swarm:check` 的 SHA 校验照旧有效。
491
+ - **`swarm.enabled` 出厂默认改为 `false`(破坏性)**:包内没有资产的前提下默认开,等于让每个新用户开箱撞一个不存在的文件。所有开关、代码路径一律保留,不动一行行为逻辑。
492
+ - **资产缺失改为静默降级**:`swarmBootScript()` 读资产包了 `try/catch`,读不到就注入一段空注释脚本。它挂在上下文创建的 `addInitScript` 上,裸抛会把"动效没了"放大成"浏览器打不开",这条是本次改动的安全底线。
493
+ - **想要动效的人**:把 `assets/swarm-rebuild.js` 放进包根 `assets/`,再在设置卡里打开 swarm 开关即可恢复,功能与 0.7.x 完全一致。
494
+ - **0.7.9 一并交付**:该版只有本地 tag,npm 上一直停在 0.7.8,从 0.7.8 升级会一次拿到 0.7.9 + 0.8.0 的全部内容,细节见 CHANGELOG。
495
+
473
496
  ### 0.7.0(2026-10-19)
474
497
 
475
498
  - **两个插件合成一个(破坏性)**:设置卡片并入本包(`dsh.client` + `./client`),`dsh-browser-toggle` 退役;`cordis.patch.yml` 从三行 loader 收成**一行** —— `- id: browser` 加嵌套的 `service` / `playwright` / `tool`。这是硬约束:`dsh-client-modules` 拒绝一个包被多个 loader 行挂载,所以带 client 的包只能有一个入口。
@@ -9,6 +9,8 @@ import fs from 'node:fs';
9
9
  import { AUTO_CHANNELS, HUMANIZED_LAUNCH } from "./config.js";
10
10
  import { SNAPSHOT_SCRIPT } from "./injected.js";
11
11
  import { BrowserError, launchFailed } from "./errors.js";
12
+ import { forgetOnBrowserClose, showWelcome } from "./swarm-intro.js";
13
+ import { swarmBootScript } from "./swarm-curtain.js";
12
14
  /**
13
15
  * Ensure the shared persistent context is alive (persistent mode). A
14
16
  * profile-backed window is launched once and reused until closed by the
@@ -50,8 +52,16 @@ async function launchContext(provider) {
50
52
  const headless = provider.desiredHeadless();
51
53
  const headful = !headless;
52
54
  const attempts = [];
55
+ // launchPersistentContext waits forever by default (issue #5): a profile another
56
+ // process is holding, a stalled singleton handshake or a wedged GPU init hangs
57
+ // acquire() with no deadline at all. navigationTimeoutMs is the only time scale
58
+ // the user can tune, but it may legitimately be as low as 1s (navigation), which
59
+ // is far too short for a cold Chrome start — so the launch deadline never drops
60
+ // below 30s.
61
+ const launchTimeoutMs = Math.max(provider.config.launch.navigationTimeoutMs, 30_000);
53
62
  const baseOptions = {
54
63
  headless,
64
+ timeout: launchTimeoutMs,
55
65
  // Headful windows must render like a normal Chrome the user opened
56
66
  // by hand: the page viewport equals the real window size and the
57
67
  // system DPI scaling (e.g. 150%) is honoured. Playwright's default
@@ -87,7 +97,11 @@ async function launchContext(provider) {
87
97
  // A stale Chrome singleton lock from a dirty shutdown breaks a relaunch
88
98
  // into the same profile; clear the lock files and retry once before
89
99
  // surfacing the failure.
90
- if (error instanceof Error && /user data directory|SingletonLock|ProcessSingleton/i.test(error.message)) {
100
+ // Playwright reports the same situation as a timeout ("browserType.launchPersistentContext:
101
+ // Timeout 30000ms exceeded") rather than as a lock message, so match both shapes
102
+ // or the retry never fires against the profile lock it was written for.
103
+ if (error instanceof Error &&
104
+ /user data directory|SingletonLock|ProcessSingleton|Timeout \d+ms exceeded/i.test(error.message)) {
91
105
  await provider.clearChromeLocks();
92
106
  provider.context = await chromium.launchPersistentContext(provider.profileDir, { ...baseOptions, ...extra });
93
107
  provider.launchedHeadless = headless;
@@ -100,6 +114,8 @@ async function launchContext(provider) {
100
114
  // External close (user closed the window, crash): forget every
101
115
  // session handle bound to this context; the next acquire relaunches.
102
116
  void ctx.on('close', () => {
117
+ // 整个插件浏览器窗口关掉:默认忘掉这趟播过哪些站,下次重开全部重播。
118
+ forgetOnBrowserClose(ctx);
103
119
  if (provider.context !== ctx)
104
120
  return;
105
121
  provider.context = undefined;
@@ -122,11 +138,21 @@ async function launchContext(provider) {
122
138
  // page cannot leave the list by itself (navigation-guard.ts).
123
139
  await provider.installHostGuard(ctx);
124
140
  await ctx.addInitScript(SNAPSHOT_SCRIPT);
141
+ // 共享窗口(persistent)这条路也要注册「同一个引擎」的提前启动脚本。
142
+ // 之前只加在 provider.ts 的 newContext 分支,用户实际用的是这里,
143
+ // 第一帧脚本根本没注册 —— 标记留在 window.name 里没被消费(2026-11-09 实测)。
144
+ await ctx.addInitScript({ content: swarmBootScript() });
125
145
  // The initial blank page predates addInitScript: reload so the
126
146
  // snapshot engine and restored localStorage apply on first use.
127
147
  for (const page of ctx.pages()) {
128
148
  await page.reload({ waitUntil: 'domcontentloaded' }).catch(() => { });
129
149
  }
150
+ // 刚开的窗口停在 about:blank —— 一张纯白占位页。换成启动卡片,并让光标把
151
+ // 它画一遍。只处理还没去任何网址的初始页,不动已恢复的标签。
152
+ const firstPage = ctx.pages()[0];
153
+ if (firstPage !== undefined && firstPage.url() === 'about:blank') {
154
+ await showWelcome(firstPage, provider.config);
155
+ }
130
156
  return ctx;
131
157
  };
132
158
  if (executablePath !== undefined) {