dsh-auto-compaction 0.2.1 → 0.2.3
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/cordis.patch.yml +479 -0
- package/lib/client.js +190 -15
- package/lib/index.js +86 -17
- package/lib/takeover-mount.js +306 -0
- package/package.json +2 -2
package/cordis.patch.yml
CHANGED
|
@@ -8,3 +8,482 @@
|
|
|
8
8
|
- insert:
|
|
9
9
|
- id: dsh-auto-compaction
|
|
10
10
|
name: dsh-auto-compaction
|
|
11
|
+
# >>> handoff:takeover:begin (dsh-auto-compaction 按当前预设组合生成,勿手改本块)
|
|
12
|
+
- id: preset-cordis
|
|
13
|
+
config:
|
|
14
|
+
id: cordis
|
|
15
|
+
order: 4
|
|
16
|
+
plugins:
|
|
17
|
+
- id: persona
|
|
18
|
+
name: '@deepseek-ai/dsh-persona'
|
|
19
|
+
config:
|
|
20
|
+
prefix: You are a coding agent powered by the {{model}} model.
|
|
21
|
+
- id: agent-instructions
|
|
22
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
23
|
+
config:
|
|
24
|
+
maxBytes: 65536
|
|
25
|
+
- id: time-context
|
|
26
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
27
|
+
- id: tool-bash
|
|
28
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
29
|
+
disabled: !!js process.platform === 'win32'
|
|
30
|
+
- id: tool-pwsh
|
|
31
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
32
|
+
disabled: !!js process.platform !== 'win32'
|
|
33
|
+
- id: tool-fs
|
|
34
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
35
|
+
- id: tool-fs-search
|
|
36
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
37
|
+
config:
|
|
38
|
+
sampleOverCapGlobResults: false
|
|
39
|
+
- id: tool-jobs
|
|
40
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
41
|
+
- id: tool-schedule
|
|
42
|
+
name: '@deepseek-ai/dsh-tool-schedule'
|
|
43
|
+
- id: command-goal
|
|
44
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
45
|
+
- id: tool-goal
|
|
46
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
47
|
+
- id: planning
|
|
48
|
+
name: cordis:group
|
|
49
|
+
group: true
|
|
50
|
+
isolate:
|
|
51
|
+
planMode: true
|
|
52
|
+
config:
|
|
53
|
+
- id: plan-mode
|
|
54
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
55
|
+
config:
|
|
56
|
+
section: |
|
|
57
|
+
You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
58
|
+
|
|
59
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
60
|
+
|
|
61
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
62
|
+
|
|
63
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
64
|
+
|
|
65
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
66
|
+
|
|
67
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
68
|
+
- id: compaction
|
|
69
|
+
name: cordis:group
|
|
70
|
+
group: true
|
|
71
|
+
isolate:
|
|
72
|
+
compaction: true
|
|
73
|
+
toolResultPruner: true
|
|
74
|
+
config:
|
|
75
|
+
- id: compaction-basic
|
|
76
|
+
name: dsh-auto-compaction-engine
|
|
77
|
+
config:
|
|
78
|
+
thresholdRatio: 0.9
|
|
79
|
+
headroomTokens: 10000
|
|
80
|
+
maxTokens: 12288
|
|
81
|
+
idleDelayMs: 1500
|
|
82
|
+
presetId: cordis
|
|
83
|
+
- id: command-compact
|
|
84
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
85
|
+
- id: tool-result-pruner
|
|
86
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
87
|
+
config:
|
|
88
|
+
thresholdChars: 8192
|
|
89
|
+
headChars: 4096
|
|
90
|
+
tailChars: 1024
|
|
91
|
+
- id: delegation
|
|
92
|
+
name: cordis:group
|
|
93
|
+
group: true
|
|
94
|
+
isolate:
|
|
95
|
+
workflowEngine: true
|
|
96
|
+
config:
|
|
97
|
+
- id: tool-subagent-control
|
|
98
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
99
|
+
- id: tool-subagent-list-agents
|
|
100
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
101
|
+
- id: tool-subagent
|
|
102
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
103
|
+
config:
|
|
104
|
+
provider: spawn
|
|
105
|
+
toolName: subagent
|
|
106
|
+
modelSelectionSettings: true
|
|
107
|
+
- id: tool-subagent-fork
|
|
108
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
109
|
+
config:
|
|
110
|
+
provider: fork
|
|
111
|
+
toolName: subagent_fork
|
|
112
|
+
- id: workflow-ptc
|
|
113
|
+
name: '@deepseek-ai/dsh-workflow-ptc'
|
|
114
|
+
config:
|
|
115
|
+
provider: spawn
|
|
116
|
+
- id: tool-workflow
|
|
117
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
118
|
+
- id: tool-ask-user
|
|
119
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
120
|
+
- id: tool-todo
|
|
121
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
122
|
+
config:
|
|
123
|
+
allowParallelInProgress: true
|
|
124
|
+
- id: tool-web
|
|
125
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
126
|
+
config:
|
|
127
|
+
fetch: true
|
|
128
|
+
searchTimeoutMs: 60000
|
|
129
|
+
- id: tool-cordis
|
|
130
|
+
name: '@deepseek-ai/dsh-tool-cordis'
|
|
131
|
+
- id: skill-filesystem
|
|
132
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
133
|
+
config:
|
|
134
|
+
customSkillDirs:
|
|
135
|
+
- !!js process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:path').dirname(process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('@deepseek-ai/dsh-agent-preset/package.json')), 'skills')
|
|
136
|
+
- id: tool-skill
|
|
137
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
138
|
+
- id: present
|
|
139
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
140
|
+
- id: tool-plugin-manager
|
|
141
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
142
|
+
disabled: !!js '!ctx.get(''profileContext'')'
|
|
143
|
+
- id: tool-plugin-market
|
|
144
|
+
name: '@deepseek-ai/dsh-plugin-market/tools'
|
|
145
|
+
disabled: !!js '!ctx.get(''profileContext'')'
|
|
146
|
+
- id: preset-minimal
|
|
147
|
+
config:
|
|
148
|
+
id: minimal
|
|
149
|
+
order: 3
|
|
150
|
+
plugins:
|
|
151
|
+
- id: persona
|
|
152
|
+
name: '@deepseek-ai/dsh-persona'
|
|
153
|
+
config:
|
|
154
|
+
prefix: You are a helpful software engineer assistant.
|
|
155
|
+
complete: true
|
|
156
|
+
includeRuntimeContext: false
|
|
157
|
+
- id: tool-fs
|
|
158
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
159
|
+
config:
|
|
160
|
+
read: false
|
|
161
|
+
readImage: true
|
|
162
|
+
write: false
|
|
163
|
+
edit: false
|
|
164
|
+
- id: compaction
|
|
165
|
+
name: cordis:group
|
|
166
|
+
group: true
|
|
167
|
+
isolate:
|
|
168
|
+
compaction: true
|
|
169
|
+
config:
|
|
170
|
+
- id: compaction-basic
|
|
171
|
+
name: dsh-auto-compaction-engine
|
|
172
|
+
config:
|
|
173
|
+
thresholdRatio: 0.9
|
|
174
|
+
headroomTokens: 10000
|
|
175
|
+
maxTokens: 12288
|
|
176
|
+
idleDelayMs: 1500
|
|
177
|
+
presetId: minimal
|
|
178
|
+
- id: command-compact
|
|
179
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
180
|
+
- id: persistent-shell
|
|
181
|
+
name: cordis:group
|
|
182
|
+
group: true
|
|
183
|
+
isolate:
|
|
184
|
+
terminals: true
|
|
185
|
+
config:
|
|
186
|
+
- id: pty
|
|
187
|
+
name: '@deepseek-ai/dsh-terminal'
|
|
188
|
+
- id: terminal-bash
|
|
189
|
+
name: '@deepseek-ai/dsh-terminal-bash'
|
|
190
|
+
disabled: !!js process.platform === 'win32'
|
|
191
|
+
config:
|
|
192
|
+
timeoutMs: 300000
|
|
193
|
+
- id: persistent-bash
|
|
194
|
+
name: '@deepseek-ai/dsh-tool-bash-persistent'
|
|
195
|
+
disabled: !!js process.platform === 'win32'
|
|
196
|
+
config:
|
|
197
|
+
timeoutMs: 300000
|
|
198
|
+
description: |-
|
|
199
|
+
Run commands in a bash shell
|
|
200
|
+
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
|
|
201
|
+
* Network access depends on the task environment. Prefer configured mirrors/proxies when they are available.
|
|
202
|
+
* State is persistent across command calls and discussions with the user.
|
|
203
|
+
* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
|
|
204
|
+
* Please avoid commands that may produce a very large amount of output.
|
|
205
|
+
* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
|
|
206
|
+
- id: terminal-pwsh
|
|
207
|
+
name: '@deepseek-ai/dsh-terminal-bash'
|
|
208
|
+
disabled: !!js process.platform !== 'win32'
|
|
209
|
+
config:
|
|
210
|
+
shellDialect: pwsh
|
|
211
|
+
timeoutMs: 300000
|
|
212
|
+
- id: persistent-pwsh
|
|
213
|
+
name: '@deepseek-ai/dsh-tool-pwsh-persistent'
|
|
214
|
+
disabled: !!js process.platform !== 'win32'
|
|
215
|
+
config:
|
|
216
|
+
timeoutMs: 300000
|
|
217
|
+
description: |-
|
|
218
|
+
Run commands in a PowerShell shell
|
|
219
|
+
* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
|
|
220
|
+
* You don't have access to the internet via this tool.
|
|
221
|
+
* State is persistent across command calls and discussions with the user.
|
|
222
|
+
* Use native Windows paths (C:\...) and $env:NAME variables; this is PowerShell, not bash.
|
|
223
|
+
* Please avoid commands that may produce a very large amount of output.
|
|
224
|
+
* Please run long lived commands in the background, e.g. 'Start-Job' or start a server with Start-Process.
|
|
225
|
+
- id: preset-ptc
|
|
226
|
+
config:
|
|
227
|
+
id: ptc
|
|
228
|
+
order: 2
|
|
229
|
+
plugins:
|
|
230
|
+
- id: persona
|
|
231
|
+
name: '@deepseek-ai/dsh-persona'
|
|
232
|
+
config:
|
|
233
|
+
prefix: You are a coding agent powered by the {{model}} model.
|
|
234
|
+
- id: agent-instructions
|
|
235
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
236
|
+
config:
|
|
237
|
+
maxBytes: 65536
|
|
238
|
+
- id: time-context
|
|
239
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
240
|
+
- id: tool-bash
|
|
241
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
242
|
+
disabled: !!js process.platform === 'win32'
|
|
243
|
+
- id: tool-pwsh
|
|
244
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
245
|
+
disabled: !!js process.platform !== 'win32'
|
|
246
|
+
- id: tool-fs
|
|
247
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
248
|
+
- id: tool-fs-search
|
|
249
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
250
|
+
config:
|
|
251
|
+
sampleOverCapGlobResults: false
|
|
252
|
+
- id: tool-jobs
|
|
253
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
254
|
+
- id: tool-schedule
|
|
255
|
+
name: '@deepseek-ai/dsh-tool-schedule'
|
|
256
|
+
- id: skill-filesystem
|
|
257
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
258
|
+
- id: tool-skill
|
|
259
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
260
|
+
- id: command-goal
|
|
261
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
262
|
+
- id: tool-goal
|
|
263
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
264
|
+
- id: planning
|
|
265
|
+
name: cordis:group
|
|
266
|
+
group: true
|
|
267
|
+
isolate:
|
|
268
|
+
planMode: true
|
|
269
|
+
config:
|
|
270
|
+
- id: plan-mode
|
|
271
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
272
|
+
config:
|
|
273
|
+
section: |
|
|
274
|
+
You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
275
|
+
|
|
276
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
277
|
+
|
|
278
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
279
|
+
|
|
280
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
281
|
+
|
|
282
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
283
|
+
|
|
284
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
285
|
+
- id: compaction
|
|
286
|
+
name: cordis:group
|
|
287
|
+
group: true
|
|
288
|
+
isolate:
|
|
289
|
+
compaction: true
|
|
290
|
+
toolResultPruner: true
|
|
291
|
+
config:
|
|
292
|
+
- id: compaction-basic
|
|
293
|
+
name: dsh-auto-compaction-engine
|
|
294
|
+
config:
|
|
295
|
+
thresholdRatio: 0.9
|
|
296
|
+
headroomTokens: 10000
|
|
297
|
+
maxTokens: 12288
|
|
298
|
+
idleDelayMs: 1500
|
|
299
|
+
presetId: ptc
|
|
300
|
+
- id: command-compact
|
|
301
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
302
|
+
- id: tool-result-pruner
|
|
303
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
304
|
+
config:
|
|
305
|
+
thresholdChars: 8192
|
|
306
|
+
headChars: 4096
|
|
307
|
+
tailChars: 1024
|
|
308
|
+
- id: delegation
|
|
309
|
+
name: cordis:group
|
|
310
|
+
group: true
|
|
311
|
+
isolate:
|
|
312
|
+
workflowEngine: true
|
|
313
|
+
config:
|
|
314
|
+
- id: tool-subagent-control
|
|
315
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
316
|
+
- id: tool-subagent-list-agents
|
|
317
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
318
|
+
- id: tool-subagent
|
|
319
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
320
|
+
config:
|
|
321
|
+
provider: spawn
|
|
322
|
+
toolName: subagent
|
|
323
|
+
modelSelectionSettings: true
|
|
324
|
+
- id: tool-subagent-fork
|
|
325
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
326
|
+
config:
|
|
327
|
+
provider: fork
|
|
328
|
+
toolName: subagent_fork
|
|
329
|
+
- id: workflow-ptc
|
|
330
|
+
name: '@deepseek-ai/dsh-workflow-ptc'
|
|
331
|
+
disabled: true
|
|
332
|
+
config:
|
|
333
|
+
provider: spawn
|
|
334
|
+
- id: tool-workflow
|
|
335
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
336
|
+
disabled: true
|
|
337
|
+
- id: tool-ask-user
|
|
338
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
339
|
+
- id: tool-todo
|
|
340
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
341
|
+
config:
|
|
342
|
+
allowParallelInProgress: true
|
|
343
|
+
- id: tool-web
|
|
344
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
345
|
+
config:
|
|
346
|
+
fetch: true
|
|
347
|
+
searchTimeoutMs: 60000
|
|
348
|
+
- id: tool-presentation
|
|
349
|
+
name: '@deepseek-ai/dsh-agent-tool-presentation'
|
|
350
|
+
config:
|
|
351
|
+
mode: ptc
|
|
352
|
+
- id: present
|
|
353
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
354
|
+
- id: tool-plugin-manager
|
|
355
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
356
|
+
disabled: true
|
|
357
|
+
- id: tool-plugin-market
|
|
358
|
+
name: '@deepseek-ai/dsh-plugin-market/tools'
|
|
359
|
+
disabled: true
|
|
360
|
+
- id: preset-standard
|
|
361
|
+
config:
|
|
362
|
+
id: standard
|
|
363
|
+
order: 1
|
|
364
|
+
plugins:
|
|
365
|
+
- id: persona
|
|
366
|
+
name: '@deepseek-ai/dsh-persona'
|
|
367
|
+
config:
|
|
368
|
+
prefix: You are a coding agent powered by the {{model}} model.
|
|
369
|
+
- id: agent-instructions
|
|
370
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
371
|
+
config:
|
|
372
|
+
maxBytes: 65536
|
|
373
|
+
- id: time-context
|
|
374
|
+
name: '@deepseek-ai/dsh-time-context'
|
|
375
|
+
- id: tool-bash
|
|
376
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
377
|
+
disabled: !!js process.platform === 'win32'
|
|
378
|
+
- id: tool-pwsh
|
|
379
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
380
|
+
disabled: !!js process.platform !== 'win32'
|
|
381
|
+
- id: tool-fs
|
|
382
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
383
|
+
- id: tool-fs-search
|
|
384
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
385
|
+
config:
|
|
386
|
+
sampleOverCapGlobResults: false
|
|
387
|
+
- id: tool-jobs
|
|
388
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
389
|
+
- id: tool-schedule
|
|
390
|
+
name: '@deepseek-ai/dsh-tool-schedule'
|
|
391
|
+
- id: skill-filesystem
|
|
392
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
393
|
+
- id: tool-skill
|
|
394
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
395
|
+
- id: command-goal
|
|
396
|
+
name: '@deepseek-ai/dsh-command-goal'
|
|
397
|
+
- id: tool-goal
|
|
398
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
399
|
+
- id: planning
|
|
400
|
+
name: cordis:group
|
|
401
|
+
group: true
|
|
402
|
+
isolate:
|
|
403
|
+
planMode: true
|
|
404
|
+
config:
|
|
405
|
+
- id: plan-mode
|
|
406
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
407
|
+
config:
|
|
408
|
+
section: |
|
|
409
|
+
You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
410
|
+
|
|
411
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
412
|
+
|
|
413
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
414
|
+
|
|
415
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
416
|
+
|
|
417
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
418
|
+
|
|
419
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
420
|
+
- id: compaction
|
|
421
|
+
name: cordis:group
|
|
422
|
+
group: true
|
|
423
|
+
isolate:
|
|
424
|
+
compaction: true
|
|
425
|
+
toolResultPruner: true
|
|
426
|
+
config:
|
|
427
|
+
- id: compaction-basic
|
|
428
|
+
name: dsh-auto-compaction-engine
|
|
429
|
+
config:
|
|
430
|
+
thresholdRatio: 0.9
|
|
431
|
+
headroomTokens: 10000
|
|
432
|
+
maxTokens: 12288
|
|
433
|
+
idleDelayMs: 1500
|
|
434
|
+
presetId: standard
|
|
435
|
+
- id: command-compact
|
|
436
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
437
|
+
- id: tool-result-pruner
|
|
438
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
439
|
+
config:
|
|
440
|
+
thresholdChars: 8192
|
|
441
|
+
headChars: 4096
|
|
442
|
+
tailChars: 1024
|
|
443
|
+
- id: delegation
|
|
444
|
+
name: cordis:group
|
|
445
|
+
group: true
|
|
446
|
+
isolate:
|
|
447
|
+
workflowEngine: true
|
|
448
|
+
config:
|
|
449
|
+
- id: tool-subagent-control
|
|
450
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
451
|
+
- id: tool-subagent-list-agents
|
|
452
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
453
|
+
- id: tool-subagent
|
|
454
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
455
|
+
config:
|
|
456
|
+
provider: spawn
|
|
457
|
+
toolName: subagent
|
|
458
|
+
modelSelectionSettings: true
|
|
459
|
+
- id: tool-subagent-fork
|
|
460
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
461
|
+
config:
|
|
462
|
+
provider: fork
|
|
463
|
+
toolName: subagent_fork
|
|
464
|
+
- id: workflow-ptc
|
|
465
|
+
name: '@deepseek-ai/dsh-workflow-ptc'
|
|
466
|
+
config:
|
|
467
|
+
provider: spawn
|
|
468
|
+
- id: tool-workflow
|
|
469
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
470
|
+
- id: tool-ask-user
|
|
471
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
472
|
+
- id: tool-todo
|
|
473
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
474
|
+
config:
|
|
475
|
+
allowParallelInProgress: true
|
|
476
|
+
- id: tool-web
|
|
477
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
478
|
+
config:
|
|
479
|
+
fetch: true
|
|
480
|
+
searchTimeoutMs: 60000
|
|
481
|
+
- id: present
|
|
482
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
483
|
+
- id: tool-plugin-manager
|
|
484
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
485
|
+
disabled: true
|
|
486
|
+
- id: tool-plugin-market
|
|
487
|
+
name: '@deepseek-ai/dsh-plugin-market/tools'
|
|
488
|
+
disabled: true
|
|
489
|
+
# <<< handoff:takeover:end
|
package/lib/client.js
CHANGED
|
@@ -32,10 +32,21 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
var DEFAULT_TEMPLATE = "~/.dsh-handoff-compaction/template.md";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
/* Preset names come from DSH's own registry through the `scope` RPC, never
|
|
36
|
+
* from a list baked in here: a release that renames or adds a preset must
|
|
37
|
+
* not leave this page showing names that no longer exist. */
|
|
38
|
+
var presetLabels = {};
|
|
39
|
+
function rememberPresetLabels(list) {
|
|
40
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
41
|
+
var entry = list[i];
|
|
42
|
+
if (entry && typeof entry.id === "string") {
|
|
43
|
+
presetLabels[entry.id] = typeof entry.label === "string" && entry.label.length > 0 ? entry.label : entry.id;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function presetLabel(id) {
|
|
48
|
+
return presetLabels[String(id)] || String(id || "未命名预设");
|
|
49
|
+
}
|
|
39
50
|
var DISPLAY_OPTIONS = [
|
|
40
51
|
{ id: "orbit", label: "轨道环" },
|
|
41
52
|
{ id: "strip", label: "状态条" },
|
|
@@ -480,7 +491,7 @@ window.__ModuleLoader__.load({
|
|
|
480
491
|
indicator,
|
|
481
492
|
h("div", { style: { fontSize: 12.5, fontWeight: 600, color: STATUS_TEXT } }, title),
|
|
482
493
|
h("div", { style: { fontSize: 11, color: STATUS_MUTED } }, cardSub),
|
|
483
|
-
h("div", { style: { fontSize: 10.5, color: STATUS_MUTED, opacity: 0.85 } }, "
|
|
494
|
+
h("div", { style: { fontSize: 10.5, color: STATUS_MUTED, opacity: 0.85 } }, "完成后把最旧的这段历史收成一条交接文档")
|
|
484
495
|
)
|
|
485
496
|
),
|
|
486
497
|
document.body
|
|
@@ -687,8 +698,31 @@ window.__ModuleLoader__.load({
|
|
|
687
698
|
var draft = draftState[0], setDraft = draftState[1];
|
|
688
699
|
var statusState = React.useState({ loading: true, error: "", saving: false, saved: false });
|
|
689
700
|
var status = statusState[0], setStatus = statusState[1];
|
|
701
|
+
var scopeState = React.useState({ loading: true, error: "", value: null });
|
|
702
|
+
var scope = scopeState[0], setScope = scopeState[1];
|
|
703
|
+
var tokenState = React.useState(0);
|
|
704
|
+
var token = tokenState[0], setToken = tokenState[1];
|
|
690
705
|
ensureStatusStyles();
|
|
691
706
|
|
|
707
|
+
// Which presets exist and which of them this plugin's patch reaches is
|
|
708
|
+
// DSH's answer, not ours, so it is read fresh each time the section opens.
|
|
709
|
+
React.useEffect(function () {
|
|
710
|
+
if (!connection) {
|
|
711
|
+
setScope({ loading: false, error: "连接服务不可用", value: null });
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
var alive = true;
|
|
715
|
+
setScope(function (previous) { return { loading: true, error: "", value: previous.value }; });
|
|
716
|
+
call(connection, "scope").then(function (value) {
|
|
717
|
+
if (!alive) return;
|
|
718
|
+
rememberPresetLabels(value && Array.isArray(value.presets) ? value.presets : []);
|
|
719
|
+
setScope({ loading: false, error: "", value: value });
|
|
720
|
+
}).catch(function (error) {
|
|
721
|
+
if (alive) setScope({ loading: false, error: String(error && error.message || error), value: null });
|
|
722
|
+
});
|
|
723
|
+
return function () { alive = false; };
|
|
724
|
+
}, [connection, token]);
|
|
725
|
+
|
|
692
726
|
React.useEffect(function () {
|
|
693
727
|
var alive = true;
|
|
694
728
|
if (!connection) { setStatus({ loading: false, error: "连接服务不可用", saving: false, saved: false }); return; }
|
|
@@ -715,6 +749,95 @@ window.__ModuleLoader__.load({
|
|
|
715
749
|
var defaultTemplate = base.defaultTemplate || DEFAULT_TEMPLATE;
|
|
716
750
|
var themeStyle = normalizeDisplayStyle(base.displayStyle);
|
|
717
751
|
|
|
752
|
+
// Detection only: who compacts is decided by the preset slot (read from
|
|
753
|
+
// disk) times the 启用插件 switch. This row never changes either of them.
|
|
754
|
+
var SCOPE_TONE = {
|
|
755
|
+
native: { dot: "#6b7178", text: C.muted },
|
|
756
|
+
plugin: { dot: C.accent, text: "#cfe0ff" },
|
|
757
|
+
part: { dot: "#d29922", text: "#e6c98a" },
|
|
758
|
+
err: { dot: "#e5534b", text: "#f0b8b4" },
|
|
759
|
+
};
|
|
760
|
+
var scopeView = (function () {
|
|
761
|
+
var st = scope.value;
|
|
762
|
+
if (!st) {
|
|
763
|
+
return {
|
|
764
|
+
tone: "err",
|
|
765
|
+
text: scope.loading ? "读取中…" : "读不到生效情况",
|
|
766
|
+
sub: scope.loading
|
|
767
|
+
? "正在向 DSH 读取预设名单与本插件的接管行。"
|
|
768
|
+
: "读不到预设名单:" + (scope.error || "未知原因") + "。",
|
|
769
|
+
rows: [], retry: true,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
var list = Array.isArray(st.presets) ? st.presets : [];
|
|
773
|
+
rememberPresetLabels(list);
|
|
774
|
+
var rows = list.map(function (p) {
|
|
775
|
+
return {
|
|
776
|
+
key: String(p.id),
|
|
777
|
+
label: presetLabel(p.id),
|
|
778
|
+
taken: p.effective === true,
|
|
779
|
+
owner: p.effective === true ? "插件" : p.covered === true ? "原生(未勾选)" : "原生(未覆盖)",
|
|
780
|
+
};
|
|
781
|
+
});
|
|
782
|
+
var names = rows.map(function (r) { return r.label; });
|
|
783
|
+
var takenNames = rows.filter(function (r) { return r.taken; }).map(function (r) { return r.label; });
|
|
784
|
+
var restNames = rows.filter(function (r) { return !r.taken; }).map(function (r) { return r.label; });
|
|
785
|
+
var total = Number(st.total) || list.length;
|
|
786
|
+
var covered = Number(st.covered) || 0;
|
|
787
|
+
var effective = Number(st.effective) || 0;
|
|
788
|
+
var globalSaved = st.scope !== "preset";
|
|
789
|
+
var scopeLead = globalSaved ? "全局生效:" : "按预设生效:";
|
|
790
|
+
var mechanism = "接管行写在本插件包内的补丁里,卸载插件时随包一起消失,你的预设文件不会被改动。";
|
|
791
|
+
|
|
792
|
+
if (total === 0) {
|
|
793
|
+
return {
|
|
794
|
+
tone: "err", text: "读不到预设", rows: [], retry: true,
|
|
795
|
+
sub: "DSH 没有返回任何 Agent 预设" + (st.reason ? "(" + st.reason + ")" : "") + ",所以无法判断压缩由谁负责。重新读取试试。",
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
if (covered === 0) {
|
|
799
|
+
return {
|
|
800
|
+
tone: "native", text: "DSH 原生压缩", rows: rows, retry: true,
|
|
801
|
+
sub: "本插件的接管行还没覆盖到任何预设" + (st.reason ? "(" + st.reason + ")" : "")
|
|
802
|
+
+ ",压缩目前由 DSH 自己完成。" + mechanism + " 新生成或新装的预设需要重启 DSH 重新生成接管行。",
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
if (effective === 0 && !enabled) {
|
|
806
|
+
return {
|
|
807
|
+
tone: "native", text: "DSH 原生压缩(插件已停用)", rows: rows, retry: false,
|
|
808
|
+
sub: "「启用插件」已关闭:" + takenNames.length + "/" + total + " 个预设虽然被接管行覆盖,但插件把压缩交回 DSH 原生实现。打开开关即恢复,不需要重启。",
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
if (effective === 0) {
|
|
812
|
+
return {
|
|
813
|
+
tone: "native", text: "DSH 原生压缩", rows: rows, retry: false,
|
|
814
|
+
sub: scopeLead + "当前没有任何预设处于接管状态(" + names.join("、") + " 都走原生)。"
|
|
815
|
+
+ "勾选预设或切到「全局生效」并保存即可,下一次判定就生效,不需要重启。",
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
if (effective < total) {
|
|
819
|
+
return {
|
|
820
|
+
tone: "part", text: effective + "/" + total + " 预设已由插件压缩", rows: rows, retry: false,
|
|
821
|
+
sub: scopeLead + "由插件负责:" + takenNames.join("、") + ";" + restNames.join("、") + " 仍走各自的原生压缩。" + mechanism,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
tone: "plugin",
|
|
826
|
+
text: globalSaved ? "已由插件接管(全局)" : "已由插件接管",
|
|
827
|
+
rows: rows, retry: false,
|
|
828
|
+
sub: scopeLead + (globalSaved
|
|
829
|
+
? "这 " + total + " 个预设(" + names.join("、") + ")"
|
|
830
|
+
: "勾选的 " + effective + " 个预设(" + takenNames.join("、") + ")")
|
|
831
|
+
+ " 的压缩由本插件的交接文档负责,且「启用插件」是开的。" + mechanism,
|
|
832
|
+
};
|
|
833
|
+
})();
|
|
834
|
+
var scopeTone = SCOPE_TONE[scopeView.tone] || SCOPE_TONE.native;
|
|
835
|
+
/* Only presets the takeover rows reach can be selected; the rest stay
|
|
836
|
+
* native and would be misleading to offer. */
|
|
837
|
+
var takeable = scope.value && Array.isArray(scope.value.presets)
|
|
838
|
+
? scope.value.presets.filter(function (p) { return p && p.covered === true; })
|
|
839
|
+
: [];
|
|
840
|
+
|
|
718
841
|
var budget = EXAMPLE_WINDOW - EXAMPLE_RESERVED;
|
|
719
842
|
var ceiling = budget - EXAMPLE_HEADROOM;
|
|
720
843
|
var eff = Math.min(Math.round(budget * ratio / 100), ceiling);
|
|
@@ -726,6 +849,20 @@ window.__ModuleLoader__.load({
|
|
|
726
849
|
(settings.templateFile || "") !== template ||
|
|
727
850
|
normalizeDisplayStyle(settings.displayStyle) !== themeStyle;
|
|
728
851
|
|
|
852
|
+
// The detection row reports what is on disk, so every unsaved choice that
|
|
853
|
+
// would change that report has to be named — otherwise it looks ignored.
|
|
854
|
+
var enabledIsPending = settings !== null && (settings.enabled !== false) !== enabled;
|
|
855
|
+
var scopeIsPending = settings !== null && (settings.scope === "preset") !== scopePreset;
|
|
856
|
+
var presetsIsPending = settings !== null && scopePreset &&
|
|
857
|
+
JSON.stringify(presets.slice().sort()) !== JSON.stringify((settings.presets || []).slice().sort());
|
|
858
|
+
var pendingParts = [];
|
|
859
|
+
if (enabledIsPending) pendingParts.push("「启用插件」改成" + (enabled ? "开" : "关"));
|
|
860
|
+
if (scopeIsPending) pendingParts.push("生效范围改成「" + (scopePreset ? "按预设生效" : "全局生效") + "」");
|
|
861
|
+
if (presetsIsPending) pendingParts.push("勾选的预设改成" + (presets.length ? presets.map(presetLabel).join("、") : "(都不勾)"));
|
|
862
|
+
var pendingNote = pendingParts.length
|
|
863
|
+
? "还没保存:" + pendingParts.join(";") + "。保存后开关与勾选在下一次判定就生效,不需要重启;只有接管行本身要覆盖新的预设时才需要重启 DSH 重新生成。"
|
|
864
|
+
: "";
|
|
865
|
+
|
|
729
866
|
function patch(next) { setDraft(Object.assign({}, base, next)); setStatus({ loading: false, error: "", saving: false, saved: false }); }
|
|
730
867
|
function setPreset(id) {
|
|
731
868
|
var next = presets.indexOf(id) >= 0 ? presets.filter(function (v) { return v !== id; }) : presets.concat([id]);
|
|
@@ -744,10 +881,12 @@ window.__ModuleLoader__.load({
|
|
|
744
881
|
setSettings(value); setDraft(value);
|
|
745
882
|
publishDisplayStyle(value && value.displayStyle);
|
|
746
883
|
setStatus({ loading: false, error: "", saving: false, saved: true });
|
|
884
|
+
setToken(token + 1);
|
|
747
885
|
}).catch(function (error) {
|
|
748
886
|
setStatus({ loading: false, error: String(error && error.message || error), saving: false, saved: false });
|
|
749
887
|
});
|
|
750
888
|
}
|
|
889
|
+
|
|
751
890
|
function discard() { setDraft(settings); setStatus({ loading: false, error: "", saving: false, saved: false }); }
|
|
752
891
|
function open(kind) {
|
|
753
892
|
call(connection, "open", { kind: kind, path: template }).then(function () {
|
|
@@ -791,7 +930,7 @@ window.__ModuleLoader__.load({
|
|
|
791
930
|
h("div", { style: rowStyle },
|
|
792
931
|
h("div", null,
|
|
793
932
|
h("div", { style: labelStyle }, "生效范围"),
|
|
794
|
-
h("div", { style: subStyle }, scopePreset ? "
|
|
933
|
+
h("div", { style: subStyle }, scopePreset ? "只有勾选的预设由插件压缩,其余预设保持 DSH 原生行为;名单来自 DSH 的预设注册表" : "所有能被本插件接管的预设都由插件压缩,共用这份设置")
|
|
795
934
|
),
|
|
796
935
|
h("div", { style: { display: "inline-flex", border: "1px solid " + C.line, borderRadius: 8, overflow: "hidden" } },
|
|
797
936
|
seg(!scopePreset, "全局生效", function () { patch({ scope: "global" }); }),
|
|
@@ -799,15 +938,51 @@ window.__ModuleLoader__.load({
|
|
|
799
938
|
)
|
|
800
939
|
),
|
|
801
940
|
scopePreset ? h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", padding: "10px 0", borderBottom: "1px solid " + C.line } },
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
941
|
+
takeable.length === 0
|
|
942
|
+
? h("span", { style: subStyle }, scope.loading
|
|
943
|
+
? "正在向 DSH 读取预设名单…"
|
|
944
|
+
: "本插件的接管行还没覆盖到任何预设。重启 DSH 会重新生成接管行;仍未出现预设名单说明 DSH 没有返回预设。")
|
|
945
|
+
: takeable.map(function (p) {
|
|
946
|
+
var on = presets.indexOf(p.id) >= 0;
|
|
947
|
+
return h("button", {
|
|
948
|
+
key: p.id, type: "button", onClick: function () { setPreset(p.id); },
|
|
949
|
+
style: { fontSize: 12, padding: "5px 11px", borderRadius: 999, cursor: "pointer", font: "inherit", border: "1px solid " + (on ? C.accent : C.line), background: on ? "rgba(76,141,255,.12)" : "#191a1d", color: on ? C.text : C.muted },
|
|
950
|
+
}, presetLabel(p.id));
|
|
951
|
+
})
|
|
809
952
|
) : null,
|
|
810
953
|
|
|
954
|
+
h("div", { style: rowStyle },
|
|
955
|
+
h("div", { style: { minWidth: 0 } },
|
|
956
|
+
h("div", { style: labelStyle }, "当前生效情况"),
|
|
957
|
+
h("div", { style: subStyle }, scopeView.sub),
|
|
958
|
+
scopeView.rows.length
|
|
959
|
+
? h("div", { style: { display: "flex", gap: "6px 16px", flexWrap: "wrap", marginTop: 7 } },
|
|
960
|
+
scopeView.rows.map(function (r) {
|
|
961
|
+
return h("span", { key: r.key, style: { display: "inline-flex", alignItems: "center", gap: 6, fontSize: 12, color: C.muted } },
|
|
962
|
+
h("i", { "aria-hidden": true, style: { width: 6, height: 6, borderRadius: "50%", background: r.taken ? C.accent : "#6b7178" } }),
|
|
963
|
+
r.label,
|
|
964
|
+
h("em", { style: { fontStyle: "normal", color: C.faint } }, r.owner));
|
|
965
|
+
})
|
|
966
|
+
)
|
|
967
|
+
: null,
|
|
968
|
+
pendingNote ? h("div", { style: { fontSize: 11.5, color: "#d29922", marginTop: 6 } }, pendingNote) : null,
|
|
969
|
+
scope.error && scope.value ? h("div", { style: { fontSize: 11.5, color: "#e5534b", marginTop: 6 } }, "读取失败:" + scope.error) : null
|
|
970
|
+
),
|
|
971
|
+
h("div", { style: { display: "inline-flex", alignItems: "center", gap: 12, flex: "0 0 auto" } },
|
|
972
|
+
h("span", { "aria-live": "polite", style: { display: "inline-flex", alignItems: "center", gap: 7, fontSize: 12.5, color: scopeTone.text } },
|
|
973
|
+
h("i", { "aria-hidden": true, style: { width: 7, height: 7, borderRadius: "50%", background: scopeTone.dot } }),
|
|
974
|
+
scopeView.text
|
|
975
|
+
),
|
|
976
|
+
scopeView.retry && !scope.loading
|
|
977
|
+
? h("button", {
|
|
978
|
+
type: "button",
|
|
979
|
+
onClick: function () { setToken(token + 1); },
|
|
980
|
+
style: Object.assign({}, btnBase, { fontSize: 11.5, padding: "4px 9px" }),
|
|
981
|
+
}, "重新读取")
|
|
982
|
+
: null
|
|
983
|
+
)
|
|
984
|
+
),
|
|
985
|
+
|
|
811
986
|
h("div", { style: { padding: "12px 0", borderBottom: "1px solid " + C.line } },
|
|
812
987
|
h("div", null,
|
|
813
988
|
h("div", { style: labelStyle }, "压缩显示方式", h("span", { style: newBadgeStyle }, "新")),
|
|
@@ -881,8 +1056,8 @@ window.__ModuleLoader__.load({
|
|
|
881
1056
|
h("ol", { style: { margin: 0, paddingLeft: 18, color: C.muted, fontSize: 12.5, lineHeight: 1.85 } },
|
|
882
1057
|
h("li", null, "到点后不立即动手,等 agent 结束当前回合。"),
|
|
883
1058
|
h("li", null, "用会话自己的前缀(命中缓存)让模型按模板写交接文档,不额外全量 prefill。"),
|
|
884
|
-
h("li", null, "
|
|
885
|
-
h("li", null, "
|
|
1059
|
+
h("li", null, "只替换最旧的这一段历史,换成一条交接文档;最近约 16% 窗口的原文逐字留着,切点也不会把工具调用和它的结果拆开。"),
|
|
1060
|
+
h("li", null, "下一轮 = 系统提示词 + 工具定义 + 这条交接文档 + 那段逐字保留的最近消息;旧消息仍在会话日志里,rewind 可回滚。")
|
|
886
1061
|
),
|
|
887
1062
|
h("div", { style: { marginTop: 8, fontSize: 11.5, color: C.faint } }, "手动压缩:会话里发送 /compact 可随时折叠一次,不受这个比例限制。")
|
|
888
1063
|
),
|
package/lib/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createRequire } from "node:module";
|
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { dirname, join } from "node:path";
|
|
19
19
|
import { execFile, spawn } from "node:child_process";
|
|
20
|
+
import { mountTakeover, presetInventory } from "./takeover-mount.js";
|
|
20
21
|
|
|
21
22
|
export const name = "dsh-auto-compaction";
|
|
22
23
|
export const inject = [];
|
|
@@ -25,7 +26,7 @@ export const Config = z.object({
|
|
|
25
26
|
enabled: z.boolean().default(true),
|
|
26
27
|
thresholdRatio: z.number().default(90),
|
|
27
28
|
scopePreset: z.boolean().default(false),
|
|
28
|
-
presets: z.string().default("
|
|
29
|
+
presets: z.string().default(""),
|
|
29
30
|
templateFile: z.string().default(""),
|
|
30
31
|
openTemplate: z.string().default(""),
|
|
31
32
|
displayStyle: z.string().default("orbit"),
|
|
@@ -143,9 +144,9 @@ function scopeScriptPath() {
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
function execSwitchOnce(command, env, script) {
|
|
147
|
+
function execSwitchOnce(command, env, script, action) {
|
|
147
148
|
return new Promise((resolve, reject) => {
|
|
148
|
-
execFile(command, [script,
|
|
149
|
+
execFile(command, [script, action], { timeout: 6000, windowsHide: true, env }, (error, stdout, stderr) => {
|
|
149
150
|
if (error) {
|
|
150
151
|
error.stdout = stdout;
|
|
151
152
|
error.stderr = stderr;
|
|
@@ -158,15 +159,14 @@ function execSwitchOnce(command, env, script) {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* when the host has no node on PATH.
|
|
162
|
+
* Run one preset-scope action. `node` is the normal path; the Electron binary
|
|
163
|
+
* with ELECTRON_RUN_AS_NODE is the fallback when the host has no node on PATH.
|
|
164
164
|
*/
|
|
165
|
-
async function
|
|
165
|
+
async function runScope(action) {
|
|
166
166
|
const script = scopeScriptPath();
|
|
167
167
|
if (!existsSync(script)) {
|
|
168
168
|
const message = "preset-scope.mjs not found: " + script;
|
|
169
|
-
trace("scope
|
|
169
|
+
trace("scope " + action + " failed: " + message);
|
|
170
170
|
return { ok: false, message };
|
|
171
171
|
}
|
|
172
172
|
const explicit = process.env.DSH_NODE;
|
|
@@ -179,10 +179,11 @@ async function applyScopeSwitch() {
|
|
|
179
179
|
let lastError;
|
|
180
180
|
for (const attempt of attempts) {
|
|
181
181
|
try {
|
|
182
|
-
const result = await execSwitchOnce(attempt.command, attempt.env, script);
|
|
183
|
-
const output = `${result.stdout ?? ""}
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
const result = await execSwitchOnce(attempt.command, attempt.env, script, action);
|
|
183
|
+
const output = `${result.stdout ?? ""}`.trim();
|
|
184
|
+
const noise = `${result.stderr ?? ""}`.trim();
|
|
185
|
+
trace("scope " + action + " ok" + (output ? " | " + output : "") + (noise ? " | " + noise : ""));
|
|
186
|
+
return { ok: true, output, noise };
|
|
186
187
|
} catch (error) {
|
|
187
188
|
lastError = error;
|
|
188
189
|
if (error?.code !== "ENOENT") break;
|
|
@@ -190,10 +191,44 @@ async function applyScopeSwitch() {
|
|
|
190
191
|
}
|
|
191
192
|
const output = `${lastError?.stdout ?? ""}${lastError?.stderr ?? ""}`.trim();
|
|
192
193
|
const message = (lastError?.message ?? String(lastError)) + (output ? "\n" + output : "");
|
|
193
|
-
trace("scope
|
|
194
|
+
trace("scope " + action + " failed: " + message);
|
|
194
195
|
return { ok: false, message };
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Hand compaction back where the old mechanism left it.
|
|
200
|
+
*
|
|
201
|
+
* Up to 0.2.x the takeover was written into the user's own preset files plus a
|
|
202
|
+
* root-level guard in the profile patch. Those rows now live inside this
|
|
203
|
+
* package, so a leftover switch would mount two providers in the same realm and
|
|
204
|
+
* DSH would refuse to start. Detection goes through the script's own read-only
|
|
205
|
+
* report rather than a second YAML parser; the revert runs once and is recorded
|
|
206
|
+
* so later boots skip the subprocess entirely.
|
|
207
|
+
*/
|
|
208
|
+
async function revertLegacyTakeover(ctx) {
|
|
209
|
+
const settings = readSettings();
|
|
210
|
+
if (settings?.legacyRevertedAt !== undefined) return { reverted: false, skipped: "already" };
|
|
211
|
+
const state = await runScope("state");
|
|
212
|
+
if (!state.ok) return { reverted: false, reason: state.message };
|
|
213
|
+
const line = String(state.output ?? "").split(/\r?\n/).filter(Boolean).pop() ?? "";
|
|
214
|
+
let value;
|
|
215
|
+
try {
|
|
216
|
+
value = JSON.parse(line);
|
|
217
|
+
} catch {
|
|
218
|
+
return { reverted: false, reason: "unreadable legacy state" };
|
|
219
|
+
}
|
|
220
|
+
const legacy = Number(value?.taken) > 0 || value?.rootGuard === "off";
|
|
221
|
+
if (!legacy) return { reverted: false, legacy: false };
|
|
222
|
+
|
|
223
|
+
const disabled = await runScope("disable");
|
|
224
|
+
if (!disabled.ok) return { reverted: false, reason: disabled.message };
|
|
225
|
+
|
|
226
|
+
writeSettings(ctx, { ...settings, legacyRevertedAt: new Date().toISOString() });
|
|
227
|
+
trace(`legacy takeover reverted: taken=${value?.taken}/${value?.total} rootGuard=${value?.rootGuard}`);
|
|
228
|
+
ctx.logger?.info?.("[handoff-ui] 旧版接管残留已恢复为原生压缩(预设文件与根级守卫)");
|
|
229
|
+
return { reverted: true, taken: value?.taken, total: value?.total };
|
|
230
|
+
}
|
|
231
|
+
|
|
197
232
|
const CHANNEL = "/dsh-auto-compaction";
|
|
198
233
|
|
|
199
234
|
function readSettings() {
|
|
@@ -224,6 +259,22 @@ function sanitize(payload) {
|
|
|
224
259
|
export function apply(ctx, config = {}) {
|
|
225
260
|
const seeded = normalize(config);
|
|
226
261
|
if (readSettings() === undefined) writeSettings(ctx, seeded);
|
|
262
|
+
// Takeover rows are regenerated from the live preset definitions on every
|
|
263
|
+
// load, into this package's own patch file. Nothing here depends on the
|
|
264
|
+
// user's scope selection: an unselected preset is handled by the engine
|
|
265
|
+
// delegating to the stock one, so installing is enough to take over and
|
|
266
|
+
// uninstalling is enough to hand compaction back.
|
|
267
|
+
void (async () => {
|
|
268
|
+
const legacy = await revertLegacyTakeover(ctx);
|
|
269
|
+
if (legacy.reverted) trace("legacy takeover reverted before mounting");
|
|
270
|
+
else if (legacy.reason) trace("legacy takeover check skipped: " + legacy.reason);
|
|
271
|
+
return mountTakeover(ctx);
|
|
272
|
+
})().then((result) => {
|
|
273
|
+
trace(`takeover mount ${result.ok ? "ok" : "failed"} taken=${result.taken?.join(",") ?? "-"} mounted=${result.mounted?.join(",") ?? "-"} skipped=${result.skipped?.join(",") ?? "-"} ${result.reason ?? ""}`);
|
|
274
|
+
}).catch((error) => {
|
|
275
|
+
trace("takeover mount threw: " + (error instanceof Error ? error.message : String(error)));
|
|
276
|
+
ctx.logger?.warn?.(`[handoff-ui] takeover mount failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
277
|
+
});
|
|
227
278
|
void handleOpen(ctx, config.openTemplate, seeded.templateFile || TEMPLATE_FILE).catch((error) => {
|
|
228
279
|
ctx.logger?.warn?.(`[handoff-ui] openTemplate failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
229
280
|
});
|
|
@@ -259,11 +310,29 @@ export function apply(ctx, config = {}) {
|
|
|
259
310
|
const next = sanitize(payload);
|
|
260
311
|
trace("set " + JSON.stringify(next));
|
|
261
312
|
writeSettings(ctx, next);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
313
|
+
// No rewriting of preset files: the engine applies the switch and
|
|
314
|
+
// the scope per preset at decision time, so a save is live at the
|
|
315
|
+
// next idle check and uninstalling still leaves nothing behind.
|
|
316
|
+
return { ok: true, value: { ...next, defaultTemplate: TEMPLATE_FILE } };
|
|
317
|
+
}
|
|
318
|
+
if (endpoint === "scope") {
|
|
319
|
+
const stored = readSettings() ?? seeded;
|
|
320
|
+
try {
|
|
321
|
+
const inventory = await presetInventory(ctx, stored);
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
value: {
|
|
325
|
+
enabled: stored.enabled !== false,
|
|
326
|
+
scope: stored.scope === "preset" ? "preset" : "global",
|
|
327
|
+
source: "bundle-patch",
|
|
328
|
+
...inventory,
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
333
|
+
trace("scope inventory failed: " + message);
|
|
334
|
+
return { ok: false, error: { code: "auto-compaction/scope-unknown", message } };
|
|
265
335
|
}
|
|
266
|
-
return { ok: true, value: { ...next, switchApplied: true, defaultTemplate: TEMPLATE_FILE } };
|
|
267
336
|
}
|
|
268
337
|
if (endpoint === "open") {
|
|
269
338
|
trace("open " + JSON.stringify(payload));
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount the takeover, in this package's own patch file.
|
|
3
|
+
*
|
|
4
|
+
* The rows that hand compaction over to this plugin live in the bundle patch
|
|
5
|
+
* shipped inside the package, because that is the only place uninstalling
|
|
6
|
+
* removes them automatically: DSH's uninstall neither clears nor refuses
|
|
7
|
+
* config-replacement rows in a user's own patch, so writing them there would
|
|
8
|
+
* leave a preset pointing at a package that no longer exists — a broken
|
|
9
|
+
* compaction setup the user cannot see. Writing here costs nothing on removal.
|
|
10
|
+
*
|
|
11
|
+
* Rows are generated from the live preset definitions rather than baked in, so
|
|
12
|
+
* a preset that gains a tool in a later DSH release keeps it. The generator
|
|
13
|
+
* lives in the engine package and is loaded lazily: DSH's host module fallback
|
|
14
|
+
* cannot resolve a linked package's subpath from here, and a top-level import
|
|
15
|
+
* blocks startup.
|
|
16
|
+
*/
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
|
|
21
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
22
|
+
|
|
23
|
+
/** The patch file this package is mounted from. */
|
|
24
|
+
function patchFile() {
|
|
25
|
+
const override = process.env.DSH_HANDOFF_TAKEOVER_FILE;
|
|
26
|
+
if (typeof override === "string" && override.trim().length > 0) return override.trim();
|
|
27
|
+
return join(dirname(requireFromHere.resolve("../package.json")), "cordis.patch.yml");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load the row generator without importing the engine package statically.
|
|
32
|
+
* @returns the module, or undefined when it cannot be resolved.
|
|
33
|
+
*/
|
|
34
|
+
async function loadGenerator() {
|
|
35
|
+
// Dynamic import rather than createRequire: the generator is ESM. Installed,
|
|
36
|
+
// the engine sits beside this package in the same node_modules and publishes
|
|
37
|
+
// ./lib/*; in development it is the monorepo sibling, since the workspace
|
|
38
|
+
// installs nothing and everything runs through junctions.
|
|
39
|
+
for (const attempt of [
|
|
40
|
+
() => import("dsh-auto-compaction-engine/lib/takeover-rows.js"),
|
|
41
|
+
() => import("../../dsh-auto-compaction-engine/lib/takeover-rows.js"),
|
|
42
|
+
]) {
|
|
43
|
+
try {
|
|
44
|
+
return await attempt();
|
|
45
|
+
} catch { /* try the next location */ }
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The profile manifest DSH composes its layers from. */
|
|
51
|
+
function profileManifest() {
|
|
52
|
+
const override = process.env.DSH_PROFILE_MANIFEST;
|
|
53
|
+
if (typeof override === "string" && override.trim().length > 0) return override.trim();
|
|
54
|
+
const appData = process.env.APP_DATA
|
|
55
|
+
?? path.join(process.env.SystemDrive ?? "C:", "Users", process.env.USERNAME ?? process.env.USER ?? "", "AppData", "Roaming");
|
|
56
|
+
return path.join(appData, "dsh-desktop", "harness", "profiles", "web", "package.json");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Keep this package's patch layer behind every preset declaration.
|
|
61
|
+
*
|
|
62
|
+
* Layer order is the order of `dsh.profile.bundles`, and a takeover row is an
|
|
63
|
+
* update of a preset row that has to exist by the time our layer applies:
|
|
64
|
+
* presets declared by a later layer would leave our row with nothing to point
|
|
65
|
+
* at, silently. DSH's own presets come early, but a preset installed from the
|
|
66
|
+
* market or written by the user can come after us, so the entry belongs last.
|
|
67
|
+
*
|
|
68
|
+
* The desktop market appends newly installed bundles to the end, so this is a
|
|
69
|
+
* no-op for a fresh install; it matters when another plugin is installed after
|
|
70
|
+
* us. Verified on this machine: without the entry 4 of 6 presets mounted, with
|
|
71
|
+
* it at the end all 6 did.
|
|
72
|
+
*
|
|
73
|
+
* Text surgery rather than a JSON rewrite, because this file is the user's and
|
|
74
|
+
* the market rewrites it too.
|
|
75
|
+
*/
|
|
76
|
+
export function ensureBundleLayer(text, name = "dsh-auto-compaction") {
|
|
77
|
+
if (typeof text !== "string" || text.length === 0) return { text, changed: false, reason: "manifest unreadable" };
|
|
78
|
+
const start = text.indexOf("\"bundles\"");
|
|
79
|
+
const open = start < 0 ? -1 : text.indexOf("[", start);
|
|
80
|
+
const close = open < 0 ? -1 : text.indexOf("]", open);
|
|
81
|
+
if (close < 0) return { text, changed: false, reason: "no dsh.profile.bundles array" };
|
|
82
|
+
const body = text.slice(open + 1, close);
|
|
83
|
+
const entries = [...body.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
84
|
+
if (entries.length > 0 && entries[entries.length - 1] === name) return { text, changed: false, reason: "" };
|
|
85
|
+
|
|
86
|
+
const kept = entries.filter((entry) => entry !== name);
|
|
87
|
+
const rebuilt = kept.length === 0 ? `["${name}"]` : `[\n${kept.concat([name]).map((entry) => ` "${entry}"`).join(",\n")}\n ]`;
|
|
88
|
+
return { text: text.slice(0, open) + rebuilt + text.slice(close + 1), changed: true, reason: "" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Apply the layer rule to the profile manifest, writing only on change. */
|
|
92
|
+
export function ensureBundleLayerFile() {
|
|
93
|
+
const file = profileManifest();
|
|
94
|
+
let text = "";
|
|
95
|
+
try {
|
|
96
|
+
text = readFileSync(file, "utf8");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return { ok: false, file, reason: `manifest missing: ${error instanceof Error ? error.message : String(error)}` };
|
|
99
|
+
}
|
|
100
|
+
const result = ensureBundleLayer(text);
|
|
101
|
+
if (!result.changed) return { ok: true, file, changed: false, reason: result.reason };
|
|
102
|
+
try {
|
|
103
|
+
JSON.parse(result.text);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return { ok: false, file, reason: `refusing to write invalid JSON: ${error.message}` };
|
|
106
|
+
}
|
|
107
|
+
const tmp = `${file}.tmp`;
|
|
108
|
+
writeFileSync(tmp, result.text, "utf8");
|
|
109
|
+
renameSync(tmp, file);
|
|
110
|
+
return { ok: true, file, changed: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Ask DSH which agent presets exist and what each one currently declares.
|
|
115
|
+
*
|
|
116
|
+
* `list()` publishes the display metadata ({id, name, description, order,
|
|
117
|
+
* broken?}) and `readDocument()` the plugin list as a YAML string; a preset's
|
|
118
|
+
* config is exactly `{id, name, description, order, plugins}`, so the two
|
|
119
|
+
* together are the whole definition and nothing here is guessed.
|
|
120
|
+
*/
|
|
121
|
+
async function readPresets(ctx, generator) {
|
|
122
|
+
let service;
|
|
123
|
+
try {
|
|
124
|
+
service = ctx.get?.("agentPresets");
|
|
125
|
+
} catch {
|
|
126
|
+
return { presets: [], failures: [], reason: "agentPresets service unavailable" };
|
|
127
|
+
}
|
|
128
|
+
if (service === undefined || typeof service.list !== "function") {
|
|
129
|
+
return { presets: [], failures: [], reason: "agentPresets service has no list()" };
|
|
130
|
+
}
|
|
131
|
+
if (generator === undefined) {
|
|
132
|
+
return { presets: [], failures: [], reason: "row generator not resolvable" };
|
|
133
|
+
}
|
|
134
|
+
let listed;
|
|
135
|
+
try {
|
|
136
|
+
listed = await service.list();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return { presets: [], failures: [], reason: `list() failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
139
|
+
}
|
|
140
|
+
const entries = Array.isArray(listed) ? listed : Array.isArray(listed?.presets) ? listed.presets : [];
|
|
141
|
+
const presets = [];
|
|
142
|
+
const failures = [];
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const id = typeof entry?.id === "string" && entry.id.length > 0 ? entry.id : undefined;
|
|
145
|
+
if (id === undefined) continue;
|
|
146
|
+
let document;
|
|
147
|
+
try {
|
|
148
|
+
document = typeof service.readDocument === "function" ? await service.readDocument(id) : undefined;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
failures.push(`${id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const parsed = generator.parsePlugins(document?.content);
|
|
154
|
+
if (parsed.plugins === undefined) {
|
|
155
|
+
failures.push(`${id}: ${parsed.reason}`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const config = { id };
|
|
159
|
+
for (const key of ["name", "description", "order"]) {
|
|
160
|
+
if (entry[key] !== undefined) config[key] = entry[key];
|
|
161
|
+
}
|
|
162
|
+
config.plugins = parsed.plugins;
|
|
163
|
+
presets.push({
|
|
164
|
+
id,
|
|
165
|
+
config,
|
|
166
|
+
label: typeof entry.name === "string" && entry.name.length > 0 ? entry.name : id,
|
|
167
|
+
// What the composition actually registers, and whether it failed to.
|
|
168
|
+
provider: generator.compactionProvider?.(config)?.name ?? "",
|
|
169
|
+
broken: typeof entry.broken === "string" ? entry.broken : "",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return { presets, failures };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Write the managed block, atomically, only when it actually changed. */
|
|
176
|
+
function writeBlock(text, file) {
|
|
177
|
+
const tmp = `${file}.tmp`;
|
|
178
|
+
writeFileSync(tmp, text, "utf8");
|
|
179
|
+
renameSync(tmp, file);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Regenerate the takeover rows at activation.
|
|
184
|
+
*
|
|
185
|
+
* @returns {ok, file, taken, skipped, reason} for the log and the settings page.
|
|
186
|
+
*/
|
|
187
|
+
export async function mountTakeover(ctx) {
|
|
188
|
+
const generator = await loadGenerator();
|
|
189
|
+
if (generator === undefined) {
|
|
190
|
+
return { ok: false, reason: "row generator not resolvable", taken: [], skipped: [] };
|
|
191
|
+
}
|
|
192
|
+
const file = patchFile();
|
|
193
|
+
let current = "";
|
|
194
|
+
try {
|
|
195
|
+
current = readFileSync(file, "utf8");
|
|
196
|
+
} catch {
|
|
197
|
+
current = "";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { presets, failures = [], reason = "" } = await readPresets(ctx, generator);
|
|
201
|
+
if (presets.length === 0) {
|
|
202
|
+
// Nothing could be read: leave the file as it is rather than wiping the
|
|
203
|
+
// shipped rows on a transient host failure.
|
|
204
|
+
return { ok: false, file, taken: [], skipped: [], reason: reason || failures[0] || "no presets declared" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const engineName = process.env.DSH_HANDOFF_ENGINE_NAME ?? "dsh-auto-compaction-engine";
|
|
208
|
+
const layer = ensureBundleLayerFile();
|
|
209
|
+
if (layer.changed) ctx.logger?.info?.(`[handoff-ui] bundle layer moved to the end of dsh.profile.bundles`);
|
|
210
|
+
else if (layer.reason) ctx.logger?.warn?.(`[handoff-ui] bundle layer left as is: ${layer.reason}`);
|
|
211
|
+
const rows = generator.takeoverRows(presets, { engineName });
|
|
212
|
+
const taken = rows.map((row) => String(row.id).replace(/^preset-/, ""));
|
|
213
|
+
const skipped = presets.map((preset) => preset.id).filter((id) => !taken.includes(id));
|
|
214
|
+
/* Written rows and effective rows are different facts: the composition reads
|
|
215
|
+
* the patch at startup, so rows first written by this activation only take
|
|
216
|
+
* effect next boot. Reporting both is what keeps the settings page honest. */
|
|
217
|
+
const mounted = presets.filter((preset) => preset.provider === engineName).map((preset) => preset.id);
|
|
218
|
+
const broken = presets.filter((preset) => preset.broken).map((preset) => `${preset.id}: ${preset.broken}`);
|
|
219
|
+
/* Saying which presets were left out and why is the difference between an
|
|
220
|
+
* honest "not taken over" and a silent one. */
|
|
221
|
+
const skippedReasons = presets
|
|
222
|
+
.filter((preset) => !taken.includes(preset.id))
|
|
223
|
+
.map((preset) => {
|
|
224
|
+
const unsupported = generator.findUnsupported?.(preset.config ?? {});
|
|
225
|
+
return `${preset.id}: ${unsupported ? `含无法原样重写的值(${unsupported})` : "没有可接管的 compaction 组"}`;
|
|
226
|
+
});
|
|
227
|
+
const notes = [];
|
|
228
|
+
if (failures.length > 0) notes.push(`unreadable: ${failures.join("; ")}`);
|
|
229
|
+
if (skippedReasons.length > 0) notes.push(`skipped: ${skippedReasons.join("; ")}`);
|
|
230
|
+
if (broken.length > 0) notes.push(`broken: ${broken.join("; ")}`);
|
|
231
|
+
const block = generator.renderBlock(rows);
|
|
232
|
+
|
|
233
|
+
// Nothing to take over and nothing recorded before: leave the shipped file
|
|
234
|
+
// alone rather than appending an empty block.
|
|
235
|
+
if (rows.length === 0 && !current.includes(generator.BLOCK_MARKERS.BEGIN)) {
|
|
236
|
+
return { ok: true, file, taken: [], skipped, mounted, broken, reason: notes.join(" | ") };
|
|
237
|
+
}
|
|
238
|
+
if (generator.needsRewrite(current, block)) {
|
|
239
|
+
try {
|
|
240
|
+
writeBlock(generator.mergeBlock(current, block), file);
|
|
241
|
+
ctx.logger?.info?.(`[handoff-ui] takeover rows written to ${file} (${taken.length} preset(s))`);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false, file, taken, skipped,
|
|
245
|
+
reason: `patch write failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { ok: true, file, taken, skipped, mounted, broken, reason: notes.join(" | ") };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Which presets the shipped block currently covers. */
|
|
253
|
+
export async function readTakenPresets() {
|
|
254
|
+
const generator = await loadGenerator();
|
|
255
|
+
const file = patchFile();
|
|
256
|
+
if (generator === undefined) return [];
|
|
257
|
+
let text = "";
|
|
258
|
+
try {
|
|
259
|
+
text = readFileSync(file, "utf8");
|
|
260
|
+
} catch {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const { BEGIN, END } = generator.BLOCK_MARKERS;
|
|
264
|
+
const begin = text.indexOf(BEGIN);
|
|
265
|
+
const end = text.indexOf(END);
|
|
266
|
+
if (begin < 0 || end < 0 || end < begin) return [];
|
|
267
|
+
return [...text.slice(begin + BEGIN.length, end).matchAll(/^-\s+id:\s*preset-(\S+)/gm)].map((m) => m[1]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Everything the settings page needs to answer "who compacts each preset".
|
|
272
|
+
*
|
|
273
|
+
* `covered` is what the block in this package's patch reaches; `effective` adds
|
|
274
|
+
* the runtime gates the engine applies per preset (the switch and the scope).
|
|
275
|
+
*/
|
|
276
|
+
export async function presetInventory(ctx, settings = {}) {
|
|
277
|
+
const generator = await loadGenerator();
|
|
278
|
+
const covered = new Set(await readTakenPresets());
|
|
279
|
+
const { presets, failures = [], reason = "" } = await readPresets(ctx, generator);
|
|
280
|
+
const selected = new Set(Array.isArray(settings.presets) ? settings.presets : []);
|
|
281
|
+
const global = settings.scope !== "preset";
|
|
282
|
+
const enabled = settings.enabled !== false;
|
|
283
|
+
const rows = presets.map((preset) => {
|
|
284
|
+
const isCovered = covered.has(preset.id);
|
|
285
|
+
const isSelected = global ? isCovered : isCovered && selected.has(preset.id);
|
|
286
|
+
return {
|
|
287
|
+
id: preset.id,
|
|
288
|
+
label: typeof preset.label === "string" ? preset.label : undefined,
|
|
289
|
+
covered: isCovered,
|
|
290
|
+
selected: isSelected,
|
|
291
|
+
effective: isSelected && enabled,
|
|
292
|
+
// Whether this boot's composition already registers our provider; a row
|
|
293
|
+
// written during this activation only shows up here after a restart.
|
|
294
|
+
mounted: preset.provider === (process.env.DSH_HANDOFF_ENGINE_NAME ?? "dsh-auto-compaction-engine"),
|
|
295
|
+
broken: preset.broken || undefined,
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
presets: rows,
|
|
300
|
+
total: rows.length,
|
|
301
|
+
covered: rows.filter((row) => row.covered).length,
|
|
302
|
+
mounted: rows.filter((row) => row.mounted).length,
|
|
303
|
+
effective: rows.filter((row) => row.effective).length,
|
|
304
|
+
reason: reason || (failures.length > 0 ? `unreadable: ${failures.join("; ")}` : ""),
|
|
305
|
+
};
|
|
306
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-auto-compaction",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"dsh-auto-compaction-engine": "^0.2.
|
|
54
|
+
"dsh-auto-compaction-engine": "^0.2.3"
|
|
55
55
|
}
|
|
56
56
|
}
|