dsh-superinput 0.0.0-stage → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -0
- package/README.md +38 -2
- package/client.js +5292 -0
- package/cordis.patch.yml +6 -0
- package/index.js +34 -0
- package/modules/cost.js +1346 -0
- package/modules/enhancer.js +726 -0
- package/modules/followup.js +718 -0
- package/modules/hub.client.js +218 -0
- package/modules/hub.js +78 -0
- package/modules/picker.js +133 -0
- package/modules/prompts.client.js +698 -0
- package/modules/prompts.js +305 -0
- package/modules/voice.js +17 -0
- package/package.json +61 -4
- package/scripts/build-suite.mjs +315 -0
- package/scripts/smoke.mjs +121 -0
- package/server.js +3428 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-superinput 套件生成器:从五个独立插件机械搬迁模块代码。
|
|
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
|
+
* 上游插件更新后重跑本脚本即可再生成;重复执行幂等。
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
16
|
+
import { dirname, join } from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
|
|
19
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url))) // packages/dsh-superinput
|
|
20
|
+
const pkgRoot = join(root, '..')
|
|
21
|
+
|
|
22
|
+
/** key → [包目录, 客户端工厂名] */
|
|
23
|
+
const MODULES = [
|
|
24
|
+
['cost', 'dsh-cost', 'createCostClient'],
|
|
25
|
+
['picker', 'dsh-power-picker', 'createPickerClient'],
|
|
26
|
+
['enhancer', 'dsh-prompt-enhancer', 'createEnhancerClient'],
|
|
27
|
+
['followup', 'dsh-followup', 'createFollowupClient'],
|
|
28
|
+
['voice', 'dsh-siri-voice', 'createVoiceClient'],
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
function transformServer(pkg, key) {
|
|
32
|
+
let src = readFileSync(join(pkgRoot, pkg, 'index.js'), 'utf8')
|
|
33
|
+
const head = `/* 由 scripts/build-suite.mjs 从 ${pkg}/index.js 机械生成;勿手改,重跑脚本再生成。 */\n`
|
|
34
|
+
src = src.replace(/^export const name = /m, 'const name = ')
|
|
35
|
+
src = src.replace(/^export default \{[^\n]*\}\s*$/m, '')
|
|
36
|
+
if (/^const inject = /m.test(src)) src += '\nexport { inject }\n'
|
|
37
|
+
if (!/export const Config/.test(src)) {
|
|
38
|
+
src += "import Schema from '@deepseek-ai/schemastery'\nexport const Config = Schema.object({ enabled: Schema.boolean().default(true).description('启用') })\n"
|
|
39
|
+
}
|
|
40
|
+
return head + src
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function transformClient(pkg, factoryName) {
|
|
44
|
+
let src = readFileSync(join(pkgRoot, pkg, 'client.js'), 'utf8').trimEnd()
|
|
45
|
+
// 不锚定行首:各包文件头都有注释块。
|
|
46
|
+
const headRe = /window\.__ModuleLoader__\.load\(\{\s*\n\s*id:\s*'[^']+',\s*\n\s*factory\(require\) \{/
|
|
47
|
+
if (!headRe.test(src)) throw new Error(`${pkg}/client.js head pattern mismatch`)
|
|
48
|
+
src = src.replace(headRe, `function ${factoryName}(require) {`)
|
|
49
|
+
const tailRe = /\n \},\n\}\)\s*$/
|
|
50
|
+
if (!tailRe.test(src)) throw new Error(`${pkg}/client.js tail pattern mismatch`)
|
|
51
|
+
src = src.replace(tailRe, '\n}')
|
|
52
|
+
return `/* 由 scripts/build-suite.mjs 从 ${pkg}/client.js 机械生成;勿手改。 */\n${src}\n`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ---- 服务端:单文件内联 ---- */
|
|
56
|
+
|
|
57
|
+
/** 抽顶层 import(单行)、去 export 关键字,返回 { imports, body }。 */
|
|
58
|
+
function inlineSource(src) {
|
|
59
|
+
const imports = []
|
|
60
|
+
let body = src.replace(/^import [^\n]+$/gm, (line) => { imports.push(line.trim()); return '' })
|
|
61
|
+
body = body
|
|
62
|
+
.replace(/^export const name = /m, 'const name = ')
|
|
63
|
+
.replace(/^export const inject = /m, 'const moduleInject = ')
|
|
64
|
+
.replace(/^export const Config/m, 'const Config')
|
|
65
|
+
.replace(/^export function apply\(/m, 'function apply(')
|
|
66
|
+
.replace(/^export \{ inject \}\s*$/m, '')
|
|
67
|
+
.replace(/^export default \{[^\n]*\}\s*$/m, '')
|
|
68
|
+
return { imports, body: body.trim() }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 按 source 分组合并 import 绑定符(整行去重不够:不同行的子集会重复声明)。 */
|
|
72
|
+
function mergeImports(importLines) {
|
|
73
|
+
const bySource = new Map() // source → { defaults:Set, names:Set }
|
|
74
|
+
for (const line of importLines) {
|
|
75
|
+
const m = line.match(/^import\s+(?:([A-Za-z_$][\w$]*)\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*['"]([^'"]+)['"]/)
|
|
76
|
+
if (!m) throw new Error(`unparsed import: ${line}`)
|
|
77
|
+
const [, defaults, named, source] = m
|
|
78
|
+
const entry = bySource.get(source) ?? { defaults: new Set(), names: new Set() }
|
|
79
|
+
if (defaults) entry.defaults.add(defaults)
|
|
80
|
+
if (named) for (const part of named.split(',')) {
|
|
81
|
+
const name = part.trim()
|
|
82
|
+
if (name) entry.names.add(name)
|
|
83
|
+
}
|
|
84
|
+
bySource.set(source, entry)
|
|
85
|
+
}
|
|
86
|
+
const lines = []
|
|
87
|
+
for (const [source, { defaults, names }] of bySource) {
|
|
88
|
+
const parts = []
|
|
89
|
+
if (defaults.size > 0) parts.push([...defaults][0])
|
|
90
|
+
if (names.size > 0) parts.push(`{ ${[...names].join(', ')} }`)
|
|
91
|
+
lines.push(`import ${parts.join(', ')} from '${source}'`)
|
|
92
|
+
}
|
|
93
|
+
return lines.sort()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
mkdirSync(join(root, 'modules'), { recursive: true })
|
|
97
|
+
const importLines = []
|
|
98
|
+
const iifes = []
|
|
99
|
+
for (const [key, pkg] of MODULES) {
|
|
100
|
+
const transformed = transformServer(pkg, key)
|
|
101
|
+
writeFileSync(join(root, 'modules', `${key}.js`), transformed) // 可读中间产物
|
|
102
|
+
const { imports, body } = inlineSource(transformed)
|
|
103
|
+
importLines.push(...imports.filter((line) => !line.includes("@deepseek-ai/schemastery")))
|
|
104
|
+
iifes.push(`/* ==== module: ${key}(原 ${pkg}) ==== */\nconst ${key}Module = (() => {\n${body}\n return { apply, Config, inject: typeof moduleInject === 'undefined' ? undefined : moduleInject }\n})()`)
|
|
105
|
+
console.log(`✓ inline ${key}`)
|
|
106
|
+
}
|
|
107
|
+
// prompts(手写模块源)同样内联。
|
|
108
|
+
{
|
|
109
|
+
const src = readFileSync(join(root, 'modules', 'prompts.js'), 'utf8')
|
|
110
|
+
const { imports, body } = inlineSource(src)
|
|
111
|
+
importLines.push(...imports)
|
|
112
|
+
iifes.push(`/* ==== module: prompts(手写) ==== */\nconst promptsModule = (() => {\n${body}\n return { apply, Config, inject: typeof moduleInject === 'undefined' ? undefined : moduleInject }\n})()`)
|
|
113
|
+
console.log('✓ inline prompts')
|
|
114
|
+
}
|
|
115
|
+
// hub(手写模块源)同样内联。
|
|
116
|
+
{
|
|
117
|
+
const src = readFileSync(join(root, 'modules', 'hub.js'), 'utf8')
|
|
118
|
+
const { imports, body } = inlineSource(src)
|
|
119
|
+
importLines.push(...imports)
|
|
120
|
+
iifes.push(`/* ==== module: hub(手写) ==== */\nconst hubModule = (() => {\n${body}\n return { apply, Config, inject: typeof moduleInject === 'undefined' ? undefined : moduleInject }\n})()`)
|
|
121
|
+
console.log('✓ inline hub')
|
|
122
|
+
}
|
|
123
|
+
const allImports = mergeImports(importLines)
|
|
124
|
+
// schemastery 由 glue 使用,保证在列。
|
|
125
|
+
if (!allImports.some((line) => line.includes('@deepseek-ai/schemastery'))) {
|
|
126
|
+
allImports.unshift("import Schema from '@deepseek-ai/schemastery'")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const server = `/* dsh-superinput 服务端核心(由 scripts/build-suite.mjs 生成;勿手改)。
|
|
130
|
+
* 入口拆分:index.js 是零依赖探针(先 trace 再动态 import 本文件并捕获失败)——
|
|
131
|
+
* 顶层静态 import 的失败发生在入口任何语句之前,静默且永远取不到证据。 */
|
|
132
|
+
import { appendFileSync as diagAppend, mkdirSync as diagMkdir } from 'node:fs'
|
|
133
|
+
import { homedir as diagHomedir } from 'node:os'
|
|
134
|
+
import { dirname as diagDirname, join as pathJoin } from 'node:path'
|
|
135
|
+
${allImports.join('\n')}
|
|
136
|
+
|
|
137
|
+
const SUITE_DIAG = pathJoin(diagHomedir(), '.dsh', 'superinput-debug.log')
|
|
138
|
+
const trace = (line) => {
|
|
139
|
+
try {
|
|
140
|
+
diagMkdir(diagDirname(SUITE_DIAG), { recursive: true })
|
|
141
|
+
diagAppend(SUITE_DIAG, \`\${new Date().toISOString()} \${line}\\n\`, 'utf8')
|
|
142
|
+
} catch { /* ignore */ }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
${iifes.join('\n\n')}
|
|
146
|
+
|
|
147
|
+
const MODULES = [
|
|
148
|
+
['cost', costModule],
|
|
149
|
+
['picker', pickerModule],
|
|
150
|
+
['enhancer', enhancerModule],
|
|
151
|
+
['followup', followupModule],
|
|
152
|
+
['voice', voiceModule],
|
|
153
|
+
['prompts', promptsModule],
|
|
154
|
+
['hub', hubModule],
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
export const name = 'superinput'
|
|
158
|
+
|
|
159
|
+
export const inject = [...new Set(MODULES.flatMap(([, mod]) => mod.inject ?? []))]
|
|
160
|
+
|
|
161
|
+
export const Config = Schema.object({
|
|
162
|
+
cost: costModule.Config,
|
|
163
|
+
picker: pickerModule.Config,
|
|
164
|
+
enhancer: enhancerModule.Config,
|
|
165
|
+
followup: followupModule.Config,
|
|
166
|
+
voice: voiceModule.Config,
|
|
167
|
+
prompts: promptsModule.Config,
|
|
168
|
+
hub: hubModule.Config,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 嵌套默认值兜底:宿主对插件 row config 的解析深度未知——套件层自己跑
|
|
173
|
+
* 一遍每个模块的 Config({}) 拿完整默认,与宿主传入的 slice 合并。
|
|
174
|
+
*/
|
|
175
|
+
const DEFAULTS = new Map()
|
|
176
|
+
for (const [key, mod] of MODULES) {
|
|
177
|
+
try { DEFAULTS.set(key, mod.Config ? mod.Config({}) : {}) } catch { DEFAULTS.set(key, {}) }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function apply(ctx, config) {
|
|
181
|
+
const keys = Object.keys(config ?? {})
|
|
182
|
+
console.info(\`[dsh-superinput] server apply: host config keys=\${keys.length ? keys.join(',') : '(none)'} — merging schema defaults\`)
|
|
183
|
+
// 先算出每个模块的最终生效配置,hub 用它做总览(自己拿不到别人的 config)。
|
|
184
|
+
const merged = {}
|
|
185
|
+
for (const [key] of MODULES) {
|
|
186
|
+
merged[key] = { ...(DEFAULTS.get(key) ?? {}), ...(config?.[key] ?? {}) }
|
|
187
|
+
}
|
|
188
|
+
for (const [key, mod] of MODULES) {
|
|
189
|
+
try {
|
|
190
|
+
const own = merged[key]
|
|
191
|
+
mod.apply(ctx, key === 'hub' ? { ...own, snapshot: merged } : own)
|
|
192
|
+
trace(\`module "\${key}" applied OK\`)
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(\`[dsh-superinput] server module "\${key}" failed:\`, error)
|
|
195
|
+
trace(\`module "\${key}" FAILED: \${String(error?.stack ?? error).slice(0, 600)}\`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default { name, Config, apply }
|
|
201
|
+
`
|
|
202
|
+
|
|
203
|
+
const entry = `/* dsh-superinput 入口:零依赖探针(由 scripts/build-suite.mjs 生成;勿手改)。
|
|
204
|
+
* 静态 import 的失败发生在本文件第一条语句之前(宿主静默),所以核心代码
|
|
205
|
+
* 全部放在 server.js 由这里动态加载——成败都会留下取证记录。 */
|
|
206
|
+
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
207
|
+
import { homedir } from 'node:os'
|
|
208
|
+
import { dirname, join } from 'node:path'
|
|
209
|
+
|
|
210
|
+
const DIAG = join(homedir(), '.dsh', 'superinput-debug.log')
|
|
211
|
+
const trace = (line) => {
|
|
212
|
+
try {
|
|
213
|
+
mkdirSync(dirname(DIAG), { recursive: true })
|
|
214
|
+
appendFileSync(DIAG, \`\${new Date().toISOString()} \${line}\\n\`, 'utf8')
|
|
215
|
+
} catch { /* ignore */ }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
trace(\`suite ENTRY pid=\${process.pid}\`)
|
|
219
|
+
|
|
220
|
+
let core
|
|
221
|
+
try {
|
|
222
|
+
core = await import('./server.js')
|
|
223
|
+
trace(\`server.js LOADED pid=\${process.pid}\`)
|
|
224
|
+
} catch (error) {
|
|
225
|
+
trace(\`server.js FAILED: \${String(error?.stack ?? error).slice(0, 600)}\`)
|
|
226
|
+
throw error
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const name = core.name
|
|
230
|
+
export const inject = core.inject
|
|
231
|
+
export const Config = core.Config
|
|
232
|
+
export function apply(ctx, config) {
|
|
233
|
+
trace(\`suite APPLY pid=\${process.pid}\`)
|
|
234
|
+
return core.apply(ctx, config)
|
|
235
|
+
}
|
|
236
|
+
export default core.default
|
|
237
|
+
`
|
|
238
|
+
writeFileSync(join(root, 'server.js'), server)
|
|
239
|
+
writeFileSync(join(root, 'index.js'), entry)
|
|
240
|
+
console.log(`✓ index.js 零依赖探针入口(${entry.length} bytes)+ server.js 核心(${server.length} bytes)`)
|
|
241
|
+
|
|
242
|
+
/* ---- 客户端:六工厂内联 + glue ---- */
|
|
243
|
+
const clientFiles = []
|
|
244
|
+
for (const [key, pkg, factory] of MODULES) {
|
|
245
|
+
clientFiles.push(transformClient(pkg, factory))
|
|
246
|
+
console.log(`✓ client ${factory}`)
|
|
247
|
+
}
|
|
248
|
+
clientFiles.push(readFileSync(join(root, 'modules', 'prompts.client.js'), 'utf8'))
|
|
249
|
+
clientFiles.push(readFileSync(join(root, 'modules', 'hub.client.js'), 'utf8'))
|
|
250
|
+
|
|
251
|
+
const glue = `/* dsh-superinput 客户端:单 __ModuleLoader__ 入口,内部逐模块装配。
|
|
252
|
+
* 本文件由 scripts/build-suite.mjs 生成(模块工厂内联 + 下方 glue);勿手改模块工厂。 */
|
|
253
|
+
|
|
254
|
+
${clientFiles.join('\n')}
|
|
255
|
+
|
|
256
|
+
window.__ModuleLoader__.load({
|
|
257
|
+
id: 'dsh-superinput',
|
|
258
|
+
factory(require) {
|
|
259
|
+
const defs = [
|
|
260
|
+
['cost', createCostClient],
|
|
261
|
+
['picker', createPickerClient],
|
|
262
|
+
['enhancer', createEnhancerClient],
|
|
263
|
+
['followup', createFollowupClient],
|
|
264
|
+
['voice', createVoiceClient],
|
|
265
|
+
['prompts', createPromptsClient],
|
|
266
|
+
['hub', createHubClient],
|
|
267
|
+
]
|
|
268
|
+
// 模块异常上报:console 不落盘,失败必须走 prompts /diag 留痕。
|
|
269
|
+
const reportModuleError = (key, when, error) => {
|
|
270
|
+
console.error(\`[dsh-superinput] client module "\${key}" \${when}:\`, error)
|
|
271
|
+
try {
|
|
272
|
+
void fetch('/api/dsh-superinput/prompts/diag', {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: { 'content-type': 'application/json' },
|
|
275
|
+
body: JSON.stringify({ message: \`client module "\${key}" \${when}: \${String(error?.stack ?? error).slice(0, 400)}\` }),
|
|
276
|
+
}).catch(() => {})
|
|
277
|
+
} catch { /* ignore */ }
|
|
278
|
+
}
|
|
279
|
+
const modules = []
|
|
280
|
+
for (const [key, create] of defs) {
|
|
281
|
+
try { modules.push([key, create(require)]) }
|
|
282
|
+
catch (error) { reportModuleError(key, 'factory failed', error) }
|
|
283
|
+
}
|
|
284
|
+
console.info('[dsh-superinput] client active:', modules.map(([key]) => key).join('+'))
|
|
285
|
+
return {
|
|
286
|
+
// 各模块 client 的 inject 并集:picker 需要 sessions/remote/modelDirectories
|
|
287
|
+
// 等,只声明 slots 会让它在 apply 时报 cannot get property without inject。
|
|
288
|
+
inject: [...new Set(modules.flatMap(([, mod]) => mod.inject ?? ['slots']))],
|
|
289
|
+
apply(ctx) {
|
|
290
|
+
for (const [key, mod] of modules) {
|
|
291
|
+
try { mod.apply(ctx) }
|
|
292
|
+
catch (error) { reportModuleError(key, 'apply failed', error) }
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
`
|
|
299
|
+
// Electron 渲染器禁用 API 门禁:window.prompt/alert/confirm 在 Electron 直接
|
|
300
|
+
// 抛错(2026-11-01 真机事故:prompts 模板一点就消失)。构建期拦截。
|
|
301
|
+
const forbiddenHits = []
|
|
302
|
+
for (const [re, why] of [
|
|
303
|
+
[/window\.prompt\s*\(/, 'window.prompt(Electron 不支持,改用面板内表单)'],
|
|
304
|
+
[/window\.alert\s*\(/, 'window.alert(Electron 不支持,改用面板内提示)'],
|
|
305
|
+
[/window\.confirm\s*\(/, 'window.confirm(Electron 不支持,改用面板内按钮)'],
|
|
306
|
+
]) {
|
|
307
|
+
if (re.test(glue)) forbiddenHits.push(why)
|
|
308
|
+
}
|
|
309
|
+
if (forbiddenHits.length) {
|
|
310
|
+
console.error('✗ client.js 含 Electron 渲染器禁用 API:\n ' + forbiddenHits.join('\n '))
|
|
311
|
+
process.exit(1)
|
|
312
|
+
}
|
|
313
|
+
writeFileSync(join(root, 'client.js'), glue)
|
|
314
|
+
const clientModuleCount = (glue.match(/\['\w+', create\w+\]/g) ?? []).length
|
|
315
|
+
console.log(`✓ client.js (${glue.length} bytes, ${clientModuleCount} modules)`)
|
|
@@ -0,0 +1,121 @@
|
|
|
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 services = {
|
|
22
|
+
webServer: { register(spec) { webPaths.push(spec.path); if (spec.path.endsWith('/prompts')) promptsHandler = spec.handler; if (spec.path === '/api/prompt-enhancer') enhancerHandler = spec.handler } },
|
|
23
|
+
tools: { register() {} },
|
|
24
|
+
// stub llm 流:/rewrite 端到端用它验证「请求期经嵌套作用域取 llm」的数据流
|
|
25
|
+
llm: {
|
|
26
|
+
stream() {
|
|
27
|
+
return (async function* () { yield { type: 'text-delta', text: 'ENHANCED OUTPUT TEXT' } })()
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
slots: { inject() {}, register() {} },
|
|
31
|
+
}
|
|
32
|
+
const ctx = {
|
|
33
|
+
effect(fn) { fn() },
|
|
34
|
+
get() { return undefined }, // defaultRoute() 的 ctx.get('agentDefaultModel') 安全回落
|
|
35
|
+
inject(deps, cb) {
|
|
36
|
+
const scope = Object.create(this)
|
|
37
|
+
for (const d of deps) if (!(d in scope)) scope[d] = services[d] ?? {}
|
|
38
|
+
cb(scope)
|
|
39
|
+
},
|
|
40
|
+
on() { return () => {} },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const assert = (cond, label) => { if (!cond) throw new Error(label); console.log('ok:', label) }
|
|
44
|
+
const call = async (handler, prefix, method, sub, body) => {
|
|
45
|
+
const chunks = body === undefined ? [] : [JSON.stringify(body)]
|
|
46
|
+
const req = { url: `${prefix}${sub}`, method, async *[Symbol.asyncIterator]() { for (const c of chunks) yield c } }
|
|
47
|
+
let status = 0
|
|
48
|
+
let out = null
|
|
49
|
+
const res = { setHeader() {}, set statusCode(v) { status = v }, end(b) { out = b ? JSON.parse(b) : null } }
|
|
50
|
+
await handler(req, res)
|
|
51
|
+
return { status, out }
|
|
52
|
+
}
|
|
53
|
+
const prompts = (method, sub, body) => call(promptsHandler, '/api/dsh-superinput/prompts', method, sub, body)
|
|
54
|
+
const byLastUsed = (x, y) => (y.usedAt ?? y.at ?? 0) - (x.usedAt ?? x.at ?? 0)
|
|
55
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
56
|
+
|
|
57
|
+
let failed = false
|
|
58
|
+
try {
|
|
59
|
+
const config = {}
|
|
60
|
+
// enhancer 给显式 provider/model,让 configRoute 命中、不依赖 agentDefaultModel
|
|
61
|
+
for (const key of ['cost', 'picker', 'enhancer', 'followup', 'voice', 'prompts', 'hub']) config[key] = {}
|
|
62
|
+
config.enhancer = { provider: 'stub', model: 'stub-model' }
|
|
63
|
+
mod.apply(ctx, config)
|
|
64
|
+
for (const path of ['/api/dsh-cost', '/api/dsh-superinput/prompts', '/api/dsh-superinput/hub', '/api/prompt-enhancer']) {
|
|
65
|
+
assert(webPaths.includes(path), `module apply: ${path} registered`)
|
|
66
|
+
}
|
|
67
|
+
assert(!!promptsHandler, 'module apply: prompts handler captured')
|
|
68
|
+
assert(!!enhancerHandler, 'module apply: enhancer handler captured')
|
|
69
|
+
|
|
70
|
+
// /rewrite 端到端:回归「请求期经嵌套作用域取 llm」——处理器若退回外层
|
|
71
|
+
// ctx(0.3.1 真机事故),stub 环境里 ctx.llm 为 undefined → 502,此处必挂。
|
|
72
|
+
const rw = await call(enhancerHandler, '/api/prompt-enhancer', 'POST', '/rewrite', { text: '帮我写个东西' })
|
|
73
|
+
assert(rw.status === 200 && rw.out?.text === 'ENHANCED OUTPUT TEXT', `rewrite e2e: status=${rw.status} text=${JSON.stringify(rw.out?.text ?? rw.out)}`)
|
|
74
|
+
|
|
75
|
+
const a = await prompts('POST', '/save', { title: 'e2e-写周报', body: '写周报,日期 {{日期}}', tags: ['test'] })
|
|
76
|
+
const b = await prompts('POST', '/save', { title: 'e2e-plain', body: 'plain body' })
|
|
77
|
+
assert(a.out.ok && b.out.ok, 'save two templates')
|
|
78
|
+
const idA = a.out.id
|
|
79
|
+
const idB = b.out.id
|
|
80
|
+
|
|
81
|
+
await sleep(5) // 避免 usedAt 与保存 at 同毫秒打平(稳定排序会保留原序)
|
|
82
|
+
assert((await prompts('POST', '/use', { id: idA })).out.ok, 'use A')
|
|
83
|
+
assert((await prompts('POST', '/use', { id: idA })).out.ok, 'use A again')
|
|
84
|
+
assert((await prompts('POST', '/slash', { id: idA, slash: true })).out.ok, 'slash A on')
|
|
85
|
+
assert((await prompts('POST', '/slash', { id: idB, slash: true })).out.ok, 'slash B on')
|
|
86
|
+
|
|
87
|
+
const list1 = (await prompts('GET', '/list')).out.items
|
|
88
|
+
const itemA = list1.find((i) => i.id === idA)
|
|
89
|
+
assert(itemA.uses === 2 && itemA.usedAt > 0, 'list: A carries uses=2 + usedAt')
|
|
90
|
+
assert(itemA.slash === true && list1.find((i) => i.id === idB).slash === true, 'list: slash flags on')
|
|
91
|
+
assert([...list1].sort(byLastUsed)[0].id === idA, 'client sort: A (recently used) first')
|
|
92
|
+
|
|
93
|
+
const dump = (await prompts('GET', '/export')).out
|
|
94
|
+
assert(dump.version === 1 && dump.items.length >= 2, 'export: version+items')
|
|
95
|
+
|
|
96
|
+
const modA = { ...dump.items.find((i) => i.id === idA), title: 'e2e-写周报-v2', at: Date.now() + 10 }
|
|
97
|
+
const c = await prompts('POST', '/save', { title: 'e2e-temp-c', body: 'ccc' })
|
|
98
|
+
const delC = c.out.id
|
|
99
|
+
await prompts('POST', '/remove', { id: delC })
|
|
100
|
+
const importedC = { id: delC, title: 'e2e-C-imported', body: 'ccc', tags: [], at: Date.now(), pinned: false }
|
|
101
|
+
const stale = { ...dump.items.find((i) => i.id === idB), body: 'stale-overwrite-should-skip', at: 1 }
|
|
102
|
+
const imp = await prompts('POST', '/import', { items: [modA, importedC, stale, { body: '' }] })
|
|
103
|
+
assert(imp.out.ok && imp.out.updated === 1 && imp.out.added === 1, `import: added=${imp.out.added} updated=${imp.out.updated}`)
|
|
104
|
+
const list2 = (await prompts('GET', '/list')).out.items
|
|
105
|
+
assert(list2.find((i) => i.id === idA).title === 'e2e-写周报-v2', 'import: newer A overwrote')
|
|
106
|
+
assert(list2.find((i) => i.id === delC).title === 'e2e-C-imported', 'import: deleted C back with original id')
|
|
107
|
+
assert(list2.find((i) => i.id === idB).body !== 'stale-overwrite-should-skip', 'import: stale skipped')
|
|
108
|
+
assert((await prompts('POST', '/import', { items: 'not-an-array' })).status === 400, 'import: invalid payload rejected')
|
|
109
|
+
|
|
110
|
+
console.log('SMOKE ALL PASS')
|
|
111
|
+
} catch (error) {
|
|
112
|
+
failed = true
|
|
113
|
+
console.log('SMOKE FAIL:', error?.message ?? error)
|
|
114
|
+
} finally {
|
|
115
|
+
try {
|
|
116
|
+
if (backup) writeFileSync(STORE, backup)
|
|
117
|
+
else unlinkSync(STORE)
|
|
118
|
+
} catch { /* 恢复失败只能报出来 */ }
|
|
119
|
+
if (!backup) console.log('(no prior store: test artifacts removed)')
|
|
120
|
+
}
|
|
121
|
+
process.exit(failed ? 1 : 0)
|