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.
@@ -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.7.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, seven input-area modules with per-module failure isolation.",
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",
@@ -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
- on() { return () => {} },
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)