dsh-browser-playwright-codex 0.7.0 → 0.7.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 CHANGED
@@ -3,6 +3,24 @@
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.7.1 — 2026-10-19
7
+
8
+ 一句话摘要:**只修文档与一处口径** —— 外部评分(8.9/10)点名的问题:CHANGELOG 的数字手写写错、对比表把五个闸门写成三个、有头模式的 viewport 仍然不生效却没标注。行为零变化。
9
+
10
+ ### Added
11
+
12
+ - **设置卡新增第三个工具开关:诊断工具**(`browser_console_messages` / `browser_network_requests`)。与 `evaluate` / `cdp` 同一套机制:`POST /api/browser-playwright/gates` 现在接受 `diagnostics`,home 补丁块的整行 config 一并写入,`tool.ts` 把**实际生效值**(而不是猜的)发布给卡片;卡片上显示"当前:N 个开着"也把这一项算进去。
13
+ - **「已登录的站点」默认折叠**:标题行直接给出「N 个域 · M 个 cookie · 点击展开」,点开才渲染说明与名单(分组仍各自折叠)。设置页不再被这份名单占掉大半屏。
14
+
15
+ ### Fixed
16
+
17
+ - **CHANGELOG 0.7.0 的数字按实测改正**:常驻是 **16 工具 / 11,179 字符**(此前手写成 11,005 字符,token 数也跟着漂),对比基线 18 工具 / 12,755 字符保持不变,字符降幅 **−1,576(−12.4%)**。这是连续第三个版本出现"主文档被守卫钉住、CHANGELOG 叙事手写漂移"——本轮起新增加一条守卫:**最新一节 CHANGELOG 不许出现手写的 `≈ N token/轮` 数字**,要写就写实测命令与结果表。
18
+ - **对比与优势.md 的闸门清单**:"五个闸门"此前只列了 `evaluate` / `cdp` / `extract` 三个名字,补上 `console_messages` / `network_requests`,并把它与全量注册的字符差从旧的 2,853 更正为实测 4,603 字符(token 换算见 README 成本表)。
19
+
20
+ ### Changed
21
+
22
+ - **有头模式的 `viewport` 标注**:`browser-lifecycle.ts:44` 用 `viewport: null` 跟随真实窗口,所以 `launch.viewport` 只在 `headless: true` 时生效;README 配置示例与 Known Limitations 现在明写这一点(此前"配了也不生效"没有任何提示,被外部评分连续四次点出)。
23
+
6
24
  ## 0.7.0 — 2026-10-19
7
25
 
8
26
  一句话摘要:**两个插件合成一个** —— 设置卡片并入本包;加载行从三行收成一行(这是 `dsh-client-modules` 允许本包带 client 模块的前提),独立的 `dsh-browser-toggle` 退役。
@@ -19,7 +37,7 @@ languages, the individual entries follow the language the surrounding code and d
19
37
 
20
38
  ### Changed
21
39
 
22
- - **两个诊断工具改为门控**:`browser_console_messages` / `browser_network_requests` 不再常驻 —— 新增 `tool.diagnostics: false`(默认关),开启后才注册。常驻成本 **18 工具 12,755 字符 ≈ 3,543 token/轮 → 16 工具 11,005 字符 ≈ 3,057 token/轮(−486,−13.7%)**;全量注册仍是 21 工具 ≈ 4,384。
40
+ - **两个诊断工具改为门控**:`browser_console_messages` / `browser_network_requests` 不再常驻 —— 新增 `tool.diagnostics: false`(默认关),开启后才注册。常驻成本 **18 工具 12,755 字符 ≈ 3,543 token/轮 → 16 工具 11,179 字符 ≈ 3,105 token/轮(−438,−12.4%)**;全量注册仍是 21 工具 15,782 字符 ≈ 4,384。
23
41
  - **快照瘦身(运行时大头)**:无名无状态的 `generic` 包装层不再逐层输出(真实页面常把一个链接包在 7~8 层 div 里),显示用 `href` 去掉追踪参数(`utm_*` / `gclid` / `rsv_*` / `sa` 等)并截到 160 字符。百度结果页形状的样本:**6,169 → 2,557 字符(−58.6%)**,行数 108 → 24。refs 与点击行为不变 —— 省的是模型读到的东西。
