dsh-browser-playwright-codex 0.6.1 → 0.6.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-browser-playwright-codex",
3
- "version": "0.6.1",
3
+ "version": "0.6.6",
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",
@@ -32,13 +32,14 @@
32
32
  },
33
33
  "files": [
34
34
  "lib",
35
- "scripts",
36
35
  "cordis.patch.yml",
37
36
  "README.md",
38
37
  "CHANGELOG.md",
39
38
  "SECURITY.md",
40
39
  "NOTICE.md",
41
- "locale"
40
+ "locale",
41
+ "scripts/doctor.mjs",
42
+ "scripts/dsh-browser-verify.mjs"
42
43
  ],
43
44
  "scripts": {
44
45
  "build": "node node_modules/typescript/bin/tsc -p tsconfig.json",
@@ -56,13 +57,13 @@
56
57
  "preflight": "node scripts/preflight.mjs ."
57
58
  },
58
59
  "dependencies": {
59
- "@deepseek-ai/schemastery": "*",
60
+ "@deepseek-ai/schemastery": "^3.18.4",
60
61
  "playwright-core": "~1.62.1"
61
62
  },
62
63
  "peerDependencies": {
63
- "@deepseek-ai/cordis": "*",
64
- "@deepseek-ai/dsh-llm": "*",
65
- "@deepseek-ai/dsh-tools": "*"
64
+ "@deepseek-ai/cordis": ">=4.0.4",
65
+ "@deepseek-ai/dsh-llm": ">=0.2.0-rc.1",
66
+ "@deepseek-ai/dsh-tools": ">=0.2.0-rc.1"
66
67
  },
