dsh-browser-playwright-codex 0.6.5 → 0.7.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/lib/tool.js CHANGED
@@ -21,6 +21,7 @@ export const Config = z.object({
21
21
  allowEvaluate: z.boolean().default(false),
22
22
  allowCdp: z.boolean().default(false),
23
23
  registerDisabledTools: z.boolean().default(false),
24
+ diagnostics: z.boolean().default(false),
24
25
  maxWaitMs: z.number().default(60000),
25
26
  interactiveOnlyDefault: z.boolean().default(false),
26
27
  extract: z.object({
@@ -50,6 +51,7 @@ export function apply(ctx, config) {
50
51
  allowEvaluate: config.allowEvaluate,
51
52
  allowCdp: config.allowCdp,
52
53
  extractConfigured: config.extract?.provider !== undefined && config.extract?.model !== undefined,
54
+ diagnostics: config.diagnostics,
53
55
  registerDisabledTools: config.registerDisabledTools,
54
56
  };
55
57
  const registered = new Set(registeredSuffixes(surface));
@@ -262,6 +264,8 @@ export function apply(ctx, config) {
262
264
  description: 'Replace the value of the input or select referenced by the latest snapshot. ' +
263
265
  '`value` is accepted as an alias of `text`. ' +
264
266
  'For <select> elements the text selects the matching option label. ' +
267
+ 'Credential fields (password, one-time code, card number) are refused with CREDENTIAL_FIELD: ' +
268
+ 'a human types secrets in the visible window, so ask them and re-snapshot instead. ' +
265
269
  'IRON RULE: after filling, verify in the returned snapshot that the field actually holds the value. ' +
266
270
  'Frameworks can silently reject or transform input (controlled components, contenteditable editors, ' +
267
271
  'input-mask/currency fields). If the value is wrong or missing, do NOT blindly re-fill or append — ' +
@@ -584,58 +588,62 @@ export function apply(ctx, config) {
584
588
  return (await session.cdp(args.method, args.params ?? {}, exec.signal));
585
589
  },
586
590
  }));
587
- ctx.tools.register(defineTool({
588
- name: p('console_messages'),
589
- description: 'Return recent console messages and uncaught page errors from the tab this session is driving, newest last. ' +
590
- 'Use it right after an action that reported success but produced no visible change: a thrown page error or a failed ' +
591
- 'subresource is the usual reason a click or fill did not take effect. Read-only observation, it never acts on the page. ' +
592
- 'Capture starts when this session first drives a tab, so an empty list on a fresh tab is normal. ' +
593
- 'In persistent mode all sessions share one window, so entries describe that shared page.',
594
- parameters: {
595
- level: {
596
- type: 'string',
597
- description: "Filter by level: 'all' (default), 'error' (errors + uncaught page errors), 'warning', 'info', 'log', 'debug'.",
591
+ if (config.diagnostics || config.registerDisabledTools) {
592
+ ctx.tools.register(defineTool({
593
+ name: p('console_messages'),
594
+ description: 'Return recent console messages and uncaught page errors from the tab this session is driving, newest last. ' +
595
+ 'Use it right after an action that reported success but produced no visible change: a thrown page error or a failed ' +
596
+ 'subresource is the usual reason a click or fill did not take effect. Read-only observation, it never acts on the page. ' +
597
+ 'Capture starts when this session first drives a tab, so an empty list on a fresh tab is normal. ' +
598
+ 'In persistent mode all sessions share one window, so entries describe that shared page.',
599
+ parameters: {
600
+ level: {
601
+ type: 'string',
602
+ description: "Filter by level: 'all' (default), 'error' (errors + uncaught page errors), 'warning', 'info', 'log', 'debug'.",
603
+ },
604
+ limit: { type: 'number', description: 'Max entries to return, newest last. Default 50, cap 200.' },
598
605
  },
599
- limit: { type: 'number', description: 'Max entries to return, newest last. Default 50, cap 200.' },
600
- },
601
- output: {
602
- schema: { type: 'json' },
603
- render: (_args, value) => [
604
- { type: 'text', text: renderDiagnostics('console', value) },
605
- ],
606
- },
607
- async execute(args, exec) {
608
- const session = await acquire(exec);
609
- // The canonical value is a wire value; the framework types tool outputs as JSON.
610
- return session.consoleMessages(args ?? {});
611
- },
612
- }));
613
- ctx.tools.register(defineTool({
614
- name: p('network_requests'),
615
- description: 'Return recent network requests from the tab this session is driving, newest last: method, URL, status, ' +
616
- 'resource type, duration, and the failure text for requests that never completed. Use it to tell "the page did not ' +
617
- 'react" apart from "the request behind it failed", and to find the API call an action triggered. ' +
618
- 'Read-only; response bodies are not captured. In persistent mode all sessions share one window, so entries describe that shared page.',
619
- parameters: {
620
- urlContains: {
621
- type: 'string',
622
- description: 'Only return requests whose URL contains this substring (case-insensitive).',
606
+ output: {
607
+ schema: { type: 'json' },
608
+ render: (_args, value) => [
609
+ { type: 'text', text: renderDiagnostics('console', value) },
610
+ ],
623
611
  },
624
- failedOnly: { type: 'boolean', description: 'Only return requests that failed or returned status >= 400.' },
625
- limit: { type: 'number', description: 'Max entries to return, newest last. Default 50, cap 200.' },
626
- },
627
- output: {
628
- schema: { type: 'json' },
629
- render: (_args, value) => [
630
- { type: 'text', text: renderDiagnostics('network', value) },
631
- ],
632
- },
633
- async execute(args, exec) {
634
- const session = await acquire(exec);
635
- // The canonical value is a wire value; the framework types tool outputs as JSON.
636
- return session.networkRequests(args ?? {});
637
- },
638
- }));
612
+ async execute(args, exec) {
613
+ const session = await acquire(exec);
614
+ // The canonical value is a wire value; the framework types tool outputs as JSON.
615
+ return session.consoleMessages(args ?? {});
616
+ },
617
+ }));
618
+ }
619
+ if (config.diagnostics || config.registerDisabledTools) {
620
+ ctx.tools.register(defineTool({
621
+ name: p('network_requests'),
622
+ description: 'Return recent network requests from the tab this session is driving, newest last: method, URL, status, ' +
623
+ 'resource type, duration, and the failure text for requests that never completed. Use it to tell "the page did not ' +
624
+ 'react" apart from "the request behind it failed", and to find the API call an action triggered. ' +
625
+ 'Read-only; response bodies are not captured. In persistent mode all sessions share one window, so entries describe that shared page.',
626
+ parameters: {
627
+ urlContains: {
628
+ type: 'string',
629
+ description: 'Only return requests whose URL contains this substring (case-insensitive).',
630
+ },
631
+ failedOnly: { type: 'boolean', description: 'Only return requests that failed or returned status >= 400.' },
632
+ limit: { type: 'number', description: 'Max entries to return, newest last. Default 50, cap 200.' },
633
+ },
634
+ output: {
635
+ schema: { type: 'json' },
636
+ render: (_args, value) => [
637
+ { type: 'text', text: renderDiagnostics('network', value) },
638
+ ],
639
+ },
640
+ async execute(args, exec) {
641
+ const session = await acquire(exec);
642
+ // The canonical value is a wire value; the framework types tool outputs as JSON.
643
+ return session.networkRequests(args ?? {});
644
+ },
645
+ }));
646
+ }
639
647
  ctx.tools.register(defineTool({
640
648
  name: p('close'),
641
649
  description: "Release this session's browser window. Persistent mode shares one window, so it closes only when the last session releases it; the profile keeps login state either way, so the next browser call reopens it still signed in (reset the profile to clear cookies).",
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Browser Automation",
4
- "description": "Playwright-powered browser capability for DeepSeek Harness: accessibility-snapshot interaction with stable element refs, session-scoped browser sessions, screenshots as attachments."
4
+ "description": "Playwright-powered browser capability for DeepSeek Harness: accessibility-snapshot interaction with stable element refs, session-scoped browser sessions, screenshots as attachments — with the Settings card (on/off, page-reading mode, capability gates, signed-in sites) built in."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "浏览器自动化",
4
- "description": "浏览器自动化(Playwright):无障碍快照交互 + 稳定元素引用、会话级浏览器实例、截图作为附件。"
4
+ "description": "浏览器自动化(Playwright):无障碍快照交互 + 稳定元素引用 + 会话级浏览器实例 + 截图附件;自带设置卡(总开关 / 页面识别方式 / 强力工具闸门 / 已登录站点名单)。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-browser-playwright-codex",
3
- "version": "0.6.5",
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.",
3
+ "version": "0.7.0",
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",
7
7
  "main": "lib/index.js",
@@ -28,7 +28,14 @@
28
28
  },
29
29
  "./cordis.patch.yml": "./cordis.patch.yml",
30
30
  "./package.json": "./package.json",
31
- "./locale/*.json": "./locale/*.json"
31
+ "./locale/*.json": "./locale/*.json",
32
+ "./client": {
33
+ "default": "./lib/client.js"
34
+ },
35
+ "./plugin": {
36
+ "types": "./lib/plugin.d.ts",
37
+ "default": "./lib/plugin.js"
38
+ }
32
39
  },
33
40
  "files": [
34
41
  "lib",
@@ -42,7 +49,7 @@
42
49
  "scripts/dsh-browser-verify.mjs"
43
50
  ],
44
51
  "scripts": {
45
- "build": "node node_modules/typescript/bin/tsc -p tsconfig.json",
52
+ "build": "node node_modules/typescript/bin/tsc -p tsconfig.json && node scripts/build-client.mjs",
46
53
  "typecheck": "node node_modules/typescript/bin/tsc -p tsconfig.tests.json",
47
54
  "test": "node --test \"tests/*.test.ts\"",
48
55
  "test:scenarios": "node --test tests/scenarios.test.ts tests/tool-scenarios.test.ts",
@@ -57,13 +64,13 @@
57
64
  "preflight": "node scripts/preflight.mjs ."
58
65
  },
59
66
  "dependencies": {
60
- "@deepseek-ai/schemastery": "*",
67
+ "@deepseek-ai/schemastery": "^3.18.4",
61
68
  "playwright-core": "~1.62.1"
62
69
  },
63
70
  "peerDependencies": {
64
- "@deepseek-ai/cordis": "*",
65
- "@deepseek-ai/dsh-llm": "*",
66
- "@deepseek-ai/dsh-tools": "*"
71
+ "@deepseek-ai/cordis": ">=4.0.4",
72
+ "@deepseek-ai/dsh-llm": ">=0.2.0-rc.1",
73
+ "@deepseek-ai/dsh-tools": ">=0.2.0-rc.1"
67
74
  },
68
75
  "devDependencies": {
69
76
  "@deepseek-ai/cordis": "4.0.4",
@@ -82,8 +89,21 @@
82
89
  "dsh": {
83
90
  "bundle": {
84
91
  "patch": "./cordis.patch.yml"
92
+ },
93
+ "client": {
94
+ "platform": "web",
95
+ "inject": []
85
96
  }
86
97
  },
98
+ "engines": {
99
+ "node": "^22.19.0 || >=24.0.0"
100
+ },
101
+ "packageManager": "pnpm@10.31.0",
102
+ "repository": {
103
+ "type": "git",
104
+ "url": "git+https://github.com/miqian-nomad/dsh-browser-playwright-codex.git"
105
+ },
106
+ "homepage": "https://github.com/miqian-nomad/dsh-browser-playwright-codex#readme",
87
107
  "keywords": [
88
108
  "deepseek-harness",
89
109
  "dsh",
@@ -98,15 +118,7 @@
98
118
  "form-filling",
99
119
  "deepseek",
100
120
  "ai-agent",
101
- "rpa"
102
- ],
103
- "engines": {
104
- "node": "^22.19.0 || >=24.0.0"
105
- },
106
- "packageManager": "pnpm@10.31.0",
107
- "repository": {
108
- "type": "git",
109
- "url": "git+https://github.com/miqian-nomad/dsh-browser-playwright-codex.git"
110
- },
111
- "homepage": "https://github.com/miqian-nomad/dsh-browser-playwright-codex#readme"
121
+ "rpa",
122
+ "settings"
123
+ ]
112
124
  }
@@ -13,6 +13,8 @@
13
13
  *
14
14
  * 读 lib/,但**必须先 build**:npm run verify 会先跑 npm run build,直接
15
15
  * node scripts/... 跑则会撞上最后一组检查(lib/ 不旧于 src/)。
16
+ * npm 安装形态没有 src/(files 里不含它):最后一组自动**跳过并说明**,不算失败,
17
+ * 所以从 npm 装上之后这条命令照样能跑通 —— README 的「装到即可跑」必须是真的。
16
18
  *
17
19
  * 为什么不像测试那样用 tsx 直读 src/:tsx(esbuild keepNames)会把传给
18
20
  * page.evaluate 的函数包成 __name(...),而页面里没有这个符号 ——
@@ -20,7 +22,7 @@
20
22
  * 失效。lib/ 是编译产物,没有这层包裹,也正好是宿主真正加载的东西。
21
23
  */
22
24
  import http from 'node:http'
23
- import { readdirSync, statSync } from 'node:fs'
25
+ import { existsSync, readdirSync, statSync } from 'node:fs'
24
26
  import os from 'node:os'
25
27
  import path from 'node:path'
26
28
  import { PlaywrightProvider } from '../lib/provider.js'
@@ -32,12 +34,13 @@ import { getEffectiveEngine, setSnapshotEngine } from '../lib/runtime-state.js'
32
34
  // 设置页的「页面识别方式」存在 ~/.dsh/dsh-browser-playwright.state.json,运行时状态层
33
35
  // 允许用 DSH_BROWSER_STATE_FILE 把它指向别处(src/runtime-state.ts 里为测试留的开关)。
34
36
  // 指向一个不存在的文件 = engine 回到配置默认值 legacy,本脚本的期望值就是按它写的。
35
- // 不隔离的话:用户把开关拨到「智能模式」,这份 57 项验收就会假失败(2026-10-01 实测)。
37
+ // 不隔离的话:用户把开关拨到「智能模式」,这份验收就会假失败(2026-10-01 实测)。
36
38
  // 注意 [1]–[7] 因此都跑在兼容模式下;智能模式由 [8] 显式切过去跑(隔离状态文件里切,
37
39
  // 不碰用户的真实选择)。
38
40
  process.env.DSH_BROWSER_STATE_FILE = path.join(os.tmpdir(), 'dsh-browser-verify-state-isolated.json')
39
41
 
40
42
  let pass = 0
43
+ let skipped = 0
41
44
  const failures = []
42
45
  function check(name, ok, detail) {
43
46
  if (ok) {
@@ -455,22 +458,31 @@ await provider.dispose()
455
458
  server.close()
456
459
 
457
460
  // [7] lib/ 是宿主真正加载的产物:源码改完没 build,跑绿了也是白跑。
461
+ // npm 包里没有 src/(package.json 的 files 只收 lib/),安装形态下这一组无从比较:
462
+ // 跳过并说明原因,不记失败、不影响退出码 —— 否则装上 npm 包之后这条命令必崩,
463
+ // 与 README/CHANGELOG 的「装到即可跑」直接矛盾。
458
464
  console.log('[7] lib/ 与 src/ 同步(宿主加载 lib/)')
459
- const stale = []
460
- for (const file of readdirSync(new URL('../src', import.meta.url))) {
461
- if (!file.endsWith('.ts') || file.endsWith('.d.ts')) continue
462
- const src = statSync(new URL('../src/' + file, import.meta.url)).mtimeMs
463
- const built = statSync(new URL('../lib/' + file.replace(/\.ts$/, '.js'), import.meta.url), {
464
- throwIfNoEntry: false,
465
- })
466
- if (built === undefined || built.mtimeMs < src) stale.push(file.replace(/\.ts$/, '.js'))
465
+ if (!existsSync(new URL('../src', import.meta.url))) {
466
+ skipped += 1
467
+ console.log(' (跳过)没有 src/:这是 npm 安装形态')
468
+ } else {
469
+ const stale = []
470
+ for (const file of readdirSync(new URL('../src', import.meta.url))) {
471
+ if (!file.endsWith('.ts') || file.endsWith('.d.ts')) continue
472
+ const src = statSync(new URL('../src/' + file, import.meta.url)).mtimeMs
473
+ const built = statSync(new URL('../lib/' + file.replace(/\.ts$/, '.js'), import.meta.url), {
474
+ throwIfNoEntry: false,
475
+ })
476
+ if (built === undefined || built.mtimeMs < src) stale.push(file.replace(/\.ts$/, '.js'))
477
+ }
478
+ check('lib/ 不旧于 src/(旧了就先 npm run build)', stale.length === 0, stale.join(', '))
467
479
  }
468
- check('lib/ 不旧于 src/(旧了就先 npm run build)', stale.length === 0, stale.join(', '))
469
480
 
470
481
  console.log('')
482
+ const skipNote = skipped > 0 ? ',跳过 ' + skipped + ' 项' : ''
471
483
  console.log(
472
484
  failures.length === 0
473
- ? '全部通过 (' + pass + ' 项)'
485
+ ? '全部通过 (' + pass + ' 项' + skipNote + ')'
474
486
  : '失败 ' + failures.length + ' 项 / 共 ' + (pass + failures.length) + ' 项: ' + failures.join(', '),
475
487
  )
476
488
  process.exit(failures.length === 0 ? 0 : 1)