dsh-wisp 0.6.0 → 1.25.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.
Files changed (68) hide show
  1. package/README.md +869 -10
  2. package/assets/canon/attn.webp +0 -0
  3. package/assets/canon/happy.webp +0 -0
  4. package/assets/canon/idle.webp +0 -0
  5. package/assets/canon/poked.webp +0 -0
  6. package/assets/canon/proud.webp +0 -0
  7. package/assets/canon/sleepy.webp +0 -0
  8. package/assets/canon/work.webp +0 -0
  9. package/assets/classic/attn.webp +0 -0
  10. package/assets/classic/happy.webp +0 -0
  11. package/assets/classic/idle.webp +0 -0
  12. package/assets/classic/poked.webp +0 -0
  13. package/assets/classic/proud.webp +0 -0
  14. package/assets/classic/sleepy.webp +0 -0
  15. package/assets/classic/work.webp +0 -0
  16. package/assets/deepsea/attn.webp +0 -0
  17. package/assets/deepsea/happy.webp +0 -0
  18. package/assets/deepsea/idle.webp +0 -0
  19. package/assets/deepsea/poked.webp +0 -0
  20. package/assets/deepsea/proud.webp +0 -0
  21. package/assets/deepsea/sleepy.webp +0 -0
  22. package/assets/deepsea/work.webp +0 -0
  23. package/assets/night/attn.webp +0 -0
  24. package/assets/night/happy.webp +0 -0
  25. package/assets/night/idle.webp +0 -0
  26. package/assets/night/poked.webp +0 -0
  27. package/assets/night/proud.webp +0 -0
  28. package/assets/night/sleepy.webp +0 -0
  29. package/assets/night/work.webp +0 -0
  30. package/assets/pajama/attn.webp +0 -0
  31. package/assets/pajama/happy.webp +0 -0
  32. package/assets/pajama/idle.webp +0 -0
  33. package/assets/pajama/poked.webp +0 -0
  34. package/assets/pajama/proud.webp +0 -0
  35. package/assets/pajama/sleepy.webp +0 -0
  36. package/assets/pajama/work.webp +0 -0
  37. package/audit/generation-audit.jsonl +35 -0
  38. package/build.mjs +93 -30
  39. package/cordis.patch.yml +2 -0
  40. package/lib/client.js +1293 -41
  41. package/lib/client.template.js +1248 -37
  42. package/lib/index.js +115 -12
  43. package/package.json +8 -1
  44. package/tools/CHARACTER-PROMPT.md +221 -3
  45. package/tools/assets.mjs +39 -16
  46. package/tools/audit-log.mjs +245 -0
  47. package/tools/bundle-cost.mjs +105 -0
  48. package/tools/pack-check.mjs +78 -0
  49. package/tools/perf.mjs +186 -0
  50. package/tools/smoke.mjs +577 -0
  51. package/tools/tiers.mjs +104 -0
  52. package/verify-wisp.mjs +2681 -872
  53. package/assets/happy.webp +0 -0
  54. package/assets/happy_hi.webp +0 -0
  55. package/assets/happy_md.webp +0 -0
  56. package/assets/happy_sm.webp +0 -0
  57. package/assets/idle.webp +0 -0
  58. package/assets/idle_hi.webp +0 -0
  59. package/assets/idle_md.webp +0 -0
  60. package/assets/idle_sm.webp +0 -0
  61. package/assets/sleepy.webp +0 -0
  62. package/assets/sleepy_hi.webp +0 -0
  63. package/assets/sleepy_md.webp +0 -0
  64. package/assets/sleepy_sm.webp +0 -0
  65. package/assets/work.webp +0 -0
  66. package/assets/work_hi.webp +0 -0
  67. package/assets/work_md.webp +0 -0
  68. package/assets/work_sm.webp +0 -0
