dsh-superinput 0.0.0-stage → 0.12.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.
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dsh-superinput 套件生成器:从 upstream/ 内的六个模块源机械搬迁模块代码。
4
+ *
5
+ * 服务端:**单文件内联**——宿主 interception loader 对 link 包内部的相对
6
+ * 导入没有先例(六个旧插件都是自包含单文件;套件首版因相对导入静默不被
7
+ * import)。六个模块各包进 IIFE(作用域隔离顶层 const),import 提升到
8
+ * 顶部去重,modules/*.js 仅作生成中间产物保留可读源。
9
+ * 客户端:client.js 单文件闭包(宿主不拆模块),各包 __ModuleLoader__
10
+ * 头尾改写为 `function createX(require)` 工厂按序拼入,套件 factory 里
11
+ * 逐模块 try/catch(一个模块失败不拖垮其他)。
12
+ *
13
+ * 六个模块源原本是独立 git 仓(2026-11-07 收编进本仓 upstream/,GitHub
14
+ * 上游仓已归档);改上游源码直接改 upstream/<包名>/,重跑本脚本再生成。
15
+ * 重复执行幂等。
16
+ */
17
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
18
+ import { dirname, join } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const root = dirname(dirname(fileURLToPath(import.meta.url))) // packages/dsh-superinput
22
+ const pkgRoot = join(root, 'upstream')
23
+
24
+ /** key → [包目录, 客户端工厂名] */
25
+ const MODULES = [
26
+ ['cost', 'dsh-cost', 'createCostClient'],
27
+ ['picker', 'dsh-power-picker', 'createPickerClient'],
28
+ ['enhancer', 'dsh-prompt-enhancer', 'createEnhancerClient'],
29
+ ['followup', 'dsh-followup', 'createFollowupClient'],
30
+ ['voice', 'dsh-siri-voice', 'createVoiceClient'],
31
+ // 0.10.0 起并入套件(此前独立插件);apply 顺序在 enhancer 之后——
32
+ // pre-step 瀑布里压缩落档消息在增强之后追加,不会被二次改写。
33
+ ['compaction', 'dsh-compaction-control', 'createCompactionClient'],
34
+ ]
35
+
36
+ function transformServer(pkg, key) {
37
+ let src = readFileSync(join(pkgRoot, pkg, 'index.js'), 'utf8')
38
+ const head = `/* 由 scripts/build-suite.mjs 从 ${pkg}/index.js 机械生成;勿手改,重跑脚本再生成。 */\n`
39
+ src = src.replace(/^export const name = /m, 'const name = ')
40
+ src = src.replace(/^export default \{[^\n]*\}\s*$/m, '')
41
+ if (/^const inject = /m.test(src)) src += '\nexport { inject }\n'
42
+ if (!/export const Config/.test(src)) {
43
+ src += "import Schema from '@deepseek-ai/schemastery'\nexport const Config = Schema.object({ enabled: Schema.boolean().default(true).description('启用') })\n"
44
+ }
45
+ return head + src
46
+ }
47
+
48
+ function transformClient(pkg, factoryName) {
49
+ let src = readFileSync(join(pkgRoot, pkg, 'client.js'), 'utf8').trimEnd()
50
+ // 不锚定行首:各包文件头都有注释块。
51
+ const headRe = /window\.__ModuleLoader__\.load\(\{\s*\n\s*id:\s*'[^']+',\s*\n\s*factory\(require\) \{/
52
+ if (!headRe.test(src)) throw new Error(`${pkg}/client.js head pattern mismatch`)
53
+ src = src.replace(headRe, `function ${factoryName}(require) {`)
54
+ const tailRe = /\n \},\n\}\)\s*$/
55
+ if (!tailRe.test(src)) throw new Error(`${pkg}/client.js tail pattern mismatch`)
56
+ src = src.replace(tailRe, '\n}')
57
+ return `/* 由 scripts/build-suite.mjs 从 ${pkg}/client.js 机械生成;勿手改。 */\n${src}\n`
58
+ }
59
+
60
+ /* ---- 服务端:单文件内联 ---- */
61
+
62
+ /** 抽顶层 import(单行)、去 export 关键字,返回 { imports, body }。 */
63
+ function inlineSource(src) {
64
+ const imports = []
65
+ let body = src.replace(/^import [^\n]+$/gm, (line) => { imports.push(line.trim()); return '' })
66
+ body = body
67
+ .replace(/^export const name = /m, 'const name = ')
68
+ .replace(/^export const inject = /m, 'const moduleInject = ')
69
+ .replace(/^export const Config/m, 'const Config')
70
+ .replace(/^export function apply\(/m, 'function apply(')
71
+ .replace(/^export \{ inject \}\s*$/m, '')
72
+ .replace(/^export default \{[^\n]*\}\s*$/m, '')
73
+ // compaction-control 尾部的离线自测导出(CI import 用,运行时无用)
74
+ .replace(/^export const __internals/m, 'const __internals')
75
+ return { imports, body: body.trim() }
76
+ }
77
+
78
+ /** 按 source 分组合并 import 绑定符(整行去重不够:不同行的子集会重复声明)。 */
79
+ function mergeImports(importLines) {
80
+ const bySource = new Map() // source → { defaults:Set, names:Set }
81
+ for (const line of importLines) {
82
+ const m = line.match(/^import\s+(?:([A-Za-z_$][\w$]*)\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*['"]([^'"]+)['"]/)
83
+ if (!m) throw new Error(`unparsed import: ${line}`)
84
+ const [, defaults, named, source] = m
85
+ const entry = bySource.get(source) ?? { defaults: new Set(), names: new Set() }
86
+ if (defaults) entry.defaults.add(defaults)
87
+ if (named) for (const part of named.split(',')) {
88
+ const name = part.trim()
89
+ if (name) entry.names.add(name)
90
+ }
91
+ bySource.set(source, entry)
92
+ }
93
+ const lines = []
94
+ for (const [source, { defaults, names }] of bySource) {
95
+ const parts = []
96
+ if (defaults.size > 0) parts.push([...defaults][0])
97
+ if (names.size > 0) parts.push(`{ ${[...names].join(', ')} }`)
98
+ lines.push(`import ${parts.join(', ')} from '${source}'`)
99
+ }
100
+ return lines.sort()
101
+ }
102
+
103
+ mkdirSync(join(root, 'modules'), { recursive: true })
104
+ const importLines = []
105
+ const iifes = []
106
+ for (const [key, pkg] of MODULES) {
107
+ const transformed = transformServer(pkg, key)
108
+ writeFileSync(join(root, 'modules', `${key}.js`), transformed) // 可读中间产物
109
+ const { imports, body } = inlineSource(transformed)
110
+ importLines.push(...imports.filter((line) => !line.includes("@deepseek-ai/schemastery")))
111
+ iifes.push(`/* ==== module: ${key}(原 ${pkg}) ==== */\nconst ${key}Module = (() => {\n${body}\n return { apply, Config, inject: typeof moduleInject === 'undefined' ? undefined : moduleInject }\n})()`)
112
+ console.log(`✓ inline ${key}`)
113
+ }
114
+ // prompts(手写模块源)同样内联。
115
+ {
116
+ const src = readFileSync(join(root, 'modules', 'prompts.js'), 'utf8')
117
+ const { imports, body } = inlineSource(src)
118
+ importLines.push(...imports)
119
+ iifes.push(`/* ==== module: prompts(手写) ==== */\nconst promptsModule = (() => {\n${body}\n return { apply, Config, inject: typeof moduleInject === 'undefined' ? undefined : moduleInject }\n})()`)
120
+ console.log('✓ inline prompts')
121
+ }
122
+ // hub(手写模块源)同样内联。
123
+ {
124
+ const src = readFileSync(join(root, 'modules', 'hub.js'), 'utf8')
125
+ const { imports, body } = inlineSource(src)
126
+ importLines.push(...imports)
127
+ iifes.push(`/* ==== module: hub(手写) ==== */\nconst hubModule = (() => {\n${body}\n return { apply, Config, inject: typeof moduleInject === 'undefined' ? undefined : moduleInject }\n})()`)
128
+ console.log('✓ inline hub')
129
+ }
130
+ const allImports = mergeImports(importLines)
131
+ // schemastery 由 glue 使用,保证在列。
132
+ if (!allImports.some((line) => line.includes('@deepseek-ai/schemastery'))) {
133
+ allImports.unshift("import Schema from '@deepseek-ai/schemastery'")
134
+ }
135
+
136
+ const server = `/* dsh-superinput 服务端核心(由 scripts/build-suite.mjs 生成;勿手改)。
137
+ * 入口拆分:index.js 是零依赖探针(先 trace 再动态 import 本文件并捕获失败)——
138
+ * 顶层静态 import 的失败发生在入口任何语句之前,静默且永远取不到证据。 */
139
+ import { appendFileSync as diagAppend, mkdirSync as diagMkdir } from 'node:fs'
140
+ import { homedir as diagHomedir } from 'node:os'
141
+ import { dirname as diagDirname, join as pathJoin } from 'node:path'
142
+ ${allImports.join('\n')}
143
+
144
+ const SUITE_DIAG = pathJoin(diagHomedir(), '.dsh', 'superinput-debug.log')
145
+ const trace = (line) => {
146
+ try {
147
+ diagMkdir(diagDirname(SUITE_DIAG), { recursive: true })
148
+ diagAppend(SUITE_DIAG, \`\${new Date().toISOString()} \${line}\\n\`, 'utf8')
149
+ } catch { /* ignore */ }
150
+ }
151
+
152
+ ${iifes.join('\n\n')}
153
+
154
+ const MODULES = [
155
+ ['cost', costModule],
156
+ ['picker', pickerModule],
157
+ ['enhancer', enhancerModule],
158
+ ['followup', followupModule],
159
+ ['voice', voiceModule],
160
+ ['prompts', promptsModule],
161
+ ['compaction', compactionModule],
162
+ ['hub', hubModule],
163
+ ]
164
+
165
+ export const name = 'superinput'
166
+
167
+ export const inject = [...new Set(MODULES.flatMap(([, mod]) => mod.inject ?? []))]
168
+
169
+ export const Config = Schema.object({
170
+ cost: costModule.Config,
171
+ picker: pickerModule.Config,
172
+ enhancer: enhancerModule.Config,
173
+ followup: followupModule.Config,
174
+ voice: voiceModule.Config,
175
+ prompts: promptsModule.Config,
176
+ compaction: compactionModule.Config,
177
+ hub: hubModule.Config,
178
+ })
179
+
180
+ /**
181
+ * 嵌套默认值兜底:宿主对插件 row config 的解析深度未知——套件层自己跑
182
+ * 一遍每个模块的 Config({}) 拿完整默认,与宿主传入的 slice 合并。
183
+ */
184
+ const DEFAULTS = new Map()
185
+ for (const [key, mod] of MODULES) {
186
+ try { DEFAULTS.set(key, mod.Config ? mod.Config({}) : {}) } catch { DEFAULTS.set(key, {}) }
187
+ }
188
+
189
+ export function apply(ctx, config) {
190
+ const keys = Object.keys(config ?? {})
191
+ console.info(\`[dsh-superinput] server apply: host config keys=\${keys.length ? keys.join(',') : '(none)'} — merging schema defaults\`)
192
+ // 先算出每个模块的最终生效配置,hub 用它做总览(自己拿不到别人的 config)。
193
+ const merged = {}
194
+ for (const [key] of MODULES) {
195
+ merged[key] = { ...(DEFAULTS.get(key) ?? {}), ...(config?.[key] ?? {}) }
196
+ }
197
+ for (const [key, mod] of MODULES) {
198
+ try {
199
+ const own = merged[key]
200
+ mod.apply(ctx, key === 'hub' ? { ...own, snapshot: merged } : own)
201
+ trace(\`module "\${key}" applied OK\`)
202
+ } catch (error) {
203
+ console.error(\`[dsh-superinput] server module "\${key}" failed:\`, error)
204
+ trace(\`module "\${key}" FAILED: \${String(error?.stack ?? error).slice(0, 600)}\`)
205
+ }
206
+ }
207
+ }
208
+
209
+ export default { name, Config, apply }
210
+ `
211
+
212
+ const entry = `/* dsh-superinput 入口:零依赖探针(由 scripts/build-suite.mjs 生成;勿手改)。
213
+ * 静态 import 的失败发生在本文件第一条语句之前(宿主静默),所以核心代码
214
+ * 全部放在 server.js 由这里动态加载——成败都会留下取证记录。 */
215
+ import { appendFileSync, mkdirSync } from 'node:fs'
216
+ import { homedir } from 'node:os'
217
+ import { dirname, join } from 'node:path'
218
+
219
+ const DIAG = join(homedir(), '.dsh', 'superinput-debug.log')
220
+ const trace = (line) => {
221
+ try {
222
+ mkdirSync(dirname(DIAG), { recursive: true })
223
+ appendFileSync(DIAG, \`\${new Date().toISOString()} \${line}\\n\`, 'utf8')
224
+ } catch { /* ignore */ }
225
+ }
226
+
227
+ trace(\`suite ENTRY pid=\${process.pid}\`)
228
+
229
+ let core
230
+ try {
231
+ core = await import('./server.js')
232
+ trace(\`server.js LOADED pid=\${process.pid}\`)
233
+ } catch (error) {
234
+ trace(\`server.js FAILED: \${String(error?.stack ?? error).slice(0, 600)}\`)
235
+ throw error
236
+ }
237
+
238
+ export const name = core.name
239
+ export const inject = core.inject
240
+ export const Config = core.Config
241
+ export function apply(ctx, config) {
242
+ trace(\`suite APPLY pid=\${process.pid}\`)
243
+ return core.apply(ctx, config)
244
+ }
245
+ export default core.default
246
+ `
247
+ writeFileSync(join(root, 'server.js'), server)
248
+ writeFileSync(join(root, 'index.js'), entry)
249
+ console.log(`✓ index.js 零依赖探针入口(${entry.length} bytes)+ server.js 核心(${server.length} bytes)`)
250
+
251
+ /* ---- 客户端:六工厂内联 + glue ---- */
252
+ const clientFiles = []
253
+ for (const [key, pkg, factory] of MODULES) {
254
+ clientFiles.push(transformClient(pkg, factory))
255
+ console.log(`✓ client ${factory}`)
256
+ }
257
+ clientFiles.push(readFileSync(join(root, 'modules', 'prompts.client.js'), 'utf8'))
258
+ clientFiles.push(readFileSync(join(root, 'modules', 'hub.client.js'), 'utf8'))
259
+
260
+ const glue = `/* dsh-superinput 客户端:单 __ModuleLoader__ 入口,内部逐模块装配。
261
+ * 本文件由 scripts/build-suite.mjs 生成(模块工厂内联 + 下方 glue);勿手改模块工厂。 */
262
+
263
+ ${clientFiles.join('\n')}
264
+
265
+ window.__ModuleLoader__.load({
266
+ id: 'dsh-superinput',
267
+ factory(require) {
268
+ const defs = [
269
+ ['cost', createCostClient],
270
+ ['picker', createPickerClient],
271
+ ['enhancer', createEnhancerClient],
272
+ ['followup', createFollowupClient],
273
+ ['voice', createVoiceClient],
274
+ ['prompts', createPromptsClient],
275
+ ['compaction', createCompactionClient],
276
+ ['hub', createHubClient],
277
+ ]
278
+ // 模块异常上报:console 不落盘,失败必须走 prompts /diag 留痕。
279
+ const reportModuleError = (key, when, error) => {
280
+ console.error(\`[dsh-superinput] client module "\${key}" \${when}:\`, error)
281
+ try {
282
+ void fetch('/api/dsh-superinput/prompts/diag', {
283
+ method: 'POST',
284
+ headers: { 'content-type': 'application/json' },
285
+ body: JSON.stringify({ message: \`client module "\${key}" \${when}: \${String(error?.stack ?? error).slice(0, 400)}\` }),
286
+ }).catch(() => {})
287
+ } catch { /* ignore */ }
288
+ }
289
+ const modules = []
290
+ for (const [key, create] of defs) {
291
+ try { modules.push([key, create(require)]) }
292
+ catch (error) { reportModuleError(key, 'factory failed', error) }
293
+ }
294
+ console.info('[dsh-superinput] client active:', modules.map(([key]) => key).join('+'))
295
+ return {
296
+ // 各模块 client 的 inject 并集:picker 需要 sessions/remote/modelDirectories
297
+ // 等,只声明 slots 会让它在 apply 时报 cannot get property without inject。
298
+ inject: [...new Set(modules.flatMap(([, mod]) => mod.inject ?? ['slots']))],
299
+ apply(ctx) {
300
+ for (const [key, mod] of modules) {
301
+ try { mod.apply(ctx) }
302
+ catch (error) { reportModuleError(key, 'apply failed', error) }
303
+ }
304
+ },
305
+ }
306
+ },
307
+ })
308
+ `
309
+ // Electron 渲染器禁用 API 门禁:window.prompt/alert/confirm 在 Electron 直接
310
+ // 抛错(2026-11-01 真机事故:prompts 模板一点就消失)。构建期拦截。
311
+ const forbiddenHits = []
312
+ for (const [re, why] of [
313
+ [/window\.prompt\s*\(/, 'window.prompt(Electron 不支持,改用面板内表单)'],
314
+ [/window\.alert\s*\(/, 'window.alert(Electron 不支持,改用面板内提示)'],
315
+ [/window\.confirm\s*\(/, 'window.confirm(Electron 不支持,改用面板内按钮)'],
316
+ ]) {
317
+ if (re.test(glue)) forbiddenHits.push(why)
318
+ }
319
+ if (forbiddenHits.length) {
320
+ console.error('✗ client.js 含 Electron 渲染器禁用 API:\n ' + forbiddenHits.join('\n '))
321
+ process.exit(1)
322
+ }
323
+ writeFileSync(join(root, 'client.js'), glue)
324
+ const clientModuleCount = (glue.match(/\['\w+', create\w+\]/g) ?? []).length
325
+ console.log(`✓ client.js (${glue.length} bytes, ${clientModuleCount} modules)`)
@@ -0,0 +1,172 @@
1
+ /**
2
+ * dsh-superinput 冒烟测试:桩服务逐模块 apply + prompts 端到端
3
+ * (save/use/slash/list/export/import 合并逻辑)。
4
+ *
5
+ * 会临时写真实存储 ~/.dsh/storages/dsh-superinput/prompts.json——
6
+ * 启动时自动备份,结束时(无论成败)恢复原文件;存储不存在则清理测试残留。
7
+ * 用法:node scripts/smoke.mjs(退出码 0 = 全过)
8
+ */
9
+ import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs'
10
+ import { homedir } from 'node:os'
11
+ import { join } from 'node:path'
12
+ import mod from '../server.js'
13
+
14
+ const STORE = join(homedir(), '.dsh', 'storages', 'dsh-superinput', 'prompts.json')
15
+ const hadStore = existsSync(STORE)
16
+ const backup = hadStore ? readFileSync(STORE) : null
17
+
18
+ let promptsHandler = null
19
+ let enhancerHandler = null
20
+ const webPaths = []
21
+ const eventHandlers = {}
22
+ const services = {
23
+ webServer: { register(spec) { webPaths.push(spec.path); if (spec.path.endsWith('/prompts')) promptsHandler = spec.handler; if (spec.path === '/api/prompt-enhancer') enhancerHandler = spec.handler } },
24
+ tools: { register() {} },
25
+ // stub llm 流:/rewrite 端到端用它验证「请求期经嵌套作用域取 llm」的数据流
26
+ llm: {
27
+ stream() {
28
+ return (async function* () { yield { type: 'text-delta', text: 'ENHANCED OUTPUT TEXT' } })()
29
+ },
30
+ },
31
+ slots: { inject() {}, register() {} },
32
+ }
33
+ const ctx = {
34
+ effect(fn) { fn() },
35
+ get() { return undefined }, // defaultRoute() 的 ctx.get('agentDefaultModel') 安全回落
36
+ inject(deps, cb) {
37
+ const scope = Object.create(this)
38
+ for (const d of deps) if (!(d in scope)) scope[d] = services[d] ?? {}
39
+ cb(scope)
40
+ },
41
+ // cordis 瀑布语义:同名事件多个监听按注册顺序链式执行(后者拿到前者
42
+ // 的 next 结果)。套件合并后 pre-step 有多个监听(prompts /context 注入、
43
+ // compaction 落档、enhancer),单槽覆盖会让测试调到错误的那个。
44
+ on(name, handler) {
45
+ (eventHandlers[name] ??= []).push(handler)
46
+ return () => {
47
+ const list = eventHandlers[name]
48
+ if (!Array.isArray(list)) return
49
+ const index = list.indexOf(handler)
50
+ if (index >= 0) list.splice(index, 1)
51
+ }
52
+ },
53
+ }
54
+
55
+ const assert = (cond, label) => { if (!cond) throw new Error(label); console.log('ok:', label) }
56
+ const call = async (handler, prefix, method, sub, body) => {
57
+ const chunks = body === undefined ? [] : [JSON.stringify(body)]
58
+ const req = { url: `${prefix}${sub}`, method, async *[Symbol.asyncIterator]() { for (const c of chunks) yield c } }
59
+ let status = 0
60
+ let out = null
61
+ const res = { setHeader() {}, set statusCode(v) { status = v }, end(b) { out = b ? JSON.parse(b) : null } }
62
+ await handler(req, res)
63
+ return { status, out }
64
+ }
65
+ const prompts = (method, sub, body) => call(promptsHandler, '/api/dsh-superinput/prompts', method, sub, body)
66
+ const byLastUsed = (x, y) => (y.usedAt ?? y.at ?? 0) - (x.usedAt ?? x.at ?? 0)
67
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
68
+
69
+ let failed = false
70
+ try {
71
+ const config = {}
72
+ // enhancer 给显式 provider/model,让 configRoute 命中、不依赖 agentDefaultModel
73
+ for (const key of ['cost', 'picker', 'enhancer', 'followup', 'voice', 'prompts', 'compaction', 'hub']) config[key] = {}
74
+ config.enhancer = { provider: 'stub', model: 'stub-model' }
75
+ mod.apply(ctx, config)
76
+ for (const path of ['/api/dsh-cost', '/api/dsh-superinput/prompts', '/api/dsh-superinput/hub', '/api/prompt-enhancer', '/api/dsh-compaction-control']) {
77
+ assert(webPaths.includes(path), `module apply: ${path} registered`)
78
+ }
79
+ assert(!!promptsHandler, 'module apply: prompts handler captured')
80
+ assert(!!enhancerHandler, 'module apply: enhancer handler captured')
81
+
82
+ // /rewrite 端到端:回归「请求期经嵌套作用域取 llm」——处理器若退回外层
83
+ // ctx(0.3.1 真机事故),stub 环境里 ctx.llm 为 undefined → 502,此处必挂。
84
+ const rw = await call(enhancerHandler, '/api/prompt-enhancer', 'POST', '/rewrite', { text: '帮我写个东西' })
85
+ assert(rw.status === 200 && rw.out?.text === 'ENHANCED OUTPUT TEXT', `rewrite e2e: status=${rw.status} text=${JSON.stringify(rw.out?.text ?? rw.out)}`)
86
+
87
+ const a = await prompts('POST', '/save', { title: 'e2e-写周报', body: '写周报,日期 {{日期}}', tags: ['test'] })
88
+ const b = await prompts('POST', '/save', { title: 'e2e-plain', body: 'plain body' })
89
+ assert(a.out.ok && b.out.ok, 'save two templates')
90
+ const idA = a.out.id
91
+ const idB = b.out.id
92
+
93
+ await sleep(5) // 避免 usedAt 与保存 at 同毫秒打平(稳定排序会保留原序)
94
+ assert((await prompts('POST', '/use', { id: idA })).out.ok, 'use A')
95
+ assert((await prompts('POST', '/use', { id: idA })).out.ok, 'use A again')
96
+ assert((await prompts('POST', '/slash', { id: idA, slash: true })).out.ok, 'slash A on')
97
+ assert((await prompts('POST', '/slash', { id: idB, slash: true })).out.ok, 'slash B on')
98
+
99
+ const list1 = (await prompts('GET', '/list')).out.items
100
+ const itemA = list1.find((i) => i.id === idA)
101
+ assert(itemA.uses === 2 && itemA.usedAt > 0, 'list: A carries uses=2 + usedAt')
102
+ assert(itemA.slash === true && list1.find((i) => i.id === idB).slash === true, 'list: slash flags on')
103
+ assert([...list1].sort(byLastUsed)[0].id === idA, 'client sort: A (recently used) first')
104
+
105
+ const dump = (await prompts('GET', '/export')).out
106
+ assert(dump.version === 1 && dump.items.length >= 2, 'export: version+items')
107
+
108
+ const modA = { ...dump.items.find((i) => i.id === idA), title: 'e2e-写周报-v2', at: Date.now() + 10 }
109
+ const c = await prompts('POST', '/save', { title: 'e2e-temp-c', body: 'ccc' })
110
+ const delC = c.out.id
111
+ await prompts('POST', '/remove', { id: delC })
112
+ const importedC = { id: delC, title: 'e2e-C-imported', body: 'ccc', tags: [], at: Date.now(), pinned: false }
113
+ const stale = { ...dump.items.find((i) => i.id === idB), body: 'stale-overwrite-should-skip', at: 1 }
114
+ const imp = await prompts('POST', '/import', { items: [modA, importedC, stale, { body: '' }] })
115
+ assert(imp.out.ok && imp.out.updated === 1 && imp.out.added === 1, `import: added=${imp.out.added} updated=${imp.out.updated}`)
116
+ const list2 = (await prompts('GET', '/list')).out.items
117
+ assert(list2.find((i) => i.id === idA).title === 'e2e-写周报-v2', 'import: newer A overwrote')
118
+ assert(list2.find((i) => i.id === delC).title === 'e2e-C-imported', 'import: deleted C back with original id')
119
+ assert(list2.find((i) => i.id === idB).body !== 'stale-overwrite-should-skip', 'import: stale skipped')
120
+ assert((await prompts('POST', '/import', { items: 'not-an-array' })).status === 400, 'import: invalid payload rejected')
121
+
122
+ // 划选上下文:/context 暂存 → agent/pre-step 一次性注入
123
+ const ctxStore = await prompts('POST', '/context', { sessionId: 'smoke-session', items: [{ text: '选中的对话内容', comment: '帮我解释这一段' }] })
124
+ assert(ctxStore.status === 200 && ctxStore.out?.count === 1, 'context: stored')
125
+ // 套件合并后 pre-step 是瀑布链(prompts /context + compaction 落档 +
126
+ // enhancer……),按注册顺序链式调用,断言走到全链后的结果。
127
+ const preHandlers = eventHandlers['agent/pre-step'] ?? []
128
+ assert(Array.isArray(preHandlers) && preHandlers.length >= 2, `context: pre-step waterfall registered (${preHandlers.length} listeners)`)
129
+ const runPreStep = async (payload) => {
130
+ const run = async (index) => {
131
+ if (index >= preHandlers.length) return { kind: 'enter', messages: [] }
132
+ return preHandlers[index](payload, () => run(index + 1))
133
+ }
134
+ return run(0)
135
+ }
136
+ // followup 的 pre-step 读 payload.messages(真机宿主字段)——冒烟桩也要给
137
+ const prePayload = () => ({ agent: { session: 'smoke-session' }, messages: [], step: 1, signal: { aborted: false } })
138
+ const preOnce = await runPreStep(prePayload())
139
+ const injected = preOnce.messages?.map((m) => m?.content?.[0]?.text ?? '').join('\n') ?? ''
140
+ assert(injected.includes('选中的对话内容') && injected.includes('帮我解释这一段'), 'context: text + comment injected')
141
+ const preTwice = await runPreStep(prePayload())
142
+ const twiceText = preTwice.messages?.map((m) => m?.content?.[0]?.text ?? '').join('\n') ?? ''
143
+ assert(!twiceText.includes('选中的对话内容'), 'context: one-shot consumed')
144
+
145
+ // 客户端草稿桥:划选「添加到对话」/模板插入/斜杠命令都依赖它;
146
+ // 0.4.0 移除 snippets 时整块丢失过一次,静态断言防再犯。
147
+ const clientSource = readFileSync(new URL('../client.js', import.meta.url), 'utf8')
148
+ assert(clientSource.includes('window.__dshSuperinputSetDraft ='), 'client: draft bridge setter present')
149
+ assert(clientSource.includes('dsh-superinput-prompts-bridge'), 'client: draft bridge registered on input.dock')
150
+ assert(clientSource.includes("superinput-selection"), 'client: inline selection reference source registered')
151
+ assert(clientSource.includes("slash/input-insert-reference"), 'client: chip inserted through the official input event')
152
+ assert(clientSource.includes('conversation.user.text.node'), 'client: sent-message selection chip renderer registered')
153
+ assert(clientSource.includes('__dshSuperinputCommentContext'), 'client: selection comment card entry present')
154
+ assert(clientSource.includes('dsh-si-comment-editor'), 'client: anchored comment editor present (Trae previewCommentCard)')
155
+ assert(clientSource.includes('__dshSuperinputDropContext'), 'client: drag-drop insert entry present (Trae dragDropMention)')
156
+ assert(clientSource.includes('application/x-dsh-selection'), 'client: drag payload marker present')
157
+ assert(clientSource.includes('__dshSuperinputInsertQuote'), 'client: official insertQuote bridge present (引用文本)')
158
+ assert(clientSource.includes('[class*="U4oBDW_action"]'), 'client: official floating pill hidden (menu replaces it)')
159
+
160
+ console.log('SMOKE ALL PASS')
161
+ } catch (error) {
162
+ failed = true
163
+ console.log('SMOKE FAIL:', error?.message ?? error)
164
+ if (error?.stack) console.log(error.stack.split('\n').slice(1, 6).join('\n'))
165
+ } finally {
166
+ try {
167
+ if (backup) writeFileSync(STORE, backup)
168
+ else unlinkSync(STORE)
169
+ } catch { /* 恢复失败只能报出来 */ }
170
+ if (!backup) console.log('(no prior store: test artifacts removed)')
171
+ }
172
+ process.exit(failed ? 1 : 0)