dsh-browser-playwright-codex 0.5.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 +486 -0
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/SECURITY.md +114 -0
- package/cordis.patch.yml +66 -0
- package/lib/cdp-policy.d.ts +69 -0
- package/lib/cdp-policy.js +105 -0
- package/lib/compat.d.ts +37 -0
- package/lib/compat.js +31 -0
- package/lib/contract.d.ts +74 -0
- package/lib/contract.js +117 -0
- package/lib/errors.d.ts +25 -0
- package/lib/errors.js +21 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +12 -0
- package/lib/injected.d.ts +35 -0
- package/lib/injected.js +350 -0
- package/lib/playwright.d.ts +647 -0
- package/lib/playwright.js +2515 -0
- package/lib/runtime-state.d.ts +45 -0
- package/lib/runtime-state.js +181 -0
- package/lib/service.d.ts +56 -0
- package/lib/service.js +96 -0
- package/lib/snapshot-aria.d.ts +121 -0
- package/lib/snapshot-aria.js +485 -0
- package/lib/snapshot-render.d.ts +28 -0
- package/lib/snapshot-render.js +100 -0
- package/lib/tool.d.ts +77 -0
- package/lib/tool.js +907 -0
- package/lib/types.d.ts +242 -0
- package/lib/types.js +5 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +108 -0
- package/scripts/analysis/hl-compare.mjs +149 -0
- package/scripts/analysis/live-handoff-probe.mjs +97 -0
- package/scripts/analysis/needs-human-probe.mjs +83 -0
- package/scripts/analysis/repro-minimize.mjs +112 -0
- package/scripts/autocommit.ps1 +23 -0
- package/scripts/check-doc-links.py +112 -0
- package/scripts/doctor.mjs +144 -0
- package/scripts/dsh-browser-verify.mjs +475 -0
- package/scripts/measure-prompt-cost.mjs +57 -0
- package/scripts/prompt-cost.mjs +73 -0
- package/scripts/shipped-gates.mjs +43 -0
|
@@ -0,0 +1,83 @@
|
|
|
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 })
|
|
@@ -0,0 +1,112 @@
|
|
|
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 })
|
|
@@ -0,0 +1,23 @@
|
|
|
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'))
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""校验仓库里所有 markdown 链接:文件路径要存在,锚点要能在目标文档里找到。
|
|
2
|
+
|
|
3
|
+
为什么需要:
|
|
4
|
+
1. 那几篇文档原来放在 GitHub 的 docs/ 下,链接写的是 ../xxx;搬进本仓库根目录后
|
|
5
|
+
../ 就指到仓库外了。靠肉眼容易漏,脚本一跑就知道。
|
|
6
|
+
2. 锚点也会坏:曾经有过 `[help wanted #2](README.md#help-wanted--求助)`,
|
|
7
|
+
而 README 里根本没有那一节 —— 文件在、锚点不在,照样是死链。
|
|
8
|
+
|
|
9
|
+
跑法(无需依赖,Python 3.8+):
|
|
10
|
+
python scripts/check-doc-links.py
|
|
11
|
+
退出码 0 = 全部有效;1 = 有坏链(逐条列出,标明是文件缺失还是锚点缺失)。
|
|
12
|
+
"""
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
# 以本文件位置为准:scripts/ 的上一级就是仓库根,换机器也不用改路径
|
|
18
|
+
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
19
|
+
LINK = re.compile(r"\]\(([^)\s]+)\)")
|
|
20
|
+
HEADING = re.compile(r"^#{1,6}\s+(.*?)\s*$")
|
|
21
|
+
FENCE = re.compile(r"^\s*(```|~~~)")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def slug(heading: str) -> str:
|
|
25
|
+
"""GitHub 的标题锚点规则(近似实现):小写;去掉非字母数字/连字符/下划线;
|
|
26
|
+
空格转连字符。中文是字母数字(str.isalnum() 为真)所以保留,与 GitHub 一致。"""
|
|
27
|
+
out = []
|
|
28
|
+
for ch in heading.strip().lower():
|
|
29
|
+
if ch.isalnum() or ch in "-_":
|
|
30
|
+
out.append(ch)
|
|
31
|
+
elif ch == " ":
|
|
32
|
+
out.append("-")
|
|
33
|
+
return "".join(out)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def anchors_of(path: str) -> set:
|
|
37
|
+
"""该文档所有标题的锚点。代码块内的 # 行不算标题。"""
|
|
38
|
+
found = set()
|
|
39
|
+
in_fence = False
|
|
40
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
41
|
+
for line in f:
|
|
42
|
+
if FENCE.match(line):
|
|
43
|
+
in_fence = not in_fence
|
|
44
|
+
continue
|
|
45
|
+
if in_fence:
|
|
46
|
+
continue
|
|
47
|
+
m = HEADING.match(line)
|
|
48
|
+
if m:
|
|
49
|
+
found.add(slug(m.group(1)))
|
|
50
|
+
return found
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main() -> int:
|
|
54
|
+
bad = []
|
|
55
|
+
checked_files = 0
|
|
56
|
+
checked_anchors = 0
|
|
57
|
+
anchor_cache = {}
|
|
58
|
+
|
|
59
|
+
for root, dirs, files in os.walk(REPO):
|
|
60
|
+
dirs[:] = [d for d in dirs if d not in (".git", "node_modules", "lib")]
|
|
61
|
+
for name in files:
|
|
62
|
+
if not name.endswith(".md"):
|
|
63
|
+
continue
|
|
64
|
+
path = os.path.join(root, name)
|
|
65
|
+
rel = os.path.relpath(path, REPO)
|
|
66
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
67
|
+
text = f.read()
|
|
68
|
+
|
|
69
|
+
for target in LINK.findall(text):
|
|
70
|
+
if target.startswith(("http://", "https://", "mailto:")):
|
|
71
|
+
continue
|
|
72
|
+
file_part, _, anchor = target.partition("#")
|
|
73
|
+
file_part = file_part.split("?", 1)[0]
|
|
74
|
+
|
|
75
|
+
if file_part == "":
|
|
76
|
+
# 同文档锚点:指向"更靠下的小节标题"是常见写法,这里只校验它存在
|
|
77
|
+
target_path = path
|
|
78
|
+
else:
|
|
79
|
+
target_path = os.path.normpath(
|
|
80
|
+
os.path.join(os.path.dirname(path), file_part.replace("/", os.sep))
|
|
81
|
+
)
|
|
82
|
+
checked_files += 1
|
|
83
|
+
if not os.path.exists(target_path):
|
|
84
|
+
bad.append((rel, target, "文件不存在: " + os.path.relpath(target_path, REPO)))
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if anchor == "":
|
|
88
|
+
continue
|
|
89
|
+
if not target_path.endswith(".md"):
|
|
90
|
+
continue
|
|
91
|
+
checked_anchors += 1
|
|
92
|
+
if target_path not in anchor_cache:
|
|
93
|
+
anchor_cache[target_path] = anchors_of(target_path)
|
|
94
|
+
if anchor not in anchor_cache[target_path]:
|
|
95
|
+
bad.append(
|
|
96
|
+
(
|
|
97
|
+
rel,
|
|
98
|
+
target,
|
|
99
|
+
"锚点不存在: #" + anchor + " (该文档有 "
|
|
100
|
+
+ str(len(anchor_cache[target_path]))
|
|
101
|
+
+ " 个标题锚点)",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
print(f"检查了 {checked_files} 个文件链接、{checked_anchors} 个锚点,坏链 {len(bad)} 个")
|
|
106
|
+
for src, target, why in bad:
|
|
107
|
+
print(f" BAD {src} -> {target} ({why})")
|
|
108
|
+
return 1 if bad else 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
sys.exit(main())
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* doctor —— 一句话回答「DSH 又发版了,这个插件还能不能跑」。
|
|
4
|
+
*
|
|
5
|
+
* 逐个 import 这个插件依赖的 @deepseek-ai/* 包,检查源码真正用到的具名导出是否
|
|
6
|
+
* 还在,并打印每个包实际装的版本。类型层面的兼容性由 tsc 回答(npm run build)。
|
|
7
|
+
*
|
|
8
|
+
* 用法: node scripts/doctor.mjs
|
|
9
|
+
* 退出码 0 = 全部可用;1 = 至少一个包需要处理。
|
|
10
|
+
*
|
|
11
|
+
* 出现 ❌ 时按这个顺序处理:
|
|
12
|
+
* 1. 只是导出改名 → 把引用搬进 src/compat.ts(那里是唯一的边界适配层);
|
|
13
|
+
* 2. 是插件真用到的运行时 API 没了 → 适配代码,别去改 peer 范围;
|
|
14
|
+
* 3. 装错版本 → 在 profile 里对齐版本,而不是把本目录钉成旧版。
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
17
|
+
import { createRequire } from 'node:module'
|
|
18
|
+
import { homedir } from 'node:os'
|
|
19
|
+
import { join } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
/** 运行时真正 import 的名字;缺一个就是载荷失败。 */
|
|
23
|
+
const REQUIRED = {
|
|
24
|
+
'@deepseek-ai/cordis': ['Service'],
|
|
25
|
+
'@deepseek-ai/schemastery': ['default'],
|
|
26
|
+
'@deepseek-ai/dsh-tools': ['default', 'defineTool'],
|
|
27
|
+
'@deepseek-ai/dsh-llm': ['BlockAssembler', 'createUserMessage'],
|
|
28
|
+
// The only runtime dependency that is not the harness: without it the whole
|
|
29
|
+
// browser entry fails to load, before any DSH API question even matters.
|
|
30
|
+
'playwright-core': ['chromium'],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 只有测试 / 开发路径用到的包;缺了只提醒,不算失败。 */
|
|
34
|
+
const OPTIONAL = {
|
|
35
|
+
'@deepseek-ai/dsh-system-prompt': ['default'],
|
|
36
|
+
'@deepseek-ai/dsh-session': [],
|
|
37
|
+
'@deepseek-ai/dsh-attachment': [],
|
|
38
|
+
'@deepseek-ai/dsh-agent': [],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const require = createRequire(import.meta.url)
|
|
42
|
+
|
|
43
|
+
/** Installed version of one package, or a marker when it is absent. */
|
|
44
|
+
function versionOf(pkg) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(readFileSync(require.resolve(pkg + '/package.json'), 'utf8')).version
|
|
47
|
+
} catch {
|
|
48
|
+
return '(未安装)'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Probe one package and return the missing export names. */
|
|
53
|
+
async function probe(pkg, names) {
|
|
54
|
+
let mod
|
|
55
|
+
try {
|
|
56
|
+
mod = await import(pkg)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return { missing: names, module: undefined, fatal: String(error.message ?? error) }
|
|
59
|
+
}
|
|
60
|
+
return { missing: names.filter((name) => !(name in mod)), module: mod, fatal: undefined }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let failed = 0
|
|
64
|
+
console.log('依赖体检(运行时导出)')
|
|
65
|
+
console.log('')
|
|
66
|
+
|
|
67
|
+
for (const [pkg, names] of Object.entries(REQUIRED)) {
|
|
68
|
+
const { missing, fatal } = await probe(pkg, names)
|
|
69
|
+
const version = versionOf(pkg)
|
|
70
|
+
if (fatal !== undefined) {
|
|
71
|
+
failed += 1
|
|
72
|
+
console.log('❌ ' + pkg + ' ' + version)
|
|
73
|
+
console.log(' 导入失败: ' + fatal)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (missing.length > 0) {
|
|
77
|
+
failed += 1
|
|
78
|
+
console.log('❌ ' + pkg + ' ' + version + ' —— 缺少导出: ' + missing.join(', '))
|
|
79
|
+
console.log(' ' + (names.length > 0 ? '需要: ' + names.join(', ') : ''))
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
console.log('✅ ' + pkg + ' ' + version + (names.length > 0 ? ' (' + names.join(', ') + ')' : ''))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('')
|
|
86
|
+
console.log('可选依赖(测试 / 开发路径)')
|
|
87
|
+
for (const [pkg, names] of Object.entries(OPTIONAL)) {
|
|
88
|
+
const { missing, fatal } = await probe(pkg, names)
|
|
89
|
+
const version = versionOf(pkg)
|
|
90
|
+
if (fatal !== undefined || missing.length > 0) {
|
|
91
|
+
console.log(
|
|
92
|
+
'⚠️ ' + pkg + ' ' + version + (fatal !== undefined ? ' —— 导入失败' : ' —— 缺少: ' + missing.join(', ')),
|
|
93
|
+
)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
console.log('✅ ' + pkg + ' ' + version)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log('')
|
|
100
|
+
console.log('部署侧解析(profile 用 junction 挂这个插件,运行时到底从哪里取 playwright-core)')
|
|
101
|
+
const pluginDir = fileURLToPath(new URL('..', import.meta.url))
|
|
102
|
+
const profilesRoot = join(homedir(), '.dsh', 'profiles')
|
|
103
|
+
/** Resolve one package from one directory, the way Node would at load time. */
|
|
104
|
+
function resolveFrom(pkg, dir) {
|
|
105
|
+
try {
|
|
106
|
+
const resolved = createRequire(join(dir, 'package.json')).resolve(pkg + '/package.json')
|
|
107
|
+
return { path: resolved, version: JSON.parse(readFileSync(resolved, 'utf8')).version }
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const anchors = [{ label: '插件目录', dir: pluginDir }]
|
|
113
|
+
try {
|
|
114
|
+
for (const name of readdirSync(profilesRoot)) {
|
|
115
|
+
if (name === 'node_modules') continue
|
|
116
|
+
anchors.push({ label: 'profile ' + name, dir: join(profilesRoot, name) })
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
console.log('ℹ️ 没有 ' + profilesRoot + ':只检查插件自身')
|
|
120
|
+
}
|
|
121
|
+
let resolvedAnywhere = 0
|
|
122
|
+
for (const { label, dir } of anchors) {
|
|
123
|
+
const found = resolveFrom('playwright-core', dir)
|
|
124
|
+
if (found === undefined) {
|
|
125
|
+
// Junction 挂载时 Node 按插件真实路径解析,profile 目录里没有这个包是正常的;
|
|
126
|
+
// 只有"哪里都解析不到"才是部署级失败(下面的 resolvedAnywhere 判定)。
|
|
127
|
+
console.log('ℹ️ ' + label + ' 本目录没有 playwright-core(junction 会落到插件真实路径)—— ' + dir)
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
resolvedAnywhere += 1
|
|
131
|
+
console.log('✅ ' + label + ' → playwright-core ' + found.version + ' ' + found.path)
|
|
132
|
+
}
|
|
133
|
+
if (resolvedAnywhere === 0) {
|
|
134
|
+
failed += 1
|
|
135
|
+
console.log('❌ 没有任何位置能解析 playwright-core:部署侧 browser entry 会在加载期直接失败')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log('')
|
|
139
|
+
if (failed === 0) {
|
|
140
|
+
console.log('全部可用。接着跑 `npm test` 与 `npm run verify` 确认行为没变。')
|
|
141
|
+
process.exit(0)
|
|
142
|
+
}
|
|
143
|
+
console.log(failed + ' 个包需要处理:优先把改名搬进 src/compat.ts,别去钉版本。')
|
|
144
|
+
process.exit(1)
|