24
42
  - 闸门写入块改为**整行 config**(新标记 `# >>> dsh-browser:`),并兼容旧的 `dsh-browser-toggle` 标记就地替换 —— 避免同一个 id 被两段 patch 争抢。
25
43
  - `scripts/shipped-gates.mjs` 按新行名读取出厂闸门;`scripts/build-client.mjs` 把 `client/client.js` 复制进 `lib/`。
package/README.md CHANGED
@@ -227,7 +227,7 @@ All tunables are patchable through the profile's `cordis.patch.yml` (later layer
227
227
  headless: false # visible window you can watch and rescue
228
228
  persistent: true # profile-backed window, login survives close
229
229
  # profileDir: ~/.dsh/browser-profiles/playwright by default
230
- viewport: { width: 1280, height: 800 }
230
+ viewport: { width: 1280, height: 800 } # headful: IGNORED (the window tracks the desktop; only headless: true pins it)
231
231
  navigationTimeoutMs: 30000
232
232
  ignoreHTTPSErrors: false
233
233
  allowedDomains: [] # e.g. ['github.com'] restricts navigation
@@ -431,6 +431,7 @@ links below go to GitHub rather than to files inside the package:
431
431
  registerDisabledTools: false
432
432
  maxWaitMs: 60000
433
433
  ```
434
+ - **`launch.viewport` only applies headless** — the visible window is a real window: it tracks the desktop size (`browser-lifecycle.ts` passes `viewport: null` in headful), so setting 1280×800 there changes nothing. Pin the size by running `headless: true`, or resize the window itself.
434
435
  - **Chromium family only** — Firefox/WebKit channels are not probed; providers are swappable if another engine is needed.
435
436
  - **Extract needs a dedicated model route** — it does not reuse the main request's route; misconfiguration fails loudly at call time.
436
437
  - **JS-driven navigations are not statically checkable** — `allowedDomains` covers `browser_navigate`, `browser_open_tab`, every navigating click (`browser_click`: `ref`, `text`, `precise` and raw-coordinate modes) and `browser_switch_tab`; `mode: "hover"` reveals content without activating anything, so it is not checked against the URL policy. A button whose handler runs `location.href = …` can still leave the allowed hosts, and so can a `<form>` whose `action` points at another host (pressing its submit button is a navigation this check does not statically see), and a tab that is already sitting on a disallowed host stays open (the agent simply refuses to drive it); deploy an external network guard for hard isolation.
package/lib/client.js CHANGED
@@ -176,6 +176,11 @@ window.__ModuleLoader__.load({
176
176
  label: '允许在页面里执行任意 JavaScript',
177
177
  hint: '打开后 AI 能在任意网页里运行脚本:能读走页面上的任何内容,也能用你的登录状态去点、去填、去提交。真正需要时才打开。',
178
178
  },
179
+ {
180
+ key: 'diagnostics',
181
+ label: '诊断工具:控制台消息与网络请求',
182
+ hint: '给排障用:能读出页面控制台的报错和最近的网络请求(URL、状态、失败原因)。平时用不到;遇到"点了没反应"或"背后的请求失败了"再打开。',
183
+ },
179
184
  {
180
185
  key: 'allowCdp',
181
186
  label: '允许原始 CDP 调试协议',
@@ -224,13 +229,14 @@ window.__ModuleLoader__.load({
224
229
  const [enabled, setEnabled] = useState(null);
225
230
  const [engine, setEngine] = useState(null);
226
231
  const [engineUserChosen, setEngineUserChosen] = useState(false);
227
- const [gates, setGates] = useState({ allowEvaluate: null, allowCdp: null });
232
+ const [gates, setGates] = useState({ allowEvaluate: null, allowCdp: null, diagnostics: null });
228
233
  const [busy, setBusy] = useState(false);
229
234
  const [msg, setMsg] = useState('');
230
235
  const [logins, setLogins] = useState(null);
231
236
  const [showAll, setShowAll] = useState(false);
232
237
  const [openGroups, setOpenGroups] = useState([]);
233
238
  const [hiddenDomains, setHiddenDomains] = useState(readHiddenDomains());
239
+ const [loginsOpen, setLoginsOpen] = useState(false);
234
240
  const mountedRef = useRef(true);
235
241
 
236
242
  const refresh = async () => {
@@ -245,6 +251,7 @@ window.__ModuleLoader__.load({
245
251
  setGates({
246
252
  allowEvaluate: typeof s.allowEvaluate === 'boolean' ? s.allowEvaluate : null,
247
253
  allowCdp: typeof s.allowCdp === 'boolean' ? s.allowCdp : null,
254
+ diagnostics: typeof s.diagnostics === 'boolean' ? s.diagnostics : null,
248
255
  });
249
256
  }
250
257
  catch (error) {
@@ -333,16 +340,21 @@ window.__ModuleLoader__.load({
333
340
  const payload = {
334
341
  allowEvaluate: gates.allowEvaluate === true,
335
342
  allowCdp: gates.allowCdp === true,
343
+ diagnostics: gates.diagnostics === true,
336
344
  };
337
345
  payload[key] = next;
338
346
  try {
339
347
  const r = await api('/gates', 'POST', payload);
340
348
  if (r && r.error)
341
349
  throw new Error(r.error.message || r.error.code || 'unknown');
342
- setGates({ allowEvaluate: r.allowEvaluate === true, allowCdp: r.allowCdp === true });
350
+ setGates({
351
+ allowEvaluate: r.allowEvaluate === true,
352
+ allowCdp: r.allowCdp === true,
353
+ diagnostics: r.diagnostics === true,
354
+ });
343
355
  setMsg(r.changed
344
356
  ? (next
345
- ? '已打开:' + (key === 'allowEvaluate' ? '页面任意 JS' : '原始 CDP') + '。工具会在热重载后出现(约 1-2 秒);若一直没出现,重启一次 DSH。'
357
+ ? '已打开:' + ((GATE_ROWS.find((row) => row.key === key) || {}).label || key) + '。工具会在热重载后出现(约 1-2 秒);若一直没出现,重启一次 DSH。'
346
358
  : '已关闭。工具会在热重载后消失;若还在,重启一次 DSH。')
347
359
  : '状态未变化');
348
360
  }
@@ -425,15 +437,25 @@ window.__ModuleLoader__.load({
425
437
  gates.allowEvaluate === null && gates.allowCdp === null
426
438
  ? h('span', { key: 'unknown', style: S.tip }, '当前:读取不到(这个 profile 里的插件版本还没上报闸门状态)')
427
439
  : h('span', { key: 'sum', style: S.tip }, '当前:' + (gatesOn === 0 ? '都关着' : gatesOn + ' 个开着'))),
428
- h('div', { key: 'loginsGroup', style: S.group }, '已登录的站点(只给你自己看)'),
429
- h(Notes, { key: 'loginsNotes', title: '说明' },
440
+ h('div', {
441
+ key: 'loginsGroup',
442
+ style: { ...S.group, cursor: 'pointer', userSelect: 'none' },
443
+ onClick: () => setLoginsOpen((open) => !open),
444
+ }, '已登录的站点(只给你自己看)'
445
+ + (logins === null || logins.sites === undefined
446
+ ? ''
447
+ : ' · ' + logins.sites.length + ' 个域 · ' + logins.sites.reduce((acc, site) => acc + site.cookies, 0) + ' 个 cookie')
448
+ + (loginsOpen ? ' ▾ 点击收起' : ' ▸ 点击展开')),
449
+ loginsOpen ? h(Notes, { key: 'loginsNotes', title: '说明' },
430
450
  h('p', { style: S.note },
431
451
  '只列**这台浏览器真的访问过**的域(你在 DSH 里打开过的页面),第三方统计/广告域已排除 ——'
432
452
  + '别人的 cookie 属于你当时浏览的那个站,不代表你登录了它。判据是 OWASP 的会话特征:'
433
453
  + 'httpOnly 且(会话级 或 值像令牌),名字不作为依据。'
434
454
  + '每一行的说明来自网页自己的标题,或一份离线对照表(认不出的就直接显示域名,不编)。'
435
- + '这里不显示 cookie 的名字和值;名单只存在于本机内存、不写磁盘;也没有任何工具能让 AI 看到它。')),
436
- logins === null
455
+ + '这里不显示 cookie 的名字和值;名单只存在于本机内存、不写磁盘;也没有任何工具能让 AI 看到它。')) : null,
456
+ loginsOpen === false
457
+ ? null
458
+ : logins === null
437
459
  ? h('div', { key: 'loginsUnknown', style: S.tip },
438
460
  '读取不到:这个 profile 里的插件版本还没上报,或还没打开过浏览器。')
439
461
  : (() => {
@@ -16,15 +16,17 @@ export declare function homePatchPath(env?: NodeJS.ProcessEnv): string;
16
16
  *
17
17
  * @param allowEvaluate - register browser_evaluate
18
18
  * @param allowCdp - register browser_cdp
19
+ * @param diagnostics - register the two diagnostics tools
19
20
  * @returns the YAML block, markers included
20
21
  */
21
- export declare function renderBrowserBlock(allowEvaluate: boolean, allowCdp: boolean): string;
22
+ export declare function renderBrowserBlock(allowEvaluate: boolean, allowCdp: boolean, diagnostics: boolean): string;
22
23
  /** Merge the managed block into `original`, replacing an existing one in place. */
23
- export declare function mergeBrowserBlock(original: string, allowEvaluate: boolean, allowCdp: boolean): string;
24
+ export declare function mergeBrowserBlock(original: string, allowEvaluate: boolean, allowCdp: boolean, diagnostics: boolean): string;
24
25
  /** Read the current managed block, or undefined when there is none. */
25
26
  export declare function parseBrowserBlock(text: string): {
26
27
  allowEvaluate: boolean;
27
28
  allowCdp: boolean;
29
+ diagnostics: boolean;
28
30
  } | undefined;
29
31
  /** Back-compat aliases used by the absorbed settings-card host half. */
30
32
  export declare const parseGatesBlock: typeof parseBrowserBlock;
@@ -39,8 +41,9 @@ export declare const mergeGatesBlock: typeof mergeBrowserBlock;
39
41
  * @param env - environment (DSH_HOME override for tests)
40
42
  * @returns the file written and the values it now carries
41
43
  */
42
- export declare function writeGates(allowEvaluate: boolean, allowCdp: boolean, env?: NodeJS.ProcessEnv): Promise<{
44
+ export declare function writeGates(allowEvaluate: boolean, allowCdp: boolean, diagnostics: boolean, env?: NodeJS.ProcessEnv): Promise<{
43
45
  patch: string;
44
46
  allowEvaluate: boolean;
45
47
  allowCdp: boolean;
48
+ diagnostics: boolean;
46
49
  }>;
@@ -41,9 +41,10 @@ export function homePatchPath(env = process.env) {
41
41
  *
42
42
  * @param allowEvaluate - register browser_evaluate
43
43
  * @param allowCdp - register browser_cdp
44
+ * @param diagnostics - register the two diagnostics tools
44
45
  * @returns the YAML block, markers included
45
46
  */
46
- export function renderBrowserBlock(allowEvaluate, allowCdp) {
47
+ export function renderBrowserBlock(allowEvaluate, allowCdp, diagnostics) {
47
48
  return [
48
49
  BEGIN,
49
50
  '# allowEvaluate: 注册 browser_evaluate(页面任意 JS,能用你的登录态操作网站)',
@@ -72,6 +73,7 @@ export function renderBrowserBlock(allowEvaluate, allowCdp) {
72
73
  ' toolPrefix: "browser_"',
73
74
  ' allowEvaluate: ' + (allowEvaluate ? 'true' : 'false'),
74
75
  ' allowCdp: ' + (allowCdp ? 'true' : 'false'),
76
+ ' diagnostics: ' + (diagnostics ? 'true' : 'false'),
75
77
  ' registerDisabledTools: false',
76
78
  ' diagnostics: false',
77
79
  ' maxWaitMs: 60000',
@@ -98,8 +100,8 @@ function findBlock(original) {
98
100
  return undefined;
99
101
  }
100
102
  /** Merge the managed block into `original`, replacing an existing one in place. */
101
- export function mergeBrowserBlock(original, allowEvaluate, allowCdp) {
102
- const block = renderBrowserBlock(allowEvaluate, allowCdp);
103
+ export function mergeBrowserBlock(original, allowEvaluate, allowCdp, diagnostics) {
104
+ const block = renderBrowserBlock(allowEvaluate, allowCdp, diagnostics);
103
105
  const found = findBlock(original);
104
106
  if (found !== undefined)
105
107
  return original.slice(0, found.start) + block + original.slice(found.after);
@@ -115,7 +117,12 @@ export function parseBrowserBlock(text) {
115
117
  const cdp = /allowCdp:\s*(true|false)/.exec(body);
116
118
  if (evaluate === null || cdp === null)
117
119
  return undefined;
118
- return { allowEvaluate: evaluate[1] === 'true', allowCdp: cdp[1] === 'true' };
120
+ const diag = /diagnostics:\s*(true|false)/.exec(body);
121
+ return {
122
+ allowEvaluate: evaluate[1] === 'true',
123
+ allowCdp: cdp[1] === 'true',
124
+ diagnostics: diag === null ? false : diag[1] === 'true',
125
+ };
119
126
  }
120
127
  /** Back-compat aliases used by the absorbed settings-card host half. */
121
128
  export const parseGatesBlock = parseBrowserBlock;
@@ -130,7 +137,7 @@ export const mergeGatesBlock = mergeBrowserBlock;
130
137
  * @param env - environment (DSH_HOME override for tests)
131
138
  * @returns the file written and the values it now carries
132
139
  */
133
- export async function writeGates(allowEvaluate, allowCdp, env = process.env) {
140
+ export async function writeGates(allowEvaluate, allowCdp, diagnostics, env = process.env) {
134
141
  const file = homePatchPath(env);
135
142
  let original = '';
136
143
  try {
@@ -139,7 +146,7 @@ export async function writeGates(allowEvaluate, allowCdp, env = process.env) {
139
146
  catch {
140
147
  original = '';
141
148
  }
142
- const next = mergeBrowserBlock(original, allowEvaluate, allowCdp);
149
+ const next = mergeBrowserBlock(original, allowEvaluate, allowCdp, diagnostics);
143
150
  if (original !== '')
144
151
  await copyFile(file, file + '.bak-browser-toggle').catch(() => { });
145
152
  const tmp = file + '.tmp-browser-toggle';
@@ -147,12 +154,15 @@ export async function writeGates(allowEvaluate, allowCdp, env = process.env) {
147
154
  await rename(tmp, file);
148
155
  const check = await readFile(file, 'utf8');
149
156
  const parsed = parseBrowserBlock(check);
150
- if (parsed === undefined || parsed.allowEvaluate !== allowEvaluate || parsed.allowCdp !== allowCdp) {
157
+ if (parsed === undefined ||
158
+ parsed.allowEvaluate !== allowEvaluate ||
159
+ parsed.allowCdp !== allowCdp ||
160
+ parsed.diagnostics !== diagnostics) {
151
161
  if (original !== '') {
152
162
  await writeFile(file, original, 'utf8');
153
163
  throw new Error('patch self-check failed after write; previous content restored');
154
164
  }
155
165
  throw new Error('patch self-check failed after write');
156
166
  }
157
- return { patch: file, allowEvaluate, allowCdp };
167
+ return { patch: file, allowEvaluate, allowCdp, diagnostics };
158
168
  }
@@ -64,11 +64,13 @@ export declare function isEngineUserChosen(): boolean;
64
64
  export declare function publishGates(gates: {
65
65
  allowEvaluate: boolean;
66
66
  allowCdp: boolean;
67
+ diagnostics: boolean;
67
68
  }): void;
68
69
  /** The published gates, or undefined while the tool family has not been applied. */
69
70
  export declare function getGates(): {
70
71
  allowEvaluate: boolean;
71
72
  allowCdp: boolean;
73
+ diagnostics: boolean;
72
74
  } | undefined;
73
75
  /**
74
76
  * The last computed "which sites look logged in" summary, or undefined before anything
@@ -149,7 +149,11 @@ export function isEngineUserChosen() {
149
149
  * @param gates - whether browser_evaluate / browser_cdp are registered.
150
150
  */
151
151
  export function publishGates(gates) {
152
- _gates = { allowEvaluate: gates.allowEvaluate === true, allowCdp: gates.allowCdp === true };
152
+ _gates = {
153
+ allowEvaluate: gates.allowEvaluate === true,
154
+ allowCdp: gates.allowCdp === true,
155
+ diagnostics: gates.diagnostics === true,
156
+ };
153
157
  }
154
158
  /** The published gates, or undefined while the tool family has not been applied. */
155
159
  export function getGates() {
@@ -135,6 +135,7 @@ export function apply(ctx) {
135
135
  // provider build in this profile is too old to report them.
136
136
  allowEvaluate: gates ? gates.allowEvaluate : null,
137
137
  allowCdp: gates ? gates.allowCdp : null,
138
+ diagnostics: gates ? gates.diagnostics : null,
138
139
  // What the user's own layer says right now. Differs from the above
139
140
  // between a write and the live reload that applies it.
140
141
  patchGates: managedBlockGates() ?? null,
@@ -209,6 +210,8 @@ export function apply(ctx) {
209
210
  catch {
210
211
  return sendJson(res, 400, { error: { code: 'INVALID_JSON', message: 'body must be JSON' } });
211
212
  }
213
+ if (typeof payload.diagnostics !== 'boolean')
214
+ payload.diagnostics = false;
212
215
  if (typeof payload.allowEvaluate !== 'boolean' || typeof payload.allowCdp !== 'boolean')
213
216
  return sendJson(res, 400, {
214
217
  error: {
@@ -217,16 +220,20 @@ export function apply(ctx) {
217
220
  },
218
221
  });
219
222
  const before = readGates();
220
- if (before && before.allowEvaluate === payload.allowEvaluate && before.allowCdp === payload.allowCdp)
223
+ if (before &&
224
+ before.allowEvaluate === payload.allowEvaluate &&
225
+ before.allowCdp === payload.allowCdp &&
226
+ before.diagnostics === payload.diagnostics)
221
227
  return sendJson(res, 200, { ...before, changed: false, patch: null });
222
228
  try {
223
- const written = await writeGates(payload.allowEvaluate, payload.allowCdp);
229
+ const written = await writeGates(payload.allowEvaluate, payload.allowCdp, payload.diagnostics);
224
230
  // The effective values only move once the loader re-applies the tree
225
231
  // (profile patchReload: live); report what was written, and let the
226
232
  // panel's next /state poll show whether it took effect.
227
233
  sendJson(res, 200, {
228
234
  allowEvaluate: written.allowEvaluate,
229
235
  allowCdp: written.allowCdp,
236
+ diagnostics: written.diagnostics,
230
237
  changed: true,
231
238
  patch: written.patch,
232
239
  });
package/lib/tool.js CHANGED
@@ -60,7 +60,11 @@ export function apply(ctx, config) {
60
60
  // home-level patch), so the tool family publishes it at registration and the
61
61
  // switch shows the truth instead of a guess. Driven by the same values that
62
62
  // decided which tools register, so the two can never disagree.
63
- publishGates({ allowEvaluate: registered.has('evaluate'), allowCdp: registered.has('cdp') });
63
+ publishGates({
64
+ allowEvaluate: registered.has('evaluate'),
65
+ allowCdp: registered.has('cdp'),
66
+ diagnostics: registered.has('console_messages'),
67
+ });
64
68
  /** Register one gated tool only when this configuration keeps it live. */
65
69
  const registerMaybe = (suffix, definition) => {
66
70
  if (registered.has(suffix))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-browser-playwright-codex",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Browser automation for DeepSeek Harness, one plugin: 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; native dialogs are parked instead of confirmed, CAPTCHA pages are flagged with a note that says stop and hand the page back, password / one-time-code / card fields are refused by browser_fill, and the Settings card (enable/disable, page-reading mode, capability gates) now lives inside this package.",
5
5
  "license": "MIT",
6
6
  "type": "module",