dsh-browser-playwright-codex 0.5.3 → 0.5.5
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 +43 -0
- package/NOTICE.md +39 -0
- package/README.md +17 -7
- package/SECURITY.md +15 -10
- package/package.json +4 -2
- package/scripts/preflight.mjs +192 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,49 @@
|
|
|
3
3
|
Bilingual by intent: the version headings and the summary of each release are given in both
|
|
4
4
|
languages, the individual entries follow the language the surrounding code and docs use.
|
|
5
5
|
|
|
6
|
+
## 0.5.5
|
|
7
|
+
|
|
8
|
+
一句话摘要:**把 `NOTICE.md` 补进 npm 包**(此前从未随包发布过),并把发版审计固化成 `npm run preflight` —— 挂在 `prepublishOnly` 上,不通过就发不出去。
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`NOTICE.md` 从未随包发布**:`package.json#files` 里没有它,而 npm 只会自动带上 `LICENSE`、不会带 `NOTICE`。那份文件说明的是"MIT 衍生部分保持 MIT、只有本项目的分析性散文才是 CC BY-NC-SA",属于归属声明,必须送到用户手里。已加入 `files`:现在包内 79 个文件,含 NOTICE.md。
|
|
13
|
+
- 上游那个插件的 README 放在仓库根目录,容易与本包混淆(它写着旧包名与旧版本号)—— 顶部加了一行说明。
|
|
14
|
+
- 0.5.4 的 CHANGELOG 条目描述"哪里写错了"时,把失效路径与失效行号照抄进了正文,于是审计(正确地)把它们当成真引用。改为描述而不照抄。
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`scripts/preflight.mjs`(`npm run preflight`)**:发版前审计,并挂到 `prepublishOnly`(`build && test && verify && preflight`)。检查项:
|
|
19
|
+
- 包里装了什么 —— 必备文件清单(NOTICE / LICENSE / SECURITY / CHANGELOG / locale / cordis.patch.yml)、`lib/` 必须在、`src/` 与 `tests/` 与 `.bak` 必须不在;
|
|
20
|
+
- 文档"关于世界的断言" —— 本包版本号、写死的工具数、写死的总 token 数、旧包名、反引号路径、「文件:行号」是否越界、markdown 相对链接是否失效;
|
|
21
|
+
- 基线取自**命令本身**(`npm run cost`、`node --test`),不是文档自述;
|
|
22
|
+
- 输出分 **✗ 必须修 / ? 待确认 / ✓ 已核对** 三类,规则刻意收窄避免噪声。
|
|
23
|
+
|
|
24
|
+
## 0.5.4
|
|
25
|
+
|
|
26
|
+
一句话摘要:**把第三方评审(2026-10-11,8.6/10)指出的"对外表述与现实脱节"修干净,并把这些说法变成守卫** —— 运行时行为零变化。但 README / SECURITY / NOTICE 是**随包发布**的,所以必须发一版:不发版,从 npm 装的用户看到的仍是那句"不在 npm 上"。
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **README 声称"这个包不在 npm 上"**(与同一文件里的 `# from npm` 自相矛盾):包从 v0.5.1 起已在 npm 上。改为「From npm (published since v0.5.1)」并给出 npm 安装命令,仓库与本地 link 两种方式保留。
|
|
31
|
+
- **NOTICE.md 的 7 个路径全部失效**(这 7 份文档都写成 `docs/` 前缀,实际都在仓库根目录):已按根相对路径改正。同一段里指向**上游文档**的 `docs/browser-safety.md` 保持原样 —— 那是给读者的来源索引,不是我们的文件。顺带修掉 `MODE-TRADE-OFFS.md` 里同类一处。
|
|
32
|
+
- **README"安全一览"写死"2 Critical"**,而 dsh.so 当前扫描是 **1 Critical / 3 Warning / 22 Info**(v0.5.3、75 文件):改为不含数字的说法。
|
|
33
|
+
- **SECURITY.md §2 与表格按当前扫描重写**:Critical 就是 `browser_evaluate` 的动态求值(`src/session.ts` → `lib/session.js`,出厂关闭)、3 条 Warning、22 条 Info;v0.4.9 的 2/5/26 作为历史留在引用块里,并写明"以他们的报告为准"。
|
|
34
|
+
- **SECURITY.md 里"某个文件 + 行号"式的引用在 0.5.x 拆分后失效**(该文件现在 55 行):改为模块名 —— 行号会漂,模块名不会。
|
|
35
|
+
- **三份发帖草稿里的"2 个 Critical"**:去掉数字,改为"那个 Critical"。
|
|
36
|
+
- **几处把上游文件写得像我们自己的**:`docs/articles/focus-stealing.md`、`patches/README.md`、`codex-rules.md` 里标明那是**上游仓库**的文件。
|
|
37
|
+
- **README 增加 "Compatibility" 段**:写明构建与测试用的 DSH 版本是 `0.2.0-rc.1`、peer 范围故意留 `*`(换新版 harness 仍可安装)、升级后跑 `npm run doctor` 复核本插件导入的具名导出是否还在。
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`tests/doc-reality.test.ts`(5 条守卫)**,把"文档对世界说的话"变成 CI 会拦的事实:
|
|
42
|
+
1. 现行文档不得声称"不在 npm 上"(CHANGELOG 是历史,跳过);
|
|
43
|
+
2. README 必须给出 npm 安装路径(与第 1 条互为孪生);
|
|
44
|
+
3. 反引号包着的仓库路径必须真的在那个位置(规则:该路径不存在、但同名文件在本仓库里存在才报 —— 不误伤上游引用;`./`、`../` 相对所在文档解析,其余相对仓库根);
|
|
45
|
+
4. 文档里的「文件:行号」引用不得越界(行号 > 该文件现在的行数即报错 —— 正是 0.5.x 重构制造的那类腐烂);
|
|
46
|
+
5. 带数字的 `N Critical` 只允许出现在 SECURITY.md(计数会变,其它文档一律用文字指过去)。
|
|
47
|
+
每条都做了"有牙复检":把对应的旧文案塞回去,守卫立刻报红。
|
|
48
|
+
|
|
6
49
|
## 0.5.3
|
|
7
50
|
|
|
8
51
|
一句话摘要:**源码按职责拆开、可维护性大幅提升,并修掉拆分过程中暴露的三处隐患** —— 对用户可见的行为零变化:工具面、配置接口、出厂默认值全部不动。本版同时修掉中文说明里一处会让人误判权限状态的过期说法。
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# NOTICE — licensing split, provenance, and third-party content
|
|
2
|
+
|
|
3
|
+
This repository mixes work from three different origins. They are **not** under one licence, and the split is deliberate.
|
|
4
|
+
|
|
5
|
+
本仓库的内容来自三个不同来源,**不是单一许可**,切分如下。
|
|
6
|
+
|
|
7
|
+
## 1. The plugin — MIT (`./LICENSE`)
|
|
8
|
+
|
|
9
|
+
Covers everything that is part of the plugin: `src/`, `tests/`, `scripts/`, `cordis.patch.yml`, `tsconfig.json`, `package.json`, `CHANGELOG.md`, and the plugin's own documentation (`PLUGIN-README.md`, `DIALOG-POLICY.md`, `SAFETY-RULES.md`, `SNAPSHOT-RULES.md`, `codex-rules.md`).
|
|
10
|
+
|
|
11
|
+
| Origin | What | Licence |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| [ChenyuHeee/dsh-browser-playwright](https://github.com/ChenyuHeee/dsh-browser-playwright) v0.1.1 | The accessibility-snapshot engine (`src/injected.ts`, `src/snapshot-render.ts`), the tool family (`src/tool.ts`), the service/provider seams (`src/service.ts`, `src/playwright.ts`), the URL policy, and the test-suite shape. Copyright (c) 2026 dsh-browser-playwright contributors. | **MIT** |
|
|
14
|
+
| This fork's merged layer | Geometric click (`browser_click_at`) with landing-element reporting, the CDP policy layer (`src/cdp-policy.ts`), the persistent profile + login-state export (`src/runtime-state.ts`, `persistState`/`restoreState`), dialog parking, the console/network diagnostics tools, the contract/compat modules, and the fixes listed in [`CHANGELOG.md`](CHANGELOG.md) | **MIT** |
|
|
15
|
+
|
|
16
|
+
**Why the plugin cannot be non-commercial:** it is a derivative of MIT-licensed work. MIT grants everyone the right to use, modify and redistribute the software **including commercially**, and a redistributor cannot add a non-commercial restriction to it. Any non-commercial claim over `src/` would be an unauthorised restriction — worse for downstream users than leaving it MIT. The MIT text in `./LICENSE` (including the upstream copyright notice) is unchanged.
|
|
17
|
+
|
|
18
|
+
## 2. This repository's analysis prose — CC BY-NC-SA 4.0 (`./LICENSE-DOCS`)
|
|
19
|
+
|
|
20
|
+
Covers only the writing produced *for this repository*:
|
|
21
|
+
|
|
22
|
+
- `README.md` (this repository's landing page)
|
|
23
|
+
- `FOCUS-STEALING.md`
|
|
24
|
+
- `MODE-TRADE-OFFS.md`
|
|
25
|
+
|
|
26
|
+
Share and adapt for **non-commercial** purposes with attribution and share-alike; commercial use is not permitted. Legal code: [`LICENSE-DOCS`](LICENSE-DOCS).
|
|
27
|
+
|
|
28
|
+
Note the deliberate boundary: the plugin's *own* documentation is not in this set — it ships with the plugin under MIT, so that the whole plugin stays redistributable on MIT terms.
|
|
29
|
+
|
|
30
|
+
## 3. Third-party content that is **not** redistributed here
|
|
31
|
+
|
|
32
|
+
- **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.
|
|
33
|
+
- `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.
|
|
34
|
+
- **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.
|
|
35
|
+
- `openai/codex` (the CLI) is Apache-2.0. It is mentioned for context only; no code from it is included.
|
|
36
|
+
|
|
37
|
+
## 4. If you need a different split
|
|
38
|
+
|
|
39
|
+
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
|
@@ -31,16 +31,22 @@ dsh plugin --profile <name> add github:miqian-nomad/dsh-browser-playwright-codex
|
|
|
31
31
|
- **强力工具默认关闭** — `browser_evaluate` / `browser_cdp` are **not registered at all** unless your
|
|
32
32
|
deployment opts in; this package ships both off.
|
|
33
33
|
|
|
34
|
-
**安全一览 / Security at a glance** — dsh.so's scanner
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
can and cannot do with your logins (URL policy, CDP allow-list, dialog policy, no telemetry, and it
|
|
38
|
-
|
|
34
|
+
**安全一览 / Security at a glance** — dsh.so's scanner flags this repository's **one Critical finding**, and that finding **is** the
|
|
35
|
+
implementation of `browser_evaluate` (its dynamic evaluation), which this package ships switched off.
|
|
36
|
+
[SECURITY.md](SECURITY.md) maps every finding to the gate that contains it, and explains what the plugin
|
|
37
|
+
can and cannot do with your logins (URL policy, CDP allow-list, dialog policy, no telemetry, and it never
|
|
38
|
+
types passwords or 2FA codes). Counts move as the scanner and the code change — their own report is the
|
|
39
|
+
live number.
|
|
39
40
|
|
|
40
41
|
## Install
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
From npm (published since v0.5.1 — [npmjs.com/package/dsh-browser-playwright-codex](https://www.npmjs.com/package/dsh-browser-playwright-codex)):
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
dsh plugin --profile <name> add dsh-browser-playwright-codex
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or straight from the repository / a checkout:
|
|
44
50
|
|
|
45
51
|
```sh
|
|
46
52
|
dsh plugin --profile <name> add github:miqian-nomad/dsh-browser-playwright-codex
|
|
@@ -49,6 +55,10 @@ dsh plugin --profile <name> add github:miqian-nomad/dsh-browser-playwright-codex
|
|
|
49
55
|
dsh plugin --profile <name> add link:<absolute path to this directory>
|
|
50
56
|
```
|
|
51
57
|
|
|
58
|
+
**Compatibility.** Built and tested against DSH `0.2.0-rc.1` (`@deepseek-ai/*` peer ranges are deliberately
|
|
59
|
+
`*` so a newer harness still installs). After a harness upgrade, `npm run doctor` re-checks that every named
|
|
60
|
+
export this plugin imports still exists — that is the fast way to tell "the harness moved" from "the plugin broke".
|
|
61
|
+
|
|
52
62
|
`lib/` ships in the repository (and there is deliberately no `prepare` hook), so a fresh clone loads
|
|
53
63
|
without a build step — `npm run build` is only needed after editing `src/`.
|
|
54
64
|
|
package/SECURITY.md
CHANGED
|
@@ -4,7 +4,7 @@ This plugin drives a **real browser with your login state**. That is the point o
|
|
|
4
4
|
the thing worth being careful about. This file answers the two questions a reader actually has:
|
|
5
5
|
|
|
6
6
|
1. **What can it do to me?**
|
|
7
|
-
2. **Why does dsh.so's scanner report
|
|
7
|
+
2. **Why does dsh.so's scanner report a Critical finding?**
|
|
8
8
|
|
|
9
9
|
If you only read one line: **the two findings are the implementation of `browser_evaluate`, a tool
|
|
10
10
|
this package ships switched off** — it is not registered at all until a deployment turns it on.
|
|
@@ -68,18 +68,22 @@ Nothing else.
|
|
|
68
68
|
|
|
69
69
|
---
|
|
70
70
|
|
|
71
|
-
## 2.
|
|
71
|
+
## 2. What the scanner reports, and which gate contains it
|
|
72
72
|
|
|
73
|
-
dsh.so runs `dsh-plugin-vet` over the whole repository.
|
|
74
|
-
|
|
75
|
-
that decides whether it is reachable at all.
|
|
73
|
+
dsh.so runs `dsh-plugin-vet` over the whole repository. Their scan of **v0.5.3 (2026-10-11)** reports
|
|
74
|
+
**1 Critical, 3 Warning, 22 Info** across 75 files — and it is **right about the capability**. Here is
|
|
75
|
+
each one, with the gate that decides whether it is reachable at all.
|
|
76
|
+
|
|
77
|
+
> For the record: their v0.4.9 scan — before this plugin was split into responsibility-scoped modules —
|
|
78
|
+
> reported 2 Critical, 5 Warning, 26 Info across 57 files. Counts move as the scanner and the code
|
|
79
|
+
> change; **their report is the live number**, and this table is a map, not a snapshot.
|
|
76
80
|
|
|
77
81
|
| Level | Finding (as reported) | What it actually is | Reachable when |
|
|
78
82
|
|---|---|---|---|
|
|
79
|
-
| Critical | `new Function('return (' + expression + ')')` — `
|
|
80
|
-
| Warning | `fs.writeFileSync` of a JSON blob (
|
|
83
|
+
| Critical | `new Function('return (' + expression + ')')` — reported in `lib/session.js` (source: `src/session.ts`) | The body of **`browser_evaluate`**: it wraps your expression and runs it in the page. This is what "run JavaScript in the page" means; there is no way to implement it without dynamic evaluation. | Only when `allowEvaluate: true`. **The package ships `false`, so the tool is not registered.** |
|
|
84
|
+
| Warning | `fs.writeFileSync` of a JSON blob (`lib/login-state.js`, `lib/runtime-state.js`) | Exporting the login state (`cookies` + `localStorage`) so a closed window can be reopened still logged in, and the small runtime state file behind the Settings switches. | Always — it is how login persistence works. The files live under your DSH home. |
|
|
81
85
|
| Warning | HTTP request to a bare IP (`scripts/dsh-browser-verify.mjs`) | The acceptance suite starts a **local** `http.server` on `127.0.0.1` with a fixture page, so `npm run verify` needs no DSH and no internet. | Only when you run that script. |
|
|
82
|
-
| Info ×
|
|
86
|
+
| Info ×22 | `localStorage` reads, `process.env` reads, hardcoded `127.0.0.1`, dev-only probe scripts | Restoring login state; test hooks (`DSH_BROWSER_STATE_FILE`, `PLAYWRIGHT_DOWNLOAD_HOST`); and scripts under `scripts/analysis/` that the scanner itself marks *"Dev-only file — not executed at install/runtime"*. | — |
|
|
83
87
|
|
|
84
88
|
Two honest notes:
|
|
85
89
|
|
|
@@ -87,8 +91,9 @@ Two honest notes:
|
|
|
87
91
|
your deployment turns it on. A scanner that reads code cannot see a runtime gate.
|
|
88
92
|
- We did **not** try to hide the finding (moving the call, obfuscating it, or suppressing the rule).
|
|
89
93
|
That would be deceiving the tool that is trying to protect users, and it would make this file a lie.
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
- Module names, not line numbers: this file used to cite a line number in `src/playwright.ts`, which the
|
|
95
|
+
0.5.x split made meaningless (the code lives in `src/session.ts` now, and that file is a 55-line
|
|
96
|
+
entry point). File names survive refactors; line numbers do not.
|
|
92
97
|
|
|
93
98
|
## 3. How to check all of this yourself
|
|
94
99
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-browser-playwright-codex",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Browser automation for DeepSeek Harness: fill in forms, scrape lists and work across tabs in real pages, using the logins you already have. Runs in a visible window that stays quiet when minimised, and stops for sign-ins, CAPTCHAs and native dialogs instead of confirming for you.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"README.md",
|
|
38
38
|
"CHANGELOG.md",
|
|
39
39
|
"SECURITY.md",
|
|
40
|
+
"NOTICE.md",
|
|
40
41
|
"locale"
|
|
41
42
|
],
|
|
42
43
|
"scripts": {
|
|
@@ -50,7 +51,8 @@
|
|
|
50
51
|
"cost": "node node_modules/tsx/dist/cli.mjs scripts/measure-prompt-cost.mjs",
|
|
51
52
|
"format": "node node_modules/prettier/bin/prettier.cjs --write \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.mjs\"",
|
|
52
53
|
"format:check": "node node_modules/prettier/bin/prettier.cjs --check \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.mjs\"",
|
|
53
|
-
"prepublishOnly": "npm run build && npm test"
|
|
54
|
+
"prepublishOnly": "npm run build && npm test && npm run verify && npm run preflight",
|
|
55
|
+
"preflight": "node scripts/preflight.mjs ."
|
|
54
56
|
},
|
|
55
57
|
"dependencies": {
|
|
56
58
|
"@deepseek-ai/schemastery": "*",
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发版前置审计(preflight):把"文档对世界说的话"和"包里到底装了什么"一次查干净。
|
|
3
|
+
*
|
|
4
|
+
* 为什么有这个脚本(2026-10-11,用户第二次因此不满):
|
|
5
|
+
* 之前是"只改点不看面"—— 修了 README 的 npm 说法,却没查 SECURITY 的计数、NOTICE 的路径、
|
|
6
|
+
* 发帖草稿里的数字、以及"文档引用的文件/行号在重构后是否还存在"。用户的原话是
|
|
7
|
+
* 「你能不能检查清楚再发新版本啊,别给我检查出来又一堆 bug」。所以这份检查挂在 `prepublishOnly`
|
|
8
|
+
* 上:**不通过就发不出去**,而不是等用户事后发现。
|
|
9
|
+
*
|
|
10
|
+
* 三类输出,语义严格区分:
|
|
11
|
+
* ✗ 必须修 —— 硬错误(文件读不到、清单缺项、路径/行号/链接失效、测试失败、计数与实测不符)
|
|
12
|
+
* ? 待确认 —— 可能是历史、上游或有意为之(例如 "published since v0.5.1"、上游插件的旧包名)
|
|
13
|
+
* ✓ 已一致 —— 已经核对过的基线数字
|
|
14
|
+
*
|
|
15
|
+
* 规则刻意收窄避免噪声(第一版误报过:把 IP 当版本号、把上游文档的版本当我们的、把竞品的工具数
|
|
16
|
+
* 当我们的、把"三个闸门合计 127 token"这个子项当成总成本)。
|
|
17
|
+
*
|
|
18
|
+
* 用法: node preflight.mjs [项目根] 退出码: 0 通过 / 2 有硬错误
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from 'node:child_process'
|
|
21
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
22
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
23
|
+
import { fileURLToPath } from 'node:url'
|
|
24
|
+
|
|
25
|
+
const root = resolve(process.argv[2] ?? join(dirname(fileURLToPath(import.meta.url)), '..'))
|
|
26
|
+
const problems = []
|
|
27
|
+
const confirm = []
|
|
28
|
+
const ok = []
|
|
29
|
+
|
|
30
|
+
const readOrThrow = (rel) => {
|
|
31
|
+
const p = join(root, rel)
|
|
32
|
+
if (!existsSync(p)) throw new Error('读不到文件(硬错误,不许当通过):' + rel)
|
|
33
|
+
return readFileSync(p, 'utf8')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── A. 包里装了什么 ───────────────────────────────────────────────────────────
|
|
37
|
+
const packed = new Set(
|
|
38
|
+
JSON.parse(
|
|
39
|
+
execFileSync('npm', ['pack', '--dry-run', '--json'], { cwd: root, encoding: 'utf8', shell: true }),
|
|
40
|
+
)[0].files.map((f) => f.path),
|
|
41
|
+
)
|
|
42
|
+
// 这些是"用户从 npm 装到之后必须能看到"的文件:许可证与归属声明(NOTICE)、安全说明、变更记录、
|
|
43
|
+
// 以及让 Gradle 式的 loader 能挂载的 cordis.patch.yml。2026-10-11 的审计就是靠这份清单抓到
|
|
44
|
+
// NOTICE.md 一直没进包(npm 只会自动带上 LICENSE,不会带 NOTICE)。
|
|
45
|
+
const SHOULD_SHIP = [
|
|
46
|
+
'package.json',
|
|
47
|
+
'README.md',
|
|
48
|
+
'CHANGELOG.md',
|
|
49
|
+
'SECURITY.md',
|
|
50
|
+
'NOTICE.md',
|
|
51
|
+
'LICENSE',
|
|
52
|
+
'cordis.patch.yml',
|
|
53
|
+
'locale/en.json',
|
|
54
|
+
'locale/zh.json',
|
|
55
|
+
]
|
|
56
|
+
for (const f of SHOULD_SHIP) if (!packed.has(f)) problems.push('包里缺文件:' + f)
|
|
57
|
+
if (![...packed].some((p) => p.startsWith('lib/'))) problems.push('包里没有 lib/(插件实际加载的就是它)')
|
|
58
|
+
if ([...packed].some((p) => p.startsWith('src/') || p.startsWith('tests/'))) problems.push('包里不该有 src/ 或 tests/')
|
|
59
|
+
if ([...packed].some((p) => p.endsWith('.bak'))) problems.push('包里有 .bak 残渣')
|
|
60
|
+
ok.push('包内 ' + packed.size + ' 个文件;已确认含 lib/、locale/、cordis.patch.yml,且不含 src/tests/.bak')
|
|
61
|
+
|
|
62
|
+
// ── B. 基线数字(来自命令本身,不是文档自述) ─────────────────────────────────
|
|
63
|
+
const pkg = JSON.parse(readOrThrow('package.json'))
|
|
64
|
+
const cost = execFileSync('npm', ['run', 'cost'], { cwd: root, encoding: 'utf8', shell: true })
|
|
65
|
+
const toolCount = Number((cost.match(/工具数[::]\s*(\d+)/) ?? [])[1])
|
|
66
|
+
const chars = Number((cost.match(/字符总数[::]\s*(\d+)/) ?? [])[1])
|
|
67
|
+
const tokens = Number((cost.match(/粗估 token[::]\s*~?(\d+)/) ?? [])[1])
|
|
68
|
+
if (!toolCount || !chars || !tokens) problems.push('拿不到 npm run cost 的数字')
|
|
69
|
+
else ok.push('成本基线:' + toolCount + ' 工具 / ' + chars + ' 字符 / ~' + tokens + ' token/轮')
|
|
70
|
+
|
|
71
|
+
const suite = execFileSync('node', ['--test', 'tests/*.test.ts'], { cwd: root, encoding: 'utf8', shell: true })
|
|
72
|
+
const n = (re) => Number((suite.match(re) ?? [])[1])
|
|
73
|
+
if (n(/ℹ fail (\d+)/) > 0) problems.push('测试有失败:' + n(/ℹ fail (\d+)/) + ' 条')
|
|
74
|
+
else
|
|
75
|
+
ok.push(
|
|
76
|
+
'测试:' + n(/ℹ tests (\d+)/) + ' / pass ' + n(/ℹ pass (\d+)/) + ' / fail 0 / skipped ' + n(/ℹ skipped (\d+)/),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// ── C. 文档断言 ───────────────────────────────────────────────────────────────
|
|
80
|
+
const docFiles = []
|
|
81
|
+
const walk = (dir) => {
|
|
82
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
83
|
+
if (e.name === 'node_modules' || e.name === '.git') continue
|
|
84
|
+
const p = join(dir, e.name)
|
|
85
|
+
if (e.isDirectory()) walk(p)
|
|
86
|
+
else if (e.name.endsWith('.md')) docFiles.push(relative(root, p).replace(/\\/g, '/'))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
walk(root)
|
|
90
|
+
const HISTORY = new Set(['CHANGELOG.md'])
|
|
91
|
+
/** 上游项目(被借鉴的那个插件)自己的文档:里面出现旧包名/旧版本是对的。 */
|
|
92
|
+
const UPSTREAM_DOCS = new Set([
|
|
93
|
+
'PLUGIN-README.md',
|
|
94
|
+
'codex-rules.md',
|
|
95
|
+
'patches/README.md',
|
|
96
|
+
'docs/articles/focus-stealing.md',
|
|
97
|
+
])
|
|
98
|
+
const current = docFiles.filter((f) => !HISTORY.has(f))
|
|
99
|
+
|
|
100
|
+
const basenames = new Set()
|
|
101
|
+
for (const dir of ['', 'src', 'lib', 'scripts', 'tests', 'locale', 'patches', 'docs', 'docs/articles']) {
|
|
102
|
+
const abs = dir ? join(root, dir) : root
|
|
103
|
+
if (!existsSync(abs)) continue
|
|
104
|
+
for (const e of readdirSync(abs, { withFileTypes: true })) if (e.isFile()) basenames.add(e.name)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const f of docFiles) {
|
|
108
|
+
const text = readOrThrow(f)
|
|
109
|
+
const currentDoc = current.includes(f)
|
|
110
|
+
text.split('\n').forEach((line, i) => {
|
|
111
|
+
const where = f + ':' + (i + 1)
|
|
112
|
+
const historical = /(since|since v|起|之前|以后|published|发布|历史|历史值|for the record|v0[.]4)/i.test(line)
|
|
113
|
+
|
|
114
|
+
// C1. 看起来是"本包版本"的数字
|
|
115
|
+
if (currentDoc && !historical) {
|
|
116
|
+
const notOurs =
|
|
117
|
+
/(Node|node|playwright-core|upstream|上游|Chenyu|Codex|scan of|扫描|dsh-plugin-vet|electron)/i.test(line)
|
|
118
|
+
for (const m of line.matchAll(/\bv(\d+\.\d+\.\d+)\b/g)) {
|
|
119
|
+
if (notOurs) break
|
|
120
|
+
if (m[1] !== pkg.version)
|
|
121
|
+
confirm.push(where + ' 提到版本 v' + m[1] + '(当前 ' + pkg.version + ')→ ' + line.trim().slice(0, 80))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// C2. 写死的工具数:只有当这句在说"我们"时才核对(竞品对照说的是别人)
|
|
126
|
+
const tc = line.match(/\b(\d{2})\s*(?:个)?\s*(?:browser_\*?\s*)?(?:tools|工具)/i)
|
|
127
|
+
if (currentDoc && tc && Number(tc[1]) >= 10) {
|
|
128
|
+
const aboutUs = /(我们|this package|本包|registered|注册)/i.test(line)
|
|
129
|
+
const aboutThem = /(同类|竞品|其它|其他|competitor|alternative|更轻)/i.test(line)
|
|
130
|
+
if (aboutUs && !aboutThem && Number(tc[1]) !== toolCount)
|
|
131
|
+
problems.push(where + ' 写死工具数 ' + tc[1] + '(实测 ' + toolCount + ')')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// C3. 写死的总成本:只核对"总计/常驻"这类句子,且量级要像总数(≈127 这种子项不算)
|
|
135
|
+
const tk = line.match(/\b(\d[\d,]{2,6})\s*(?:tokens?|token)/i)
|
|
136
|
+
if (currentDoc && tk) {
|
|
137
|
+
const value = Number(tk[1].replace(/,/g, ''))
|
|
138
|
+
const aboutTotal = /(常驻|总计|合计|resident|per turn|每轮)/i.test(line)
|
|
139
|
+
if (aboutTotal && value >= 500 && Math.abs(value - tokens) > 200)
|
|
140
|
+
problems.push(where + ' 写死的总 token 数 ' + tk[1] + '(实测 ~' + tokens + ')')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// C4. 旧包名:.state.json 是刻意保留的;上游文档里出现是对的
|
|
144
|
+
if (
|
|
145
|
+
/dsh-browser-playwright(?!-codex)/.test(line) &&
|
|
146
|
+
!UPSTREAM_DOCS.has(f) &&
|
|
147
|
+
!/\.state\.json/.test(line) &&
|
|
148
|
+
!/(上游|upstream|Chenyu)/i.test(line)
|
|
149
|
+
) {
|
|
150
|
+
confirm.push(where + ' 出现旧包名(确认语境)→ ' + line.trim().slice(0, 80))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// C5. 反引号里的仓库路径
|
|
154
|
+
for (const m of line.matchAll(/`([A-Za-z0-9._/-]+\.(?:md|ts|js|mjs|cjs|json|yml|yaml))`/g)) {
|
|
155
|
+
const p = m[1]
|
|
156
|
+
if (!p.includes('/') || p.startsWith('~') || p.startsWith('http')) continue
|
|
157
|
+
const abs = p.startsWith('.') ? resolve(root, f, '..', p) : join(root, p)
|
|
158
|
+
if (existsSync(abs)) continue
|
|
159
|
+
if (basenames.has(p.slice(p.lastIndexOf('/') + 1)))
|
|
160
|
+
problems.push(where + ' 路径写错:`' + p + '`(文件在本仓库,但不在这个位置)')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// C6. 「文件:行号」引用
|
|
164
|
+
for (const m of line.matchAll(/([A-Za-z0-9_./-]+\.(?:ts|js|mjs|cjs|json|yml))[^\n]{0,12}?[:(]\s*~?:?(\d{3,5})/g)) {
|
|
165
|
+
const abs = join(root, m[1].replace(/^\.\//, ''))
|
|
166
|
+
if (!existsSync(abs)) continue
|
|
167
|
+
const lines = readFileSync(abs, 'utf8').split('\n').length
|
|
168
|
+
if (Number(m[2]) > lines)
|
|
169
|
+
problems.push(where + ' 行号越界:' + m[1] + ':' + m[2] + '(该文件现在 ' + lines + ' 行)')
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
// C7. markdown 相对链接
|
|
173
|
+
for (const m of readOrThrow(f).matchAll(/\]\(([^)#\s]+)(?:#[^)]*)?\)/g)) {
|
|
174
|
+
const target = m[1]
|
|
175
|
+
if (/^(https?:|mailto:)/.test(target) || target.startsWith('#')) continue
|
|
176
|
+
if (!existsSync(resolve(root, f, '..', target))) problems.push(f + ' markdown 链接失效:' + target)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── 输出 ─────────────────────────────────────────────────────────────────────
|
|
181
|
+
console.log('=== ✗ 必须修(硬错误)===')
|
|
182
|
+
console.log(problems.length ? problems.map((p) => ' ✗ ' + p).join('\n') : ' (无)')
|
|
183
|
+
console.log('')
|
|
184
|
+
console.log('=== ? 待确认(历史 / 上游 / 有意为之)===')
|
|
185
|
+
const uniqueConfirm = [...new Set(confirm)]
|
|
186
|
+
console.log(uniqueConfirm.length ? uniqueConfirm.map((c) => ' ? ' + c).join('\n') : ' (无)')
|
|
187
|
+
console.log('')
|
|
188
|
+
console.log('=== ✓ 已核对 ===')
|
|
189
|
+
console.log(ok.map((o) => ' ✓ ' + o).join('\n'))
|
|
190
|
+
console.log('')
|
|
191
|
+
console.log('结论:' + problems.length + ' 处硬错误、' + uniqueConfirm.length + ' 处待确认。')
|
|
192
|
+
process.exitCode = problems.length ? 2 : 0
|