dsh-browser-playwright-codex 0.7.0 → 0.7.2

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,8 +1,35 @@
1
+ ## 0.7.2 — 2026-10-19
2
+
3
+ 一句话摘要:**设置卡推倒重做** —— 用户要求按 DSH 设置页规范重新设计:首屏只留「主开关 + 三个分区摘要」,所有长说明收进折叠。0.7.1 因卡在 npm staged 状态作废(版本号让给这一版)。
4
+
5
+ ### Changed
6
+
7
+ - **设置卡重做**:主开关一行;「页面读取方式」两个紧凑单选项 + 一个「两种模式到底差在哪」折叠;「工具权限」三个开关(任意 JS / 原始 CDP / 诊断工具)+ 一个「这些开关具体放行什么」折叠,标题行直接显示「N / 3 已开启」;「已登录的站点」默认收起,标题行只给「N 个域 · M 个 cookie」,展开后仍按公司分组折叠。名单每行只保留域名 + 说明 + cookie 数,逐个站点的四个 OWASP 信号不再塞在主视图里。
8
+ - 卡片文案统一为"人话",不再出现 evaluate / cdp / 引擎 / legacy / aria 这些词。
9
+
1
10
  # Changelog
2
11
 
3
12
  Bilingual by intent: the version headings and the summary of each release are given in both
4
13
  languages, the individual entries follow the language the surrounding code and docs use.
5
14
 
15
+ ## 0.7.1 — 2026-10-19
16
+
17
+ 一句话摘要:**只修文档与一处口径** —— 外部评分(8.9/10)点名的问题:CHANGELOG 的数字手写写错、对比表把五个闸门写成三个、有头模式的 viewport 仍然不生效却没标注。行为零变化。
18
+
19
+ ### Added
20
+
21
+ - **设置卡新增第三个工具开关:诊断工具**(`browser_console_messages` / `browser_network_requests`)。与 `evaluate` / `cdp` 同一套机制:`POST /api/browser-playwright/gates` 现在接受 `diagnostics`,home 补丁块的整行 config 一并写入,`tool.ts` 把**实际生效值**(而不是猜的)发布给卡片;卡片上显示"当前:N 个开着"也把这一项算进去。
22
+ - **「已登录的站点」默认折叠**:标题行直接给出「N 个域 · M 个 cookie · 点击展开」,点开才渲染说明与名单(分组仍各自折叠)。设置页不再被这份名单占掉大半屏。
23
+
24
+ ### Fixed
25
+
26
+ - **CHANGELOG 0.7.0 的数字按实测改正**:常驻是 **16 工具 / 11,179 字符**(此前手写成 11,005 字符,token 数也跟着漂),对比基线 18 工具 / 12,755 字符保持不变,字符降幅 **−1,576(−12.4%)**。这是连续第三个版本出现"主文档被守卫钉住、CHANGELOG 叙事手写漂移"——本轮起新增加一条守卫:**最新一节 CHANGELOG 不许出现手写的 `≈ N token/轮` 数字**,要写就写实测命令与结果表。
27
+ - **对比与优势.md 的闸门清单**:"五个闸门"此前只列了 `evaluate` / `cdp` / `extract` 三个名字,补上 `console_messages` / `network_requests`,并把它与全量注册的字符差从旧的 2,853 更正为实测 4,603 字符(token 换算见 README 成本表)。
28
+
29
+ ### Changed
30
+
31
+ - **有头模式的 `viewport` 标注**:`browser-lifecycle.ts:44` 用 `viewport: null` 跟随真实窗口,所以 `launch.viewport` 只在 `headless: true` 时生效;README 配置示例与 Known Limitations 现在明写这一点(此前"配了也不生效"没有任何提示,被外部评分连续四次点出)。
32
+
6
33
  ## 0.7.0 — 2026-10-19
7
34
 
8
35
  一句话摘要:**两个插件合成一个** —— 设置卡片并入本包;加载行从三行收成一行(这是 `dsh-client-modules` 允许本包带 client 模块的前提),独立的 `dsh-browser-toggle` 退役。
@@ -19,7 +46,7 @@ languages, the individual entries follow the language the surrounding code and d
19
46
 
20
47
  ### Changed
