dsh-superinput 0.7.0 → 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.
- package/CHANGELOG.md +265 -0
- package/README.md +18 -7
- package/client.js +2356 -251
- package/modules/compaction.js +413 -0
- package/modules/cost.js +571 -3
- package/modules/prompts.client.js +1068 -57
- package/modules/prompts.js +65 -0
- package/package.json +2 -3
- package/scripts/build-suite.mjs +14 -4
- package/scripts/smoke.mjs +54 -3
- package/server.js +1057 -3
package/modules/prompts.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 存储 ~/.dsh/storages/dsh-superinput/prompts.json(结构 { items: [...] })。
|
|
8
8
|
*/
|
|
9
9
|
import Schema from '@deepseek-ai/schemastery'
|
|
10
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
10
11
|
import { readFileSync, writeFileSync, renameSync, mkdirSync, appendFileSync } from 'node:fs'
|
|
11
12
|
import { randomUUID } from 'node:crypto'
|
|
12
13
|
import { homedir } from 'node:os'
|
|
@@ -18,6 +19,24 @@ const STORE_DIR = join(homedir(), '.dsh', 'storages', 'dsh-superinput')
|
|
|
18
19
|
const STORE_PATH = join(STORE_DIR, 'prompts.json')
|
|
19
20
|
const CLIENT_DIAG = join(homedir(), '.dsh', 'superinput-debug.log')
|
|
20
21
|
|
|
22
|
+
/* 划选上下文:客户端把「选段 + 评论」暂存到这里,下一轮 pre-step 一次性
|
|
23
|
+
* 注入成一条 sourced UserMessage,随后立即删除(只影响紧接着的一次发送)。 */
|
|
24
|
+
const CONTEXT_MAX_ITEMS = 8
|
|
25
|
+
const CONTEXT_TEXT_MAX = 4000
|
|
26
|
+
const CONTEXT_COMMENT_MAX = 600
|
|
27
|
+
const CONTEXT_TTL_MS = 10 * 60_000
|
|
28
|
+
const pendingContext = new Map()
|
|
29
|
+
|
|
30
|
+
function renderSelectionContext(items) {
|
|
31
|
+
const lines = [
|
|
32
|
+
'以下是我从对话中选中的上下文,按顺序附带我的评论。回答时请优先参考这些内容,并针对评论中的问题作答。',
|
|
33
|
+
]
|
|
34
|
+
items.forEach((item, index) => {
|
|
35
|
+
lines.push(`【选段 ${index + 1}】\n${item.text}${item.comment ? `\n我的评论:${item.comment}` : ''}`)
|
|
36
|
+
})
|
|
37
|
+
return lines.join('\n\n')
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
export const Config = Schema.object({
|
|
22
41
|
enabled: Schema.boolean().default(true).description('启用提示词模板库'),
|
|
23
42
|
maxItems: Schema.number().default(500).description('最多保存多少条模板(收藏不计入上限)'),
|
|
@@ -209,6 +228,36 @@ export function apply(ctx, config) {
|
|
|
209
228
|
store.load()
|
|
210
229
|
ctx.effect(() => store.dispose(), 'dsh-superinput: prompts store')
|
|
211
230
|
|
|
231
|
+
// 划选上下文注入:挂在 pre-step 最前面,先让下游(含提示词增强)处理完,
|
|
232
|
+
// 再把上下文作为一条独立的 sourced 用户消息追加到本轮送模消息末尾。
|
|
233
|
+
ctx.on('agent/pre-step', async ({ agent, step, signal }, next) => {
|
|
234
|
+
const decision = await next()
|
|
235
|
+
if (decision.kind === 'reject' || signal?.aborted) return decision
|
|
236
|
+
const rawSession = agent?.session
|
|
237
|
+
const sessionId = typeof rawSession === 'string'
|
|
238
|
+
? rawSession
|
|
239
|
+
: rawSession?.id === undefined ? '' : String(rawSession.id)
|
|
240
|
+
const pending = sessionId ? pendingContext.get(sessionId) : undefined
|
|
241
|
+
if (step !== 1 || !pending) return decision
|
|
242
|
+
pendingContext.delete(sessionId)
|
|
243
|
+
if (pending.items.length === 0 || Date.now() - pending.at > CONTEXT_TTL_MS) return decision
|
|
244
|
+
const text = renderSelectionContext(pending.items)
|
|
245
|
+
try {
|
|
246
|
+
appendFileSync(CLIENT_DIAG, `${new Date().toISOString()} context injected session=${sessionId.slice(0, 50)} items=${pending.items.length}\n`, 'utf8')
|
|
247
|
+
} catch { /* 日志失败不影响注入 */ }
|
|
248
|
+
return {
|
|
249
|
+
...decision,
|
|
250
|
+
messages: [...decision.messages, createUserMessage({
|
|
251
|
+
content: [{ type: 'text', text }],
|
|
252
|
+
source: {
|
|
253
|
+
kind: 'dsh-superinput-selection',
|
|
254
|
+
form: 'snapshot',
|
|
255
|
+
sections: [{ name: 'selection-context', text }],
|
|
256
|
+
},
|
|
257
|
+
})],
|
|
258
|
+
}
|
|
259
|
+
}, { prepend: true })
|
|
260
|
+
|
|
212
261
|
ctx.inject(['webServer'], (webCtx) => {
|
|
213
262
|
webCtx.effect(() => webCtx.webServer.register({
|
|
214
263
|
kind: 'prefix',
|
|
@@ -256,6 +305,22 @@ export function apply(ctx, config) {
|
|
|
256
305
|
send(200, { ok: true })
|
|
257
306
|
return
|
|
258
307
|
}
|
|
308
|
+
// 划选上下文暂存:items 为空表示清除;真正的注入在 agent/pre-step。
|
|
309
|
+
if (path === '/context' && req.method === 'POST') {
|
|
310
|
+
const body = await readBody()
|
|
311
|
+
const sessionId = String(body.sessionId ?? '').trim().slice(0, 200)
|
|
312
|
+
if (!sessionId) { send(400, { error: 'missing-session' }); return }
|
|
313
|
+
const items = Array.isArray(body.items)
|
|
314
|
+
? body.items.slice(0, CONTEXT_MAX_ITEMS).map((item) => ({
|
|
315
|
+
text: String(item?.text ?? '').replace(/\0/g, '').trim().slice(0, CONTEXT_TEXT_MAX),
|
|
316
|
+
comment: String(item?.comment ?? '').replace(/\0/g, '').trim().slice(0, CONTEXT_COMMENT_MAX),
|
|
317
|
+
})).filter((item) => item.text.length > 0)
|
|
318
|
+
: []
|
|
319
|
+
if (items.length === 0) pendingContext.delete(sessionId)
|
|
320
|
+
else pendingContext.set(sessionId, { items, at: Date.now() })
|
|
321
|
+
send(200, { ok: true, count: items.length })
|
|
322
|
+
return
|
|
323
|
+
}
|
|
259
324
|
if (path === '/list' && req.method === 'GET') {
|
|
260
325
|
send(200, { items: store.list() })
|
|
261
326
|
return
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-superinput",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Super input box suite for the DeepSeek Harness (dsh): model & effort picker, prompt enhancement, follow-up suggestions, full cost engine (billing, backfill, price sync, forecast), voice input, prompt template manager and a settings hub — one plugin,
|
|
3
|
+
"version": "0.12.1",
|
|
4
|
+
"description": "Super input box suite for the DeepSeek Harness (dsh): model & effort picker, prompt enhancement, follow-up suggestions, full cost engine (billing, backfill, price sync, forecast, usage heatmap, Coding Plan quotas, bilingual UI), voice input, prompt template manager, inline selection reference chips with comments, a compaction ring and a settings hub — one plugin, eight modules with per-module failure isolation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"composer",
|
|
32
32
|
"cost",
|
|
33
33
|
"picker",
|
|
34
|
-
"snippets",
|
|
35
34
|
"suite"
|
|
36
35
|
],
|
|
37
36
|
"license": "MIT",
|
package/scripts/build-suite.mjs
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* dsh-superinput
|
|
3
|
+
* dsh-superinput 套件生成器:从 upstream/ 内的六个模块源机械搬迁模块代码。
|
|
4
4
|
*
|
|
5
5
|
* 服务端:**单文件内联**——宿主 interception loader 对 link 包内部的相对
|
|
6
|
-
*
|
|
6
|
+
* 导入没有先例(六个旧插件都是自包含单文件;套件首版因相对导入静默不被
|
|
7
7
|
* import)。六个模块各包进 IIFE(作用域隔离顶层 const),import 提升到
|
|
8
8
|
* 顶部去重,modules/*.js 仅作生成中间产物保留可读源。
|
|
9
9
|
* 客户端:client.js 单文件闭包(宿主不拆模块),各包 __ModuleLoader__
|
|
10
10
|
* 头尾改写为 `function createX(require)` 工厂按序拼入,套件 factory 里
|
|
11
11
|
* 逐模块 try/catch(一个模块失败不拖垮其他)。
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* 六个模块源原本是独立 git 仓(2026-11-07 收编进本仓 upstream/,GitHub
|
|
14
|
+
* 上游仓已归档);改上游源码直接改 upstream/<包名>/,重跑本脚本再生成。
|
|
15
|
+
* 重复执行幂等。
|
|
14
16
|
*/
|
|
15
17
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
16
18
|
import { dirname, join } from 'node:path'
|
|
17
19
|
import { fileURLToPath } from 'node:url'
|
|
18
20
|
|
|
19
21
|
const root = dirname(dirname(fileURLToPath(import.meta.url))) // packages/dsh-superinput
|
|
20
|
-
const pkgRoot = join(root, '
|
|
22
|
+
const pkgRoot = join(root, 'upstream')
|
|
21
23
|
|
|
22
24
|
/** key → [包目录, 客户端工厂名] */
|
|
23
25
|
const MODULES = [
|
|
@@ -26,6 +28,9 @@ const MODULES = [
|
|
|
26
28
|
['enhancer', 'dsh-prompt-enhancer', 'createEnhancerClient'],
|
|
27
29
|
['followup', 'dsh-followup', 'createFollowupClient'],
|
|
28
30
|
['voice', 'dsh-siri-voice', 'createVoiceClient'],
|
|
31
|
+
// 0.10.0 起并入套件(此前独立插件);apply 顺序在 enhancer 之后——
|
|
32
|
+
// pre-step 瀑布里压缩落档消息在增强之后追加,不会被二次改写。
|
|
33
|
+
['compaction', 'dsh-compaction-control', 'createCompactionClient'],
|
|
29
34
|
]
|
|
30
35
|
|
|
31
36
|
function transformServer(pkg, key) {
|
|
@@ -65,6 +70,8 @@ function inlineSource(src) {
|
|
|
65
70
|
.replace(/^export function apply\(/m, 'function apply(')
|
|
66
71
|
.replace(/^export \{ inject \}\s*$/m, '')
|
|
67
72
|
.replace(/^export default \{[^\n]*\}\s*$/m, '')
|
|
73
|
+
// compaction-control 尾部的离线自测导出(CI import 用,运行时无用)
|
|
74
|
+
.replace(/^export const __internals/m, 'const __internals')
|
|
68
75
|
return { imports, body: body.trim() }
|
|
69
76
|
}
|
|
70
77
|
|
|
@@ -151,6 +158,7 @@ const MODULES = [
|
|
|
151
158
|
['followup', followupModule],
|
|
152
159
|
['voice', voiceModule],
|
|
153
160
|
['prompts', promptsModule],
|
|
161
|
+
['compaction', compactionModule],
|
|
154
162
|
['hub', hubModule],
|
|
155
163
|
]
|
|
156
164
|
|
|
@@ -165,6 +173,7 @@ export const Config = Schema.object({
|
|
|
165
173
|
followup: followupModule.Config,
|
|
166
174
|
voice: voiceModule.Config,
|
|
167
175
|
prompts: promptsModule.Config,
|
|
176
|
+
compaction: compactionModule.Config,
|
|
168
177
|
hub: hubModule.Config,
|
|
169
178
|
})
|
|
170
179
|
|
|
@@ -263,6 +272,7 @@ window.__ModuleLoader__.load({
|
|
|
263
272
|
['followup', createFollowupClient],
|
|
264
273
|
['voice', createVoiceClient],
|
|
265
274
|
['prompts', createPromptsClient],
|
|
275
|
+
['compaction', createCompactionClient],
|
|
266
276
|
['hub', createHubClient],
|
|
267
277
|
]
|
|
268
278
|
// 模块异常上报:console 不落盘,失败必须走 prompts /diag 留痕。
|
package/scripts/smoke.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const backup = hadStore ? readFileSync(STORE) : null
|
|
|
18
18
|
let promptsHandler = null
|
|
19
19
|
let enhancerHandler = null
|
|
20
20
|
const webPaths = []
|
|
21
|
+
const eventHandlers = {}
|
|
21
22
|
const services = {
|
|
22
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 } },
|
|
23
24
|
tools: { register() {} },
|
|
@@ -37,7 +38,18 @@ const ctx = {
|
|
|
37
38
|
for (const d of deps) if (!(d in scope)) scope[d] = services[d] ?? {}
|
|
38
39
|
cb(scope)
|
|
39
40
|
},
|
|
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
|
+
},
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
const assert = (cond, label) => { if (!cond) throw new Error(label); console.log('ok:', label) }
|
|
@@ -58,10 +70,10 @@ let failed = false
|
|
|
58
70
|
try {
|
|
59
71
|
const config = {}
|
|
60
72
|
// enhancer 给显式 provider/model,让 configRoute 命中、不依赖 agentDefaultModel
|
|
61
|
-
for (const key of ['cost', 'picker', 'enhancer', 'followup', 'voice', 'prompts', 'hub']) config[key] = {}
|
|
73
|
+
for (const key of ['cost', 'picker', 'enhancer', 'followup', 'voice', 'prompts', 'compaction', 'hub']) config[key] = {}
|
|
62
74
|
config.enhancer = { provider: 'stub', model: 'stub-model' }
|
|
63
75
|
mod.apply(ctx, config)
|
|
64
|
-
for (const path of ['/api/dsh-cost', '/api/dsh-superinput/prompts', '/api/dsh-superinput/hub', '/api/prompt-enhancer']) {
|
|
76
|
+
for (const path of ['/api/dsh-cost', '/api/dsh-superinput/prompts', '/api/dsh-superinput/hub', '/api/prompt-enhancer', '/api/dsh-compaction-control']) {
|
|
65
77
|
assert(webPaths.includes(path), `module apply: ${path} registered`)
|
|
66
78
|
}
|
|
67
79
|
assert(!!promptsHandler, 'module apply: prompts handler captured')
|
|
@@ -107,10 +119,49 @@ try {
|
|
|
107
119
|
assert(list2.find((i) => i.id === idB).body !== 'stale-overwrite-should-skip', 'import: stale skipped')
|
|
108
120
|
assert((await prompts('POST', '/import', { items: 'not-an-array' })).status === 400, 'import: invalid payload rejected')
|
|
109
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
|
+
|
|
110
160
|
console.log('SMOKE ALL PASS')
|
|
111
161
|
} catch (error) {
|
|
112
162
|
failed = true
|
|
113
163
|
console.log('SMOKE FAIL:', error?.message ?? error)
|
|
164
|
+
if (error?.stack) console.log(error.stack.split('\n').slice(1, 6).join('\n'))
|
|
114
165
|
} finally {
|
|
115
166
|
try {
|
|
116
167
|
if (backup) writeFileSync(STORE, backup)
|