67
68
  "devDependencies": {
68
69
  "@deepseek-ai/cordis": "4.0.4",
@@ -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)
@@ -1,149 +0,0 @@
1
- /**
2
- * Measures what actually differs between the plugin's headless and headful
3
- * launch shapes, on the SAME persistent profile, using the plugin's real
4
- * launch options (channel chrome, HUMANIZED_LAUNCH args, viewport rules).
5
- *
6
- * MODE=headless|headful PROFILE=<dir> node hl-compare.mjs
7
- */
8
- import { createRequire } from 'node:module'
9
- import path from 'node:path'
10
- import fs from 'node:fs'
11
-
12
- const require = createRequire(import.meta.url)
13
- const { chromium } = require(process.env.PW_CORE_PATH ?? 'playwright-core')
14
-
15
- const MODE = process.env.MODE ?? 'headless'
16
- const headless = MODE === 'headless'
17
- const profile = process.env.PROFILE
18
- const probeHtml = process.env.PROBE_HTML
19
-
20
- // Mirror of the plugin's baseOptions (lib/playwright.js:437-457).
21
- const HUMANIZED_LAUNCH = { args: ['--disable-blink-features=AutomationControlled'] }
22
- const options = {
23
- headless,
24
- channel: process.env.CHANNEL || undefined,
25
- ...(headless ? { viewport: { width: 1280, height: 800 } } : { viewport: null }),
26
- ignoreHTTPSErrors: false,
27
- ...HUMANIZED_LAUNCH,
28
- args: [
29
- '--no-first-run',
30
- '--no-default-browser-check',
31
- ...(headless ? [] : ['--start-maximized']),
32
- ...HUMANIZED_LAUNCH.args,
33
- ...(process.env.EXTRA_ARG ? [process.env.EXTRA_ARG] : []),
34
- ],
35
- }
36
-
37
- const context = await chromium.launchPersistentContext(profile, options)
38
- const page = context.pages()[0] ?? (await context.newPage())
39
-
40
- // Persistent state continuity across a headless <-> headful switch.
41
- if (process.env.PHASE === 'write') {
42
- await page.goto('https://example.com', { waitUntil: 'domcontentloaded' }).catch(() => {})
43
- await context.addCookies([
44
- { name: 'switch-session', value: 'session-42', domain: 'example.com', path: '/' },
45
- {
46
- name: 'switch-durable',
47
- value: 'durable-42',
48
- domain: 'example.com',
49
- path: '/',
50
- expires: Math.floor(Date.now() / 1000) + 365 * 24 * 3600,
51
- },
52
- ])
53
- await page.evaluate(() => localStorage.setItem('switch-probe', 'kept-42')).catch(() => {})
54
- console.log(JSON.stringify({ mode: MODE, phase: 'write', cookieSet: true }))
55
- await context.close()
56
- process.exit(0)
57
- }
58
-
59
- if (process.env.PHASE === 'check') {
60
- await page.goto('https://example.com', { waitUntil: 'domcontentloaded' }).catch(() => {})
61
- const cookies = await context.cookies('https://example.com')
62
- const ls = await page.evaluate(() => localStorage.getItem('switch-probe')).catch(() => null)
63
- console.log(
64
- JSON.stringify(
65
- {
66
- mode: MODE,
67
- phase: 'check',
68
- sessionCookie: cookies.find((c) => c.name === 'switch-session')?.value ?? null,
69
- durableCookie: cookies.find((c) => c.name === 'switch-durable')?.value ?? null,
70
- localStorage: ls,
71
- },
72
- null,
73
- 2,
74
- ),
75
- )
76
- await context.close()
77
- process.exit(0)
78
- }
79
-
80
- await page.goto('file:///' + probeHtml.replace(/\\/g, '/'), { waitUntil: 'load' })
81
-
82
- const measured = await page.evaluate(async () => {
83
- const box = document.querySelector('#box').getBoundingClientRect()
84
- const cjk = document.querySelector('#cjk').getBoundingClientRect()
85
- const tail = document.querySelector('#tail').getBoundingClientRect()
86
- const rafStart = performance.now()
87
- let rafCount = 0
88
- await new Promise((resolve) => {
89
- const tick = () => {
90
- rafCount += 1
91
- if (performance.now() - rafStart > 1000) resolve()
92
- else requestAnimationFrame(tick)
93
- }
94
- requestAnimationFrame(tick)
95
- })
96
- return {
97
- ua: navigator.userAgent,
98
- webdriver: navigator.webdriver,
99
- inner: [window.innerWidth, window.innerHeight],
100
- outer: [window.outerWidth, window.outerHeight],
101
- screen: [screen.width, screen.height],
102
- avail: [screen.availWidth, screen.availHeight],
103
- dpr: window.devicePixelRatio,
104
- visibility: document.visibilityState,
105
- hasFocus: document.hasFocus(),
106
- rafPerSecond: rafCount,
107
- box: [Math.round(box.width), Math.round(box.height)],
108
- cjkWidth: Math.round(cjk.width * 100) / 100,
109
- tailY: Math.round(tail.top),
110
- fonts: document.fonts.size,
111
- // Media-feature fingerprint: Playwright's headless default args include
112
- // --blink-settings=primaryPointerType=4,availableHoverTypes=2.
113
- coarse: matchMedia('(pointer: coarse)').matches,
114
- fine: matchMedia('(pointer: fine)').matches,
115
- hoverHover: matchMedia('(hover: hover)').matches,
116
- anyHover: matchMedia('(any-hover: hover)').matches,
117
- maxTouchPoints: navigator.maxTouchPoints,
118
- languages: navigator.languages.join(','),
119
- breakpoints: {
120
- 'min-width:1000px': matchMedia('(min-width: 1000px)').matches,
121
- 'min-width:1152px': matchMedia('(min-width: 1152px)').matches,
122
- 'min-width:1200px': matchMedia('(min-width: 1200px)').matches,
123
- 'min-width:1280px': matchMedia('(min-width: 1280px)').matches,
124
- 'min-width:1440px': matchMedia('(min-width: 1440px)').matches,
125
- },
126
- }
127
- })
128
-
129
- const shot = await page.screenshot({ path: path.join(path.dirname(profile), `shot-${MODE}.png`) })
130
- const png = fs.readFileSync(path.join(path.dirname(profile), `shot-${MODE}.png`))
131
- // PNG IHDR: width/height are bytes 16-23.
132
- const shotW = png.readUInt32BE(16)
133
- const shotH = png.readUInt32BE(20)
134
-
135
- console.log(
136
- JSON.stringify(
137
- {
138
- mode: MODE,
139
- headless,
140
- ...measured,
141
- screenshotBytes: shot.length,
142
- screenshotSize: [shotW, shotH],
143
- },
144
- null,
145
- 2,
146
- ),
147
- )
148
-
149
- await context.close()
@@ -1,97 +0,0 @@
1
- /**
2
- * Does a minimize -> (agent works) -> restore cycle preserve the live page?
3
- *
4
- * If yes, a headful browser can sit minimized while the agent works, and a
5
- * "show the window" action can hand the SAME page (same captcha, same form,
6
- * same scroll position) to the human without a relaunch, a reload, or any
7
- * loss of tabs / session cookies.
8
- */
9
- import { createRequire } from 'node:module'
10
- import path from 'node:path'
11
- import fs from 'node:fs'
12
- import os from 'node:os'
13
-
14
- const require = createRequire(import.meta.url)
15
- const { chromium } = require(process.env.PW_CORE_PATH ?? 'playwright-core')
16
-
17
- const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'pw-live-handoff-'))
18
- const context = await chromium.launchPersistentContext(profile, {
19
- headless: false,
20
- channel: process.env.CHANNEL || undefined,
21
- viewport: null,
22
- ignoreDefaultArgs: ['--enable-automation'],
23
- args: [
24
- '--no-first-run',
25
- '--no-default-browser-check',
26
- '--start-maximized',
27
- '--disable-blink-features=AutomationControlled',
28
- ],
29
- })
30
- const page = context.pages()[0] ?? (await context.newPage())
31
- const out = {}
32
-
33
- // A "challenge-like" page: a form + a live counter + scroll depth, all of which
34
- // would be destroyed by a reload.
35
- await page.setContent(`<html><body style="height:5000px">
36
- <input id=t value="">
37
- <div id=status>fresh</div>
38
- <script>
39
- window.__loads = (window.__loads || 0) + 1;
40
- window.__counter = 0;
41
- setInterval(() => { window.__counter++; document.querySelector('#status').textContent = 'ticks:' + window.__counter; }, 200);
42
- window.scrollTo(0, 1500);
43
- </script>
44
- </body></html>`)
45
- await page.fill('#t', 'captcha-answer-in-progress')
46
- await page.evaluate(() => window.scrollTo(0, 1500))
47
- await new Promise((r) => setTimeout(r, 700))
48
-
49
- const cdp = await context.newCDPSession(page)
50
- const windowId = async () => (await cdp.send('Browser.getWindowForTarget')).windowId
51
- const state = async () => (await cdp.send('Browser.getWindowBounds', { windowId: await windowId() })).bounds.windowState
52
- const setState = async (s) => {
53
- const id = await windowId()
54
- return cdp.send('Browser.setWindowBounds', { windowId: id, bounds: { windowState: s } })
55
- }
56
-
57
- out.beforeMinimize = { windowState: await state() }
58
-
59
- // Human walks away / the harness wants the desktop: minimize.
60
- await setState('minimized')
61
- await new Promise((r) => setTimeout(r, 900))
62
- out.minimized = { windowState: await state() }
63
-
64
- // Agent keeps working on the SAME minimized page.
65
- const snapshotWhileMinimized = await page.evaluate(() => ({
66
- value: document.querySelector('#t').value,
67
- status: document.querySelector('#status').textContent,
68
- scrollY: Math.round(window.scrollY),
69
- loads: window.__loads,
70
- counter: window.__counter,
71
- }))
72
- await page.screenshot({ path: path.join(path.dirname(profile), 'while-minimized.png') })
73
- out.agentWorkWhileMinimized = snapshotWhileMinimized
74
-
75
- // Human needed: show the window. No relaunch, no reload.
76
- const foregroundBefore = await page.evaluate(() => document.hasFocus())
77
- await setState('normal')
78
- await page.bringToFront()
79
- await new Promise((r) => setTimeout(r, 900))
80
- out.afterShow = {
81
- windowState: await state(),
82
- documentFocused: await page.evaluate(() => document.hasFocus()),
83
- documentFocusedBefore: foregroundBefore,
84
- // The point of the whole exercise: is it still the same page instance?
85
- statePreserved: await page.evaluate(() => ({
86
- value: document.querySelector('#t').value,
87
- status: document.querySelector('#status').textContent,
88
- scrollY: Math.round(window.scrollY),
89
- loads: window.__loads,
90
- counter: window.__counter,
91
- })),
92
- }
93
-
94
- console.log(JSON.stringify(out, null, 2))
95
- await setState('normal').catch(() => {})
96
- await context.close()
97
- fs.rmSync(profile, { recursive: true, force: true })
@@ -1,83 +0,0 @@
1
- /**
2
- * Headless-mode capability probe, aimed at the three "needs a human" cases:
3
- *
4
- * 1. Does the window-state CDP call used by the minimize gate even work headless?
5
- * (If it throws, our gate returns "minimized" and silently disables the
6
- * login-state export in headless — i.e. session cookies would never be saved.)
7
- * 2. Do page-level native dialogs (alert/confirm/prompt) park and answer headless?
8
- * 3. Does a file input accept setInputFiles headless, i.e. no OS picker needed?
9
- */
10
- import { createRequire } from 'node:module'
11
- import path from 'node:path'
12
- import fs from 'node:fs'
13
- import os from 'node:os'
14
-
15
- const require = createRequire(import.meta.url)
16
- const { chromium } = require(process.env.PW_CORE_PATH ?? 'playwright-core')
17
-
18
- const headless = (process.env.MODE ?? 'headless') === 'headless'
19
- const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'pw-needsprobe-'))
20
- const context = await chromium.launchPersistentContext(profile, {
21
- headless,
22
- channel: process.env.CHANNEL || undefined,
23
- ...(headless ? { viewport: { width: 1280, height: 800 } } : { viewport: null }),
24
- ignoreDefaultArgs: ['--enable-automation'],
25
- args: ['--no-first-run', '--no-default-browser-check', '--disable-blink-features=AutomationControlled'],
26
- })
27
- const page = context.pages()[0] ?? (await context.newPage())
28
- const out = { mode: headless ? 'headless' : 'headful' }
29
-
30
- // --- 1. the window-state probe the plugin's gate relies on -------------------
31
- try {
32
- const s = await context.newCDPSession(page)
33
- const { windowId } = await s.send('Browser.getWindowForTarget')
34
- const { bounds } = await s.send('Browser.getWindowBounds', { windowId })
35
- out.windowStateProbe = { ok: true, windowId, windowState: bounds.windowState, bounds }
36
- out.gateWouldReturn = bounds.windowState === 'minimized'
37
- await s.detach()
38
- } catch (e) {
39
- out.windowStateProbe = { ok: false, error: String(e?.message ?? e).split('\n')[0] }
40
- // The plugin's catch returns true == "treat as minimized".
41
- out.gateWouldReturn = true
42
- }
43
-
44
- // --- 2. native dialog without a window --------------------------------------
45
- const dialogLog = []
46
- page.on('dialog', async (d) => {
47
- dialogLog.push({ type: d.type(), message: d.message() })
48
- await d.accept('typed-by-test').catch(async () => {
49
- await d.dismiss().catch(() => {})
50
- })
51
- })
52
-
53
- async function dialogRoundTrip() {
54
- await page.setContent('<html><body><button id=b>go</button></body></html>')
55
- const result = await page.evaluate(() => {
56
- const answer = prompt('probe-prompt', 'default')
57
- return 'prompt-returned:' + String(answer)
58
- })
59
- return result
60
- }
61
- try {
62
- out.dialog = { evaluateResult: await dialogRoundTrip(), events: dialogLog }
63
- } catch (e) {
64
- out.dialog = { error: String(e?.message ?? e).split('\n')[0], events: dialogLog }
65
- }
66
-
67
- // --- 3. file input without an OS picker ------------------------------------
68
- try {
69
- const tmpFile = path.join(os.tmpdir(), 'pw-upload-probe.txt')
70
- fs.writeFileSync(tmpFile, 'upload-probe')
71
- await page.setContent('<html><body><input id=f type=file></body></html>')
72
- await page.setInputFiles('#f', tmpFile)
73
- out.fileInput = {
74
- ok: true,
75
- files: await page.evaluate(() => Array.from(document.querySelector('#f').files).map((f) => f.name)),
76
- }
77
- } catch (e) {
78
- out.fileInput = { ok: false, error: String(e?.message ?? e).split('\n')[0] }
79
- }
80
-
81
- console.log(JSON.stringify(out, null, 2))
82
- await context.close()
83
- fs.rmSync(profile, { recursive: true, force: true })
@@ -1,112 +0,0 @@
1
- /**
2
- * Minimal, plugin-free reproduction of "automation raises a minimized window".
3
- *
4
- * Two modes, matching the two ways dsh-browser-playwright can own a window:
5
- * PW_MODE=launch -> chromium.launch() + browser.newContext() (upstream 0.1.x default shape)
6
- * PW_MODE=persistent -> chromium.launchPersistentContext() (persistent-profile forks)
7
- *
8
- * The OS window state is read back through CDP (Browser.getWindowBounds) and the
9
- * window is minimized through CDP too, so the verdict does not depend on anyone
10
- * watching the screen.
11
- *
12
- * Usage (Windows, using an installed Chrome):
13
- * set PW_MODE=launch
14
- * set PW_CHANNEL=chrome
15
- * set PW_CORE_PATH=D:\path\to\node_modules\playwright-core
16
- * node repro-minimize.mjs
17
- */
18
- import { createRequire } from 'node:module'
19
- import os from 'node:os'
20
- import path from 'node:path'
21
- import fs from 'node:fs'
22
-
23
- const require = createRequire(import.meta.url)
24
- const corePath = process.env.PW_CORE_PATH ?? 'playwright-core'
25
- const { chromium } = require(corePath)
26
- let coreVersion = 'unknown'
27
- try {
28
- coreVersion = require(path.join(corePath, 'package.json')).version
29
- } catch {}
30
-
31
- const MODE = process.env.PW_MODE ?? 'launch'
32
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
33
- const launchOptions = {
34
- headless: false,
35
- channel: process.env.PW_CHANNEL || undefined, // e.g. 'chrome' to use the local Chrome
36
- args: ['--no-first-run', '--no-default-browser-check'],
37
- }
38
-
39
- let browser
40
- let context
41
- let profile
42
- if (MODE === 'persistent') {
43
- profile = fs.mkdtempSync(path.join(os.tmpdir(), 'pw-focus-repro-'))
44
- context = await chromium.launchPersistentContext(profile, { ...launchOptions, viewport: null })
45
- } else {
46
- browser = await chromium.launch(launchOptions)
47
- context = await browser.newContext({ viewport: null })
48
- }
49
-
50
- const anchor = context.pages()[0] ?? (await context.newPage())
51
- // Real content matters: an empty about:blank window can behave differently.
52
- await anchor.goto('https://example.com', { waitUntil: 'domcontentloaded' }).catch(() => {})
53
- await sleep(500)
54
-
55
- const cdp = await context.newCDPSession(anchor)
56
- const windowId = async () => (await cdp.send('Browser.getWindowForTarget')).windowId
57
- const state = async () => (await cdp.send('Browser.getWindowBounds', { windowId: await windowId() })).bounds.windowState
58
- const setState = async (windowState) => {
59
- const id = await windowId()
60
- return cdp.send('Browser.setWindowBounds', { windowId: id, bounds: { windowState } })
61
- }
62
-
63
- const results = []
64
- async function probe(name, fn) {
65
- await setState('normal')
66
- await sleep(400)
67
- await setState('minimized')
68
- await sleep(900)
69
- const before = await state()
70
- let err = ''
71
- try {
72
- await fn()
73
- } catch (e) {
74
- err = ' [' + (e?.message ?? e) + ']'
75
- }
76
- await sleep(1400)
77
- const after = await state()
78
- const raised = before === 'minimized' && after !== 'minimized'
79
- results.push({ name, before, after, raised })
80
- console.log(`${raised ? 'RAISES ' : 'safe '} ${name.padEnd(46)} before=${before} after=${after}${err}`)
81
- }
82
-
83
- console.log(`mode=${MODE} playwright-core ${coreVersion} node ${process.version}`)
84
- console.log(`channel=${process.env.PW_CHANNEL || '(bundled chromium)'}\n`)
85
-
86
- await probe('baseline: sleep only (3s)', () => sleep(3000))
87
- await probe('context.newPage()', async () => {
88
- const p = await context.newPage()
89
- await p.goto('about:blank')
90
- await p.close()
91
- })
92
- await probe('CDP Target.createTarget({background:true})', async () => {
93
- await cdp.send('Target.createTarget', { url: 'about:blank', background: true })
94
- })
95
- await probe('page.bringToFront()', () => anchor.bringToFront())
96
- await probe('context.newCDPSession(anchor)', async () => {
97
- const s = await context.newCDPSession(anchor)
98
- await s.detach()
99
- })
100
- await probe('context.storageState()', () => context.storageState())
101
- await probe('anchor.goto() (same tab)', () =>
102
- anchor.goto('https://example.com/?x=1', { waitUntil: 'domcontentloaded' }),
103
- )
104
- await probe('anchor.screenshot()', () => anchor.screenshot({ path: path.join(os.tmpdir(), 'pw-repro.png') }))
105
-
106
- console.log('\n--- summary ---')
107
- for (const r of results) console.log(`${r.raised ? 'RAISES' : 'safe '} ${r.name}`)
108
-
109
- await setState('normal').catch(() => {})
110
- await context.close().catch(() => {})
111
- await browser?.close().catch(() => {})
112
- if (profile) fs.rmSync(profile, { recursive: true, force: true })
@@ -1,23 +0,0 @@
1
- # autocommit.ps1 —— 每日自动存档:有改动就提交一条带时间戳的 commit。
2
- #
3
- # 用法:
4
- # powershell -ExecutionPolicy Bypass -File D:\dsh-plugins\dsh-browser-playwright-codex\scripts\autocommit.ps1
5
- #
6
- # 注册成每天 23:55 的计划任务(一次即可,用户级,不需要管理员):
7
- # schtasks /create /tn "dsh-browser-playwright autocommit" /sc daily /st 23:55 ^
8
- # /tr "powershell -ExecutionPolicy Bypass -File D:\dsh-plugins\dsh-browser-playwright-codex\scripts\autocommit.ps1"
9
- #
10
- # 说明:这是"存档",不是"分支管理"。它只提交,从不 push、reset 或丢弃改动,
11
- # 所以最坏情况也只是历史里多一条记录。
12
- $ErrorActionPreference = 'Stop'
13
- $repo = Split-Path -Parent $PSScriptRoot
14
- Set-Location $repo
15
-
16
- if (-not (Test-Path (Join-Path $repo '.git'))) { Write-Output "not a git repo: $repo"; exit 0 }
17
-
18
- $changes = git status --porcelain
19
- if (-not $changes) { Write-Output '没有改动,跳过'; exit 0 }
20
-
21
- git add -A
22
- git commit -q -m ("auto: " + (Get-Date -Format 'yyyy-MM-dd HH:mm'))
23
- Write-Output ("已存档: " + (Get-Date -Format 'yyyy-MM-dd HH:mm'))