21
48
 
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。
49
+ - **两个诊断工具改为门控**:`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
50
  - **快照瘦身(运行时大头)**:无名无状态的 `generic` 包装层不再逐层输出(真实页面常把一个链接包在 7~8 层 div 里),显示用 `href` 去掉追踪参数(`utm_*` / `gclid` / `rsv_*` / `sa` 等)并截到 160 字符。百度结果页形状的样本:**6,169 → 2,557 字符(−58.6%)**,行数 108 → 24。refs 与点击行为不变 —— 省的是模型读到的东西。
24
51
  - 闸门写入块改为**整行 config**(新标记 `# >>> dsh-browser:`),并兼容旧的 `dsh-browser-toggle` 标记就地替换 —— 避免同一个 id 被两段 patch 争抢。
25
52
  - `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
  }
@@ -360,18 +372,26 @@ window.__ModuleLoader__.load({
360
372
  const statusLabel = enabled === null ? '读取中…' : (enabled ? '已启用' : '已禁用');
361
373
  const current = ENGINE_OPTIONS.find((o) => o.id === engine) || null;
362
374
 
375
+ const GATE_SHORT = {
376
+ allowEvaluate: '页面里的任意 JavaScript',
377
+ allowCdp: '原始 CDP 调试协议',
378
+ diagnostics: '控制台消息 / 网络请求',
379
+ };
380
+ const ENGINE_SHORT = { legacy: '最稳,奇怪网站也不容易出错', aria: '更准,复杂页面优先' };
381
+
363
382
  return h('div', { style: S.page }, [
364
383
  h('h2', { key: 'title', style: S.title }, '浏览器'),
365
384
  h('p', { key: 'intro', style: S.intro },
366
- '让 AI 用你的登录状态替你上网办事(查后台、填表、抓列表)。这里的改动立刻生效,不需要重启。'),
385
+ '让 AI 用你的登录状态替你上网办事。这里的改动立刻生效,不需要重启。'),
386
+
387
+ // ---- 主开关 ----
367
388
  h('div', { key: 'row', style: S.row }, [
368
389
  h(Dot, { key: 'dot', state }),
369
390
  h('div', { key: 'text', style: { flex: 1, minWidth: 0 } }, [
370
391
  h('div', { key: 'label', style: S.rowTitle }, '允许 AI 操作网页'),
371
- h('div', { key: 'desc', style: S.rowDesc }, [
392
+ h('div', { key: 'desc', style: S.rowDesc },
372
393
  h(Tag, { key: 'status' }, statusLabel),
373
- ' 关闭后已打开的窗口会自动关闭,登录状态会保留。',
374
- ]),
394
+ '关闭后窗口会关掉,登录状态保留。'),
375
395
  ]),
376
396
  h(Toggle, {
377
397
  key: 'toggle',
@@ -381,37 +401,40 @@ window.__ModuleLoader__.load({
381
401
  label: '允许 AI 操作网页',
382
402
  }),
383
403
  ]),
384
- h('div', { key: 'group', style: S.group }, '页面识别方式'),
385
- h(Notes, { key: 'groupNotes', title: '说明' },
386
- h('p', { style: S.note },
387
- '决定它怎么"看"一个网页:页面上有哪些能点的、能填的。两种都能正常用,选哪个看你常去的网站。')),
388
- h('div', { key: 'engines', style: { ...S.list, marginTop: 8 } }, ENGINE_OPTIONS.map((o) =>
389
- h(RadioRow, {
404
+
405
+ // ---- 页面读取方式 ----
406
+ h('div', { key: 'engineGroup', style: S.group }, [
407
+ '页面读取方式',
408
+ h('span', { key: 'cur', style: S.groupCount },
409
+ current ? ' 当前:' + current.label : ' 读取中…'),
410
+ ]),
411
+ h('div', { key: 'engines', style: { ...S.list, marginTop: 8 } },
412
+ ENGINE_OPTIONS.map((o) => h(RadioRow, {
390
413
  key: o.id,
391
- p: o,
414
+ p: { id: o.id, label: o.label, hint: ENGINE_SHORT[o.id] || '' },
392
415
  selected: engine === o.id,
393
416
  disabled: engine === null || busy,
394
417
  onPick: chooseEngine,
395
418
  }))),
396
- h('div', { key: 'footer', style: S.footer }, [
397
- h(Btn, { key: 'refresh', onClick: refresh, disabled: busy }, '刷新状态'),
398
- current
399
- ? h('span', { key: 'cur', style: S.tip },
400
- '当前:' + current.label + (engineUserChosen ? '(你选的)' : '(默认)'))
401
- : null,
419
+ h(Notes, {
420
+ key: 'engineNotes',
421
+ title: '两种模式到底差在哪',
422
+ children: h('div', null, ENGINE_OPTIONS.map((o) => h('p', { key: o.id, style: S.note },
423
+ o.label + ':' + o.hint))),
424
+ }),
425
+
426
+ // ---- 工具权限 ----
427
+ h('div', { key: 'gateGroup', style: S.group }, [
428
+ '工具权限',
429
+ h('span', { key: 'sum', style: S.groupCount },
430
+ gatesOn === 0 ? ' 全部关闭' : ' ' + gatesOn + ' / ' + GATE_ROWS.length + ' 已开启'),
402
431
  ]),
403
- h('div', { key: 'gatesGroup', style: S.group }, '强力工具(默认关闭)'),
404
- h(Notes, { key: 'gatesNotes', title: '说明' },
405
- h('p', { style: S.note },
406
- '这两个开关控制的是"能力",不是效果微调:打开"任意 JavaScript"就等于把页面里的东西都交给 AI。'
407
- + '插件包里默认关闭 —— 因为包是发给每个安装者的,默认打开等于替所有人做了这个决定;'
408
- + '这里打开的是你自己机器上的选择,记在你自己机器的配置层里。')),
409
- h('div', { key: 'gates', style: { ...S.list, marginTop: 8 } }, GATE_ROWS.map((row) =>
410
- h('div', { key: row.key, style: S.row }, [
432
+ h('div', { key: 'gates', style: { ...S.list, marginTop: 8 } },
433
+ GATE_ROWS.map((row) => h('div', { key: row.key, style: S.row }, [
411
434
  h(Dot, { key: 'dot', state: gateState(row.key) }),
412
435
  h('div', { key: 'text', style: { flex: 1, minWidth: 0 } }, [
413
436
  h('div', { key: 'label', style: S.rowTitle }, row.label),
414
- h('div', { key: 'desc', style: S.rowDesc }, row.hint),
437
+ h('div', { key: 'desc', style: S.rowDesc }, GATE_SHORT[row.key] || row.hint),
415
438
  ]),
416
439
  h(Toggle, {
417
440
  key: 'toggle',
@@ -421,34 +444,54 @@ window.__ModuleLoader__.load({
421
444
  label: row.label,
422
445
  }),
423
446
  ]))),
424
- h('div', { key: 'gatesFooter', style: S.footer },
425
- gates.allowEvaluate === null && gates.allowCdp === null
426
- ? h('span', { key: 'unknown', style: S.tip }, '当前:读取不到(这个 profile 里的插件版本还没上报闸门状态)')
427
- : h('span', { key: 'sum', style: S.tip }, '当前:' + (gatesOn === 0 ? '都关着' : gatesOn + ' 个开着'))),
428
- h('div', { key: 'loginsGroup', style: S.group }, '已登录的站点(只给你自己看)'),
429
- h(Notes, { key: 'loginsNotes', title: '说明' },
447
+ h(Notes, {
448
+ key: 'gateNotes',
449
+ title: '这些开关具体放行什么',
450
+ children: h('div', null, [
451
+ h('p', { key: 'evaluate', style: S.note },
452
+ '页面里执行任意 JavaScript:能读走页面内容,也能用你的登录状态去点、去填、去提交。真正需要时才打开。'),
453
+ h('p', { key: 'cdp', style: S.note },
454
+ '原始 CDP 协议:只放行查看类和模拟输入类命令;读 Cookie/存储、改网络、动安全设置一律被拦。排查疑难页面用。'),
455
+ h('p', { key: 'diagnostics', style: S.note },
456
+ '控制台消息 + 网络请求:能读出页面报错和最近的请求(URL、状态、失败原因)。平时用不到,遇到"点了没反应"再打开。'),
457
+ h('p', { key: 'effect', style: S.note },
458
+ '工具在热重载后出现或消失(约 1-2 秒);若一直没变,重启一次 DSH。'),
459
+ ]),
460
+ }),
461
+
462
+ // ---- 已登录站点(默认收起) ----
463
+ h('div', {
464
+ key: 'loginsGroup',
465
+ style: { ...S.group, cursor: 'pointer', userSelect: 'none' },
466
+ onClick: () => setLoginsOpen((open) => !open),
467
+ }, [
468
+ '已登录的站点',
469
+ h('span', { key: 'count', style: S.groupCount },
470
+ logins === null || logins.sites === undefined
471
+ ? ' 读取中…'
472
+ : ' ' + logins.sites.length + ' 个域 · '
473
+ + logins.sites.reduce((acc, site) => acc + site.cookies, 0) + ' 个 cookie'),
474
+ h('span', { key: 'hint', style: { ...S.groupCount, marginLeft: 8 } },
475
+ loginsOpen ? '点击收起' : '点击展开'),
476
+ ]),
477
+ loginsOpen ? h(Notes, { key: 'loginsNotes', title: '这份名单怎么来的' },
430
478
  h('p', { style: S.note },
431
- '只列**这台浏览器真的访问过**的域(你在 DSH 里打开过的页面),第三方统计/广告域已排除 ——'
432
- + '别人的 cookie 属于你当时浏览的那个站,不代表你登录了它。判据是 OWASP 的会话特征:'
433
- + 'httpOnly 且(会话级 或 值像令牌),名字不作为依据。'
434
- + '每一行的说明来自网页自己的标题,或一份离线对照表(认不出的就直接显示域名,不编)。'
435
- + '这里不显示 cookie 的名字和值;名单只存在于本机内存、不写磁盘;也没有任何工具能让 AI 看到它。')),
436
- logins === null
479
+ '只列这台浏览器真的访问过的域;第三方统计/广告域已排除。判据是 OWASP 的会话特征,'
480
+ + '名字不作为依据;不显示 cookie 的名字和值,名单只在本机内存,AI 看不到。')) : null,
481
+ loginsOpen === false
482
+ ? null
483
+ : logins === null
437
484
  ? h('div', { key: 'loginsUnknown', style: S.tip },
438
485
  '读取不到:这个 profile 里的插件版本还没上报,或还没打开过浏览器。')
439
486
  : (() => {
440
487
  const pool = showAll ? logins.sites : logins.sites.filter((s) => s.likely);
441
488
  const visible = pool.filter((s) => hiddenDomains.indexOf(s.domain) < 0);
442
489
  const hiddenCount = pool.length - visible.length;
443
- // 同一家公司的域折叠成一行:真实名单里京东系二十来个域各自占一行,小半屏都是它。
444
490
  const byGroup = new Map();
445
491
  const items = [];
446
492
  for (const site of visible) {
447
493
  const key = site.group ? site.group.key : null;
448
- if (key === null) {
449
- items.push({ kind: 'single', site });
450
- continue;
451
- }
494
+ if (key === null) { items.push({ kind: 'single', site }); continue; }
452
495
  let group = byGroup.get(key);
453
496
  if (group === undefined) {
454
497
  group = { kind: 'group', key, label: site.group.label, sites: [] };
@@ -457,7 +500,6 @@ window.__ModuleLoader__.load({
457
500
  }
458
501
  group.sites.push(site);
459
502
  }
460
- // 组里只剩一个域就没有折叠的意义,退回单行。
461
503
  for (let i = items.length - 1; i >= 0; i -= 1) {
462
504
  if (items[i].kind === 'group' && items[i].sites.length === 1)
463
505
  items[i] = { kind: 'single', site: items[i].sites[0] };
@@ -470,16 +512,12 @@ window.__ModuleLoader__.load({
470
512
  h('div', { key: 'text', style: { flex: 1, minWidth: 0 } }, [
471
513
  h('div', { key: 'label', style: S.rowTitle }, [
472
514
  site.likely ? h(Tag, { key: 'tag' }, '推测登录') : null,
473
- // 说明优先:光看域名认不出来是这个功能最初的痛点。
474
515
  site.alias || site.title || site.domain,
475
516
  ]),
476
517
  h('div', { key: 'desc', style: S.rowDesc },
477
518
  (site.alias ? site.domain + ' ' : '')
478
519
  + (site.title && site.title !== site.alias ? '「' + site.title + '」 ' : '')
479
- + site.cookies + ' 个 cookie(httpOnly ' + site.httpOnly
480
- + ' / 会话 ' + site.sessionLike
481
- + ' / 值像令牌 ' + site.tokenLike + ')'
482
- + (site.signals.length > 0 ? ' ' + site.signals.join(' · ') : '')),
520
+ + site.cookies + ' 个 cookie'),
483
521
  ]),
484
522
  h(Btn, {
485
523
  key: 'hide',
@@ -493,74 +531,52 @@ window.__ModuleLoader__.load({
493
531
  const renderItem = (item) => {
494
532
  if (item.kind === 'single') return rowFor(item.site, false);
495
533
  const open = openGroups.indexOf(item.key) >= 0;
496
- const anyLikely = item.sites.some((s) => s.likely);
497
534
  return h('div', { key: 'group-' + item.key }, [
498
535
  h('div', { key: 'head', style: S.row }, [
499
536
  h('div', { key: 'text', style: { flex: 1, minWidth: 0 } }, [
500
- h('div', { key: 'label', style: S.rowTitle }, [
501
- anyLikely ? h(Tag, { key: 'tag' }, '推测登录') : null,
502
- item.label + '(' + item.sites.length + ' 个域)',
503
- ]),
537
+ h('div', { key: 'label', style: S.rowTitle },
538
+ item.label + '(' + item.sites.length + ' 个域)'),
504
539
  h('div', { key: 'desc', style: S.rowDesc },
505
- sum(item.sites, (s) => s.cookies) + ' 个 cookie httpOnly '
506
- + sum(item.sites, (s) => s.httpOnly) + ' 会话 '
507
- + sum(item.sites, (s) => s.sessionLike) + ' 值像令牌 '
508
- + sum(item.sites, (s) => s.tokenLike)),
540
+ sum(item.sites, (s) => s.cookies) + ' 个 cookie'),
509
541
  ]),
510
542
  h(Btn, {
511
543
  key: 'toggle',
512
- onClick: () => {
513
- setOpenGroups(open
514
- ? openGroups.filter((k) => k !== item.key)
515
- : openGroups.concat([item.key]));
516
- },
544
+ onClick: () => setOpenGroups(open
545
+ ? openGroups.filter((k) => k !== item.key)
546
+ : openGroups.concat([item.key])),
517
547
  }, open ? '收起' : '展开'),
518
548
  ]),
519
- open
520
- ? h('div', { key: 'members' }, item.sites.map((s) => rowFor(s, true)))
521
- : null,
549
+ open ? h('div', { key: 'members' }, item.sites.map((s) => rowFor(s, true))) : null,
522
550
  ]);
523
551
  };
524
552
  return h('div', { key: 'loginsBody' }, [
525
553
  h('div', { key: 'list', style: { ...S.list, marginTop: 8 } },
526
554
  visible.length === 0
527
555
  ? h('div', { key: 'none', style: S.tip },
528
- hiddenCount > 0
529
- ? '这一页都被你隐藏了。'
530
- : '还没发现登录过的站点(用这个浏览器登录过网站后,这里会出现)。')
556
+ hiddenCount > 0 ? '这一页都被你隐藏了。' : '还没发现登录过的站点。')
531
557
  : items.map(renderItem)),
532
558
  h('div', { key: 'loginsFooter', style: S.footer }, [
533
559
  h('span', { key: 'src', style: S.tip },
534
- (logins.source === 'live' ? '实时(浏览器开着)' : '上次快照(浏览器当前没开)')
535
- + ' ' + new Date(logins.capturedAt).toLocaleTimeString()
536
- + ' 你在 DSH 里访问过 ' + logins.total + ' 个域,其中 '
537
- + logins.likelyTotal + ' 个像登录态'
538
- + (logins.thirdPartyTotal > 0
539
- ? ';另有 ' + logins.thirdPartyTotal + ' 个第三方域(统计/广告)已排除'
540
- : '')
541
- + (logins.attribution === 'unfiltered'
542
- ? '(这次没拿到浏览器打开的页面列表,未做第一方过滤:下面可能有第三方域)'
543
- : '')),
560
+ (logins.source === 'live' ? '实时' : '上次快照')
561
+ + (logins.attribution === 'unfiltered' ? '(未做第一方过滤)' : '')),
544
562
  h(Btn, {
545
563
  key: 'all',
546
564
  onClick: () => setShowAll(!showAll),
547
- }, showAll
548
- ? '只看像登录态的 ' + logins.likelyTotal + ' 个'
549
- : '显示其余 ' + (logins.total - logins.likelyTotal) + ' 个'),
550
-
565
+ }, showAll ? '只看像登录态的' : '显示其余 ' + (logins.total - logins.likelyTotal) + ' 个'),
551
566
  hiddenCount > 0
552
567
  ? h(Btn, {
553
568
  key: 'reset',
554
- onClick: () => {
555
- setHiddenDomains([]);
556
- writeHiddenDomains([]);
557
- },
558
- }, '恢复显示 ' + hiddenCount + ' 个被隐藏的')
569
+ onClick: () => { setHiddenDomains([]); writeHiddenDomains([]); },
570
+ }, '恢复 ' + hiddenCount + ' 个被隐藏的')
559
571
  : null,
560
572
  ]),
561
573
  ]);
562
574
  })(),
575
+
563
576
  msg ? h('div', { key: 'msg', style: S.msg }, msg) : null,
577
+ h('div', { key: 'foot', style: S.footer }, [
578
+ h(Btn, { key: 'refresh', onClick: refresh, disabled: busy }, '刷新状态'),
579
+ ]),
564
580
  ]);
565
581
  }
566
582
 
@@ -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.2",
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",