@@ -0,0 +1,245 @@
1
+ /* ============================================================================
2
+ audit-log.mjs — 生成过程审计日志(哈希链)
3
+
4
+ 为什么要它:Maker 代理的登记表(.maker/assets/generated-assets.json)只记
5
+ **工具名 / 提示词 / 时间 / 产物路径**,**不记参数** —— 而 generate_image 与
6
+ batch_generate_images 是允许传可选 reference_images 的。所以"没喂参考图"这件事,
7
+ 代理那边证不了。
8
+
9
+ 这个日志把两类东西分开记,谁强谁弱一目了然:
10
+
11
+ registry 代理自己写的、独立于本插件的证据:这次调用用的是哪个工具、什么时间、
12
+ 产物落在哪。(**edit_image 是唯一能做图生图的工具**,所以工具名是硬证据。)
13
+ declared 本次调用发出去的参数(含 reference_images 是否为空)。**这是自我声明**,
14
+ 强度不如上面那条 —— 但它被写进哈希链,事后改不了,而且写在调用之前。
15
+
16
+ 每条记录还带**入包素材的相对路径与 sha256** —— 于是"这个入包文件 = 那次纯文生图调用的
17
+ 产物,且此后没被改过"可以在任何机器上复查。
18
+
19
+ USAGE
20
+ node tools/audit-log.mjs --verify 校验链 + 重算产物哈希
21
+ node tools/audit-log.mjs --declare <manifest> 生成前:登记将要发出的参数
22
+ node tools/audit-log.mjs --confirm <manifest> 生成后:并入代理登记表 + 记产物哈希
23
+ node tools/audit-log.mjs --backfill --since <ISO> 回溯:只凭代理登记表补录(标注来源)
24
+
25
+ manifest(JSON 数组):
26
+ [{ "name": "ds2_idle", "prompt": "……", "skin": "deepsea", "mood": "idle",
27
+ "params": { "model": "gpt", "resolution": "4K", "target_size": "2048x3072",
28
+ "reference_images": [] } }]
29
+ ========================================================================== */
30
+
31
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'
32
+ import { createHash } from 'node:crypto'
33
+ import { dirname, join, relative, resolve } from 'node:path'
34
+ import { fileURLToPath } from 'node:url'
35
+
36
+ const here = resolve(dirname(fileURLToPath(import.meta.url)), '..') // 包根
37
+ const LOG_DIR = join(here, 'audit')
38
+ const LOG_PATH = join(LOG_DIR, 'generation-audit.jsonl')
39
+ const REGISTRY = process.env.WISP_MAKER_REGISTRY || 'F:/孤屿/.maker/assets/generated-assets.json'
40
+
41
+ const sha256 = (buf) => createHash('sha256').update(buf).digest('hex')
42
+ const fileHash = (p) => (existsSync(p) ? sha256(readFileSync(p)) : null)
43
+ const rel = (p) => {
44
+ const r = relative(here, p)
45
+ return r.startsWith('..') ? p : r.split('\\').join('/')
46
+ }
47
+ const args = process.argv.slice(2)
48
+ const flag = (name) => args.includes(name)
49
+ const valueOf = (name) => {
50
+ const i = args.indexOf(name)
51
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : null
52
+ }
53
+
54
+ const readLog = () => {
55
+ if (!existsSync(LOG_PATH)) return []
56
+ return readFileSync(LOG_PATH, 'utf8')
57
+ .split('\n')
58
+ .filter((l) => l.trim() !== '')
59
+ .map((l) => JSON.parse(l))
60
+ }
61
+
62
+ /* 链:每条记录的 hash 覆盖自身全部字段(含 prev)。改任何一条都会断链。 */
63
+ const chainHash = (entry) => {
64
+ const { hash, ...rest } = entry
65
+ return sha256(JSON.stringify(rest))
66
+ }
67
+ const append = (entry) => {
68
+ const log = readLog()
69
+ const prev = log.length === 0 ? 'genesis' : log[log.length - 1].hash
70
+ const full = { index: log.length, prev, ...entry }
71
+ full.hash = chainHash(full)
72
+ if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true })
73
+ appendFileSync(LOG_PATH, JSON.stringify(full) + '\n', 'utf8')
74
+ return full
75
+ }
76
+
77
+ const readRegistry = () => {
78
+ if (!existsSync(REGISTRY)) return null
79
+ const json = JSON.parse(readFileSync(REGISTRY, 'utf8'))
80
+ const list = Array.isArray(json) ? json : Object.values(json)
81
+ return list.filter((e) => e && typeof e.name === 'string')
82
+ }
83
+
84
+ /* 代理登记表里最后一次以该 name 开头的调用 */
85
+ const registryFor = (registry, name) => {
86
+ const hits = registry.filter((e) => e.name === name || e.name.startsWith(name + '_'))
87
+ if (hits.length === 0) return null
88
+ return hits.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)))[hits.length - 1]
89
+ }
90
+
91
+ /* ------------------------------------------------------------------ verify */
92
+ /* 导出给 verify-wisp.mjs 用:同一条检查,不起子进程,也不重复实现一遍。 */
93
+ export function verifyAudit(root = here) {
94
+ const log = readLog()
95
+ const problems = []
96
+ let prev = 'genesis'
97
+ for (const e of log) {
98
+ if (e.prev !== prev) problems.push('第 ' + e.index + ' 条断链(prev 不匹配)')
99
+ if (chainHash(e) !== e.hash) problems.push('第 ' + e.index + ' 条内容被改动(hash 不匹配)')
100
+ prev = e.hash
101
+ }
102
+ const assets = log.filter((e) => e.kind === 'confirm' && e.output && e.output.path)
103
+ for (const e of assets) {
104
+ const p = resolve(root, e.output.path)
105
+ if (!existsSync(p)) { problems.push(e.output.path + ' 不见了'); continue }
106
+ if (fileHash(p) !== e.output.sha256) problems.push(e.output.path + ' 的哈希与记录不符')
107
+ }
108
+ const img2img = log.filter((e) => e.registry && e.registry.tool === 'edit_image')
109
+ const declaredNone = log.filter((e) => e.kind === 'declare' && e.declared
110
+ && Array.isArray(e.declared.reference_images) && e.declared.reference_images.length === 0)
111
+ return { entries: log.length, assets, problems, img2img, declaredNone: declaredNone.length,
112
+ backfilled: log.filter((e) => e.source === 'registry-backfill').length }
113
+ }
114
+
115
+ if (flag('--verify')) {
116
+ const res = verifyAudit(here)
117
+ const log = readLog()
118
+ let bad = 0
119
+ let missing = 0
120
+ let prev = 'genesis'
121
+ for (const e of log) {
122
+ if (e.prev !== prev) { console.log(` ✗ 第 ${e.index} 条断链(prev 不匹配)`); bad++ }
123
+ if (chainHash(e) !== e.hash) { console.log(` ✗ 第 ${e.index} 条内容被改动(hash 不匹配)`); bad++ }
124
+ prev = e.hash
125
+ }
126
+ const assets = res.assets
127
+ for (const e of assets) {
128
+ const p = resolve(here, e.output.path)
129
+ if (!existsSync(p)) { console.log(` ✗ ${e.output.path} 不见了`); missing++; continue }
130
+ if (fileHash(p) !== e.output.sha256) { console.log(` ✗ ${e.output.path} 的哈希与记录不符`); bad++ }
131
+ }
132
+ const img2img = log.filter((e) => e.registry && e.registry.tool === 'edit_image')
133
+ const unknown = log.filter((e) => e.registry && e.registry.tool && !['generate_image', 'batch_generate_images', 'edit_image'].includes(e.registry.tool))
134
+ console.log(` 记录 ${log.length} 条(declare ${log.filter((e) => e.kind === 'declare').length} / confirm ${assets.length} / backfill ${log.filter((e) => e.source === 'registry-backfill').length})`)
135
+ console.log(` 覆盖入包素材 ${assets.length} 个,全部存在且哈希一致:${missing === 0 && bad === 0 ? '是' : '否'}`)
136
+ console.log(` 代理登记表显示使用过 edit_image(图生图)的条目:${img2img.length}`)
137
+ if (img2img.length > 0) for (const e of img2img) console.log(` ⚠ ${e.name} ${e.registry.createdAt}`)
138
+ if (unknown.length > 0) console.log(` ⚠ 未识别的工具名:${unknown.map((e) => e.registry.tool).join(', ')}`)
139
+ console.log(bad === 0 && missing === 0 ? ' AUDIT OK' : ` AUDIT FAILED(${bad + missing} 个问题)`)
140
+ process.exit(bad + missing === 0 ? 0 : 1)
141
+ }
142
+
143
+ /* --------------------------------------------------------- declare/confirm */
144
+ const manifestPath = valueOf('--declare') || valueOf('--confirm')
145
+ const isDeclare = flag('--declare')
146
+
147
+ if (manifestPath) {
148
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
149
+ if (!Array.isArray(manifest) || manifest.length === 0) {
150
+ console.error('manifest 必须是非空数组'); process.exit(1)
151
+ }
152
+ const registry = readRegistry()
153
+ const at = new Date().toISOString()
154
+
155
+ for (const item of manifest) {
156
+ if (typeof item.name !== 'string' || typeof item.prompt !== 'string') {
157
+ console.error(`manifest 项缺 name/prompt:${JSON.stringify(item).slice(0, 80)}`); process.exit(1)
158
+ }
159
+ const params = item.params || {}
160
+ /* 自我声明里也必须照实写:这一项为空才说明没传参考图 */
161
+ const refs = params.reference_images
162
+ if (refs !== undefined && (!Array.isArray(refs) || refs.length !== 0)) {
163
+ console.error(`✗ ${item.name} 声明了参考图 —— 本项目的规则是不喂参考图。拒绝记录。`)
164
+ process.exit(1)
165
+ }
166
+
167
+ if (isDeclare) {
168
+ append({
169
+ kind: 'declare',
170
+ source: 'manifest',
171
+ at,
172
+ name: item.name,
173
+ promptSha256: sha256(item.prompt),
174
+ promptPreview: item.prompt.slice(0, 60),
175
+ declared: {
176
+ model: params.model ?? null,
177
+ resolution: params.resolution ?? null,
178
+ target_size: params.target_size ?? null,
179
+ reference_images: [],
180
+ },
181
+ })
182
+ continue
183
+ }
184
+
185
+ /* confirm:并入代理登记表,并记下入包素材的哈希 */
186
+ const rec = registry ? registryFor(registry, item.name) : null
187
+ const skin = item.skin
188
+ const mood = item.mood
189
+ const assetPath = skin && mood ? join(here, 'assets', skin, mood + '.webp') : null
190
+ const master = rec && rec.absolutePath ? rec.absolutePath : null
191
+ append({
192
+ kind: 'confirm',
193
+ source: 'manifest+registry',
194
+ at,
195
+ name: item.name,
196
+ skin: skin ?? null,
197
+ mood: mood ?? null,
198
+ registry: rec
199
+ ? { tool: rec.tool, createdAt: rec.createdAt, localPath: rec.localPath ?? null }
200
+ : { tool: null, createdAt: null, localPath: null, note: '登记表里没找到这次调用' },
201
+ master: master ? { path: rel(master), sha256: fileHash(master) } : null,
202
+ output: assetPath && existsSync(assetPath)
203
+ ? { path: rel(assetPath), sha256: fileHash(assetPath), bytes: statSync(assetPath).size }
204
+ : null,
205
+ promptMatchesRegistry: rec ? sha256(item.prompt) === sha256(rec.prompt ?? '') : null,
206
+ })
207
+ }
208
+ console.log(`${isDeclare ? 'declare' : 'confirm'}:写入 ${manifest.length} 条 → ${rel(LOG_PATH)}`)
209
+ process.exit(0)
210
+ }
211
+
212
+ /* -------------------------------------------------------------- backfill */
213
+ if (flag('--backfill')) {
214
+ const since = valueOf('--since') || '1970-01-01T00:00:00Z'
215
+ const registry = readRegistry()
216
+ if (!registry) { console.error(`读不到代理登记表:${REGISTRY}`); process.exit(1) }
217
+ const SKIN_OF = { ds2: 'deepsea', cl2: 'classic', ng2: 'night', pj2: 'pajama', canon: 'canon' }
218
+ const MOODS = ['idle', 'happy', 'sleepy', 'work', 'attn', 'poked', 'proud']
219
+ let n = 0
220
+ for (const rec of registry) {
221
+ if (String(rec.createdAt) < since) continue
222
+ const [head, mood] = String(rec.name).split('_')
223
+ const skin = SKIN_OF[head]
224
+ if (!skin || !MOODS.includes(mood)) continue
225
+ const assetPath = join(here, 'assets', skin, mood + '.webp')
226
+ append({
227
+ kind: 'confirm',
228
+ source: 'registry-backfill',
229
+ at: new Date().toISOString(),
230
+ name: rec.name,
231
+ skin, mood,
232
+ registry: { tool: rec.tool, createdAt: rec.createdAt, localPath: rec.localPath ?? null },
233
+ master: rec.absolutePath ? { path: rel(rec.absolutePath), sha256: fileHash(rec.absolutePath) } : null,
234
+ output: existsSync(assetPath) ? { path: rel(assetPath), sha256: fileHash(assetPath), bytes: statSync(assetPath).size } : null,
235
+ /* 回溯条目**没有**自我声明的参数(那时还没这个日志),如实标注 */
236
+ declared: null,
237
+ note: '回溯补录:只有代理登记表的事实,没有当次自我声明的参数',
238
+ })
239
+ n++
240
+ }
241
+ console.log(`backfill:补录 ${n} 条 → ${rel(LOG_PATH)}`)
242
+ process.exit(0)
243
+ }
244
+
245
+ console.log(readFileSync(fileURLToPath(import.meta.url), 'utf8').split('USAGE')[1].split('=====')[0].trim())
@@ -0,0 +1,105 @@
1
+ /* ============================================================================
2
+ bundle-cost.mjs — 包体积到底值多少钱?
3
+
4
+ 背景:这个插件曾经在冒烟里有一条"包体不得超过 4 MB(后来 6 MB)"的守卫。
5
+ 那条红线是我为了"别让它悄悄变胖"自己拍的数,不是任何真实约束 —— 有人问
6
+ "包体哪来的上限",才把它量清楚:解析代价是 1.45 ms/MB 量级,次线性,
7
+ 再加一整套皮肤约 +1.6 ms。红线因此删掉,改成"记录体积 + 量代价"。
8
+
9
+ 做法:拿真实的 lib/client.js,追加等量字符串字面量(精灵图的 base64 也是字符串
10
+ 字面量,所以负担同源),逐个体积在真实 Chromium 里计时。
11
+
12
+ RUN
13
+ node tools/bundle-cost.mjs
14
+
15
+ 浏览器是可选依赖:找不到就以 0 退出并说明原因(同 tools/smoke.mjs)。
16
+ ========================================================================== */
17
+
18
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
19
+ import { createRequire } from 'node:module'
20
+ import { dirname, join, resolve } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+
23
+ const here = resolve(dirname(fileURLToPath(import.meta.url)), '..')
24
+
25
+ function findPlaywright() {
26
+ /* 与 tools/smoke.mjs、tools/perf.mjs 保持同一套解析顺序:
27
+ profile → 本包 → 开发机检出。Playwright 是可选依赖,找不到就跳过(不是失败)。 */
28
+ const bases = [
29
+ process.env.DSH_PROFILE_DIR ? join(process.env.DSH_PROFILE_DIR, 'package.json') : null,
30
+ join(here, 'package.json'),
31
+ 'E:/deepseek-harness/package.json',
32
+ ].filter((p) => p && existsSync(p))
33
+ for (const base of bases) {
34
+ for (const spec of ['playwright-core', 'playwright', 'playwright-chromium']) {
35
+ try {
36
+ const mod = createRequire(base)(spec)
37
+ if (mod && mod.chromium) return { chromium: mod.chromium, from: `${base} → ${spec}` }
38
+ } catch (e) { /* 试下一个 */ }
39
+ }
40
+ }
41
+ const roots = [process.env.DSH_PROFILE_DIR ? join(process.env.DSH_PROFILE_DIR, '..') : null, here, 'E:/deepseek-harness'].filter(Boolean)
42
+ for (const root of roots) {
43
+ const pnpm = join(root, 'node_modules', '.pnpm')
44
+ if (!existsSync(pnpm)) continue
45
+ for (const entry of readdirSync(pnpm)) {
46
+ if (!/^playwright(-core)?@/.test(entry)) continue
47
+ const pkg = join(pnpm, entry, 'node_modules', 'playwright-core')
48
+ if (!existsSync(pkg)) continue
49
+ try {
50
+ const mod = createRequire(pkg)(pkg)
51
+ if (mod && mod.chromium) return { chromium: mod.chromium, from: pkg }
52
+ } catch (e) { /* 下一个版本 */ }
53
+ }
54
+ }
55
+ return null
56
+ }
57
+
58
+ const playwright = findPlaywright()
59
+ if (!playwright) {
60
+ console.log('bundle-cost: 未找到 Playwright —— 跳过测量(这不是失败)')
61
+ process.exit(0)
62
+ }
63
+
64
+ const bundle = readFileSync(join(here, 'lib', 'client.js'), 'utf8')
65
+ const baseMb = Buffer.byteLength(bundle) / 1048576
66
+ console.log(`真实包: ${baseMb.toFixed(2)} MB(lib/client.js)\n`)
67
+
68
+ const chromeCandidates = [
69
+ process.env.WISP_CHROME,
70
+ process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, 'ms-playwright', 'chromium-1234', 'chrome-win64', 'chrome.exe') : null,
71
+ '/usr/bin/chromium',
72
+ '/usr/bin/google-chrome',
73
+ ].filter(Boolean)
74
+ const executablePath = chromeCandidates.find((p) => existsSync(p))
75
+ const browser = await playwright.chromium.launch({ headless: true, executablePath })
76
+
77
+ console.log('目标体积 实际体积 解析+执行 每 MB')
78
+ const rows = []
79
+ for (const target of [baseMb, 8, 16, 24]) {
80
+ const extra = Math.max(0, Math.round((target - baseMb) * 1048576))
81
+ const src = extra > 0 ? `${bundle}\n;globalThis.__pad = "${'x'.repeat(extra)}";\n` : bundle
82
+ const page = await browser.newPage()
83
+ await page.setContent('<!doctype html><html><head></head><body></body></html>')
84
+ const ms = await page.evaluate((code) => {
85
+ window.__ML = { def: null }
86
+ window.__ModuleLoader__ = { load: (d) => { window.__ML.def = d } }
87
+ const t0 = performance.now()
88
+ new Function(code)()
89
+ window.__ML.def.factory(() => ({}))
90
+ return performance.now() - t0
91
+ }, src)
92
+ await page.close()
93
+ const mb = Buffer.byteLength(src) / 1048576
94
+ rows.push({ mb, ms })
95
+ console.log(`${String(target).padStart(6)} MB ${mb.toFixed(2).padStart(8)} MB ${ms.toFixed(1).padStart(8)} ms ${(ms / mb).toFixed(2)} ms/MB`)
96
+ }
97
+
98
+ const first = rows[0]
99
+ const last = rows[rows.length - 1]
100
+ const slope = (last.ms - first.ms) / (last.mb - first.mb)
101
+ console.log('')
102
+ console.log(` 从 ${first.mb.toFixed(1)} MB 到 ${last.mb.toFixed(1)} MB:${first.ms.toFixed(1)} → ${last.ms.toFixed(1)} ms`)
103
+ console.log(` 斜率约 ${slope.toFixed(2)} ms/MB(次线性)`)
104
+ console.log(` 再加一整套皮肤(约 +1.1 MB)的解析代价约为 ${(1.1 * slope).toFixed(1)} ms`)
105
+ await browser.close()
@@ -0,0 +1,78 @@
1
+ /* ============================================================================
2
+ pack-check.mjs — 打包自足性检查:`npm pack` → 解包 → **从解出的副本**跑预检与重建。
3
+
4
+ 为什么需要它:「这个包能不能被别人装走」不能靠"我看过 files[] 了"来回答 ——
5
+ 文档指向包外、tools/ 少声明一个、素材没打进去,全都要真的解出副本才暴露。
6
+ (这个检查第一次跑就抓到了 README 里指向开发机绝对路径的死引用。)
7
+
8
+ RUN
9
+ node tools/pack-check.mjs
10
+
11
+ npm 与 tar 都是可选依赖:找不到就以 0 退出并说明原因。
12
+ ========================================================================== */
13
+
14
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs'
15
+ import { spawnSync } from 'node:child_process'
16
+ import { tmpdir } from 'node:os'
17
+ import { dirname, join, resolve } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+
20
+ const here = resolve(dirname(fileURLToPath(import.meta.url)), '..')
21
+ const isWin = process.platform === 'win32'
22
+ const npm = isWin ? 'npm.cmd' : 'npm'
23
+ const run = (cmd, args, opts = {}) => spawnSync(cmd, args, { encoding: 'utf8', shell: isWin, ...opts })
24
+
25
+ if (run(npm, ['--version']).status !== 0) {
26
+ console.log('pack-check: 未找到 npm —— 跳过打包检查(这不是失败)')
27
+ process.exit(0)
28
+ }
29
+
30
+ let failures = 0
31
+ const check = (cond, label, detail = '') => {
32
+ console.log(` ${cond ? 'PASS' : 'FAIL'} ${label}${detail ? ' — ' + detail : ''}`)
33
+ if (!cond) failures++
34
+ }
35
+
36
+ const work = mkdtempSync(join(tmpdir(), 'wisp-pack-'))
37
+ try {
38
+ console.log('== 打包 ==')
39
+ const packed = run(npm, ['pack', '--pack-destination', work], { cwd: here })
40
+ const tgz = readdirSync(work).find((f) => f.endsWith('.tgz'))
41
+ check(packed.status === 0 && tgz !== undefined, 'npm pack succeeds',
42
+ tgz ?? String(packed.stderr ?? '').slice(0, 160))
43
+ if (!tgz) { process.exit(1) }
44
+ check(statSync(join(work, tgz)).size > 1024, 'the tarball is not empty',
45
+ `${Math.round(statSync(join(work, tgz)).size / 1024)} KB`)
46
+
47
+ console.log('\n== 解包 ==')
48
+ const extract = join(work, 'x')
49
+ mkdirSync(extract, { recursive: true })
50
+ const untar = run('tar', ['-xzf', join(work, tgz), '-C', extract])
51
+ check(untar.status === 0, 'tar extracts the tarball', String(untar.stderr ?? '').slice(0, 160))
52
+ const root = join(extract, 'package')
53
+ check(existsSync(join(root, 'package.json')), 'it extracts to a package root', root)
54
+ if (!existsSync(join(root, 'package.json'))) { process.exit(1) }
55
+
56
+ console.log('\n== 发布子集是否自足 ==')
57
+ for (const required of ['lib/index.js', 'lib/client.js', 'cordis.patch.yml', 'assets']) {
58
+ check(existsSync(join(root, required)), `${required} ships`, '')
59
+ }
60
+
61
+ console.log('\n== 从解出的副本跑预检 ==')
62
+ const verify = run('node', [join(root, 'verify-wisp.mjs')])
63
+ const passed = (String(verify.stdout ?? '').match(/^\s+PASS/gm) ?? []).length
64
+ const failed = (String(verify.stdout ?? '').match(/^\s+FAIL/gm) ?? []).length
65
+ check(verify.status === 0 && failed === 0 && passed > 0,
66
+ 'the published subset verifies on its own', `${passed} passed, ${failed} failed`)
67
+
68
+ console.log('\n== 从解出的副本重建 ==')
69
+ const build = run('node', [join(root, 'build.mjs')])
70
+ const inlined = (String(build.stdout ?? '').match(/base64 inlined\s+([\d.]+) KB/) ?? [])[1]
71
+ check(build.status === 0, 'the shipped art is enough to rebuild the bundle',
72
+ inlined ? `${inlined} KB inlined` : String(build.stderr ?? '').slice(0, 160))
73
+
74
+ console.log(`\n${failures === 0 ? '=== PACK OK ===' : '=== PACK FAILED ==='}`)
75
+ process.exit(failures === 0 ? 0 : 1)
76
+ } finally {
77
+ rmSync(work, { recursive: true, force: true })
78
+ }
package/tools/perf.mjs ADDED
@@ -0,0 +1,186 @@
1
+ /* ============================================================================
2
+ perf.mjs — 性能探针:README 的「性能」一节就是用它量的。
3
+
4
+ 为什么要有这个脚本:那张表里的数字必须**可复现**,否则它只是"我说过"。
5
+ 计数与耗时都靠**包装原生 querySelector/querySelectorAll** 得到,不靠估算。
6
+
7
+ RUN
8
+ node tools/perf.mjs
9
+ node tools/perf.mjs --quick # 只测稳态,跳过交替重复的帧测量
10
+
11
+ 浏览器是**可选依赖**:找不到就以 0 退出并说明原因(同 tools/smoke.mjs)。
12
+ ========================================================================== */
13
+
14
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
15
+ import { createRequire } from 'node:module'
16
+ import { dirname, join, resolve } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ const here = resolve(dirname(fileURLToPath(import.meta.url)), '..')
20
+ const argv = process.argv.slice(2)
21
+ const quick = argv.includes('--quick')
22
+
23
+ function findPlaywright() {
24
+ const bases = [
25
+ process.env.DSH_PROFILE_DIR ? join(process.env.DSH_PROFILE_DIR, 'package.json') : null,
26
+ join(here, 'package.json'),
27
+ 'E:/deepseek-harness/package.json',
28
+ ].filter((p) => p && existsSync(p))
29
+ for (const base of bases) {
30
+ for (const spec of ['playwright-core', 'playwright', 'playwright-chromium']) {
31
+ try {
32
+ // 必须**真的**拿到 chromium 才算命中:require 成功但导出里没有它的情况是存在的,
33
+ // 那种情况下直接返回会让调用处拿到 undefined.chromium。
34
+ const mod = createRequire(base)(spec)
35
+ if (mod && mod.chromium) return { chromium: mod.chromium, from: base + ' → ' + spec }
36
+ } catch (e) { /* 试下一个 */ }
37
+ }
38
+ }
39
+ const roots = [process.env.DSH_PROFILE_DIR ? join(process.env.DSH_PROFILE_DIR, '..') : null, here, 'E:/deepseek-harness'].filter(Boolean)
40
+ for (const root of roots) {
41
+ const pnpm = join(root, 'node_modules', '.pnpm')
42
+ if (!existsSync(pnpm)) continue
43
+ for (const entry of readdirSync(pnpm)) {
44
+ if (!/^playwright(-core)?@/.test(entry)) continue
45
+ const pkg = join(pnpm, entry, 'node_modules', 'playwright-core')
46
+ if (!existsSync(pkg)) continue
47
+ try {
48
+ const mod = createRequire(pkg)(pkg)
49
+ if (mod && mod.chromium) return { chromium: mod.chromium, from: pkg }
50
+ } catch (e) { /* 下一个版本 */ }
51
+ }
52
+ }
53
+ return null
54
+ }
55
+
56
+ const playwright = findPlaywright()
57
+ if (!playwright) {
58
+ console.log('perf: 未找到 Playwright —— 跳过测量(这不是失败)')
59
+ process.exit(0)
60
+ }
61
+
62
+ const chromeCandidates = [
63
+ process.env.WISP_CHROME,
64
+ process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, 'ms-playwright', 'chromium-1234', 'chrome-win64', 'chrome.exe') : null,
65
+ '/usr/bin/chromium',
66
+ '/usr/bin/google-chrome',
67
+ ].filter(Boolean)
68
+ const executablePath = chromeCandidates.find((p) => existsSync(p))
69
+ const bundle = readFileSync(join(here, 'lib', 'client.js'), 'utf8')
70
+
71
+ /* 页面上要有东西可查 —— 否则"查询很便宜"这句话没有意义 */
72
+ const FILLER = `
73
+ const root = document.createElement('div')
74
+ for (let i = 0; i < 5000; i++) {
75
+ const row = document.createElement('div'); row.className = 'row'
76
+ const a = document.createElement('span'); a.textContent = 'message ' + i
77
+ row.appendChild(a); root.appendChild(row)
78
+ }
79
+ document.body.appendChild(root)
80
+ `
81
+
82
+ const browser = await playwright.chromium.launch({ headless: true, executablePath })
83
+
84
+ const run = async (withPlugin, withComposer = false) => {
85
+ const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } })
86
+ await page.setContent('<!doctype html><html><head></head><body></body></html>')
87
+ await page.evaluate(({ filler, withComposer }) => {
88
+ window.__q = { sel: 0, all: 0, selMs: 0, allMs: 0, deltas: [] }
89
+ const q1 = Document.prototype.querySelector
90
+ const q2 = Document.prototype.querySelectorAll
91
+ Document.prototype.querySelector = function (s) {
92
+ const t = performance.now()
93
+ const r = q1.call(this, s)
94
+ window.__q.sel++
95
+ window.__q.selMs += performance.now() - t
96
+ return r
97
+ }
98
+ Document.prototype.querySelectorAll = function (s) {
99
+ const t = performance.now()
100
+ const r = q2.call(this, s)
101
+ window.__q.all++
102
+ window.__q.allMs += performance.now() - t
103
+ return r
104
+ }
105
+ new Function(filler)()
106
+ if (withComposer) {
107
+ const box = document.createElement('textarea')
108
+ box.setAttribute('data-composer-input', '')
109
+ document.body.appendChild(box)
110
+ }
111
+ let last = performance.now()
112
+ const tick = () => {
113
+ const now = performance.now()
114
+ window.__q.deltas.push(now - last)
115
+ last = now
116
+ requestAnimationFrame(tick)
117
+ }
118
+ requestAnimationFrame(tick)
119
+ }, { filler: FILLER, withComposer })
120
+
121
+ let mountMs = null
122
+ if (withPlugin) {
123
+ mountMs = await page.evaluate((src) => {
124
+ window.__ML = { def: null }
125
+ window.__ModuleLoader__ = { load: (d) => { window.__ML.def = d } }
126
+ const t0 = performance.now()
127
+ new Function(src)()
128
+ window.__ML.def.factory(() => ({})).apply({
129
+ get: (n) => (n === 'timer'
130
+ ? { timeout: (cb, ms) => { const i = setTimeout(cb, ms); return () => clearTimeout(i) }, interval: (cb, ms) => { const i = setInterval(cb, ms); return () => clearInterval(i) } }
131
+ : n === 'styles' ? { insert: (css) => { const s = document.createElement('style'); s.textContent = css; document.head.append(s); return () => s.remove() } } : undefined),
132
+ on: () => () => {}, effect: () => () => {},
133
+ }, { wander: false })
134
+ return performance.now() - t0
135
+ }, bundle)
136
+ }
137
+
138
+ await page.waitForTimeout(1200)
139
+ const t0 = await page.evaluate(() => {
140
+ window.__q.sel = 0; window.__q.all = 0; window.__q.selMs = 0; window.__q.allMs = 0; window.__q.deltas = []
141
+ return performance.now()
142
+ })
143
+ await page.waitForTimeout(10000)
144
+ const sample = await page.evaluate((startedAt) => {
145
+ const seconds = (performance.now() - startedAt) / 1000
146
+ const d = window.__q.deltas.filter((x) => x > 0 && x < 200)
147
+ return {
148
+ seconds: Number(seconds.toFixed(2)),
149
+ sel: window.__q.sel, all: window.__q.all,
150
+ selMs: window.__q.selMs, allMs: window.__q.allMs,
151
+ meanFrame: Number((d.length ? d.reduce((a, b) => a + b, 0) / d.length : 0).toFixed(3)),
152
+ }
153
+ }, t0)
154
+ await page.close()
155
+ return { mountMs, ...sample }
156
+ }
157
+
158
+ const per = (v, s) => Number((v / s).toFixed(2))
159
+ const bare = await run(false)
160
+ const wired = await run(true, true)
161
+ const pessimistic = await run(true, false)
162
+
163
+ console.log('== 10 秒静置(页面上有 5000 行合成会话记录)==')
164
+ console.log(`无插件 帧间隔 ${bare.meanFrame} ms querySelector ${per(bare.sel, bare.seconds)}/s querySelectorAll ${per(bare.all, bare.seconds)}/s`)
165
+ console.log(`挂载后·有输入框 帧间隔 ${wired.meanFrame} ms querySelector ${per(wired.sel, wired.seconds)}/s querySelectorAll ${per(wired.all, wired.seconds)}/s ← 真实应用的形态`)
166
+ console.log(`挂载后·无输入框 帧间隔 ${pessimistic.meanFrame} ms querySelector ${per(pessimistic.sel, pessimistic.seconds)}/s ← 最悲观:每轮都要探一次输入框钩子`)
167
+ console.log('')
168
+ console.log(`DOM 查询耗时 ${wired.selMs.toFixed(2)} ms 选中 + ${wired.allMs.toFixed(2)} ms 全量`)
169
+ console.log(` = ${per(wired.selMs + wired.allMs, wired.seconds)} ms/秒(占单核 ${(per(wired.selMs + wired.allMs, wired.seconds) / 10).toFixed(3)}%)`)
170
+ console.log(`挂载一次性开销 ${wired.mountMs?.toFixed(1)} ms(含 ${(Buffer.byteLength(bundle) / 1024 / 1024).toFixed(1)} MB 包的解析)`)
171
+
172
+ if (!quick) {
173
+ /* 帧开销必须**交替重复**测:单次差值里的噪声就有 0.1 ms 量级,一次测量说明不了任何事 */
174
+ console.log('\n== 帧间隔:交替重复 4 轮(每轮 6 秒静置)==')
175
+ const samples = { bare: [], wired: [] }
176
+ for (let i = 0; i < 4; i++) {
177
+ const which = i % 2 === 0 ? 'bare' : 'wired'
178
+ const r = await run(which === 'wired')
179
+ samples[which].push(r.meanFrame)
180
+ console.log(` 第 ${i + 1} 轮 ${which === 'bare' ? '无插件' : '挂载后'} ${r.meanFrame} ms`)
181
+ }
182
+ const mean = (a) => Number((a.reduce((x, y) => x + y, 0) / a.length).toFixed(3))
183
+ console.log(` 均值:无插件 ${mean(samples.bare)} ms vs 挂载后 ${mean(samples.wired)} ms → 差 ${(mean(samples.wired) - mean(samples.bare)).toFixed(3)} ms`)
184
+ }
185
+
186
+ await browser.close()