dsh-prompt-customizer 0.4.0 → 0.4.2
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/LICENSE +21 -21
- package/README.en.md +278 -227
- package/README.i18n.yaml +2 -2
- package/README.md +278 -227
- package/client/client.js +82 -80
- package/client/client.js.map +1 -1
- package/cordis.patch.yml +8 -8
- package/lib/catalog.js +131 -131
- package/lib/effective.js +161 -158
- package/lib/index.js +943 -943
- package/lib/promotion.js +80 -80
- package/lib/schema.js +124 -124
- package/lib/sectionOps.d.mts +32 -32
- package/lib/sectionOps.mjs +128 -133
- package/lib/store.js +124 -124
- package/lib/vars.js +147 -147
- package/package.json +84 -64
- package/src/client/PresetsTab.tsx +281 -281
- package/src/client/PreviewTab.tsx +91 -91
- package/src/client/PreviewTools.tsx +33 -33
- package/src/client/SectionsTab.tsx +539 -556
- package/src/client/ToolsTab.tsx +293 -266
- package/src/client/index.ts +433 -422
- package/src/client/locales.ts +263 -259
- package/src/client/preset-io.ts +226 -226
- package/src/client/presets.ts +413 -408
- package/src/client/styles.ts +49 -49
- package/src/client/types.ts +128 -128
package/src/client/presets.ts
CHANGED
|
@@ -1,408 +1,413 @@
|
|
|
1
|
-
/** 纯函数的预设 / 段操作助手,同时被客户端 UI 与 Node 测试复用(不触碰 IO 与 React)。 */
|
|
2
|
-
import type { Config, Inventory, OverrideData, Phase, PhaseViewKey, Preset, PresetData, ToolsConfig } from './types.ts'
|
|
3
|
-
|
|
4
|
-
export interface Section {
|
|
5
|
-
name: string
|
|
6
|
-
order: number
|
|
7
|
-
text?: string
|
|
8
|
-
active: boolean
|
|
9
|
-
replaced: boolean
|
|
10
|
-
/** `system` = 读自宿主提示词清单(不可删除);`custom` = 本插件注入(可删除)。 */
|
|
11
|
-
source?: 'system' | 'custom'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** 应用预设后需要回写到设置作用域的那部分配置字段。
|
|
15
|
-
* 阶段名单为可选:旧快照不带 → 应用时保留当前值,绝不抹掉。 */
|
|
16
|
-
export interface ConfigPatch {
|
|
17
|
-
sections: string[]
|
|
18
|
-
sectionsBootstrap?: string[]
|
|
19
|
-
sectionsCompaction?: string[]
|
|
20
|
-
replace: Record<string, string>
|
|
21
|
-
inject: ResolvedInject[]
|
|
22
|
-
tools: ToolsConfig
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** 快照捕获的阶段 order 空间(always 之外的三个阶段)。 */
|
|
26
|
-
const CAPTURE_PHASES: Array<'bootstrap' | 'active' | 'compaction'> = ['bootstrap', 'active', 'compaction']
|
|
27
|
-
|
|
28
|
-
/** 把一条有序名字转成相对链(每段记住它的前一段),text / custom 只给 custom 段。 */
|
|
29
|
-
function toChain(names: string[], textOf: (name: string) => string, customOf: (name: string) => boolean, phase?: Phase): NonNullable<PresetData['order']> {
|
|
30
|
-
return names.map((name, i) => ({
|
|
31
|
-
name,
|
|
32
|
-
after: i > 0 ? names[i - 1] : undefined,
|
|
33
|
-
text: customOf(name) ? (textOf(name) ?? '') : '',
|
|
34
|
-
custom: customOf(name),
|
|
35
|
-
...(phase !== undefined ? { phase } : {}),
|
|
36
|
-
}))
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 从当前配置构建预设快照(完整捕获,含每阶段独立设定)。
|
|
41
|
-
*
|
|
42
|
-
* 全局相对链派生自合并后的完整段列表(而不仅是 `cfg.inject`),这样即使保存前
|
|
43
|
-
* 只是屏蔽了几个段(没有重排),预设仍会携带完整的有序集合,应用时才能得到一个
|
|
44
|
-
* 有意义的「激活列表」。在此之上,bootstrap / active / compaction 各再带一条自己
|
|
45
|
-
* 的相对链 —— 数据源就是 UI 逐阶段持久化的注入条目(每阶段独立的 order 空间),
|
|
46
|
-
* 否则阶段化的名单与排序在「导出 → 导入 → 应用」后就丢了。
|
|
47
|
-
*/
|
|
48
|
-
export function buildPresetData(cfg: Config, merged: Section[]): PresetData {
|
|
49
|
-
const globalNames = merged.map((sec) => sec.name)
|
|
50
|
-
const textOf = new Map(merged.map((sec) => [sec.name, sec.text ?? ''] as const))
|
|
51
|
-
const customOf = new Map(merged.map((sec) => [sec.name, sec.source === 'custom'] as const))
|
|
52
|
-
const order = toChain(globalNames, (n) => textOf.get(n) ?? '', (n) => customOf.get(n) === true)
|
|
53
|
-
for (const phase of CAPTURE_PHASES) {
|
|
54
|
-
const items = (cfg.inject ?? [])
|
|
55
|
-
.filter((item) => item.phase === phase)
|
|
56
|
-
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
57
|
-
if (items.length === 0) continue
|
|
58
|
-
order.push(...toChain(
|
|
59
|
-
items.map((item) => item.name),
|
|
60
|
-
(n) => items.find((item) => item.name === n)?.text ?? '',
|
|
61
|
-
(n) => items.find((item) => item.name === n)?.custom === true,
|
|
62
|
-
phase,
|
|
63
|
-
))
|
|
64
|
-
}
|
|
65
|
-
const data: PresetData = {
|
|
66
|
-
sections: cfg.sections,
|
|
67
|
-
replace: cfg.replace,
|
|
68
|
-
order,
|
|
69
|
-
tools: cfg.tools,
|
|
70
|
-
}
|
|
71
|
-
// 非空才捕获:没有阶段名单的配置导出的快照与改动前逐键一致。
|
|
72
|
-
if ((cfg.sectionsBootstrap ?? []).length > 0) data.sectionsBootstrap = cfg.sectionsBootstrap
|
|
73
|
-
if ((cfg.sectionsCompaction ?? []).length > 0) data.sectionsCompaction = cfg.sectionsCompaction
|
|
74
|
-
return data
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 把宿主清单段与用户注入列表按 order 合并、排序。
|
|
79
|
-
* 读自清单(其他插件产出)的段标记为 `system`;本插件生成的段带隐藏的
|
|
80
|
-
* `custom` 标记,标记为 `custom`。来源只由隐藏标记决定——绝不靠名字碰撞
|
|
81
|
-
* 判断——因此即使自定义段恰好与某个清单段重名,它也能保住自己的身份
|
|
82
|
-
* (以及可删除性),并在切换预设后依然成立。自定义段始终渲染自己的文本,
|
|
83
|
-
* 永远不会显示为「<动态生成>」。
|
|
84
|
-
*/
|
|
85
|
-
export function mergeSections(inv: Inventory | null, cfg: Config, blockedNames: ReadonlySet<string>): Section[] {
|
|
86
|
-
const map = new Map<string, Section>()
|
|
87
|
-
for (const sec of inv?.sections ?? []) map.set(sec.name, { ...sec, source: 'system' })
|
|
88
|
-
for (const item of cfg.inject ?? []) {
|
|
89
|
-
const isCustom = item.custom === true
|
|
90
|
-
const existing = map.get(item.name)
|
|
91
|
-
if (existing) {
|
|
92
|
-
map.set(item.name, isCustom
|
|
93
|
-
? { ...existing, order: item.order, text: item.text ?? '', source: 'custom' }
|
|
94
|
-
: { ...existing, order: item.order, source: 'system' })
|
|
95
|
-
} else {
|
|
96
|
-
map.set(item.name, {
|
|
97
|
-
name: item.name,
|
|
98
|
-
order: item.order,
|
|
99
|
-
text: item.text ?? '',
|
|
100
|
-
active: !blockedNames.has(item.name),
|
|
101
|
-
replaced: false,
|
|
102
|
-
source: isCustom ? 'custom' : 'system',
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return [...map.values()].sort((a, b) => a.order - b.order).map((sec, i) => ({ ...sec, order: i }))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** ConfigPatch 里对外的注入条目(always 组刻意不带 phase 键)。 */
|
|
110
|
-
export interface ResolvedInject {
|
|
111
|
-
name: string
|
|
112
|
-
order: number
|
|
113
|
-
text: string
|
|
114
|
-
custom?: boolean
|
|
115
|
-
phase?: Phase
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 把一条相对链解析为绝对有序名字(0..n-1)。
|
|
120
|
-
* 没有锚点(或锚点不在本组内)的名字按原始顺序排在最前;每个带锚点的名字
|
|
121
|
-
* 插到其锚点之后;剩余的名字(存在环)追加到末尾。
|
|
122
|
-
*/
|
|
123
|
-
function resolveChain(list: Array<{ name: string; after?: string }>): string[] {
|
|
124
|
-
const afterMap = new Map<string, string | undefined>()
|
|
125
|
-
for (const sec of list) afterMap.set(sec.name, sec.after)
|
|
126
|
-
|
|
127
|
-
const result: string[] = []
|
|
128
|
-
const placed = new Set<string>()
|
|
129
|
-
|
|
130
|
-
for (const sec of list) {
|
|
131
|
-
const anchor = afterMap.get(sec.name)
|
|
132
|
-
if (!anchor || !afterMap.has(anchor)) { result.push(sec.name); placed.add(sec.name) }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
let changed = true
|
|
136
|
-
while (changed) {
|
|
137
|
-
changed = false
|
|
138
|
-
for (const sec of list) {
|
|
139
|
-
if (placed.has(sec.name)) continue
|
|
140
|
-
const anchor = afterMap.get(sec.name)
|
|
141
|
-
if (anchor && placed.has(anchor)) {
|
|
142
|
-
result.splice(result.indexOf(anchor) + 1, 0, sec.name)
|
|
143
|
-
placed.add(sec.name)
|
|
144
|
-
changed = true
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
for (const sec of list) {
|
|
150
|
-
if (!placed.has(sec.name)) { result.push(sec.name); placed.add(sec.name) }
|
|
151
|
-
}
|
|
152
|
-
return result
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 把快照里的相对顺序解析为注入条目列表。
|
|
157
|
-
*
|
|
158
|
-
* order 条目可以带 `phase`:同名而不同 phase 的多条 = 该段在多个阶段各有自己的
|
|
159
|
-
* 位置(每阶段有独立的 order 空间,与 UI 的 phaseInjectEntries 一致)。无 phase
|
|
160
|
-
* 的条目属于 always 组 —— 也就是旧快照的单一全局序,输出形状与旧实现逐键一致
|
|
161
|
-
* (刻意不写 phase 键,「保存 → 应用 = 不动点」的不变量依赖键形状完全相同)。
|
|
162
|
-
*/
|
|
163
|
-
export function resolveOrder(presetOrder: PresetData['order']): ResolvedInject[] {
|
|
164
|
-
const list = presetOrder ?? []
|
|
165
|
-
const groups = new Map<Phase, NonNullable<PresetData['order']>>([['always', []]])
|
|
166
|
-
for (const sec of list) {
|
|
167
|
-
const phase = sec.phase ?? 'always'
|
|
168
|
-
const group = groups.get(phase)
|
|
169
|
-
if (group) group.push(sec)
|
|
170
|
-
else groups.set(phase, [sec])
|
|
171
|
-
}
|
|
172
|
-
const out: ResolvedInject[] = []
|
|
173
|
-
for (const [phase, members] of groups) {
|
|
174
|
-
const textMap = new Map(members.map((x) => [x.name, x.text ?? '']))
|
|
175
|
-
const customMap = new Map(members.map((x) => [x.name, x.custom === true]))
|
|
176
|
-
const names = resolveChain(members)
|
|
177
|
-
out.push(...names.map((name, i) => (phase === 'always'
|
|
178
|
-
? { name, order: i, text: textMap.get(name) ?? '', custom: customMap.get(name) }
|
|
179
|
-
: { name, order: i, text: textMap.get(name) ?? '', custom: customMap.get(name), phase })))
|
|
180
|
-
}
|
|
181
|
-
return out
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* 计算应用预设时的配置补丁:
|
|
186
|
-
* - 同名段被覆盖(顺序列表在运行时驱动这一行为)
|
|
187
|
-
* -
|
|
188
|
-
* - 当前有、但不在预设有序列表中的段默认被屏蔽
|
|
189
|
-
* - 只有预设的「激活段」(在 order 列表且不在其屏蔽名单中)被解除屏蔽;
|
|
190
|
-
* 预设自己屏蔽的段保持屏蔽。
|
|
191
|
-
*
|
|
192
|
-
* 当前配置里存在、但不在预设有序列表中的自定义注入段会被保留到结果的
|
|
193
|
-
* inject 列表中(因此仍然可见)并置为禁用(加入屏蔽集合),而不是被静默
|
|
194
|
-
* 丢弃——预设不能让用户自己注入的凭空消失。
|
|
195
|
-
*/
|
|
196
|
-
export function applyPresetData(data: PresetData, cfg: Config, currentNames: ReadonlySet<string>): ConfigPatch {
|
|
197
|
-
const presetOrder = data.order ?? []
|
|
198
|
-
const presetNames = new Set(presetOrder.map((x) => x.name))
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
},
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
*
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
export function
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
1
|
+
/** 纯函数的预设 / 段操作助手,同时被客户端 UI 与 Node 测试复用(不触碰 IO 与 React)。 */
|
|
2
|
+
import type { Config, Inventory, OverrideData, Phase, PhaseViewKey, Preset, PresetData, ToolsConfig } from './types.ts'
|
|
3
|
+
|
|
4
|
+
export interface Section {
|
|
5
|
+
name: string
|
|
6
|
+
order: number
|
|
7
|
+
text?: string
|
|
8
|
+
active: boolean
|
|
9
|
+
replaced: boolean
|
|
10
|
+
/** `system` = 读自宿主提示词清单(不可删除);`custom` = 本插件注入(可删除)。 */
|
|
11
|
+
source?: 'system' | 'custom'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 应用预设后需要回写到设置作用域的那部分配置字段。
|
|
15
|
+
* 阶段名单为可选:旧快照不带 → 应用时保留当前值,绝不抹掉。 */
|
|
16
|
+
export interface ConfigPatch {
|
|
17
|
+
sections: string[]
|
|
18
|
+
sectionsBootstrap?: string[]
|
|
19
|
+
sectionsCompaction?: string[]
|
|
20
|
+
replace: Record<string, string>
|
|
21
|
+
inject: ResolvedInject[]
|
|
22
|
+
tools: ToolsConfig
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 快照捕获的阶段 order 空间(always 之外的三个阶段)。 */
|
|
26
|
+
const CAPTURE_PHASES: Array<'bootstrap' | 'active' | 'compaction'> = ['bootstrap', 'active', 'compaction']
|
|
27
|
+
|
|
28
|
+
/** 把一条有序名字转成相对链(每段记住它的前一段),text / custom 只给 custom 段。 */
|
|
29
|
+
function toChain(names: string[], textOf: (name: string) => string, customOf: (name: string) => boolean, phase?: Phase): NonNullable<PresetData['order']> {
|
|
30
|
+
return names.map((name, i) => ({
|
|
31
|
+
name,
|
|
32
|
+
after: i > 0 ? names[i - 1] : undefined,
|
|
33
|
+
text: customOf(name) ? (textOf(name) ?? '') : '',
|
|
34
|
+
custom: customOf(name),
|
|
35
|
+
...(phase !== undefined ? { phase } : {}),
|
|
36
|
+
}))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 从当前配置构建预设快照(完整捕获,含每阶段独立设定)。
|
|
41
|
+
*
|
|
42
|
+
* 全局相对链派生自合并后的完整段列表(而不仅是 `cfg.inject`),这样即使保存前
|
|
43
|
+
* 只是屏蔽了几个段(没有重排),预设仍会携带完整的有序集合,应用时才能得到一个
|
|
44
|
+
* 有意义的「激活列表」。在此之上,bootstrap / active / compaction 各再带一条自己
|
|
45
|
+
* 的相对链 —— 数据源就是 UI 逐阶段持久化的注入条目(每阶段独立的 order 空间),
|
|
46
|
+
* 否则阶段化的名单与排序在「导出 → 导入 → 应用」后就丢了。
|
|
47
|
+
*/
|
|
48
|
+
export function buildPresetData(cfg: Config, merged: Section[]): PresetData {
|
|
49
|
+
const globalNames = merged.map((sec) => sec.name)
|
|
50
|
+
const textOf = new Map(merged.map((sec) => [sec.name, sec.text ?? ''] as const))
|
|
51
|
+
const customOf = new Map(merged.map((sec) => [sec.name, sec.source === 'custom'] as const))
|
|
52
|
+
const order = toChain(globalNames, (n) => textOf.get(n) ?? '', (n) => customOf.get(n) === true)
|
|
53
|
+
for (const phase of CAPTURE_PHASES) {
|
|
54
|
+
const items = (cfg.inject ?? [])
|
|
55
|
+
.filter((item) => item.phase === phase)
|
|
56
|
+
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
57
|
+
if (items.length === 0) continue
|
|
58
|
+
order.push(...toChain(
|
|
59
|
+
items.map((item) => item.name),
|
|
60
|
+
(n) => items.find((item) => item.name === n)?.text ?? '',
|
|
61
|
+
(n) => items.find((item) => item.name === n)?.custom === true,
|
|
62
|
+
phase,
|
|
63
|
+
))
|
|
64
|
+
}
|
|
65
|
+
const data: PresetData = {
|
|
66
|
+
sections: cfg.sections,
|
|
67
|
+
replace: cfg.replace,
|
|
68
|
+
order,
|
|
69
|
+
tools: cfg.tools,
|
|
70
|
+
}
|
|
71
|
+
// 非空才捕获:没有阶段名单的配置导出的快照与改动前逐键一致。
|
|
72
|
+
if ((cfg.sectionsBootstrap ?? []).length > 0) data.sectionsBootstrap = cfg.sectionsBootstrap
|
|
73
|
+
if ((cfg.sectionsCompaction ?? []).length > 0) data.sectionsCompaction = cfg.sectionsCompaction
|
|
74
|
+
return data
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 把宿主清单段与用户注入列表按 order 合并、排序。
|
|
79
|
+
* 读自清单(其他插件产出)的段标记为 `system`;本插件生成的段带隐藏的
|
|
80
|
+
* `custom` 标记,标记为 `custom`。来源只由隐藏标记决定——绝不靠名字碰撞
|
|
81
|
+
* 判断——因此即使自定义段恰好与某个清单段重名,它也能保住自己的身份
|
|
82
|
+
* (以及可删除性),并在切换预设后依然成立。自定义段始终渲染自己的文本,
|
|
83
|
+
* 永远不会显示为「<动态生成>」。
|
|
84
|
+
*/
|
|
85
|
+
export function mergeSections(inv: Inventory | null, cfg: Config, blockedNames: ReadonlySet<string>): Section[] {
|
|
86
|
+
const map = new Map<string, Section>()
|
|
87
|
+
for (const sec of inv?.sections ?? []) map.set(sec.name, { ...sec, source: 'system' })
|
|
88
|
+
for (const item of cfg.inject ?? []) {
|
|
89
|
+
const isCustom = item.custom === true
|
|
90
|
+
const existing = map.get(item.name)
|
|
91
|
+
if (existing) {
|
|
92
|
+
map.set(item.name, isCustom
|
|
93
|
+
? { ...existing, order: item.order, text: item.text ?? '', source: 'custom' }
|
|
94
|
+
: { ...existing, order: item.order, source: 'system' })
|
|
95
|
+
} else {
|
|
96
|
+
map.set(item.name, {
|
|
97
|
+
name: item.name,
|
|
98
|
+
order: item.order,
|
|
99
|
+
text: item.text ?? '',
|
|
100
|
+
active: !blockedNames.has(item.name),
|
|
101
|
+
replaced: false,
|
|
102
|
+
source: isCustom ? 'custom' : 'system',
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return [...map.values()].sort((a, b) => a.order - b.order).map((sec, i) => ({ ...sec, order: i }))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** ConfigPatch 里对外的注入条目(always 组刻意不带 phase 键)。 */
|
|
110
|
+
export interface ResolvedInject {
|
|
111
|
+
name: string
|
|
112
|
+
order: number
|
|
113
|
+
text: string
|
|
114
|
+
custom?: boolean
|
|
115
|
+
phase?: Phase
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 把一条相对链解析为绝对有序名字(0..n-1)。
|
|
120
|
+
* 没有锚点(或锚点不在本组内)的名字按原始顺序排在最前;每个带锚点的名字
|
|
121
|
+
* 插到其锚点之后;剩余的名字(存在环)追加到末尾。
|
|
122
|
+
*/
|
|
123
|
+
function resolveChain(list: Array<{ name: string; after?: string }>): string[] {
|
|
124
|
+
const afterMap = new Map<string, string | undefined>()
|
|
125
|
+
for (const sec of list) afterMap.set(sec.name, sec.after)
|
|
126
|
+
|
|
127
|
+
const result: string[] = []
|
|
128
|
+
const placed = new Set<string>()
|
|
129
|
+
|
|
130
|
+
for (const sec of list) {
|
|
131
|
+
const anchor = afterMap.get(sec.name)
|
|
132
|
+
if (!anchor || !afterMap.has(anchor)) { result.push(sec.name); placed.add(sec.name) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let changed = true
|
|
136
|
+
while (changed) {
|
|
137
|
+
changed = false
|
|
138
|
+
for (const sec of list) {
|
|
139
|
+
if (placed.has(sec.name)) continue
|
|
140
|
+
const anchor = afterMap.get(sec.name)
|
|
141
|
+
if (anchor && placed.has(anchor)) {
|
|
142
|
+
result.splice(result.indexOf(anchor) + 1, 0, sec.name)
|
|
143
|
+
placed.add(sec.name)
|
|
144
|
+
changed = true
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const sec of list) {
|
|
150
|
+
if (!placed.has(sec.name)) { result.push(sec.name); placed.add(sec.name) }
|
|
151
|
+
}
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 把快照里的相对顺序解析为注入条目列表。
|
|
157
|
+
*
|
|
158
|
+
* order 条目可以带 `phase`:同名而不同 phase 的多条 = 该段在多个阶段各有自己的
|
|
159
|
+
* 位置(每阶段有独立的 order 空间,与 UI 的 phaseInjectEntries 一致)。无 phase
|
|
160
|
+
* 的条目属于 always 组 —— 也就是旧快照的单一全局序,输出形状与旧实现逐键一致
|
|
161
|
+
* (刻意不写 phase 键,「保存 → 应用 = 不动点」的不变量依赖键形状完全相同)。
|
|
162
|
+
*/
|
|
163
|
+
export function resolveOrder(presetOrder: PresetData['order']): ResolvedInject[] {
|
|
164
|
+
const list = presetOrder ?? []
|
|
165
|
+
const groups = new Map<Phase, NonNullable<PresetData['order']>>([['always', []]])
|
|
166
|
+
for (const sec of list) {
|
|
167
|
+
const phase = sec.phase ?? 'always'
|
|
168
|
+
const group = groups.get(phase)
|
|
169
|
+
if (group) group.push(sec)
|
|
170
|
+
else groups.set(phase, [sec])
|
|
171
|
+
}
|
|
172
|
+
const out: ResolvedInject[] = []
|
|
173
|
+
for (const [phase, members] of groups) {
|
|
174
|
+
const textMap = new Map(members.map((x) => [x.name, x.text ?? '']))
|
|
175
|
+
const customMap = new Map(members.map((x) => [x.name, x.custom === true]))
|
|
176
|
+
const names = resolveChain(members)
|
|
177
|
+
out.push(...names.map((name, i) => (phase === 'always'
|
|
178
|
+
? { name, order: i, text: textMap.get(name) ?? '', custom: customMap.get(name) }
|
|
179
|
+
: { name, order: i, text: textMap.get(name) ?? '', custom: customMap.get(name), phase })))
|
|
180
|
+
}
|
|
181
|
+
return out
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 计算应用预设时的配置补丁:
|
|
186
|
+
* - 同名段被覆盖(顺序列表在运行时驱动这一行为)
|
|
187
|
+
* - 预设中有、当前系统里匹配不上的段默认跳过(跨系统导入不凭空建段)
|
|
188
|
+
* - 当前有、但不在预设有序列表中的段默认被屏蔽
|
|
189
|
+
* - 只有预设的「激活段」(在 order 列表且不在其屏蔽名单中)被解除屏蔽;
|
|
190
|
+
* 预设自己屏蔽的段保持屏蔽。
|
|
191
|
+
*
|
|
192
|
+
* 当前配置里存在、但不在预设有序列表中的自定义注入段会被保留到结果的
|
|
193
|
+
* inject 列表中(因此仍然可见)并置为禁用(加入屏蔽集合),而不是被静默
|
|
194
|
+
* 丢弃——预设不能让用户自己注入的凭空消失。
|
|
195
|
+
*/
|
|
196
|
+
export function applyPresetData(data: PresetData, cfg: Config, currentNames: ReadonlySet<string>): ConfigPatch {
|
|
197
|
+
const presetOrder = data.order ?? []
|
|
198
|
+
const presetNames = new Set(presetOrder.map((x) => x.name))
|
|
199
|
+
// 黑名单同样按当前系统过滤:匹配不上的名字跳过(屏蔽不存在的段没有意义,
|
|
200
|
+
// 还会污染配置,让下一次快照出现「sections ⊆ order」被破坏的孤儿名单)。
|
|
201
|
+
const blocked = new Set((data.sections ?? []).filter((n) => currentNames.has(n)))
|
|
202
|
+
|
|
203
|
+
// 注入列表 = 预设解析出的顺序,再追加当前配置中存在、但预设列表没有的
|
|
204
|
+
// 自定义注入段。让它们留在 `inject` 里才能保持可见;随后统一置为禁用。
|
|
205
|
+
// 被保留的自定义段接在预设自身的下标之后;若预设只覆盖一小部分段,它们
|
|
206
|
+
// 可能与清单原生 order 并列——合并视图按稳定排序(清单在前),顺序仍是
|
|
207
|
+
// 确定性的,且下一次重排/持久化会把所有下标重新连续化。
|
|
208
|
+
// 预设 order 先按当前系统的段名过滤(匹配不上就跳过,绝不凭空建段)——
|
|
209
|
+
// 在解析前过滤让保留条目的 order 保持按组连续;锚点被过滤掉时
|
|
210
|
+
// resolveChain 会把该条目回退到组尾,顺序仍然确定。
|
|
211
|
+
const inject = resolveOrder(presetOrder.filter((x) => currentNames.has(x.name)))
|
|
212
|
+
const kept = new Set(inject.map((x) => x.name))
|
|
213
|
+
let order = inject.length
|
|
214
|
+
for (const item of cfg.inject ?? []) {
|
|
215
|
+
if (!item || !item.name || presetNames.has(item.name) || kept.has(item.name)) continue
|
|
216
|
+
kept.add(item.name)
|
|
217
|
+
inject.push({ name: item.name, order: order++, text: item.text ?? '', custom: item.custom === true })
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 仅当预设确实定义了 order 列表时才强制执行「激活集合」。
|
|
221
|
+
// 空的 order(例如手工编写 / 只屏蔽了几个段的导入预设)绝不能把当前所有
|
|
222
|
+
// 段默认禁用。
|
|
223
|
+
if (presetOrder.length > 0) {
|
|
224
|
+
const activeNames = new Set([...presetNames].filter((n) => !blocked.has(n)))
|
|
225
|
+
for (const name of currentNames) if (!presetNames.has(name)) blocked.add(name)
|
|
226
|
+
for (const name of activeNames) blocked.delete(name)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 阶段化字段一律「快照有则覆盖,缺省则保留当前值」:旧快照(没有这些键)
|
|
230
|
+
// 应用后仍只换静态过滤,绝不把用户已有的阶段目录与阶段名单抹平。
|
|
231
|
+
// 缺席时不留下显式 undefined 键 —— 「保存→应用 = 不动点」的不变量依赖键形状。
|
|
232
|
+
const keptCatalogs: ToolsConfig = {}
|
|
233
|
+
const bootstrap = data.tools?.bootstrap !== undefined ? data.tools.bootstrap : cfg.tools?.bootstrap
|
|
234
|
+
const compaction = data.tools?.compaction !== undefined ? data.tools.compaction : cfg.tools?.compaction
|
|
235
|
+
if (bootstrap !== undefined) keptCatalogs.bootstrap = bootstrap
|
|
236
|
+
if (compaction !== undefined) keptCatalogs.compaction = compaction
|
|
237
|
+
const patch: ConfigPatch = {
|
|
238
|
+
sections: [...blocked],
|
|
239
|
+
replace: { ...(cfg.replace ?? {}), ...(data.replace ?? {}) },
|
|
240
|
+
inject,
|
|
241
|
+
tools: {
|
|
242
|
+
exclude: data.tools?.exclude ?? [],
|
|
243
|
+
...keptCatalogs,
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
if (Array.isArray(data.sectionsBootstrap)) patch.sectionsBootstrap = data.sectionsBootstrap
|
|
247
|
+
if (Array.isArray(data.sectionsCompaction)) patch.sectionsCompaction = data.sectionsCompaction
|
|
248
|
+
return patch
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 从配置中移除一个 custom(本插件注入)段:它从 inject 列表消失,不再被
|
|
253
|
+
* 强制加入屏蔽集合,相关的替换文本也被清除。剩余的 inject 条目会被重新
|
|
254
|
+
* 编号为连续的 0..n-1(虚拟下标模型),删除不会留下空洞。读自宿主清单的
|
|
255
|
+
* system 段无法这样移除(下次读取清单时它们又会回来)。
|
|
256
|
+
*/
|
|
257
|
+
export function removeSection(name: string, cfg: Config): Pick<Config, 'sections' | 'inject' | 'replace'> {
|
|
258
|
+
const sections = (cfg.sections ?? []).filter((n) => n !== name)
|
|
259
|
+
const inject = (cfg.inject ?? [])
|
|
260
|
+
.filter((item) => item.name !== name)
|
|
261
|
+
.map((item, i) => ({ ...item, order: i }))
|
|
262
|
+
const replace = { ...(cfg.replace ?? {}) }
|
|
263
|
+
delete replace[name]
|
|
264
|
+
return { sections, inject, replace }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── 工具过滤 ─────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
export type ToolsCfg = { exclude?: string[] }
|
|
270
|
+
|
|
271
|
+
/** 三个阶段部分的固定展示顺序:引导期 → 常驻期 → 压缩受控期。
|
|
272
|
+
* 恒定全部渲染 —— 预设没有某个阶段时该部分只是空的,绝不隐藏。 */
|
|
273
|
+
export const PART_ORDER: PhaseViewKey[] = ['bootstrap', 'active', 'compaction']
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 一个阶段部分写回哪一份过滤配置:引导期 → `tools.bootstrap`、压缩受控期 →
|
|
277
|
+
* `tools.compaction`、常驻期 → 静态 `tools.exclude`。三份名单互不继承。
|
|
278
|
+
*/
|
|
279
|
+
export function phaseConfigKey(key: PhaseViewKey): 'bootstrap' | 'compaction' | 'static' {
|
|
280
|
+
return key === 'active' ? 'static' : key
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 把一个阶段的 exclude 名单写回整份工具配置,返回新对象(其余阶段原样保留)。
|
|
285
|
+
* 一次拖放可能要同时改两个阶段的名单(搬移 = 源阶段隐藏 + 目标阶段显示),必须
|
|
286
|
+
* 在同一个对象上连续套完再落一次写入 —— 分两次写会各自基于旧 cfg 计算而互相覆盖。
|
|
287
|
+
*/
|
|
288
|
+
export function withPhaseExclude(tools: ToolsConfig | undefined, key: PhaseViewKey, exclude: string[]): ToolsConfig {
|
|
289
|
+
const base = tools ?? {}
|
|
290
|
+
const target = phaseConfigKey(key)
|
|
291
|
+
if (target === 'bootstrap') return { ...base, bootstrap: { ...(base.bootstrap ?? {}), exclude } }
|
|
292
|
+
if (target === 'compaction') return { ...base, compaction: { ...(base.compaction ?? {}), exclude } }
|
|
293
|
+
return { ...base, exclude }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 把一个阶段的 add 名单(要加回该阶段的工具)写回整份工具配置,返回新对象。
|
|
298
|
+
* 与 withPhaseExclude 对称:常驻期写静态 `tools.add`,引导期写 `tools.bootstrap.add`,
|
|
299
|
+
* 压缩受控期写 `tools.compaction.add`,其余阶段原样保留。
|
|
300
|
+
*/
|
|
301
|
+
export function withPhaseAdd(tools: ToolsConfig | undefined, key: PhaseViewKey, add: string[]): ToolsConfig {
|
|
302
|
+
const base = tools ?? {}
|
|
303
|
+
const target = phaseConfigKey(key)
|
|
304
|
+
if (target === 'bootstrap') return { ...base, bootstrap: { ...(base.bootstrap ?? {}), add } }
|
|
305
|
+
if (target === 'compaction') return { ...base, compaction: { ...(base.compaction ?? {}), add } }
|
|
306
|
+
return { ...base, add }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** 某个工具在当前黑名单配置下是否被隐藏。 */
|
|
310
|
+
export function isToolHidden(name: string, toolsCfg: ToolsCfg | undefined): boolean {
|
|
311
|
+
return (toolsCfg?.exclude ?? []).includes(name)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** 切换某个工具的隐藏状态,返回新的工具配置(不改原对象)。 */
|
|
315
|
+
export function toggleTool(name: string, currentlyHidden: boolean, toolsCfg: ToolsCfg | undefined): ToolsCfg {
|
|
316
|
+
const exclude = (toolsCfg?.exclude ?? []).slice()
|
|
317
|
+
if (currentlyHidden) { const i = exclude.indexOf(name); if (i >= 0) exclude.splice(i, 1) }
|
|
318
|
+
else if (!exclude.includes(name)) exclude.push(name)
|
|
319
|
+
return { exclude }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── 按 agent 预设的覆盖编辑 ─────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 编辑目标的「显示视图」:全局目标返回原配置;agent 预设目标把该预设
|
|
326
|
+
* override 的字段叠在全局值之上展示(空缺字段显示全局继承值)。`presets`
|
|
327
|
+
* 与 `activePreset` 永远来自全局 —— 预设快照库本身不分作用域。
|
|
328
|
+
*/
|
|
329
|
+
export function editView(cfg: Config, target: string | undefined): Config {
|
|
330
|
+
if (!target) return cfg
|
|
331
|
+
const ovr = cfg.overrides?.[target] ?? {}
|
|
332
|
+
return {
|
|
333
|
+
sections: ovr.sections ?? cfg.sections,
|
|
334
|
+
sectionsBootstrap: ovr.sectionsBootstrap ?? cfg.sectionsBootstrap,
|
|
335
|
+
sectionsCompaction: ovr.sectionsCompaction ?? cfg.sectionsCompaction,
|
|
336
|
+
replace: ovr.replace ?? cfg.replace,
|
|
337
|
+
inject: ovr.inject ?? cfg.inject,
|
|
338
|
+
tools: {
|
|
339
|
+
exclude: ovr.tools?.exclude ?? cfg.tools?.exclude,
|
|
340
|
+
add: ovr.tools?.add ?? cfg.tools?.add,
|
|
341
|
+
bootstrap: ovr.tools?.bootstrap ?? cfg.tools?.bootstrap,
|
|
342
|
+
compaction: ovr.tools?.compaction ?? cfg.tools?.compaction,
|
|
343
|
+
},
|
|
344
|
+
presets: cfg.presets,
|
|
345
|
+
activePreset: cfg.activePreset,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 往某个 agent 预设的 override 里写入一个字段,返回新的 overrides 记录
|
|
351
|
+
* (不改原对象)。与 editView 配对:编辑视图里看到的继承值一旦被修改,
|
|
352
|
+
* 就整体落成该预设自己的字段(字段级接管语义)。
|
|
353
|
+
*/
|
|
354
|
+
export function setOverrideField(
|
|
355
|
+
cfg: Config,
|
|
356
|
+
target: string,
|
|
357
|
+
field: 'sections' | 'replace' | 'inject' | 'tools',
|
|
358
|
+
value: unknown,
|
|
359
|
+
): Record<string, OverrideData> {
|
|
360
|
+
return {
|
|
361
|
+
...(cfg.overrides ?? {}),
|
|
362
|
+
[target]: { ...(cfg.overrides?.[target] ?? {}), [field]: value },
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── 预设列表操作 ──────────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
/** 序列化预设用于导出(JSON 安全的完整快照)。 */
|
|
369
|
+
export function serializePreset(preset: Preset): { name: string; data: PresetData } {
|
|
370
|
+
return { name: preset.name, data: preset.data }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Derive a safe default filename for a preset export. User-entered preset
|
|
375
|
+
* names may contain characters that are illegal in filenames (`/\:*?"<>|`,
|
|
376
|
+
* control chars) — those are replaced; a trailing dot/space is trimmed
|
|
377
|
+
* (invalid on Windows); an empty or all-illegal result falls back to
|
|
378
|
+
* `preset`.
|
|
379
|
+
*/
|
|
380
|
+
export function presetExportFilename(name: string): string {
|
|
381
|
+
const sanitized = String(name)
|
|
382
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_')
|
|
383
|
+
.trim()
|
|
384
|
+
.replace(/[. ]+$/, '')
|
|
385
|
+
const base = sanitized === '' ? 'preset' : sanitized
|
|
386
|
+
return `${base}.json`
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 导入预设:同名的会被跳过,只追加名字不同的预设。返回新的预设列表
|
|
391
|
+
* (若没有任何变更则返回原列表)。
|
|
392
|
+
*/
|
|
393
|
+
export function addImportedPresets(existing: Preset[], parsed: unknown, makeId: () => string): Preset[] {
|
|
394
|
+
const incoming = Array.isArray(parsed) ? parsed : [parsed]
|
|
395
|
+
const existingNames = new Set(existing.map((p) => p.name))
|
|
396
|
+
const added = incoming
|
|
397
|
+
.filter((p) => p && typeof (p as { name?: unknown }).name === 'string' && !existingNames.has((p as Preset).name))
|
|
398
|
+
.map((p) => ({ id: makeId(), name: (p as Preset).name, data: ((p as Preset).data ?? {}) as PresetData }))
|
|
399
|
+
return added.length > 0 ? [...existing, ...added] : existing
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** 删除某个预设;若它正是当前激活的预设,则同时清除 activeId。 */
|
|
403
|
+
export function removePreset(presets: Preset[], id: string, activeId?: string): { presets: Preset[]; activeId?: string } {
|
|
404
|
+
return {
|
|
405
|
+
presets: presets.filter((p) => p.id !== id),
|
|
406
|
+
activeId: activeId === id ? undefined : activeId,
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** 生成预设 id:时间戳 + 随机段(base36),够用且无需额外依赖。 */
|
|
411
|
+
export function genId(): string {
|
|
412
|
+
return 'p_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7)
|
|
413
|
+
}
|