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.
- package/CHANGELOG.md +405 -0
- package/README.md +49 -2
- package/client.js +7397 -0
- package/cordis.patch.yml +6 -0
- package/index.js +34 -0
- package/modules/compaction.js +413 -0
- package/modules/cost.js +1914 -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 +1709 -0
- package/modules/prompts.js +370 -0
- package/modules/voice.js +17 -0
- package/package.json +60 -4
- package/scripts/build-suite.mjs +325 -0
- package/scripts/smoke.mjs +172 -0
- package/server.js +4482 -0
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# dsh-superinput bundle patch: one host row over the Web or Desktop profile.
|
|
2
|
+
# The row name resolves from the profile's own node_modules where
|
|
3
|
+
# `dsh plugin --profile <profile> add <path>` installed this package.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: superinput
|
|
6
|
+
name: dsh-superinput
|
package/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* dsh-superinput 入口:零依赖探针(由 scripts/build-suite.mjs 生成;勿手改)。
|
|
2
|
+
* 静态 import 的失败发生在本文件第一条语句之前(宿主静默),所以核心代码
|
|
3
|
+
* 全部放在 server.js 由这里动态加载——成败都会留下取证记录。 */
|
|
4
|
+
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
const DIAG = join(homedir(), '.dsh', 'superinput-debug.log')
|
|
9
|
+
const trace = (line) => {
|
|
10
|
+
try {
|
|
11
|
+
mkdirSync(dirname(DIAG), { recursive: true })
|
|
12
|
+
appendFileSync(DIAG, `${new Date().toISOString()} ${line}\n`, 'utf8')
|
|
13
|
+
} catch { /* ignore */ }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
trace(`suite ENTRY pid=${process.pid}`)
|
|
17
|
+
|
|
18
|
+
let core
|
|
19
|
+
try {
|
|
20
|
+
core = await import('./server.js')
|
|
21
|
+
trace(`server.js LOADED pid=${process.pid}`)
|
|
22
|
+
} catch (error) {
|
|
23
|
+
trace(`server.js FAILED: ${String(error?.stack ?? error).slice(0, 600)}`)
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const name = core.name
|
|
28
|
+
export const inject = core.inject
|
|
29
|
+
export const Config = core.Config
|
|
30
|
+
export function apply(ctx, config) {
|
|
31
|
+
trace(`suite APPLY pid=${process.pid}`)
|
|
32
|
+
return core.apply(ctx, config)
|
|
33
|
+
}
|
|
34
|
+
export default core.default
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/* 由 scripts/build-suite.mjs 从 dsh-compaction-control/index.js 机械生成;勿手改,重跑脚本再生成。 */
|
|
2
|
+
/**
|
|
3
|
+
* dsh-compaction-control — 上下文压缩控制中心(DeepSeek Harness / dsh)
|
|
4
|
+
*
|
|
5
|
+
* 三件事:
|
|
6
|
+
* 1. 手动压缩:把官方 compaction 服务的 ctx.compaction.compactNow(agent, signal)
|
|
7
|
+
* 暴露成 HTTP 路由,客户端面板一键触发(等价 /compact,免打字)。
|
|
8
|
+
* 2. 调节自动压缩:把 {auto, thresholdRatio, retainRatio} 写进每个挂载了
|
|
9
|
+
* 本插件的 profile 的用户补丁层(~/.dsh/profiles/<name>/cordis.patch.yml,
|
|
10
|
+
* id 定向覆盖 compaction-basic 的 config;applyEntryPatches 后写者胜)。
|
|
11
|
+
* 补丁层是 initProfile 官方创建的用户文件,插件管理器不会回收它;
|
|
12
|
+
* 但 loader 只在启动时读取——保存后需重启 dsh 生效。
|
|
13
|
+
* 3. 状态源:~/.dsh/compaction-control.json 是唯一事实源(含 hideBuiltIn
|
|
14
|
+
* 等 UI 偏好),补丁文件由它派生,可随时重生成。
|
|
15
|
+
*
|
|
16
|
+
* 已核对的 dsh API(dsh-reference 源码对照):
|
|
17
|
+
* - ctx.agents.get(sessionId) → Agent(core/agent AgentRegistry,实现
|
|
18
|
+
* runMaintenance,满足 compaction 的 ManualCompactAgentContext)
|
|
19
|
+
* - ctx.compaction.compactNow(agent, signal) → CompactionResult |
|
|
20
|
+
* null;错误带 code: busy/cancelled/changed/summary/commit/persistence
|
|
21
|
+
* - BasicCompactionConfig 键集(compaction-basic/src/config.ts):
|
|
22
|
+
* auto/thresholdRatio/retainRatio/headroomTokens/maxTokens/
|
|
23
|
+
* summarizationProvider/summarizationModel/compactionRetries/
|
|
24
|
+
* maxOverflowRetries/modelPolicies —— 只写前三个,其余走默认
|
|
25
|
+
* - applyEntryPatches(vendor/include):{id, name, config} 直接整体
|
|
26
|
+
* 替换目标条目的 config;未知键会让 compaction-basic 加载即抛错,
|
|
27
|
+
* 所以这里的 config 永远是完整合法子集
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
31
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
32
|
+
import { createRequire } from 'node:module'
|
|
33
|
+
import { appendFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
|
34
|
+
import { homedir } from 'node:os'
|
|
35
|
+
import { dirname, join } from 'node:path'
|
|
36
|
+
|
|
37
|
+
const require = createRequire(import.meta.url)
|
|
38
|
+
|
|
39
|
+
const name = 'compaction-control'
|
|
40
|
+
|
|
41
|
+
const DSH_DIR = join(homedir(), '.dsh')
|
|
42
|
+
const PROFILES_DIR = join(DSH_DIR, 'profiles')
|
|
43
|
+
const STATE_FILE = join(DSH_DIR, 'compaction-control.json')
|
|
44
|
+
const DIAG_LOG = join(DSH_DIR, 'compaction-control-debug.log')
|
|
45
|
+
|
|
46
|
+
/** 与 compaction-basic 的默认对齐(thresholdRatio 0.8 / retainRatio 0.16 / auto)。 */
|
|
47
|
+
const DEFAULT_STATE = { auto: true, thresholdRatio: 0.8, retainRatio: 0.16, hideBuiltIn: true, checkpoint: true }
|
|
48
|
+
|
|
49
|
+
const MANAGED_BEGIN = '# BEGIN dsh-compaction-control (managed)'
|
|
50
|
+
const MANAGED_END = '# END dsh-compaction-control'
|
|
51
|
+
|
|
52
|
+
/** 补丁文件语法校验用;运行时没带 yaml 就跳过校验(写入逻辑本身保守)。 */
|
|
53
|
+
let Yaml = null
|
|
54
|
+
try {
|
|
55
|
+
const mod = require('yaml')
|
|
56
|
+
if (typeof mod?.parse === 'function') Yaml = mod
|
|
57
|
+
} catch { Yaml = null }
|
|
58
|
+
|
|
59
|
+
export const Config = Schema.object({
|
|
60
|
+
/** 关闭后客户端不再注册圆环与面板(压缩行为恢复 compaction-basic 默认)。 */
|
|
61
|
+
enabled: Schema.boolean().default(true).description('是否启用上下文压缩控制'),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
/** 校验并归一化一份状态;不合法直接抛中文错误给面板显示。 */
|
|
65
|
+
function sanitizeState(input) {
|
|
66
|
+
const ratio = (value, fallback, lo, hi, label) => {
|
|
67
|
+
const n = Number(value ?? fallback)
|
|
68
|
+
if (!Number.isFinite(n) || n < lo || n > hi) {
|
|
69
|
+
throw new Error(`${label} 需在 ${lo}~${hi} 之间`)
|
|
70
|
+
}
|
|
71
|
+
return Math.round(n * 1000) / 1000
|
|
72
|
+
}
|
|
73
|
+
const state = {
|
|
74
|
+
auto: input?.auto === undefined ? DEFAULT_STATE.auto : Boolean(input.auto),
|
|
75
|
+
thresholdRatio: ratio(input?.thresholdRatio, DEFAULT_STATE.thresholdRatio, 0.3, 0.99, '触发阈值'),
|
|
76
|
+
retainRatio: ratio(input?.retainRatio, DEFAULT_STATE.retainRatio, 0.02, 0.5, '保留比例'),
|
|
77
|
+
hideBuiltIn: input?.hideBuiltIn === undefined ? DEFAULT_STATE.hideBuiltIn : Boolean(input.hideBuiltIn),
|
|
78
|
+
checkpoint: input?.checkpoint === undefined ? DEFAULT_STATE.checkpoint : Boolean(input.checkpoint),
|
|
79
|
+
}
|
|
80
|
+
// compaction-basic 加载期硬约束:retainRatio >= thresholdRatio 直接抛错。
|
|
81
|
+
if (state.retainRatio >= state.thresholdRatio) {
|
|
82
|
+
throw new Error(`保留比例 (${state.retainRatio}) 必须小于触发阈值 (${state.thresholdRatio})`)
|
|
83
|
+
}
|
|
84
|
+
return state
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readState() {
|
|
88
|
+
try {
|
|
89
|
+
return sanitizeState(JSON.parse(await readFile(STATE_FILE, 'utf8')))
|
|
90
|
+
} catch {
|
|
91
|
+
return { ...DEFAULT_STATE }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 列出挂载了本插件的 profile 名(manifest 里出现包名即算)。 */
|
|
96
|
+
async function listMountedProfiles() {
|
|
97
|
+
let entries = []
|
|
98
|
+
try { entries = await readdir(PROFILES_DIR, { withFileTypes: true }) } catch { return [] }
|
|
99
|
+
const mounted = []
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!entry.isDirectory()) continue
|
|
102
|
+
try {
|
|
103
|
+
const raw = await readFile(join(PROFILES_DIR, entry.name, 'package.json'), 'utf8')
|
|
104
|
+
if (raw.includes('dsh-compaction-control')) mounted.push(entry.name)
|
|
105
|
+
} catch { /* 无 manifest 的目录不是 profile */ }
|
|
106
|
+
}
|
|
107
|
+
return mounted.sort()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function managedBlock(state) {
|
|
111
|
+
return [
|
|
112
|
+
MANAGED_BEGIN,
|
|
113
|
+
'- id: compaction-basic',
|
|
114
|
+
" name: '@deepseek-ai/dsh-compaction-basic'",
|
|
115
|
+
' config:',
|
|
116
|
+
` auto: ${state.auto}`,
|
|
117
|
+
` thresholdRatio: ${state.thresholdRatio}`,
|
|
118
|
+
` retainRatio: ${state.retainRatio}`,
|
|
119
|
+
MANAGED_END,
|
|
120
|
+
].join('\n')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 纯文本手术:由现有内容与目标状态算出下一版补丁文件内容(不落盘)。 */
|
|
124
|
+
function composePatch(raw, state) {
|
|
125
|
+
const begin = raw.indexOf(MANAGED_BEGIN)
|
|
126
|
+
const end = raw.indexOf(MANAGED_END)
|
|
127
|
+
if (begin !== -1 && end !== -1 && end > begin) {
|
|
128
|
+
return raw.slice(0, begin) + managedBlock(state) + raw.slice(end + MANAGED_END.length)
|
|
129
|
+
}
|
|
130
|
+
const stripped = raw
|
|
131
|
+
.split('\n')
|
|
132
|
+
.filter((line) => line.trim() !== '' && !line.trim().startsWith('#'))
|
|
133
|
+
.join('')
|
|
134
|
+
.trim()
|
|
135
|
+
if (stripped === '[]') {
|
|
136
|
+
return raw.replace('[]', managedBlock(state))
|
|
137
|
+
}
|
|
138
|
+
if (stripped === '') {
|
|
139
|
+
return `${managedBlock(state)}\n`
|
|
140
|
+
}
|
|
141
|
+
const base = raw.replace(/\s+$/, '')
|
|
142
|
+
return `${base}\n${managedBlock(state)}\n`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 移除受管块后的剩余内容;没有受管块时返回 null(无需写回)。 */
|
|
146
|
+
function stripManaged(raw) {
|
|
147
|
+
const begin = raw.indexOf(MANAGED_BEGIN)
|
|
148
|
+
const end = raw.indexOf(MANAGED_END)
|
|
149
|
+
if (begin === -1 || end === -1 || end < begin) return null
|
|
150
|
+
return raw.slice(0, begin) + raw.slice(end + MANAGED_END.length)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 官方 initProfile 的用户补丁层模板(移除后文件为空时回填)。 */
|
|
154
|
+
const PATCH_TEMPLATE = '# Your patch layer for this dsh profile, applied after every bundle layer:\n'
|
|
155
|
+
+ '# a top-level YAML array of loader patch entries (id-targeted config\n'
|
|
156
|
+
+ '# overrides, disables, and insert lists; `!!js` expressions allowed).\n'
|
|
157
|
+
+ '[]\n'
|
|
158
|
+
|
|
159
|
+
/** 恢复默认:从 profile 补丁层移除受管块,让 compaction-basic 回落自身默认。 */
|
|
160
|
+
async function clearProfilePatch(profileName, report) {
|
|
161
|
+
const file = join(PROFILES_DIR, profileName, 'cordis.patch.yml')
|
|
162
|
+
let raw = ''
|
|
163
|
+
try { raw = await readFile(file, 'utf8') } catch { return }
|
|
164
|
+
const strippedManaged = stripManaged(raw)
|
|
165
|
+
if (strippedManaged === null) return
|
|
166
|
+
const stripped = strippedManaged
|
|
167
|
+
.split('\n')
|
|
168
|
+
.filter((line) => line.trim() !== '' && !line.trim().startsWith('#'))
|
|
169
|
+
.join('')
|
|
170
|
+
.trim()
|
|
171
|
+
const next = stripped === ''
|
|
172
|
+
? PATCH_TEMPLATE
|
|
173
|
+
: `${strippedManaged.replace(/\s+$/, '')}\n`
|
|
174
|
+
if (Yaml) {
|
|
175
|
+
try {
|
|
176
|
+
const parsed = Yaml.parse(next)
|
|
177
|
+
if (!Array.isArray(parsed)) throw new Error('not an array')
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new Error(`${profileName}/cordis.patch.yml 语法校验失败,未写入:${String(error?.message ?? error)}`)
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
report(`yaml lib unavailable, rewrote ${profileName}/cordis.patch.yml without validation`)
|
|
183
|
+
}
|
|
184
|
+
await writeFile(file, next, 'utf8')
|
|
185
|
+
return file
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 把受管块写入一个 profile 的 cordis.patch.yml。
|
|
190
|
+
* 纯文本手术:只动 BEGIN/END 标记之间的内容,用户自己写的其他条目原样保留;
|
|
191
|
+
* 全新文件(官方模板是注释 + `[]`)用受管块替换 `[]`。写前用 yaml 库校验
|
|
192
|
+
* (如果运行时带了这个库),校验失败不落盘。
|
|
193
|
+
*/
|
|
194
|
+
async function syncProfilePatch(profileName, state, report) {
|
|
195
|
+
const file = join(PROFILES_DIR, profileName, 'cordis.patch.yml')
|
|
196
|
+
let raw = ''
|
|
197
|
+
try { raw = await readFile(file, 'utf8') } catch { raw = '' }
|
|
198
|
+
const next = composePatch(raw, state)
|
|
199
|
+
|
|
200
|
+
if (Yaml) {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = Yaml.parse(next)
|
|
203
|
+
if (!Array.isArray(parsed)) throw new Error('not an array')
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new Error(`${profileName}/cordis.patch.yml 语法校验失败,未写入:${String(error?.message ?? error)}`)
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
report(`yaml lib unavailable, wrote ${profileName}/cordis.patch.yml without validation`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await writeFile(file, next, 'utf8')
|
|
212
|
+
return file
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 把状态同步进所有挂载 profile;返回写入的 profile 名单。 */
|
|
216
|
+
async function syncAllProfiles(state, report) {
|
|
217
|
+
const profiles = await listMountedProfiles()
|
|
218
|
+
const written = []
|
|
219
|
+
for (const profileName of profiles) {
|
|
220
|
+
await syncProfilePatch(profileName, state, report)
|
|
221
|
+
written.push(profileName)
|
|
222
|
+
}
|
|
223
|
+
return written
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function apply(ctx, config) {
|
|
227
|
+
const say = (message) => {
|
|
228
|
+
try { console.info(`[dsh-compaction-control] ${message}`) } catch { /* ignore */ }
|
|
229
|
+
}
|
|
230
|
+
const report = async (line) => {
|
|
231
|
+
try {
|
|
232
|
+
await mkdir(dirname(DIAG_LOG), { recursive: true })
|
|
233
|
+
await appendFile(DIAG_LOG, `${new Date().toISOString()} ${line}\n`, 'utf8')
|
|
234
|
+
} catch { /* ignore */ }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
say(`server boot, enabled=${config.enabled !== false}`)
|
|
238
|
+
void report(`server boot enabled=${config.enabled !== false}`)
|
|
239
|
+
|
|
240
|
+
// 压缩前自动落档:客户端圆环在占用率上穿「阈值-5%」时布防,本进程在
|
|
241
|
+
// 该会话下一次模型调用前往消息尾追加落档指令——agent 用自己的文件工具
|
|
242
|
+
// 把项目状态写进工作区 checkpoint.md,随后 compaction-basic 照常压缩。
|
|
243
|
+
// 只注入一次(注入即解除布防;占用回落后客户端重新布防)。
|
|
244
|
+
const armedSessions = new Set()
|
|
245
|
+
const CHECKPOINT_MARKER = 'dsh-auto-checkpoint'
|
|
246
|
+
const CHECKPOINT_TEXT = [
|
|
247
|
+
'【自动落档】上下文即将压缩。请先把当前任务状态写入工作区文件 `checkpoint.md`(覆盖旧内容):',
|
|
248
|
+
'任务目标、已完成事项、待办事项、关键决定与约束、涉及的重要文件路径。',
|
|
249
|
+
'写完落档后,再回答上一条消息的问题。(dsh-auto-checkpoint)',
|
|
250
|
+
].join('\n')
|
|
251
|
+
|
|
252
|
+
ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
|
|
253
|
+
const decision = await next()
|
|
254
|
+
try {
|
|
255
|
+
if (!decision || decision.kind !== 'enter' || !Array.isArray(decision.messages)) return decision
|
|
256
|
+
const sessionId = String(agent?.sessionId ?? '')
|
|
257
|
+
if (!sessionId || !armedSessions.has(sessionId)) return decision
|
|
258
|
+
const state = await readState()
|
|
259
|
+
if (state.checkpoint === false) { armedSessions.delete(sessionId); return decision }
|
|
260
|
+
const tail = decision.messages.slice(-3).map((m) => JSON.stringify(m?.content ?? '')).join('')
|
|
261
|
+
armedSessions.delete(sessionId)
|
|
262
|
+
if (tail.includes(CHECKPOINT_MARKER)) return decision
|
|
263
|
+
const message = createUserMessage({ content: [{ type: 'text', text: CHECKPOINT_TEXT }] })
|
|
264
|
+
await report(`checkpoint injected session=${sessionId} msgs=${decision.messages.length}`)
|
|
265
|
+
return { ...decision, messages: [...decision.messages, message] }
|
|
266
|
+
} catch (error) {
|
|
267
|
+
await report(`checkpoint pre-step failed: ${String(error?.message ?? error).slice(0, 200)}`)
|
|
268
|
+
return decision
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// compaction/agents 属于可选依赖:base bundle 没带它们时路由仍注册,
|
|
273
|
+
// /compact 返回 503,面板其余功能(状态展示、参数保存)不受影响。
|
|
274
|
+
let runtime = null
|
|
275
|
+
|
|
276
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
277
|
+
webCtx.effect(() => webCtx.webServer.register({
|
|
278
|
+
kind: 'prefix',
|
|
279
|
+
path: '/api/dsh-compaction-control',
|
|
280
|
+
handler: async (req, res) => {
|
|
281
|
+
const send = (status, body) => {
|
|
282
|
+
res.statusCode = status
|
|
283
|
+
res.setHeader('content-type', 'application/json')
|
|
284
|
+
res.end(JSON.stringify(body))
|
|
285
|
+
}
|
|
286
|
+
const readBody = async () => {
|
|
287
|
+
let raw = ''
|
|
288
|
+
for await (const chunk of req) raw += chunk
|
|
289
|
+
try { return raw ? JSON.parse(raw) : {} } catch { return null }
|
|
290
|
+
}
|
|
291
|
+
const path = (req.url ?? '').split('?')[0].replace(/^\/api\/dsh-compaction-control/, '')
|
|
292
|
+
|
|
293
|
+
if (path === '/enabled' && req.method === 'GET') {
|
|
294
|
+
send(200, { enabled: config.enabled !== false, name })
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (path === '/diag' && req.method === 'POST') {
|
|
299
|
+
await report(String((await readBody())?.line ?? '').slice(0, 2000))
|
|
300
|
+
send(200, { ok: true })
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 压缩前落档布防:客户端占用率上穿阈值-5% 时 arm=true(升沿只发一次),
|
|
305
|
+
// 占用回落后 arm=false。服务端只记标记,注入发生在下一次 pre-step。
|
|
306
|
+
if (path === '/checkpoint-arm' && req.method === 'POST') {
|
|
307
|
+
const body = await readBody()
|
|
308
|
+
const sessionId = String(body?.sessionId ?? '')
|
|
309
|
+
if (!sessionId) { send(400, { error: '缺少 sessionId' }); return }
|
|
310
|
+
const arm = body?.arm === true
|
|
311
|
+
const state = await readState()
|
|
312
|
+
if (arm && state.checkpoint === false) { send(200, { ok: true, armed: false, reason: 'checkpoint-disabled' }); return }
|
|
313
|
+
if (arm) armedSessions.add(sessionId)
|
|
314
|
+
else armedSessions.delete(sessionId)
|
|
315
|
+
await report(`checkpoint ${arm ? 'armed' : 'disarmed'} session=${sessionId}`)
|
|
316
|
+
send(200, { ok: true, armed })
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (path === '/state' && req.method === 'GET') {
|
|
321
|
+
try {
|
|
322
|
+
const [state, profiles] = await Promise.all([readState(), listMountedProfiles()])
|
|
323
|
+
send(200, { ok: true, state, defaults: DEFAULT_STATE, profiles, version: '0.1.0' })
|
|
324
|
+
} catch (error) {
|
|
325
|
+
send(500, { error: String(error?.message ?? error) })
|
|
326
|
+
}
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (path === '/state' && req.method === 'POST') {
|
|
331
|
+
const body = await readBody()
|
|
332
|
+
if (body === null) { send(400, { error: '请求体不是合法 JSON' }); return }
|
|
333
|
+
try {
|
|
334
|
+
const state = sanitizeState(body)
|
|
335
|
+
await mkdir(DSH_DIR, { recursive: true })
|
|
336
|
+
await writeFile(STATE_FILE, `${JSON.stringify(state, null, 2)}\n`, 'utf8')
|
|
337
|
+
const profiles = await syncAllProfiles(state, report)
|
|
338
|
+
await report(`state saved ${JSON.stringify(state)} profiles=${profiles.join(',') || 'none'}`)
|
|
339
|
+
send(200, { ok: true, state, profiles, restartRequired: true })
|
|
340
|
+
} catch (error) {
|
|
341
|
+
send(400, { error: String(error?.message ?? error) })
|
|
342
|
+
}
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (path === '/reset' && req.method === 'POST') {
|
|
347
|
+
try {
|
|
348
|
+
const state = { ...DEFAULT_STATE }
|
|
349
|
+
await mkdir(DSH_DIR, { recursive: true })
|
|
350
|
+
await writeFile(STATE_FILE, `${JSON.stringify(state, null, 2)}\n`, 'utf8')
|
|
351
|
+
const profiles = await listMountedProfiles()
|
|
352
|
+
for (const profileName of profiles) await clearProfilePatch(profileName, report)
|
|
353
|
+
await report(`state reset to defaults, managed block removed from: ${profiles.join(',') || 'none'}`)
|
|
354
|
+
send(200, { ok: true, state, profiles, restartRequired: true })
|
|
355
|
+
} catch (error) {
|
|
356
|
+
send(400, { error: String(error?.message ?? error) })
|
|
357
|
+
}
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (path === '/compact' && req.method === 'POST') {
|
|
362
|
+
if (!runtime) { send(503, { error: '压缩服务未就绪(本 profile 未挂载 compaction/agents)' }); return }
|
|
363
|
+
const body = await readBody()
|
|
364
|
+
const sessionId = body?.sessionId
|
|
365
|
+
if (!sessionId) { send(400, { error: '缺少 sessionId' }); return }
|
|
366
|
+
const agent = runtime.agents.get(sessionId)
|
|
367
|
+
if (!agent) { send(409, { error: '未找到该会话的 agent' }); return }
|
|
368
|
+
const controller = new AbortController()
|
|
369
|
+
try {
|
|
370
|
+
const result = await runtime.compaction.compactNow(agent, controller.signal)
|
|
371
|
+
if (result === null) {
|
|
372
|
+
send(200, { ok: true, empty: true, message: '暂无可压缩的历史' })
|
|
373
|
+
} else {
|
|
374
|
+
await report(`manual compact ok: ${result.shadowedSeqs.length} items ~${result.shadowedTokenCount} tokens`)
|
|
375
|
+
send(200, {
|
|
376
|
+
ok: true,
|
|
377
|
+
shadowed: result.shadowedSeqs.length,
|
|
378
|
+
tokens: result.shadowedTokenCount,
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
const code = typeof error?.code === 'string' ? error.code.toLowerCase() : ''
|
|
383
|
+
const messages = {
|
|
384
|
+
busy: '会话正忙(运行中或已有压缩任务),请在空闲时重试',
|
|
385
|
+
cancelled: '压缩已取消',
|
|
386
|
+
changed: '待压缩历史在提交前发生了变化,本次已放弃并记录',
|
|
387
|
+
summary: '未能生成有效摘要,本次已记录',
|
|
388
|
+
commit: '压缩未干净完成,请检查会话状态后再试',
|
|
389
|
+
persistence: '压缩完成,但会话保存失败',
|
|
390
|
+
}
|
|
391
|
+
await report(`manual compact failed: code=${code || 'unknown'} ${String(error?.message ?? error).slice(0, 300)}`)
|
|
392
|
+
if (messages[code]) { send(409, { error: messages[code], code }); return }
|
|
393
|
+
send(500, { error: String(error?.message ?? error) })
|
|
394
|
+
}
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
send(404, { error: 'not-found' })
|
|
399
|
+
},
|
|
400
|
+
}), 'dsh-compaction-control: /api/dsh-compaction-control/*')
|
|
401
|
+
|
|
402
|
+
webCtx.inject(['compaction', 'agents'], (deep) => {
|
|
403
|
+
runtime = { compaction: deep.compaction, agents: deep.agents }
|
|
404
|
+
say('compaction + agents ready')
|
|
405
|
+
void report('runtime ready (compaction + agents)')
|
|
406
|
+
return () => { runtime = null }
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
/** 仅供离线自测(scripts/ 与 CI 可直接 import),运行时不使用。 */
|
|
413
|
+
export const __internals = { sanitizeState, managedBlock, composePatch, stripManaged, PATCH_TEMPLATE, DEFAULT_STATE }
|