dsh-prompt-customizer 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +171 -72
- package/README.md +170 -72
- package/client/client.js +1753 -410
- package/client/client.js.map +1 -1
- package/lib/catalog.js +131 -0
- package/lib/effective.js +109 -0
- package/lib/index.js +712 -184
- package/lib/promotion.js +80 -0
- package/lib/schema.js +78 -33
- package/lib/sectionOps.d.mts +32 -0
- package/lib/sectionOps.mjs +134 -0
- package/lib/store.js +124 -0
- package/package.json +7 -5
- package/src/client/PresetsTab.tsx +141 -48
- package/src/client/PreviewTab.tsx +67 -41
- package/src/client/PreviewTools.tsx +6 -4
- package/src/client/SectionsTab.tsx +508 -156
- package/src/client/ToolsTab.tsx +253 -35
- package/src/client/index.ts +331 -46
- package/src/client/locales.ts +135 -7
- package/src/client/preset-io.ts +225 -0
- package/src/client/presets.ts +256 -116
- package/src/client/styles.ts +9 -1
- package/src/client/types.ts +89 -44
- package/lib/client.js +0 -344
package/client/client.js
CHANGED
|
@@ -14,7 +14,7 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
unavailable: "当前环境未启用本命名空间。",
|
|
15
15
|
tabsSections: "提示词",
|
|
16
16
|
tabsTools: "工具",
|
|
17
|
-
tabsPresets: "
|
|
17
|
+
tabsPresets: "配置",
|
|
18
18
|
tabsPreview: "预览",
|
|
19
19
|
refresh: "刷新",
|
|
20
20
|
blockedOff: "未屏蔽",
|
|
@@ -24,6 +24,7 @@ window.__ModuleLoader__.load({
|
|
|
24
24
|
system: "系统",
|
|
25
25
|
replace: "编辑",
|
|
26
26
|
clearReplace: "清除",
|
|
27
|
+
clearInput: "清空",
|
|
27
28
|
injectNew: "注入新段",
|
|
28
29
|
name: "名称",
|
|
29
30
|
order: "顺序",
|
|
@@ -43,16 +44,78 @@ window.__ModuleLoader__.load({
|
|
|
43
44
|
previewSections: "段",
|
|
44
45
|
previewTools: "工具预览",
|
|
45
46
|
previewToolCount: "个工具",
|
|
46
|
-
savePreset: "
|
|
47
|
-
presetName: "
|
|
47
|
+
savePreset: "保存当前配置",
|
|
48
|
+
presetName: "配置名称",
|
|
49
|
+
saveAsPresetCard: "存为 agent 预设",
|
|
50
|
+
saveAsPresetHint: "整体复制「{name}」这个预设的组成为新预设(组成文件与伴生脚本一并带走),再把当前定制写进它的覆盖项 —— 新预设出现在顶部编辑目标里。",
|
|
51
|
+
agentPresetName: "新预设名",
|
|
52
|
+
forkSourceDefault: "默认预设",
|
|
53
|
+
saveAsPresetOk: "预设已创建,可在顶部的编辑目标选择器里选到它",
|
|
54
|
+
saveAsPresetFail: "保存预设失败",
|
|
48
55
|
save: "保存",
|
|
49
|
-
importPreset: "
|
|
56
|
+
importPreset: "导入配置",
|
|
50
57
|
import: "导入",
|
|
51
58
|
apply: "应用",
|
|
52
59
|
export: "导出",
|
|
53
60
|
delete: "删除",
|
|
61
|
+
restore: "还原",
|
|
54
62
|
active: "使用中",
|
|
55
|
-
preset: "预设"
|
|
63
|
+
preset: "预设",
|
|
64
|
+
ioFailed: "导入/导出失败",
|
|
65
|
+
importInvalid: "无效的配置文件",
|
|
66
|
+
targetGlobal: "目标:全局默认",
|
|
67
|
+
targetHint: "选择编辑目标 —— 选中某个 agent 预设后,改动只对该预设生效(字段级覆盖)。",
|
|
68
|
+
broken: "损坏",
|
|
69
|
+
brokenPreset: "该 agent 预设当前无法挂载,仍可编辑其定制(修复预设后自动生效)。",
|
|
70
|
+
phaseBootstrap: "仅引导阶段",
|
|
71
|
+
phaseActive: "晋级后",
|
|
72
|
+
phaseCompaction: "压缩后",
|
|
73
|
+
phaseStageGuide: "引导期",
|
|
74
|
+
phaseStageResident: "常驻期",
|
|
75
|
+
phaseStageControlled: "压缩受控期",
|
|
76
|
+
toolsPartGuide: "引导期工具",
|
|
77
|
+
toolsPartResident: "常驻期工具",
|
|
78
|
+
toolsPartControlled: "压缩受控期工具",
|
|
79
|
+
allToolsTitle: "本系统全部工具",
|
|
80
|
+
sectionsPartGuide: "引导期提示词",
|
|
81
|
+
sectionsPartResident: "常驻期提示词",
|
|
82
|
+
sectionsPartControlled: "压缩受控期提示词",
|
|
83
|
+
allSectionsTitle: "本系统全部提示词",
|
|
84
|
+
toolsFourHint: "三个阶段各一份名单,互不继承:勾选 = 该阶段是否对模型可见;从「全部」拖入某阶段 = 让这一个工具在该阶段出现(若该阶段默认裁掉了它,就加回去);在两个阶段之间拖动 = 复制(源阶段不动,目标阶段多一份,隐藏/启用状态一起带过去);拖回「全部」= 从该阶段拿掉它。每个动作只影响被拖的那一个工具。",
|
|
85
|
+
sectionsFourHint: "拖拽双向:把「全部」里的段拖入上方阶段 = 加入该阶段并默认开启;从阶段拖回「全部」= 从该阶段移除。屏蔽段与普通段完全一样可拖拽 / 编辑,只是不注入模型。",
|
|
86
|
+
dragHint: "拖入 = 让该工具在这个阶段出现",
|
|
87
|
+
phaseShortGuide: "引",
|
|
88
|
+
phaseShortResident: "常",
|
|
89
|
+
phaseShortControlled: "压",
|
|
90
|
+
poolMarksHint: "每个名字后的三个标记 = 该工具在引导期 / 常驻期 / 压缩受控期的状态:绿=该阶段可见(或已加回),灰=该阶段已隐藏,暗=不在该阶段、也没加回。",
|
|
91
|
+
toolAddedTag: "加回",
|
|
92
|
+
toolAdded: "已把「{name}」加回{phase}:该阶段默认裁掉了它,保存后它会重新出现在该阶段(当前预设注册表里有它)。",
|
|
93
|
+
toolNotInRegistry: "「{name}」不在本预设的注册表里(它是别的预设独有的工具),当前预设没有它的定义,加不进来。",
|
|
94
|
+
toolCopied: "已把「{name}」复制到{to}:{from}那份保持不动,隐藏/启用状态一起带过去。",
|
|
95
|
+
toolShown: "「{name}」已在{to}显示。",
|
|
96
|
+
toolHiddenIn: "「{name}」已在{to}隐藏。",
|
|
97
|
+
sectionsTakenOver: "该目标的最终系统提示词由「{name}」段整段接管:宿主在装配瀑布流之后把 sections 强制还原为那一条段,因此本插件的段级屏蔽 / 替换 / 注入 / 排序都不会进入模型看到的提示词(工具过滤仍然生效)。要让段级定制生效,需在该 agent 预设里关掉 persona 的 complete。",
|
|
98
|
+
sectionsLost: "本插件在该阶段产出的 {emitted} 个段里,只有 {survived} 个进入最终提示词:其余被下游装配规则丢弃,段级定制不会完全生效(工具过滤不受影响)。",
|
|
99
|
+
sectionBlockedStrip: "(本阶段已屏蔽,模型不可见)",
|
|
100
|
+
phaseSuppressed: "被抑制(不在任何阶段的装配中)",
|
|
101
|
+
sectionDynamicNoAdd: "「{name}」是运行时才生成的动态段,没法预先加进某个阶段(它的内容只有装配时才知道)。",
|
|
102
|
+
exportOk: "导出成功",
|
|
103
|
+
exportCancel: "已取消导出",
|
|
104
|
+
exportFail: "导出失败",
|
|
105
|
+
importOk: "导入完成",
|
|
106
|
+
importNone: "没有新增预设(同名已存在或文件为空)",
|
|
107
|
+
importCancel: "已取消导入",
|
|
108
|
+
importFail: "导入失败",
|
|
109
|
+
saveOk: "已保存",
|
|
110
|
+
saveFail: "保存失败",
|
|
111
|
+
discardConfirm: "有未保存的更改,离开将丢弃。确定离开吗?",
|
|
112
|
+
scopeFallback: "该 agent 预设的常驻 scope 未能挂载,以下显示的是全局层内容(并非该预设的原生段与工具)。",
|
|
113
|
+
previewDegraded: "伪会话触发了某个预设插件的异常,本次预览已降级为无会话装配——预设原生的阶段裁剪规则未参与运行。",
|
|
114
|
+
previewFail: "该阶段装配失败(scope 挂载失败或预设插件异常),请切换目标或查看服务日志。",
|
|
115
|
+
phaseVisible: "阶段可见",
|
|
116
|
+
previewToolsHint: "模型实际可见的工具目录(按所选阶段运行预设的全部装配规则,含原生阶段裁剪):",
|
|
117
|
+
previewToolsCount: "注册表",
|
|
118
|
+
saveAsPreset: "存为预设"
|
|
56
119
|
},
|
|
57
120
|
en: {
|
|
58
121
|
nav: "Prompt Customizer",
|
|
@@ -70,6 +133,7 @@ window.__ModuleLoader__.load({
|
|
|
70
133
|
system: "System",
|
|
71
134
|
replace: "Replace",
|
|
72
135
|
clearReplace: "Clear",
|
|
136
|
+
clearInput: "Clear",
|
|
73
137
|
injectNew: "Inject section",
|
|
74
138
|
name: "Name",
|
|
75
139
|
order: "Order",
|
|
@@ -89,21 +153,83 @@ window.__ModuleLoader__.load({
|
|
|
89
153
|
previewSections: "sections",
|
|
90
154
|
previewTools: "Tools preview",
|
|
91
155
|
previewToolCount: "tools",
|
|
92
|
-
savePreset: "Save current
|
|
93
|
-
presetName: "
|
|
156
|
+
savePreset: "Save current config",
|
|
157
|
+
presetName: "Config name",
|
|
158
|
+
saveAsPresetCard: "Save as agent preset",
|
|
159
|
+
saveAsPresetHint: "Copy the whole \"{name}\" preset into a new one (composition plus its companion scripts), then write the current customization into its override — the new preset shows up in the target selector above.",
|
|
160
|
+
agentPresetName: "New preset name",
|
|
161
|
+
forkSourceDefault: "the default preset",
|
|
162
|
+
saveAsPresetOk: "Preset created — pick it in the target selector above",
|
|
163
|
+
saveAsPresetFail: "Failed to save preset",
|
|
94
164
|
save: "Save",
|
|
95
165
|
importPreset: "Import preset",
|
|
96
166
|
import: "Import",
|
|
97
167
|
apply: "Apply",
|
|
98
168
|
export: "Export",
|
|
99
169
|
delete: "Delete",
|
|
170
|
+
restore: "Restore",
|
|
100
171
|
active: "Active",
|
|
101
|
-
preset: "Preset"
|
|
172
|
+
preset: "Preset",
|
|
173
|
+
ioFailed: "Import/export failed",
|
|
174
|
+
importInvalid: "Invalid preset file",
|
|
175
|
+
targetGlobal: "Target: global default",
|
|
176
|
+
targetHint: "Pick an edit target — changes apply only to the chosen agent preset (field-level override).",
|
|
177
|
+
broken: "broken",
|
|
178
|
+
brokenPreset: "This agent preset cannot mount right now; its customization is still editable and applies once fixed.",
|
|
179
|
+
phaseBootstrap: "Bootstrap only",
|
|
180
|
+
phaseActive: "After promotion",
|
|
181
|
+
phaseCompaction: "Post-compaction",
|
|
182
|
+
phaseStageGuide: "Guide period",
|
|
183
|
+
phaseStageResident: "Resident period",
|
|
184
|
+
phaseStageControlled: "Compaction-controlled period",
|
|
185
|
+
toolsPartGuide: "Guide-period tools",
|
|
186
|
+
toolsPartResident: "Resident-period tools",
|
|
187
|
+
toolsPartControlled: "Compaction-controlled tools",
|
|
188
|
+
allToolsTitle: "All system tools",
|
|
189
|
+
sectionsPartGuide: "Guide-period sections",
|
|
190
|
+
sectionsPartResident: "Resident-period sections",
|
|
191
|
+
sectionsPartControlled: "Compaction-controlled sections",
|
|
192
|
+
allSectionsTitle: "All system sections",
|
|
193
|
+
toolsFourHint: "Each of the three periods keeps its own list, with no inheritance: the checkbox = visible to the model in that period; dragging a tool from \"All\" into a period makes it appear there (if the period trims it by default, it is added back); dragging between two periods copies it (the source is untouched, the target gains one, and the hidden/enabled state carries over); dragging it back to \"All\" removes it from that period. Every action touches only the tool you dragged.",
|
|
194
|
+
sectionsFourHint: "Drag both ways: drop a section from \"All\" onto a period part to enable it there; drag from a part back to \"All\" to remove it from that period. Blocked sections stay fully operable — they are just not injected.",
|
|
195
|
+
dragHint: "Drag in to make this tool appear in this period",
|
|
196
|
+
phaseShortGuide: "G",
|
|
197
|
+
phaseShortResident: "R",
|
|
198
|
+
phaseShortControlled: "C",
|
|
199
|
+
poolMarksHint: "The three marks after each name = this tool’s state in the guide / resident / compaction-controlled periods: green = visible there (or added back), grey = hidden there, dim = not present and not added.",
|
|
200
|
+
toolAddedTag: "added",
|
|
201
|
+
toolAdded: "Added \"{name}\" back to {phase}: the period trims it by default, so it will reappear there once saved (it exists in this preset’s registry).",
|
|
202
|
+
toolNotInRegistry: "\"{name}\" is not in this preset’s registry (it belongs to another preset). There is no definition for it here, so it cannot be added.",
|
|
203
|
+
toolCopied: "Copied \"{name}\" to {to}: the copy in {from} is left untouched, and its hidden/enabled state carries over.",
|
|
204
|
+
toolShown: "\"{name}\" is now visible in {to}.",
|
|
205
|
+
toolHiddenIn: "\"{name}\" is now hidden in {to}.",
|
|
206
|
+
sectionsTakenOver: "The final system prompt here is taken over wholesale by the \"{name}\" section: the host restores that single section after the assembly waterfall, so this plugin’s section-level blocking / replacement / injection / ordering never reaches the model (tool filtering still applies). To make section customization effective, turn off the persona’s complete flag in this agent preset.",
|
|
207
|
+
sectionsLost: "Only {survived} of the {emitted} sections this plugin emitted for this phase reach the final prompt — the rest are dropped by downstream assembly rules, so section-level customization cannot fully apply (tool filtering is unaffected).",
|
|
208
|
+
sectionBlockedStrip: " (blocked in this period; invisible to the model)",
|
|
209
|
+
phaseSuppressed: "Suppressed (absent from every phase assembly)",
|
|
210
|
+
sectionDynamicNoAdd: "\"{name}\" is generated at runtime — it cannot be added to a phase ahead of time (its content is only known during assembly).",
|
|
211
|
+
exportOk: "Exported",
|
|
212
|
+
exportCancel: "Export cancelled",
|
|
213
|
+
exportFail: "Export failed",
|
|
214
|
+
importOk: "Import complete",
|
|
215
|
+
importNone: "Nothing imported (duplicate names or empty file)",
|
|
216
|
+
importCancel: "Import cancelled",
|
|
217
|
+
importFail: "Import failed",
|
|
218
|
+
saveOk: "Saved",
|
|
219
|
+
saveFail: "Save failed",
|
|
220
|
+
discardConfirm: "You have unsaved changes; leaving will discard them. Leave anyway?",
|
|
221
|
+
scopeFallback: "This agent preset's standing scope could not be mounted; what you see is the global layer (not the preset's native sections/tools).",
|
|
222
|
+
previewDegraded: "A preset plugin threw on the synthetic session; this preview fell back to an agent-less assembly — native phase rules did not run.",
|
|
223
|
+
previewFail: "Assembly for this phase failed (scope mount error or a preset plugin threw); switch target or check the server log.",
|
|
224
|
+
phaseVisible: "visible in phase",
|
|
225
|
+
previewToolsHint: "Tool catalog as the model actually sees it (all assembly rules run for the selected phase, including native phase narrowing):",
|
|
226
|
+
previewToolsCount: "of registry",
|
|
227
|
+
saveAsPreset: "Save as preset"
|
|
102
228
|
}
|
|
103
229
|
};
|
|
104
230
|
//#endregion
|
|
105
231
|
//#region src/client/styles.ts
|
|
106
|
-
/**
|
|
232
|
+
/** 提示词定制面板各组件共享的内联样式(颜色走半透明灰,随宿主主题自然适配)。 */
|
|
107
233
|
const s = (() => {
|
|
108
234
|
const flex = {
|
|
109
235
|
display: "flex",
|
|
@@ -327,6 +453,10 @@ window.__ModuleLoader__.load({
|
|
|
327
453
|
background: "transparent",
|
|
328
454
|
color: "inherit"
|
|
329
455
|
},
|
|
456
|
+
option: {
|
|
457
|
+
backgroundColor: "Canvas",
|
|
458
|
+
color: "CanvasText"
|
|
459
|
+
},
|
|
330
460
|
muted: {
|
|
331
461
|
color: "rgba(128,128,128,.75)",
|
|
332
462
|
fontSize: 12
|
|
@@ -335,19 +465,98 @@ window.__ModuleLoader__.load({
|
|
|
335
465
|
color: "#f85149",
|
|
336
466
|
fontSize: 12,
|
|
337
467
|
marginBottom: 6
|
|
468
|
+
},
|
|
469
|
+
noticeOk: {
|
|
470
|
+
color: "#3fb950",
|
|
471
|
+
fontSize: 12,
|
|
472
|
+
marginBottom: 6
|
|
473
|
+
},
|
|
474
|
+
noticeWarn: {
|
|
475
|
+
color: "#d29922",
|
|
476
|
+
fontSize: 12,
|
|
477
|
+
marginBottom: 6
|
|
478
|
+
},
|
|
479
|
+
groupTitle: {
|
|
480
|
+
display: "flex",
|
|
481
|
+
gap: 8,
|
|
482
|
+
alignItems: "baseline",
|
|
483
|
+
marginTop: 8,
|
|
484
|
+
marginBottom: 2,
|
|
485
|
+
color: "rgba(128,128,128,.9)",
|
|
486
|
+
fontSize: 12
|
|
487
|
+
},
|
|
488
|
+
saveBtn: {
|
|
489
|
+
padding: "4px 12px",
|
|
490
|
+
border: "1px solid rgba(128,128,128,.35)",
|
|
491
|
+
borderRadius: 6,
|
|
492
|
+
background: "transparent",
|
|
493
|
+
cursor: "pointer",
|
|
494
|
+
color: "inherit"
|
|
495
|
+
},
|
|
496
|
+
saveBtnDirty: {
|
|
497
|
+
padding: "4px 12px",
|
|
498
|
+
border: "1px solid #d29922",
|
|
499
|
+
borderRadius: 6,
|
|
500
|
+
background: "rgba(210,153,34,.15)",
|
|
501
|
+
cursor: "pointer",
|
|
502
|
+
color: "inherit"
|
|
338
503
|
}
|
|
339
504
|
};
|
|
340
505
|
})();
|
|
341
506
|
//#endregion
|
|
342
507
|
//#region src/client/presets.ts
|
|
508
|
+
/** 快照捕获的阶段 order 空间(always 之外的三个阶段)。 */
|
|
509
|
+
const CAPTURE_PHASES = [
|
|
510
|
+
"bootstrap",
|
|
511
|
+
"active",
|
|
512
|
+
"compaction"
|
|
513
|
+
];
|
|
514
|
+
/** 把一条有序名字转成相对链(每段记住它的前一段),text / custom 只给 custom 段。 */
|
|
515
|
+
function toChain(names, textOf, customOf, phase) {
|
|
516
|
+
return names.map((name, i) => ({
|
|
517
|
+
name,
|
|
518
|
+
after: i > 0 ? names[i - 1] : void 0,
|
|
519
|
+
text: customOf(name) ? textOf(name) ?? "" : "",
|
|
520
|
+
custom: customOf(name),
|
|
521
|
+
...phase !== void 0 ? { phase } : {}
|
|
522
|
+
}));
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* 从当前配置构建预设快照(完整捕获,含每阶段独立设定)。
|
|
526
|
+
*
|
|
527
|
+
* 全局相对链派生自合并后的完整段列表(而不仅是 `cfg.inject`),这样即使保存前
|
|
528
|
+
* 只是屏蔽了几个段(没有重排),预设仍会携带完整的有序集合,应用时才能得到一个
|
|
529
|
+
* 有意义的「激活列表」。在此之上,bootstrap / active / compaction 各再带一条自己
|
|
530
|
+
* 的相对链 —— 数据源就是 UI 逐阶段持久化的注入条目(每阶段独立的 order 空间),
|
|
531
|
+
* 否则阶段化的名单与排序在「导出 → 导入 → 应用」后就丢了。
|
|
532
|
+
*/
|
|
533
|
+
function buildPresetData(cfg, merged) {
|
|
534
|
+
const globalNames = merged.map((sec) => sec.name);
|
|
535
|
+
const textOf = new Map(merged.map((sec) => [sec.name, sec.text ?? ""]));
|
|
536
|
+
const customOf = new Map(merged.map((sec) => [sec.name, sec.source === "custom"]));
|
|
537
|
+
const order = toChain(globalNames, (n) => textOf.get(n) ?? "", (n) => customOf.get(n) === true);
|
|
538
|
+
for (const phase of CAPTURE_PHASES) {
|
|
539
|
+
const items = (cfg.inject ?? []).filter((item) => item.phase === phase).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
540
|
+
if (items.length === 0) continue;
|
|
541
|
+
order.push(...toChain(items.map((item) => item.name), (n) => items.find((item) => item.name === n)?.text ?? "", (n) => items.find((item) => item.name === n)?.custom === true, phase));
|
|
542
|
+
}
|
|
543
|
+
const data = {
|
|
544
|
+
sections: cfg.sections,
|
|
545
|
+
replace: cfg.replace,
|
|
546
|
+
order,
|
|
547
|
+
tools: cfg.tools
|
|
548
|
+
};
|
|
549
|
+
if ((cfg.sectionsBootstrap ?? []).length > 0) data.sectionsBootstrap = cfg.sectionsBootstrap;
|
|
550
|
+
if ((cfg.sectionsCompaction ?? []).length > 0) data.sectionsCompaction = cfg.sectionsCompaction;
|
|
551
|
+
return data;
|
|
552
|
+
}
|
|
343
553
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* always render their own text; they can never be "<动态生成>".
|
|
554
|
+
* 把宿主清单段与用户注入列表按 order 合并、排序。
|
|
555
|
+
* 读自清单(其他插件产出)的段标记为 `system`;本插件生成的段带隐藏的
|
|
556
|
+
* `custom` 标记,标记为 `custom`。来源只由隐藏标记决定——绝不靠名字碰撞
|
|
557
|
+
* 判断——因此即使自定义段恰好与某个清单段重名,它也能保住自己的身份
|
|
558
|
+
* (以及可删除性),并在切换预设后依然成立。自定义段始终渲染自己的文本,
|
|
559
|
+
* 永远不会显示为「<动态生成>」。
|
|
351
560
|
*/
|
|
352
561
|
function mergeSections(inv, cfg, blockedNames) {
|
|
353
562
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -377,24 +586,19 @@ window.__ModuleLoader__.load({
|
|
|
377
586
|
source: isCustom ? "custom" : "system"
|
|
378
587
|
});
|
|
379
588
|
}
|
|
380
|
-
return [...map.values()].sort((a, b) => a.order - b.order)
|
|
589
|
+
return [...map.values()].sort((a, b) => a.order - b.order).map((sec, i) => ({
|
|
590
|
+
...sec,
|
|
591
|
+
order: i
|
|
592
|
+
}));
|
|
381
593
|
}
|
|
382
594
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* remaining sections (cycles) are appended.
|
|
595
|
+
* 把一条相对链解析为绝对有序名字(0..n-1)。
|
|
596
|
+
* 没有锚点(或锚点不在本组内)的名字按原始顺序排在最前;每个带锚点的名字
|
|
597
|
+
* 插到其锚点之后;剩余的名字(存在环)追加到末尾。
|
|
387
598
|
*/
|
|
388
|
-
function
|
|
389
|
-
const list = presetOrder ?? [];
|
|
599
|
+
function resolveChain(list) {
|
|
390
600
|
const afterMap = /* @__PURE__ */ new Map();
|
|
391
|
-
const
|
|
392
|
-
const customMap = /* @__PURE__ */ new Map();
|
|
393
|
-
for (const sec of list) {
|
|
394
|
-
afterMap.set(sec.name, sec.after);
|
|
395
|
-
textMap.set(sec.name, sec.text);
|
|
396
|
-
customMap.set(sec.name, sec.custom === true);
|
|
397
|
-
}
|
|
601
|
+
for (const sec of list) afterMap.set(sec.name, sec.after);
|
|
398
602
|
const result = [];
|
|
399
603
|
const placed = /* @__PURE__ */ new Set();
|
|
400
604
|
for (const sec of list) {
|
|
@@ -421,45 +625,56 @@ window.__ModuleLoader__.load({
|
|
|
421
625
|
result.push(sec.name);
|
|
422
626
|
placed.add(sec.name);
|
|
423
627
|
}
|
|
424
|
-
return result
|
|
425
|
-
name,
|
|
426
|
-
order: i,
|
|
427
|
-
text: textMap.get(name) ?? "",
|
|
428
|
-
custom: customMap.get(name)
|
|
429
|
-
}));
|
|
628
|
+
return result;
|
|
430
629
|
}
|
|
431
630
|
/**
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
631
|
+
* 把快照里的相对顺序解析为注入条目列表。
|
|
632
|
+
*
|
|
633
|
+
* order 条目可以带 `phase`:同名而不同 phase 的多条 = 该段在多个阶段各有自己的
|
|
634
|
+
* 位置(每阶段有独立的 order 空间,与 UI 的 phaseInjectEntries 一致)。无 phase
|
|
635
|
+
* 的条目属于 always 组 —— 也就是旧快照的单一全局序,输出形状与旧实现逐键一致
|
|
636
|
+
* (刻意不写 phase 键,「保存 → 应用 = 不动点」的不变量依赖键形状完全相同)。
|
|
436
637
|
*/
|
|
437
|
-
function
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
638
|
+
function resolveOrder(presetOrder) {
|
|
639
|
+
const list = presetOrder ?? [];
|
|
640
|
+
const groups = /* @__PURE__ */ new Map([["always", []]]);
|
|
641
|
+
for (const sec of list) {
|
|
642
|
+
const phase = sec.phase ?? "always";
|
|
643
|
+
const group = groups.get(phase);
|
|
644
|
+
if (group) group.push(sec);
|
|
645
|
+
else groups.set(phase, [sec]);
|
|
646
|
+
}
|
|
647
|
+
const out = [];
|
|
648
|
+
for (const [phase, members] of groups) {
|
|
649
|
+
const textMap = new Map(members.map((x) => [x.name, x.text ?? ""]));
|
|
650
|
+
const customMap = new Map(members.map((x) => [x.name, x.custom === true]));
|
|
651
|
+
const names = resolveChain(members);
|
|
652
|
+
out.push(...names.map((name, i) => phase === "always" ? {
|
|
653
|
+
name,
|
|
654
|
+
order: i,
|
|
655
|
+
text: textMap.get(name) ?? "",
|
|
656
|
+
custom: customMap.get(name)
|
|
657
|
+
} : {
|
|
658
|
+
name,
|
|
659
|
+
order: i,
|
|
660
|
+
text: textMap.get(name) ?? "",
|
|
661
|
+
custom: customMap.get(name),
|
|
662
|
+
phase
|
|
663
|
+
}));
|
|
664
|
+
}
|
|
665
|
+
return out;
|
|
450
666
|
}
|
|
451
667
|
/**
|
|
452
|
-
*
|
|
453
|
-
* -
|
|
454
|
-
* -
|
|
455
|
-
* -
|
|
456
|
-
* -
|
|
457
|
-
*
|
|
668
|
+
* 计算应用预设时的配置补丁:
|
|
669
|
+
* - 同名段被覆盖(顺序列表在运行时驱动这一行为)
|
|
670
|
+
* - 预设中有、当前提示词中没有的段被添加
|
|
671
|
+
* - 当前有、但不在预设有序列表中的段默认被屏蔽
|
|
672
|
+
* - 只有预设的「激活段」(在 order 列表且不在其屏蔽名单中)被解除屏蔽;
|
|
673
|
+
* 预设自己屏蔽的段保持屏蔽。
|
|
458
674
|
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* dropped — a preset must not make the user's own injected sections vanish.
|
|
675
|
+
* 当前配置里存在、但不在预设有序列表中的自定义注入段会被保留到结果的
|
|
676
|
+
* inject 列表中(因此仍然可见)并置为禁用(加入屏蔽集合),而不是被静默
|
|
677
|
+
* 丢弃——预设不能让用户自己注入的凭空消失。
|
|
463
678
|
*/
|
|
464
679
|
function applyPresetData(data, cfg, currentNames) {
|
|
465
680
|
const presetOrder = data.order ?? [];
|
|
@@ -483,7 +698,12 @@ window.__ModuleLoader__.load({
|
|
|
483
698
|
for (const name of currentNames) if (!presetNames.has(name)) blocked.add(name);
|
|
484
699
|
for (const name of activeNames) blocked.delete(name);
|
|
485
700
|
}
|
|
486
|
-
|
|
701
|
+
const keptCatalogs = {};
|
|
702
|
+
const bootstrap = data.tools?.bootstrap !== void 0 ? data.tools.bootstrap : cfg.tools?.bootstrap;
|
|
703
|
+
const compaction = data.tools?.compaction !== void 0 ? data.tools.compaction : cfg.tools?.compaction;
|
|
704
|
+
if (bootstrap !== void 0) keptCatalogs.bootstrap = bootstrap;
|
|
705
|
+
if (compaction !== void 0) keptCatalogs.compaction = compaction;
|
|
706
|
+
const patch = {
|
|
487
707
|
sections: [...blocked],
|
|
488
708
|
replace: {
|
|
489
709
|
...cfg.replace ?? {},
|
|
@@ -492,42 +712,123 @@ window.__ModuleLoader__.load({
|
|
|
492
712
|
inject,
|
|
493
713
|
tools: {
|
|
494
714
|
exclude: data.tools?.exclude ?? [],
|
|
495
|
-
|
|
715
|
+
...keptCatalogs
|
|
496
716
|
}
|
|
497
717
|
};
|
|
718
|
+
if (Array.isArray(data.sectionsBootstrap)) patch.sectionsBootstrap = data.sectionsBootstrap;
|
|
719
|
+
if (Array.isArray(data.sectionsCompaction)) patch.sectionsCompaction = data.sectionsCompaction;
|
|
720
|
+
return patch;
|
|
498
721
|
}
|
|
722
|
+
/** 三个阶段部分的固定展示顺序:引导期 → 常驻期 → 压缩受控期。
|
|
723
|
+
* 恒定全部渲染 —— 预设没有某个阶段时该部分只是空的,绝不隐藏。 */
|
|
724
|
+
const PART_ORDER = [
|
|
725
|
+
"bootstrap",
|
|
726
|
+
"active",
|
|
727
|
+
"compaction"
|
|
728
|
+
];
|
|
499
729
|
/**
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* text for it is cleared. The remaining inject entries are re-indexed to a
|
|
503
|
-
* contiguous 0..n-1 order (the virtual index model), so removal never leaves
|
|
504
|
-
* holes. System sections read from the host inventory cannot be removed this
|
|
505
|
-
* way (they would just come back on the next inventory read).
|
|
730
|
+
* 一个阶段部分写回哪一份过滤配置:引导期 → `tools.bootstrap`、压缩受控期 →
|
|
731
|
+
* `tools.compaction`、常驻期 → 静态 `tools.exclude`。三份名单互不继承。
|
|
506
732
|
*/
|
|
507
|
-
function
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
733
|
+
function phaseConfigKey(key) {
|
|
734
|
+
return key === "active" ? "static" : key;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* 把一个阶段的 exclude 名单写回整份工具配置,返回新对象(其余阶段原样保留)。
|
|
738
|
+
* 一次拖放可能要同时改两个阶段的名单(搬移 = 源阶段隐藏 + 目标阶段显示),必须
|
|
739
|
+
* 在同一个对象上连续套完再落一次写入 —— 分两次写会各自基于旧 cfg 计算而互相覆盖。
|
|
740
|
+
*/
|
|
741
|
+
function withPhaseExclude(tools, key, exclude) {
|
|
742
|
+
const base = tools ?? {};
|
|
743
|
+
const target = phaseConfigKey(key);
|
|
744
|
+
if (target === "bootstrap") return {
|
|
745
|
+
...base,
|
|
746
|
+
bootstrap: {
|
|
747
|
+
...base.bootstrap ?? {},
|
|
748
|
+
exclude
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
if (target === "compaction") return {
|
|
752
|
+
...base,
|
|
753
|
+
compaction: {
|
|
754
|
+
...base.compaction ?? {},
|
|
755
|
+
exclude
|
|
756
|
+
}
|
|
757
|
+
};
|
|
515
758
|
return {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
759
|
+
...base,
|
|
760
|
+
exclude
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* 把一个阶段的 add 名单(要加回该阶段的工具)写回整份工具配置,返回新对象。
|
|
765
|
+
* 与 withPhaseExclude 对称:常驻期写静态 `tools.add`,引导期写 `tools.bootstrap.add`,
|
|
766
|
+
* 压缩受控期写 `tools.compaction.add`,其余阶段原样保留。
|
|
767
|
+
*/
|
|
768
|
+
function withPhaseAdd(tools, key, add) {
|
|
769
|
+
const base = tools ?? {};
|
|
770
|
+
const target = phaseConfigKey(key);
|
|
771
|
+
if (target === "bootstrap") return {
|
|
772
|
+
...base,
|
|
773
|
+
bootstrap: {
|
|
774
|
+
...base.bootstrap ?? {},
|
|
775
|
+
add
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
if (target === "compaction") return {
|
|
779
|
+
...base,
|
|
780
|
+
compaction: {
|
|
781
|
+
...base.compaction ?? {},
|
|
782
|
+
add
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
return {
|
|
786
|
+
...base,
|
|
787
|
+
add
|
|
519
788
|
};
|
|
520
789
|
}
|
|
521
|
-
/**
|
|
522
|
-
function
|
|
790
|
+
/** 某个工具在当前黑名单配置下是否被隐藏。 */
|
|
791
|
+
function isToolHidden(name, toolsCfg) {
|
|
792
|
+
return (toolsCfg?.exclude ?? []).includes(name);
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* 编辑目标的「显示视图」:全局目标返回原配置;agent 预设目标把该预设
|
|
796
|
+
* override 的字段叠在全局值之上展示(空缺字段显示全局继承值)。`presets`
|
|
797
|
+
* 与 `activePreset` 永远来自全局 —— 预设快照库本身不分作用域。
|
|
798
|
+
*/
|
|
799
|
+
function editView(cfg, target) {
|
|
800
|
+
if (!target) return cfg;
|
|
801
|
+
const ovr = cfg.overrides?.[target] ?? {};
|
|
523
802
|
return {
|
|
524
|
-
|
|
525
|
-
|
|
803
|
+
sections: ovr.sections ?? cfg.sections,
|
|
804
|
+
sectionsBootstrap: ovr.sectionsBootstrap ?? cfg.sectionsBootstrap,
|
|
805
|
+
sectionsCompaction: ovr.sectionsCompaction ?? cfg.sectionsCompaction,
|
|
806
|
+
replace: ovr.replace ?? cfg.replace,
|
|
807
|
+
inject: ovr.inject ?? cfg.inject,
|
|
808
|
+
tools: {
|
|
809
|
+
exclude: ovr.tools?.exclude ?? cfg.tools?.exclude,
|
|
810
|
+
add: ovr.tools?.add ?? cfg.tools?.add,
|
|
811
|
+
bootstrap: ovr.tools?.bootstrap ?? cfg.tools?.bootstrap,
|
|
812
|
+
compaction: ovr.tools?.compaction ?? cfg.tools?.compaction
|
|
813
|
+
},
|
|
814
|
+
presets: cfg.presets,
|
|
815
|
+
activePreset: cfg.activePreset
|
|
526
816
|
};
|
|
527
817
|
}
|
|
528
818
|
/**
|
|
529
|
-
*
|
|
530
|
-
*
|
|
819
|
+
* Derive a safe default filename for a preset export. User-entered preset
|
|
820
|
+
* names may contain characters that are illegal in filenames (`/\:*?"<>|`,
|
|
821
|
+
* control chars) — those are replaced; a trailing dot/space is trimmed
|
|
822
|
+
* (invalid on Windows); an empty or all-illegal result falls back to
|
|
823
|
+
* `preset`.
|
|
824
|
+
*/
|
|
825
|
+
function presetExportFilename(name) {
|
|
826
|
+
const sanitized = String(name).replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_").trim().replace(/[. ]+$/, "");
|
|
827
|
+
return `${sanitized === "" ? "preset" : sanitized}.json`;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* 导入预设:同名的会被跳过,只追加名字不同的预设。返回新的预设列表
|
|
831
|
+
* (若没有任何变更则返回原列表)。
|
|
531
832
|
*/
|
|
532
833
|
function addImportedPresets(existing, parsed, makeId) {
|
|
533
834
|
const incoming = Array.isArray(parsed) ? parsed : [parsed];
|
|
@@ -539,225 +840,659 @@ window.__ModuleLoader__.load({
|
|
|
539
840
|
}));
|
|
540
841
|
return added.length > 0 ? [...existing, ...added] : existing;
|
|
541
842
|
}
|
|
542
|
-
/**
|
|
843
|
+
/** 删除某个预设;若它正是当前激活的预设,则同时清除 activeId。 */
|
|
543
844
|
function removePreset(presets, id, activeId) {
|
|
544
845
|
return {
|
|
545
846
|
presets: presets.filter((p) => p.id !== id),
|
|
546
847
|
activeId: activeId === id ? void 0 : activeId
|
|
547
848
|
};
|
|
548
849
|
}
|
|
850
|
+
/** 生成预设 id:时间戳 + 随机段(base36),够用且无需额外依赖。 */
|
|
851
|
+
function genId() {
|
|
852
|
+
return "p_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region lib/sectionOps.mjs
|
|
856
|
+
/**
|
|
857
|
+
* 提示词段面板的纯状态逻辑(无 React / 无 IO)。
|
|
858
|
+
* 与 src/client/SectionsTab.tsx 共享同一份实现(浏览器里随 client bundle
|
|
859
|
+
* 打包;node --test 直接 import 本文件跑单测),保证「测试即上线代码」。
|
|
860
|
+
*
|
|
861
|
+
* 阶段模型:引导期(bootstrap)/ 常驻期(active)/ 压缩受控期(compaction)
|
|
862
|
+
* 恒定全部显示(预设没有某个阶段时该部分就是空的),各自拥有独立的注入
|
|
863
|
+
* 名单与 order 空间;屏蔽按「每阶段独立名单」写回(引导期 →
|
|
864
|
+
* sectionsBootstrap,压缩受控期 → sectionsCompaction,常驻期 → 全局 sections)。
|
|
865
|
+
* 入参统一是名义阶段键,不再有「同形折叠组」这种把引导期当常驻期用的形态。
|
|
866
|
+
*/
|
|
867
|
+
/** 名义阶段键 → 屏蔽名单写回目标(常驻期用全局 sections)。 */
|
|
868
|
+
function sectionListOf(key) {
|
|
869
|
+
return key === "bootstrap" ? "bootstrap" : key === "compaction" ? "compaction" : "global";
|
|
870
|
+
}
|
|
871
|
+
/** 名义阶段键 → 注入阶段:压缩受控期是独立注入阶段(compaction)。 */
|
|
872
|
+
function injectPhaseOf(key) {
|
|
873
|
+
return key === "bootstrap" ? "bootstrap" : key === "active" ? "active" : "compaction";
|
|
874
|
+
}
|
|
875
|
+
/** 某注入段出现在哪些阶段部分:always 全阶段;bootstrap/compaction/active
|
|
876
|
+
* 仅各自对应的阶段(与服务端 filterInjectByPhase 的三态互相独立一致)。 */
|
|
877
|
+
function acceptsInjectFor(key, phase) {
|
|
878
|
+
if (phase === "always") return true;
|
|
879
|
+
return phase === key;
|
|
880
|
+
}
|
|
881
|
+
/** 该阶段生效的屏蔽名单(全局 + 阶段独立 的 union)。 */
|
|
882
|
+
function deniedNames(cfg, key) {
|
|
883
|
+
const list = sectionListOf(key);
|
|
884
|
+
const phase = list === "bootstrap" ? cfg.sectionsBootstrap ?? [] : list === "compaction" ? cfg.sectionsCompaction ?? [] : [];
|
|
885
|
+
return [...cfg.sections ?? [], ...phase];
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* 某个阶段部分当前的注入身份(含未保存草稿)。
|
|
889
|
+
*
|
|
890
|
+
* `names` = 本部分可见的注入段名(本阶段专属 + 跨阶段的 always)。post 视图来自
|
|
891
|
+
* 「上次保存」的服务端结果,删除只改草稿,所以 post 独有段必须有 names 背书才
|
|
892
|
+
* 能显示 —— 否则刚删掉的自定义段会以「系统段」复活(身份只认 custom 标记,条目
|
|
893
|
+
* 没了就被判成系统),再经一次重排就被写成 custom:false 空文本,用户填的内容被
|
|
894
|
+
* 抹平。`custom` 只收真正的自定义段,`order` 只收本阶段的草稿序。
|
|
895
|
+
*
|
|
896
|
+
* `text` = 本阶段生效的用户文本:自定义段自带的文本,以及**系统段在本阶段被
|
|
897
|
+
* 替换后的文本**(非空即替换)。系统段的文本必须按阶段存 —— 注入条目本来就带
|
|
898
|
+
* `phase`,服务端按阶段筛选,所以三个阶段各改各的文本互不影响;旧的全局
|
|
899
|
+
* `replace` 字典一份文本对三个阶段同时生效,已不承担界面编辑。
|
|
900
|
+
*/
|
|
901
|
+
function injectedAt(cfg, key) {
|
|
902
|
+
const phase = injectPhaseOf(key);
|
|
903
|
+
const names = /* @__PURE__ */ new Set();
|
|
904
|
+
const custom = /* @__PURE__ */ new Set();
|
|
905
|
+
const text = /* @__PURE__ */ new Map();
|
|
906
|
+
const order = /* @__PURE__ */ new Map();
|
|
907
|
+
for (const item of cfg.inject ?? []) {
|
|
908
|
+
if (item === null || typeof item !== "object" || typeof item.name !== "string") continue;
|
|
909
|
+
const itemPhase = item.phase ?? "always";
|
|
910
|
+
if (itemPhase === phase) order.set(item.name, item.order ?? 0);
|
|
911
|
+
if (!acceptsInjectFor(key, itemPhase)) continue;
|
|
912
|
+
names.add(item.name);
|
|
913
|
+
if (item.text && !text.has(item.name)) text.set(item.name, item.text);
|
|
914
|
+
if (item.custom === true) custom.add(item.name);
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
phase,
|
|
918
|
+
names,
|
|
919
|
+
custom,
|
|
920
|
+
text,
|
|
921
|
+
order
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* 屏蔽/恢复一个段(当前阶段语义):返回需要写盘的字段补丁。
|
|
926
|
+
* - 屏蔽:该阶段名单加入(常驻期直接加入全局 sections)。
|
|
927
|
+
* - 恢复:从该阶段名单与全局名单同时移除(union 语义 —— 否则仍会被另一
|
|
928
|
+
* 份名单挡住,恢复不生效)。
|
|
929
|
+
* 名单无变化时不产出字段(空补丁 = 不写盘),避免把继承值冻结成无意义的
|
|
930
|
+
* 空数组覆盖。
|
|
931
|
+
*/
|
|
932
|
+
function blockPatch(cfg, key, name, blocked) {
|
|
933
|
+
const list = sectionListOf(key);
|
|
934
|
+
const g = (cfg.sections ?? []).slice();
|
|
935
|
+
const gi = g.indexOf(name);
|
|
936
|
+
const patch = {};
|
|
937
|
+
if (list === "global") {
|
|
938
|
+
if (blocked) {
|
|
939
|
+
if (gi < 0) {
|
|
940
|
+
g.push(name);
|
|
941
|
+
patch.sections = g;
|
|
942
|
+
}
|
|
943
|
+
} else if (gi >= 0) {
|
|
944
|
+
g.splice(gi, 1);
|
|
945
|
+
patch.sections = g;
|
|
946
|
+
}
|
|
947
|
+
return patch;
|
|
948
|
+
}
|
|
949
|
+
const field = list === "bootstrap" ? "sectionsBootstrap" : "sectionsCompaction";
|
|
950
|
+
const phase = (cfg[field] ?? []).slice();
|
|
951
|
+
const pi = phase.indexOf(name);
|
|
952
|
+
if (blocked) {
|
|
953
|
+
if (pi < 0) {
|
|
954
|
+
phase.push(name);
|
|
955
|
+
patch[field] = phase;
|
|
956
|
+
}
|
|
957
|
+
} else {
|
|
958
|
+
if (pi >= 0) {
|
|
959
|
+
phase.splice(pi, 1);
|
|
960
|
+
patch[field] = phase;
|
|
961
|
+
}
|
|
962
|
+
if (gi >= 0) {
|
|
963
|
+
g.splice(gi, 1);
|
|
964
|
+
patch.sections = g;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return patch;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* 重排/插入一个阶段的有序行列表:把 dragName 从列表中移除后,插入到
|
|
971
|
+
* targetName 行之上(pos='above')或之下(pos='below');返回新列表。
|
|
972
|
+
* 拖入的新段由调用方以 newRow 提供,插入后保留在列表中(其余行保持原样)。
|
|
973
|
+
*/
|
|
974
|
+
function reorderInsert(rows, dragName, targetName, pos, newRow = null) {
|
|
975
|
+
const base = rows.filter((row) => row.name !== dragName);
|
|
976
|
+
const idx = base.findIndex((row) => row.name === targetName);
|
|
977
|
+
if (idx < 0) return null;
|
|
978
|
+
const insertAt = Math.max(0, Math.min(pos === "above" ? idx : idx + 1, base.length));
|
|
979
|
+
const entry = newRow ?? rows.find((row) => row.name === dragName);
|
|
980
|
+
if (!entry) return null;
|
|
981
|
+
base.splice(insertAt, 0, entry);
|
|
982
|
+
return base;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* 持久化一个阶段的有序列表(v1 persistOrder 的逐阶段版):把 rows(有序行)
|
|
986
|
+
* 写成该阶段的注入条目,order = 连续整数(0,1,2,…)——「虚拟 order 决定注入
|
|
987
|
+
* 顺序」;文本优先级 = 本阶段的用户替换文本(row.override)→ 自定义段自带的
|
|
988
|
+
* 文本(row.text)→ 空文本(= 仅 order 覆盖,服务端保留原文)。三者缺一不可:
|
|
989
|
+
* 重排走的就是这个函数,漏掉 override 会让一次拖动把用户在该阶段改的文本抹平。
|
|
990
|
+
* 其它阶段(含 always)的注入条目原样保留。
|
|
991
|
+
*/
|
|
992
|
+
function phaseInjectEntries(cfg, key, rows) {
|
|
993
|
+
const phase = injectPhaseOf(key);
|
|
994
|
+
const others = (cfg.inject ?? []).filter((item) => (item.phase ?? "always") !== phase);
|
|
995
|
+
const entries = rows.map((row, i) => ({
|
|
996
|
+
name: row.name,
|
|
997
|
+
order: i,
|
|
998
|
+
text: row.override || (row.custom ? row.text ?? "" : ""),
|
|
999
|
+
phase,
|
|
1000
|
+
custom: row.custom
|
|
1001
|
+
}));
|
|
1002
|
+
return [...others, ...entries];
|
|
1003
|
+
}
|
|
549
1004
|
//#endregion
|
|
550
1005
|
//#region src/client/SectionsTab.tsx
|
|
551
|
-
/**
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
1006
|
+
/** 提示词段 Tab:四个部分 —— 引导期 / 常驻期 / 压缩受控期(三个阶段恒定
|
|
1007
|
+
* 显示,预设没有某个阶段时该部分只是空的)+ 本系统全部提示词(只读池)。
|
|
1008
|
+
*
|
|
1009
|
+
* 交互对齐提示词段初版(v1),并修复三类反馈缺失:
|
|
1010
|
+
* - 上三段每行 = 真实进入该阶段装配的段(post 视图,与预览一致 —— 预设
|
|
1011
|
+
* 原生阶段插件裁剪掉的段不显示)∪ 被屏蔽的段 ∪ 配置里的自定义注入段,
|
|
1012
|
+
* 顺序以 base 骨架为底再叠加草稿序;整行可拖拽,拖到某行上方/下方出现
|
|
1013
|
+
* 插入指示线,松手即在该位置重排。
|
|
1014
|
+
* - 被屏蔽的段与普通段一模一样:留在列表原位、一样可操作(拖拽/箭头/替换/
|
|
1015
|
+
* 恢复/删除),随时可互相转换,唯一区别是屏蔽勾选框与「已屏蔽」徽标 ——
|
|
1016
|
+
* 屏蔽只意味着不注入模型,不剥夺任何编辑能力(v1 语义)。屏蔽勾选直接
|
|
1017
|
+
* 绑定当前生效名单(deniedNames = 全局 + 该阶段名单,含未保存草稿),
|
|
1018
|
+
* 点击立即反馈。
|
|
1019
|
+
* - 重排(拖入/箭头)走 `persistPhase`:把该阶段全部行重写为连续整数
|
|
1020
|
+
* 虚拟 order(系统段写空文本 = 仅 order 覆盖,服务端保留原文;custom 段
|
|
1021
|
+
* 保留文本)—— UI 行序叠加同一份 order 立即重排,拖动即时可见。
|
|
1022
|
+
* - 文本编辑(「编辑」/「还原」)同样按阶段生效:写的就是本阶段注入条目的
|
|
1023
|
+
* 文本,三个阶段各改各的、互不影响(服务端让本阶段注入文本优先于全局
|
|
1024
|
+
* replace,否则一份文本会同时改坏三个阶段)。全局 replace 字典自此不再由
|
|
1025
|
+
* 界面写入,只在「还原」时顺带清除遗留条目 —— 新界面没有别的出口能恢复它。
|
|
1026
|
+
* - 拖放搬移保留行对象(system/custom 身份与文本原样带走),绝不重建行 ——
|
|
1027
|
+
* 重建会把系统段误标成 custom 并清空内容。只有从其它部分/全部池拖入的
|
|
1028
|
+
* 新段才构造新行(身份取自拖拽源)。
|
|
1029
|
+
* - 拖放事件在行上处理并 stopPropagation,避免与阶段框二次处理对冲。
|
|
1030
|
+
* - 全部提示词 → 上三段:插入该段到目标位置(已存在则只是移动排序;该段
|
|
1031
|
+
* 在目标阶段被屏蔽则同步解除屏蔽 —— 行立即出现)。上三段 → 全部:从该
|
|
1032
|
+
* 阶段移除 —— custom 段删除注入条目;系统段等价于屏蔽该阶段(系统段是
|
|
1033
|
+
* 装配的一部分,无法真正删除)。
|
|
1034
|
+
* - 解除屏蔽按份额归属分流写入:继承自全局名单的屏蔽直接改全局名单(立即
|
|
1035
|
+
* 写盘通道)—— override 无法表达「比全局更宽松」(空名单回落全局,解除
|
|
1036
|
+
* 会在保存后复现);override 自报名单与阶段名单的份额走目标草稿。
|
|
1037
|
+
* - 每个阶段部分按自己那一阶段的装配结果给出警示:该 scope 有 complete 段
|
|
1038
|
+
* 整段接管最终提示词,或本插件产出的段被下游装配规则丢弃时,明确写出
|
|
1039
|
+
* 「段级定制不会(完全)进入模型看到的提示词」,避免提示词 Tab 与预览 Tab
|
|
1040
|
+
* 各说各话。
|
|
1041
|
+
*
|
|
1042
|
+
* 全部阶段状态逻辑来自 lib/sectionOps.mjs(纯函数,node --test 单测直接
|
|
1043
|
+
* 覆盖同一份代码)。
|
|
1044
|
+
*/
|
|
1045
|
+
/** 编辑态的键:段名之外还要带阶段,否则三个部分里同名的行会同时展开编辑器。 */
|
|
1046
|
+
const editKey = (key, name) => `${key}:${name}`;
|
|
1047
|
+
function SectionsTab({ cfg, inv, phases, target, globalSections, ownedSections, t, write, writeGlobalField }) {
|
|
555
1048
|
const [editing, setEditing] = (0, react.useState)(null);
|
|
556
1049
|
const [draft, setDraft] = (0, react.useState)("");
|
|
557
1050
|
const [dragName, setDragName] = (0, react.useState)(null);
|
|
1051
|
+
const [dragSource, setDragSource] = (0, react.useState)(null);
|
|
558
1052
|
const [dropTarget, setDropTarget] = (0, react.useState)(null);
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
1053
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
1054
|
+
const scrollRef = (0, react.useRef)({
|
|
1055
|
+
raf: 0,
|
|
1056
|
+
clientY: 0
|
|
1057
|
+
});
|
|
1058
|
+
const stopAutoScroll = () => {
|
|
1059
|
+
if (scrollRef.current.raf !== 0) {
|
|
1060
|
+
cancelAnimationFrame(scrollRef.current.raf);
|
|
1061
|
+
scrollRef.current.raf = 0;
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
const autoScrollTick = () => {
|
|
1065
|
+
scrollRef.current.raf = 0;
|
|
1066
|
+
const edge = 48;
|
|
1067
|
+
const y = scrollRef.current.clientY;
|
|
1068
|
+
const inner = window.innerHeight;
|
|
1069
|
+
let node = rootRef.current?.parentElement;
|
|
1070
|
+
while (node !== null && node !== void 0) {
|
|
1071
|
+
if (node.scrollHeight > node.clientHeight) {
|
|
1072
|
+
if (y < edge) node.scrollTop -= Math.max(6, (edge - y) / 2);
|
|
1073
|
+
else if (y > inner - edge) node.scrollTop += Math.max(6, (y - (inner - edge)) / 2);
|
|
1074
|
+
else return;
|
|
1075
|
+
scrollRef.current.raf = requestAnimationFrame(autoScrollTick);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
node = node.parentElement;
|
|
1079
|
+
}
|
|
1080
|
+
if (y < edge) window.scrollBy({ top: -Math.max(6, (edge - y) / 2) });
|
|
1081
|
+
else if (y > inner - edge) window.scrollBy({ top: Math.max(6, (y - (inner - edge)) / 2) });
|
|
1082
|
+
if (y < edge || y > inner - edge) scrollRef.current.raf = requestAnimationFrame(autoScrollTick);
|
|
1083
|
+
};
|
|
1084
|
+
const rootRef = (0, react.useRef)(null);
|
|
1085
|
+
const rootDragOver = (e) => {
|
|
1086
|
+
if (!dragName) return;
|
|
1087
|
+
scrollRef.current.clientY = e.clientY;
|
|
1088
|
+
if (scrollRef.current.raf === 0) scrollRef.current.raf = requestAnimationFrame(autoScrollTick);
|
|
578
1089
|
};
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1090
|
+
const parts = PART_ORDER;
|
|
1091
|
+
const partNote = (key) => {
|
|
1092
|
+
const view = phases?.[key];
|
|
1093
|
+
if (view === void 0 || view === null) return null;
|
|
1094
|
+
if (view.takenOverBy !== void 0) return (0, react.createElement)("div", { style: s.noticeWarn }, t("sectionsTakenOver", { name: view.takenOverBy }));
|
|
1095
|
+
const lost = view.lostSections;
|
|
1096
|
+
if (lost !== void 0) return (0, react.createElement)("div", { style: s.noticeWarn }, t("sectionsLost", {
|
|
1097
|
+
emitted: lost.emitted,
|
|
1098
|
+
survived: lost.survived
|
|
1099
|
+
}));
|
|
1100
|
+
return null;
|
|
583
1101
|
};
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
1102
|
+
const rowsOf = (key) => {
|
|
1103
|
+
const replace = cfg.replace ?? {};
|
|
1104
|
+
const view = phases?.[key];
|
|
1105
|
+
const denied = new Set(deniedNames(cfg, key));
|
|
1106
|
+
const postByName = new Map((view?.sections ?? []).map((sec) => [sec.name, sec]));
|
|
1107
|
+
const baseByName = new Map((view?.baseSections ?? []).map((sec) => [sec.name, sec]));
|
|
1108
|
+
const { phase, names: injectedHere, custom: customNames, text, order: phaseOrder } = injectedAt(cfg, key);
|
|
1109
|
+
const names = [];
|
|
1110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1111
|
+
const push = (name) => {
|
|
1112
|
+
if (!seen.has(name)) {
|
|
1113
|
+
seen.add(name);
|
|
1114
|
+
names.push(name);
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
for (const sec of view?.baseSections ?? []) if (postByName.has(sec.name) || denied.has(sec.name) || sec.blocked === true) push(sec.name);
|
|
1118
|
+
for (const name of postByName.keys()) if (baseByName.has(name) || injectedHere.has(name)) push(name);
|
|
1119
|
+
for (const item of cfg.inject ?? []) {
|
|
1120
|
+
if ((item.phase ?? "always") !== phase) continue;
|
|
1121
|
+
if (item.custom === true || text.get(item.name) || baseByName.has(item.name) || postByName.has(item.name)) push(item.name);
|
|
1122
|
+
}
|
|
1123
|
+
for (const name of customNames) push(name);
|
|
1124
|
+
const rows = names.map((name) => {
|
|
1125
|
+
const override = text.get(name) ?? "";
|
|
588
1126
|
return {
|
|
589
|
-
name
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
custom:
|
|
1127
|
+
name,
|
|
1128
|
+
text: postByName.get(name)?.text ?? baseByName.get(name)?.text ?? "",
|
|
1129
|
+
replaced: override !== "" || Object.hasOwn(replace, name),
|
|
1130
|
+
custom: customNames.has(name),
|
|
1131
|
+
override,
|
|
1132
|
+
blocked: denied.has(name)
|
|
593
1133
|
};
|
|
594
1134
|
});
|
|
595
|
-
|
|
1135
|
+
if (phaseOrder.size > 0) {
|
|
1136
|
+
const fallback = new Map(rows.map((row, i) => [row.name, i]));
|
|
1137
|
+
rows.sort((a, b) => (phaseOrder.get(a.name) ?? fallback.get(a.name) ?? 0) - (phaseOrder.get(b.name) ?? fallback.get(b.name) ?? 0));
|
|
1138
|
+
}
|
|
1139
|
+
return rows;
|
|
596
1140
|
};
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1141
|
+
const applyBlock = (key, name, blocked) => {
|
|
1142
|
+
if (blocked || !target) {
|
|
1143
|
+
const patch = blockPatch(cfg, key, name, blocked);
|
|
1144
|
+
for (const [field, value] of Object.entries(patch)) {
|
|
1145
|
+
if (JSON.stringify(value) === JSON.stringify(cfg[field] ?? [])) continue;
|
|
1146
|
+
write(field, value);
|
|
1147
|
+
}
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const patch = blockPatch(cfg, key, name, false);
|
|
1151
|
+
const effectiveOwned = ownedSections && ownedSections.length > 0 ? ownedSections : void 0;
|
|
1152
|
+
for (const [field, value] of Object.entries(patch)) {
|
|
1153
|
+
if (field === "sections") {
|
|
1154
|
+
if ((globalSections ?? []).includes(name) && (!effectiveOwned || ownedSections.includes(name))) writeGlobalField("sections", (globalSections ?? []).filter((n) => n !== name));
|
|
1155
|
+
if (effectiveOwned?.includes(name)) write("sections", value);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (JSON.stringify(value) === JSON.stringify(cfg[field] ?? [])) continue;
|
|
1159
|
+
write(field, value);
|
|
1160
|
+
}
|
|
602
1161
|
};
|
|
603
|
-
const
|
|
1162
|
+
const toggleBlocked = (key, name) => applyBlock(key, name, !deniedNames(cfg, key).includes(name));
|
|
1163
|
+
const inBaseOf = (key, name) => (phases?.[key]?.baseSections ?? []).some((sec) => sec.name === name);
|
|
1164
|
+
const persistPhase = (key, rows) => {
|
|
1165
|
+
write("inject", phaseInjectEntries(cfg, key, rows));
|
|
1166
|
+
};
|
|
1167
|
+
const moveRow = (key, index, dir) => {
|
|
1168
|
+
const rows = rowsOf(key);
|
|
604
1169
|
const target = index + dir;
|
|
605
|
-
if (target < 0 || target >=
|
|
606
|
-
const next =
|
|
1170
|
+
if (target < 0 || target >= rows.length) return;
|
|
1171
|
+
const next = rows.slice();
|
|
607
1172
|
const [item] = next.splice(index, 1);
|
|
608
1173
|
next.splice(target, 0, item);
|
|
609
|
-
|
|
1174
|
+
persistPhase(key, next);
|
|
1175
|
+
};
|
|
1176
|
+
const removeFromPart = (key, name) => {
|
|
1177
|
+
const phase = injectPhaseOf(key);
|
|
1178
|
+
write("inject", (cfg.inject ?? []).filter((x) => {
|
|
1179
|
+
if (x.name !== name) return true;
|
|
1180
|
+
const itemPhase = x.phase ?? "always";
|
|
1181
|
+
return !(itemPhase === phase || itemPhase === "always");
|
|
1182
|
+
}));
|
|
1183
|
+
applyBlock(key, name, false);
|
|
1184
|
+
};
|
|
1185
|
+
const startReplace = (key, row) => {
|
|
1186
|
+
setEditing(editKey(key, row.name));
|
|
1187
|
+
setDraft(row.override || row.text);
|
|
610
1188
|
};
|
|
611
|
-
const
|
|
1189
|
+
const commitReplace = (key, row) => {
|
|
1190
|
+
const next = rowsOf(key).map((r) => r.name !== row.name ? r : {
|
|
1191
|
+
...r,
|
|
1192
|
+
override: draft,
|
|
1193
|
+
text: draft
|
|
1194
|
+
});
|
|
1195
|
+
persistPhase(key, next);
|
|
1196
|
+
setEditing(null);
|
|
1197
|
+
};
|
|
1198
|
+
const restoreReplace = (key, row) => {
|
|
1199
|
+
const next = rowsOf(key).map((r) => r.name !== row.name ? r : {
|
|
1200
|
+
...r,
|
|
1201
|
+
override: "",
|
|
1202
|
+
text: r.custom ? r.text : ""
|
|
1203
|
+
});
|
|
1204
|
+
persistPhase(key, next);
|
|
1205
|
+
if (!row.custom && Object.hasOwn(cfg.replace ?? {}, row.name)) {
|
|
1206
|
+
const rest = { ...cfg.replace ?? {} };
|
|
1207
|
+
delete rest[row.name];
|
|
1208
|
+
write("replace", rest);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
const addSection = (name, text, phase) => {
|
|
1212
|
+
const inject = (cfg.inject ?? []).slice();
|
|
1213
|
+
inject.push({
|
|
1214
|
+
name,
|
|
1215
|
+
order: 120 + inject.length,
|
|
1216
|
+
text,
|
|
1217
|
+
phase,
|
|
1218
|
+
custom: true
|
|
1219
|
+
});
|
|
1220
|
+
write("inject", inject);
|
|
1221
|
+
};
|
|
1222
|
+
const startDrag = (e, name, source) => {
|
|
612
1223
|
setDragName(name);
|
|
1224
|
+
setDragSource(source);
|
|
613
1225
|
e.dataTransfer.effectAllowed = "move";
|
|
614
1226
|
try {
|
|
615
1227
|
e.dataTransfer.setData("text/plain", name);
|
|
616
1228
|
} catch {}
|
|
617
1229
|
};
|
|
618
|
-
const
|
|
1230
|
+
const rowDragOver = (e, key, name) => {
|
|
619
1231
|
if (!dragName || dragName === name) return;
|
|
620
1232
|
e.preventDefault();
|
|
621
1233
|
e.dataTransfer.dropEffect = "move";
|
|
622
1234
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
623
1235
|
const pos = e.clientY < rect.top + rect.height / 2 ? "above" : "below";
|
|
624
1236
|
setDropTarget({
|
|
1237
|
+
part: key,
|
|
625
1238
|
name,
|
|
626
1239
|
pos
|
|
627
1240
|
});
|
|
628
1241
|
};
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1242
|
+
const carryFor = (key, source, name) => {
|
|
1243
|
+
if (source.kind !== "all" || source.custom) return {
|
|
1244
|
+
text: "",
|
|
1245
|
+
blocked: false
|
|
1246
|
+
};
|
|
1247
|
+
if (inBaseOf(key, name)) return {
|
|
1248
|
+
text: "",
|
|
1249
|
+
blocked: false
|
|
1250
|
+
};
|
|
1251
|
+
if (source.text === "") return {
|
|
1252
|
+
text: "",
|
|
1253
|
+
blocked: true
|
|
1254
|
+
};
|
|
1255
|
+
return {
|
|
1256
|
+
text: source.text,
|
|
1257
|
+
blocked: false
|
|
1258
|
+
};
|
|
1259
|
+
};
|
|
1260
|
+
const insertInto = (key, targetName, pos) => {
|
|
1261
|
+
if (!dragName || !dragSource) return;
|
|
1262
|
+
const carry = carryFor(key, dragSource, dragName);
|
|
1263
|
+
if (carry.blocked) {
|
|
1264
|
+
setNotice(t("sectionDynamicNoAdd", { name: dragName }));
|
|
634
1265
|
return;
|
|
635
1266
|
}
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1267
|
+
const rows = rowsOf(key);
|
|
1268
|
+
const newRow = rows.find((row) => row.name === dragName) ?? {
|
|
1269
|
+
name: dragName,
|
|
1270
|
+
text: dragSource.custom ? dragSource.text : "",
|
|
1271
|
+
replaced: Object.hasOwn(cfg.replace ?? {}, dragName),
|
|
1272
|
+
custom: dragSource.custom,
|
|
1273
|
+
override: carry.text,
|
|
1274
|
+
blocked: false
|
|
1275
|
+
};
|
|
1276
|
+
const next = reorderInsert(rows, dragName, targetName, pos, newRow);
|
|
1277
|
+
if (next === null) return;
|
|
1278
|
+
if (dragSource.kind === "all" && inBaseOf(key, dragName) && deniedNames(cfg, key).includes(dragName)) applyBlock(key, dragName, false);
|
|
1279
|
+
persistPhase(key, next);
|
|
1280
|
+
};
|
|
1281
|
+
const rowDrop = (e, key, name) => {
|
|
1282
|
+
e.preventDefault();
|
|
1283
|
+
e.stopPropagation();
|
|
1284
|
+
if (dragName && dragName !== name) {
|
|
1285
|
+
const pos = dropTarget?.part === key && dropTarget.name === name && dropTarget.pos ? dropTarget.pos : "below";
|
|
1286
|
+
insertInto(key, name, pos);
|
|
641
1287
|
}
|
|
642
|
-
const next = merged.filter((x) => x.name !== dragName);
|
|
643
|
-
const idx = next.findIndex((x) => x.name === target.name);
|
|
644
|
-
const pos = dropTarget?.name === target.name ? dropTarget.pos : "below";
|
|
645
|
-
next.splice(pos === "above" ? idx : idx + 1, 0, dragged);
|
|
646
|
-
persistOrder(next);
|
|
647
1288
|
setDragName(null);
|
|
1289
|
+
setDragSource(null);
|
|
648
1290
|
setDropTarget(null);
|
|
649
1291
|
};
|
|
650
|
-
const
|
|
1292
|
+
const partDropEnd = (e, key) => {
|
|
1293
|
+
e.preventDefault();
|
|
1294
|
+
if (dragName && dragSource) {
|
|
1295
|
+
const carry = carryFor(key, dragSource, dragName);
|
|
1296
|
+
if (!carry.blocked) {
|
|
1297
|
+
const rows = rowsOf(key);
|
|
1298
|
+
const existing = rows.find((row) => row.name === dragName);
|
|
1299
|
+
const next = rows.filter((row) => row.name !== dragName);
|
|
1300
|
+
next.push(existing ?? {
|
|
1301
|
+
name: dragName,
|
|
1302
|
+
text: dragSource.custom ? dragSource.text : "",
|
|
1303
|
+
replaced: Object.hasOwn(cfg.replace ?? {}, dragName),
|
|
1304
|
+
custom: dragSource.custom,
|
|
1305
|
+
override: carry.text,
|
|
1306
|
+
blocked: false
|
|
1307
|
+
});
|
|
1308
|
+
if (dragSource.kind === "all" && inBaseOf(key, dragName) && deniedNames(cfg, key).includes(dragName)) applyBlock(key, dragName, false);
|
|
1309
|
+
persistPhase(key, next);
|
|
1310
|
+
} else setNotice(t("sectionDynamicNoAdd", { name: dragName }));
|
|
1311
|
+
}
|
|
651
1312
|
setDragName(null);
|
|
1313
|
+
setDragSource(null);
|
|
652
1314
|
setDropTarget(null);
|
|
653
1315
|
};
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
source: "custom"
|
|
663
|
-
};
|
|
664
|
-
const existing = next.findIndex((x) => x.name === name);
|
|
665
|
-
if (existing >= 0) next[existing] = entry;
|
|
666
|
-
else {
|
|
667
|
-
const idx = next.findIndex((x) => x.order > order);
|
|
668
|
-
if (idx < 0) next.push(entry);
|
|
669
|
-
else next.splice(idx, 0, entry);
|
|
1316
|
+
const allBoxDrop = (e) => {
|
|
1317
|
+
e.preventDefault();
|
|
1318
|
+
if (dragName && dragSource?.kind === "part" && dragSource.key) {
|
|
1319
|
+
const src = dragSource.key;
|
|
1320
|
+
const row = rowsOf(src).find((r) => r.name === dragName);
|
|
1321
|
+
const addedSystem = !!row && !row.custom && !inBaseOf(src, dragName) && row.override !== "";
|
|
1322
|
+
if (row && !row.custom && !addedSystem) applyBlock(src, dragName, true);
|
|
1323
|
+
else removeFromPart(src, dragName);
|
|
670
1324
|
}
|
|
671
|
-
|
|
1325
|
+
setDragName(null);
|
|
1326
|
+
setDragSource(null);
|
|
1327
|
+
setDropTarget(null);
|
|
672
1328
|
};
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1329
|
+
const endDrag = () => {
|
|
1330
|
+
stopAutoScroll();
|
|
1331
|
+
setDragName(null);
|
|
1332
|
+
setDragSource(null);
|
|
1333
|
+
setDropTarget(null);
|
|
1334
|
+
};
|
|
1335
|
+
const partNoun = (key) => key === "bootstrap" ? t("sectionsPartGuide") : key === "compaction" ? t("sectionsPartControlled") : t("sectionsPartResident");
|
|
1336
|
+
const isEditing = (key, row) => editing === editKey(key, row.name);
|
|
1337
|
+
const renderRow = (key, row, index, total) => {
|
|
1338
|
+
const isDragging = dragName === row.name;
|
|
1339
|
+
const isDropTarget = dropTarget?.part === key && dropTarget.name === row.name;
|
|
1340
|
+
const rowStyle = {
|
|
1341
|
+
...s.row,
|
|
1342
|
+
...isDragging ? s.rowDragging : {},
|
|
1343
|
+
...isDropTarget && dropTarget && dropTarget.pos === "above" ? s.rowDropAbove : {},
|
|
1344
|
+
...isDropTarget && dropTarget && dropTarget.pos === "below" ? s.rowDropBelow : {},
|
|
1345
|
+
cursor: "move"
|
|
1346
|
+
};
|
|
1347
|
+
return (0, react.createElement)("div", {
|
|
1348
|
+
key: row.name,
|
|
1349
|
+
style: rowStyle,
|
|
1350
|
+
draggable: true,
|
|
1351
|
+
onDragStart: (e) => startDrag(e, row.name, {
|
|
1352
|
+
kind: "part",
|
|
1353
|
+
key,
|
|
1354
|
+
text: row.custom ? row.override || row.text : "",
|
|
1355
|
+
custom: row.custom
|
|
1356
|
+
}),
|
|
1357
|
+
onDragOver: (e) => rowDragOver(e, key, row.name),
|
|
1358
|
+
onDrop: (e) => rowDrop(e, key, row.name),
|
|
1359
|
+
onDragEnd: endDrag
|
|
1360
|
+
}, [
|
|
1361
|
+
(0, react.createElement)("span", {
|
|
1362
|
+
style: s.dragHandle,
|
|
1363
|
+
title: t("drag")
|
|
1364
|
+
}, "⠿"),
|
|
1365
|
+
(0, react.createElement)("label", { style: s.switchWrap }, [(0, react.createElement)("input", {
|
|
1366
|
+
type: "checkbox",
|
|
1367
|
+
checked: row.blocked,
|
|
1368
|
+
onChange: () => toggleBlocked(key, row.name)
|
|
1369
|
+
}), (0, react.createElement)("span", { style: row.blocked ? s.badgeBlocked : s.badgeOk }, row.blocked ? t("blockedOn") : t("blockedOff"))]),
|
|
1370
|
+
(0, react.createElement)("div", { style: s.rowBody }, [(0, react.createElement)("div", { style: s.rowTitle }, [
|
|
1371
|
+
(0, react.createElement)("span", { style: s.code }, row.name),
|
|
1372
|
+
(0, react.createElement)("span", { style: s.orderTag }, "#" + index),
|
|
1373
|
+
(0, react.createElement)("span", { style: row.custom ? s.badgeCustom : s.badgeSystem }, row.custom ? t("manual") : t("system")),
|
|
1374
|
+
row.replaced ? (0, react.createElement)("span", { style: s.badgeReplaced }, t("replaced")) : null
|
|
1375
|
+
]), isEditing(key, row) ? (0, react.createElement)("div", { style: s.editBox }, [(0, react.createElement)("textarea", {
|
|
1376
|
+
style: s.editInput,
|
|
1377
|
+
value: draft,
|
|
1378
|
+
onChange: (e) => setDraft(e.target.value),
|
|
1379
|
+
rows: 3
|
|
1380
|
+
}), (0, react.createElement)("div", { style: s.injectRow }, [
|
|
1381
|
+
(0, react.createElement)("button", {
|
|
718
1382
|
style: s.mini,
|
|
719
|
-
onClick: () =>
|
|
720
|
-
}, t("
|
|
721
|
-
(0, react.createElement)("div", { style: s.arrowCol }, [(0, react.createElement)("button", {
|
|
722
|
-
style: s.arrow,
|
|
723
|
-
disabled: index === 0,
|
|
724
|
-
onClick: () => moveOrder(index, -1),
|
|
725
|
-
title: t("moveUp")
|
|
726
|
-
}, "↑"), (0, react.createElement)("button", {
|
|
727
|
-
style: s.arrow,
|
|
728
|
-
disabled: index === merged.length - 1,
|
|
729
|
-
onClick: () => moveOrder(index, 1),
|
|
730
|
-
title: t("moveDown")
|
|
731
|
-
}, "↓")]),
|
|
1383
|
+
onClick: () => commitReplace(key, row)
|
|
1384
|
+
}, t("save")),
|
|
732
1385
|
(0, react.createElement)("button", {
|
|
733
1386
|
style: s.mini,
|
|
734
|
-
onClick: () =>
|
|
735
|
-
},
|
|
736
|
-
|
|
1387
|
+
onClick: () => setEditing(null)
|
|
1388
|
+
}, t("clearInput")),
|
|
1389
|
+
!row.custom && (row.override || Object.hasOwn(cfg.replace ?? {}, row.name)) ? (0, react.createElement)("button", {
|
|
737
1390
|
style: s.mini,
|
|
738
|
-
onClick: () =>
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1391
|
+
onClick: () => restoreReplace(key, row)
|
|
1392
|
+
}, t("restore")) : null
|
|
1393
|
+
])]) : (0, react.createElement)("div", { style: s.preview }, String(row.override || row.text || "").slice(0, 140) || (row.custom ? t("empty") : t("dynamic")))]),
|
|
1394
|
+
(0, react.createElement)("div", { style: s.arrowCol }, [(0, react.createElement)("button", {
|
|
1395
|
+
style: s.arrow,
|
|
1396
|
+
disabled: index === 0,
|
|
1397
|
+
onClick: () => moveRow(key, index, -1),
|
|
1398
|
+
title: t("moveUp")
|
|
1399
|
+
}, "↑"), (0, react.createElement)("button", {
|
|
1400
|
+
style: s.arrow,
|
|
1401
|
+
disabled: index === total - 1,
|
|
1402
|
+
onClick: () => moveRow(key, index, 1),
|
|
1403
|
+
title: t("moveDown")
|
|
1404
|
+
}, "↓")]),
|
|
1405
|
+
isEditing(key, row) ? null : (0, react.createElement)("button", {
|
|
1406
|
+
style: s.mini,
|
|
1407
|
+
onClick: () => startReplace(key, row)
|
|
1408
|
+
}, t("replace")),
|
|
1409
|
+
!isEditing(key, row) && !row.custom && (row.override || Object.hasOwn(cfg.replace ?? {}, row.name)) ? (0, react.createElement)("button", {
|
|
1410
|
+
style: s.mini,
|
|
1411
|
+
onClick: () => restoreReplace(key, row)
|
|
1412
|
+
}, t("restore")) : null,
|
|
1413
|
+
row.custom ? (0, react.createElement)("button", {
|
|
1414
|
+
style: s.mini,
|
|
1415
|
+
onClick: () => removeFromPart(key, row.name),
|
|
1416
|
+
title: t("delete")
|
|
1417
|
+
}, t("delete")) : null
|
|
1418
|
+
]);
|
|
1419
|
+
};
|
|
1420
|
+
const renderPart = (key) => {
|
|
1421
|
+
const rows = rowsOf(key);
|
|
1422
|
+
return (0, react.createElement)("div", {
|
|
1423
|
+
key,
|
|
1424
|
+
style: s.injectBox,
|
|
1425
|
+
onDragOver: (e) => {
|
|
1426
|
+
if (dragName) {
|
|
1427
|
+
e.preventDefault();
|
|
1428
|
+
e.dataTransfer.dropEffect = "move";
|
|
1429
|
+
}
|
|
1430
|
+
},
|
|
1431
|
+
onDrop: (e) => partDropEnd(e, key)
|
|
1432
|
+
}, [
|
|
1433
|
+
(0, react.createElement)("div", { style: s.rowTitle }, [(0, react.createElement)("span", null, partNoun(key)), (0, react.createElement)("span", { style: s.orderTag }, `${t("phaseVisible")} ${rows.length}`)]),
|
|
1434
|
+
partNote(key),
|
|
1435
|
+
rows.map((row, i) => renderRow(key, row, i, rows.length)),
|
|
1436
|
+
(0, react.createElement)(InjectForm, {
|
|
1437
|
+
onAdd: (name, text) => addSection(name, text, injectPhaseOf(key)),
|
|
1438
|
+
lockedPhase: injectPhaseOf(key),
|
|
1439
|
+
t
|
|
1440
|
+
})
|
|
1441
|
+
]);
|
|
1442
|
+
};
|
|
1443
|
+
return (0, react.createElement)("div", {
|
|
1444
|
+
style: s.list,
|
|
1445
|
+
ref: rootRef,
|
|
1446
|
+
onDragOver: rootDragOver
|
|
1447
|
+
}, [
|
|
1448
|
+
notice ? (0, react.createElement)("div", { style: s.noticeWarn }, notice) : null,
|
|
1449
|
+
parts.map(renderPart),
|
|
1450
|
+
(0, react.createElement)("div", {
|
|
1451
|
+
style: s.injectBox,
|
|
1452
|
+
onDragOver: (e) => {
|
|
1453
|
+
if (dragName) {
|
|
1454
|
+
e.preventDefault();
|
|
1455
|
+
e.dataTransfer.dropEffect = "move";
|
|
1456
|
+
}
|
|
1457
|
+
},
|
|
1458
|
+
onDrop: allBoxDrop
|
|
1459
|
+
}, [
|
|
1460
|
+
(0, react.createElement)("div", { style: s.rowTitle }, [t("allSectionsTitle"), (0, react.createElement)("span", { style: s.orderTag }, `${(inv?.sections ?? []).length}`)]),
|
|
1461
|
+
(0, react.createElement)("div", { style: {
|
|
1462
|
+
...s.muted,
|
|
1463
|
+
marginBottom: 4
|
|
1464
|
+
} }, t("sectionsFourHint")),
|
|
1465
|
+
(inv?.sections ?? []).length === 0 ? (0, react.createElement)("div", { style: s.muted }, t("empty")) : null,
|
|
1466
|
+
(inv?.sections ?? []).map((sec) => (0, react.createElement)("div", {
|
|
1467
|
+
key: sec.name,
|
|
1468
|
+
style: {
|
|
1469
|
+
...s.row,
|
|
1470
|
+
cursor: "copy",
|
|
1471
|
+
opacity: .92
|
|
1472
|
+
},
|
|
1473
|
+
draggable: true,
|
|
1474
|
+
onDragStart: (e) => startDrag(e, sec.name, {
|
|
1475
|
+
kind: "all",
|
|
1476
|
+
text: (sec.text ?? "").startsWith("<") || !sec.text ? "" : sec.text,
|
|
1477
|
+
custom: false
|
|
1478
|
+
}),
|
|
1479
|
+
onDragEnd: endDrag
|
|
1480
|
+
}, [(0, react.createElement)("div", { style: s.rowBody }, [(0, react.createElement)("div", { style: s.rowTitle }, (0, react.createElement)("span", { style: s.code }, sec.name)), (0, react.createElement)("div", { style: s.preview }, String(sec.text ?? "").slice(0, 140) || t("dynamic"))])]))
|
|
1481
|
+
])
|
|
747
1482
|
]);
|
|
748
1483
|
}
|
|
749
|
-
|
|
1484
|
+
/** 注入新段表单:阶段部分内联形态,阶段锁定为所在部分。 */
|
|
1485
|
+
function InjectForm({ onAdd, lockedPhase, t }) {
|
|
750
1486
|
const [name, setName] = (0, react.useState)("");
|
|
751
|
-
const [order, setOrder] = (0, react.useState)("120");
|
|
752
1487
|
const [text, setText] = (0, react.useState)("");
|
|
753
1488
|
const submit = () => {
|
|
754
1489
|
if (!name.trim()) return;
|
|
755
|
-
onAdd(name.trim(),
|
|
1490
|
+
onAdd(name.trim(), text);
|
|
756
1491
|
setName("");
|
|
757
1492
|
setText("");
|
|
758
|
-
setOrder("120");
|
|
759
1493
|
};
|
|
760
|
-
|
|
1494
|
+
const phaseLabel = lockedPhase === "bootstrap" ? t("phaseStageGuide") : lockedPhase === "compaction" ? t("phaseStageControlled") : t("phaseStageResident");
|
|
1495
|
+
return (0, react.createElement)("div", { style: s.injectRow }, [
|
|
761
1496
|
(0, react.createElement)("input", {
|
|
762
1497
|
style: {
|
|
763
1498
|
...s.input,
|
|
@@ -767,22 +1502,12 @@ window.__ModuleLoader__.load({
|
|
|
767
1502
|
value: name,
|
|
768
1503
|
onChange: (e) => setName(e.target.value)
|
|
769
1504
|
}),
|
|
770
|
-
(0, react.createElement)("input", {
|
|
771
|
-
style: {
|
|
772
|
-
...s.input,
|
|
773
|
-
width: "12%"
|
|
774
|
-
},
|
|
775
|
-
type: "number",
|
|
776
|
-
placeholder: t("order"),
|
|
777
|
-
value: order,
|
|
778
|
-
onChange: (e) => setOrder(e.target.value)
|
|
779
|
-
}),
|
|
780
1505
|
(0, react.createElement)("input", {
|
|
781
1506
|
style: {
|
|
782
1507
|
...s.input,
|
|
783
1508
|
flex: 1
|
|
784
1509
|
},
|
|
785
|
-
placeholder: t("text")
|
|
1510
|
+
placeholder: `${t("text")}(${phaseLabel})`,
|
|
786
1511
|
value: text,
|
|
787
1512
|
onChange: (e) => setText(e.target.value)
|
|
788
1513
|
}),
|
|
@@ -790,63 +1515,439 @@ window.__ModuleLoader__.load({
|
|
|
790
1515
|
style: s.mini,
|
|
791
1516
|
onClick: submit
|
|
792
1517
|
}, t("add"))
|
|
793
|
-
])
|
|
1518
|
+
]);
|
|
794
1519
|
}
|
|
795
1520
|
//#endregion
|
|
796
1521
|
//#region src/client/ToolsTab.tsx
|
|
797
|
-
/**
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1522
|
+
/** 工具 Tab:四个部分 —— 引导期 / 常驻期 / 压缩受控期(三个阶段恒定显示,
|
|
1523
|
+
* 预设没有某个阶段时该部分只是空的)+ 本系统全部工具(只读池)。
|
|
1524
|
+
* - 上三段每行 = 该阶段进入过滤的目录 ∪ 用户加回的工具(预过滤视图,隐藏后
|
|
1525
|
+
* 仍在列表可反选),勾选 = 该阶段可见 / 隐藏。三份名单互不继承。
|
|
1526
|
+
* - 「加回」:被该阶段默认裁掉、但仍在当前预设注册表里的工具,可以拖进来重新
|
|
1527
|
+
* 出现(写该阶段的 add 名单,装配时从注册表查回 schema 追加)。注册表里根本
|
|
1528
|
+
* 没有的(别的预设独有)加不进来,界面明确说明。
|
|
1529
|
+
* - 全部工具 = 跨预设累积的注册表并集(只读池),每行标出该工具在三个阶段的
|
|
1530
|
+
* 状态;它按定义比任何单一预设都大,所以里面的工具完全可能不在当前预设注册表。
|
|
1531
|
+
* - 拖动:全部 → 某阶段 = 让该工具在这一阶段出现;阶段 → 阶段 = 搬移(源阶段
|
|
1532
|
+
* 隐藏 + 目标阶段显示);上三段 → 全部 = 在该阶段隐藏它。
|
|
1533
|
+
* - 只有黑名单,没有白名单:每个动作只作用于被拖 / 被点的那一个工具,绝不
|
|
1534
|
+
* 因为「拖进了一个工具」而把该阶段其它工具一起关掉。 */
|
|
1535
|
+
function ToolsTab({ cfg, inv, phases, t, write }) {
|
|
1536
|
+
const [dragFrom, setDragFrom] = (0, react.useState)(null);
|
|
1537
|
+
const [draggedName, setDraggedName] = (0, react.useState)(null);
|
|
1538
|
+
const [dropHint, setDropHint] = (0, react.useState)(null);
|
|
1539
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
1540
|
+
const excludeOf = (key) => {
|
|
1541
|
+
const tools = cfg.tools ?? {};
|
|
1542
|
+
return (key === "bootstrap" ? tools.bootstrap?.exclude : key === "compaction" ? tools.compaction?.exclude : tools.exclude) ?? [];
|
|
1543
|
+
};
|
|
1544
|
+
const addOf = (key) => {
|
|
1545
|
+
const tools = cfg.tools ?? {};
|
|
1546
|
+
return (key === "bootstrap" ? tools.bootstrap?.add : key === "compaction" ? tools.compaction?.add : tools.add) ?? [];
|
|
1547
|
+
};
|
|
1548
|
+
const writeLists = (key, exclude, add) => {
|
|
1549
|
+
write("tools", withPhaseAdd(withPhaseExclude(cfg.tools ?? {}, key, exclude), key, add));
|
|
1550
|
+
};
|
|
1551
|
+
const catalogOf = (key) => (phases?.[key]?.baseTools ?? []).map((tool) => tool.name);
|
|
1552
|
+
const isInCatalog = (key, name) => catalogOf(key).includes(name);
|
|
1553
|
+
const registry = (() => {
|
|
1554
|
+
const list = PART_ORDER.map((key) => phases?.[key]?.registryTools).find((x) => Array.isArray(x) && x.length > 0) ?? [];
|
|
1555
|
+
return new Set(list);
|
|
1556
|
+
})();
|
|
1557
|
+
const isInRegistry = (name) => registry.has(name);
|
|
1558
|
+
const addToPhase = (key, name, hidden) => {
|
|
1559
|
+
let exclude = excludeOf(key);
|
|
1560
|
+
let add = addOf(key);
|
|
1561
|
+
if (!isInCatalog(key, name) && !add.includes(name)) add = [...add, name];
|
|
1562
|
+
exclude = hidden ? exclude.includes(name) ? exclude : [...exclude, name] : exclude.filter((x) => x !== name);
|
|
1563
|
+
writeLists(key, exclude, add);
|
|
1564
|
+
};
|
|
1565
|
+
const rowsOf = (key) => {
|
|
1566
|
+
const filter = { exclude: excludeOf(key) };
|
|
1567
|
+
const catalog = (phases?.[key]?.baseTools ?? []).map((tool) => ({
|
|
1568
|
+
name: tool.name,
|
|
1569
|
+
description: tool.description ?? "",
|
|
1570
|
+
hidden: isToolHidden(tool.name, filter),
|
|
1571
|
+
added: false
|
|
1572
|
+
}));
|
|
1573
|
+
const names = new Set(catalog.map((row) => row.name));
|
|
1574
|
+
const added = addOf(key).filter((name) => !names.has(name)).map((name) => ({
|
|
1575
|
+
name,
|
|
1576
|
+
description: "",
|
|
1577
|
+
hidden: false,
|
|
1578
|
+
added: true
|
|
1579
|
+
}));
|
|
1580
|
+
return [...catalog, ...added];
|
|
1581
|
+
};
|
|
1582
|
+
const toggleHide = (key, name, currentlyHidden) => {
|
|
1583
|
+
if (currentlyHidden) addToPhase(key, name, false);
|
|
1584
|
+
else {
|
|
1585
|
+
let exclude = excludeOf(key);
|
|
1586
|
+
let add = addOf(key);
|
|
1587
|
+
if (add.includes(name)) add = add.filter((x) => x !== name);
|
|
1588
|
+
else if (!exclude.includes(name)) exclude = [...exclude, name];
|
|
1589
|
+
writeLists(key, exclude, add);
|
|
1590
|
+
}
|
|
1591
|
+
setNotice(null);
|
|
1592
|
+
};
|
|
1593
|
+
const phaseNoun = (key) => key === "bootstrap" ? t("phaseStageGuide") : key === "compaction" ? t("phaseStageControlled") : t("phaseStageResident");
|
|
1594
|
+
const dropOnPart = (dest, name, from) => {
|
|
1595
|
+
if (!isInCatalog(dest, name) && !isInRegistry(name)) {
|
|
1596
|
+
setNotice({
|
|
1597
|
+
kind: "warn",
|
|
1598
|
+
text: t("toolNotInRegistry", { name })
|
|
1599
|
+
});
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const copied = from.kind === "part" && from.key !== dest;
|
|
1603
|
+
const hidden = copied ? excludeOf(from.key).includes(name) : false;
|
|
1604
|
+
addToPhase(dest, name, hidden);
|
|
1605
|
+
setNotice({
|
|
1606
|
+
kind: "ok",
|
|
1607
|
+
text: copied ? t("toolCopied", {
|
|
1608
|
+
name,
|
|
1609
|
+
from: phaseNoun(from.key),
|
|
1610
|
+
to: phaseNoun(dest)
|
|
1611
|
+
}) : isInCatalog(dest, name) ? t("toolShown", {
|
|
1612
|
+
name,
|
|
1613
|
+
to: phaseNoun(dest)
|
|
1614
|
+
}) : t("toolAdded", {
|
|
1615
|
+
name,
|
|
1616
|
+
phase: phaseNoun(dest)
|
|
1617
|
+
})
|
|
811
1618
|
});
|
|
812
1619
|
};
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1620
|
+
const dropOnAll = (name, from) => {
|
|
1621
|
+
if (from.kind !== "part") return;
|
|
1622
|
+
const key = from.key;
|
|
1623
|
+
let exclude = excludeOf(key);
|
|
1624
|
+
let add = addOf(key);
|
|
1625
|
+
if (add.includes(name)) add = add.filter((x) => x !== name);
|
|
1626
|
+
else if (!exclude.includes(name)) exclude = [...exclude, name];
|
|
1627
|
+
writeLists(key, exclude, add);
|
|
1628
|
+
setNotice({
|
|
1629
|
+
kind: "ok",
|
|
1630
|
+
text: t("toolHiddenIn", {
|
|
1631
|
+
name,
|
|
1632
|
+
to: phaseNoun(key)
|
|
1633
|
+
})
|
|
817
1634
|
});
|
|
818
1635
|
};
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1636
|
+
const visibleCount = (key) => {
|
|
1637
|
+
const rows = rowsOf(key);
|
|
1638
|
+
return `${rows.filter((row) => !row.hidden).length} / ${rows.length}`;
|
|
1639
|
+
};
|
|
1640
|
+
const partNoun = (key) => key === "bootstrap" ? t("toolsPartGuide") : key === "compaction" ? t("toolsPartControlled") : t("toolsPartResident");
|
|
1641
|
+
const startDrag = (e, from, name) => {
|
|
1642
|
+
setDragFrom(from);
|
|
1643
|
+
setDraggedName(name);
|
|
1644
|
+
e.dataTransfer.effectAllowed = "copy";
|
|
1645
|
+
try {
|
|
1646
|
+
e.dataTransfer.setData("text/plain", name);
|
|
1647
|
+
} catch {}
|
|
1648
|
+
};
|
|
1649
|
+
const zoneDragOver = (e, zone) => {
|
|
1650
|
+
if (!dragFrom || !draggedName) return;
|
|
1651
|
+
const same = zone === "all" ? dragFrom.kind === "all" : dragFrom.kind === "part" && dragFrom.key === zone.replace("part:", "");
|
|
1652
|
+
if (zone === "all" && dragFrom.kind === "all") {
|
|
1653
|
+
e.preventDefault();
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
if (same) return;
|
|
1657
|
+
e.preventDefault();
|
|
1658
|
+
e.dataTransfer.dropEffect = "copy";
|
|
1659
|
+
setDropHint(zone);
|
|
1660
|
+
};
|
|
1661
|
+
const zoneDrop = (e, zone) => {
|
|
1662
|
+
e.preventDefault();
|
|
1663
|
+
if (dragFrom && draggedName) {
|
|
1664
|
+
if (zone === "all") dropOnAll(draggedName, dragFrom);
|
|
1665
|
+
else {
|
|
1666
|
+
const dest = zone.replace("part:", "");
|
|
1667
|
+
if (PART_ORDER.includes(dest)) dropOnPart(dest, draggedName, dragFrom);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
setDragFrom(null);
|
|
1671
|
+
setDraggedName(null);
|
|
1672
|
+
setDropHint(null);
|
|
1673
|
+
};
|
|
1674
|
+
const endDrag = () => {
|
|
1675
|
+
setDragFrom(null);
|
|
1676
|
+
setDraggedName(null);
|
|
1677
|
+
setDropHint(null);
|
|
1678
|
+
};
|
|
1679
|
+
const zoneStyle = (zone) => dropHint === zone ? {
|
|
1680
|
+
outline: "2px dashed #7aa7ff",
|
|
1681
|
+
outlineOffset: 2
|
|
1682
|
+
} : void 0;
|
|
1683
|
+
const allTools = (inv?.tools ?? []).map((tool) => ({
|
|
1684
|
+
name: tool.name,
|
|
1685
|
+
description: typeof tool === "string" ? "" : tool.description ?? ""
|
|
1686
|
+
}));
|
|
1687
|
+
const renderPhasePart = (key) => {
|
|
1688
|
+
const rows = rowsOf(key);
|
|
1689
|
+
const zoneKey = `part:${key}`;
|
|
827
1690
|
return (0, react.createElement)("div", {
|
|
828
|
-
key:
|
|
829
|
-
style: s.
|
|
830
|
-
|
|
1691
|
+
key: zoneKey,
|
|
1692
|
+
style: s.injectBox,
|
|
1693
|
+
onDragOver: (e) => zoneDragOver(e, zoneKey),
|
|
1694
|
+
onDrop: (e) => zoneDrop(e, zoneKey),
|
|
1695
|
+
...zoneStyle(zoneKey) ?? {}
|
|
1696
|
+
}, [(0, react.createElement)("div", { style: s.rowTitle }, [
|
|
1697
|
+
(0, react.createElement)("span", null, partNoun(key)),
|
|
1698
|
+
(0, react.createElement)("span", { style: s.orderTag }, `${t("phaseVisible")} ${visibleCount(key)}`),
|
|
1699
|
+
rows.length === 0 ? (0, react.createElement)("span", { style: s.muted }, t("dragHint")) : null
|
|
1700
|
+
]), (0, react.createElement)("div", { style: s.toolWrap }, rows.map((row) => (0, react.createElement)("span", {
|
|
1701
|
+
key: row.name,
|
|
1702
|
+
style: {
|
|
1703
|
+
...s.toolChip,
|
|
1704
|
+
cursor: "move",
|
|
1705
|
+
display: "inline-flex",
|
|
1706
|
+
alignItems: "center",
|
|
1707
|
+
gap: 4
|
|
1708
|
+
},
|
|
1709
|
+
title: row.description.slice(0, 120),
|
|
1710
|
+
draggable: true,
|
|
1711
|
+
onDragStart: (e) => startDrag(e, {
|
|
1712
|
+
kind: "part",
|
|
1713
|
+
key
|
|
1714
|
+
}, row.name),
|
|
1715
|
+
onDragEnd: endDrag
|
|
1716
|
+
}, [(0, react.createElement)("input", {
|
|
831
1717
|
type: "checkbox",
|
|
832
|
-
checked:
|
|
833
|
-
onChange: () =>
|
|
834
|
-
|
|
835
|
-
|
|
1718
|
+
checked: !row.hidden,
|
|
1719
|
+
onChange: () => toggleHide(key, row.name, row.hidden),
|
|
1720
|
+
style: {
|
|
1721
|
+
margin: 0,
|
|
1722
|
+
cursor: "pointer"
|
|
1723
|
+
}
|
|
1724
|
+
}), (0, react.createElement)("span", { style: row.hidden ? s.badgeBlocked : s.badgeOk }, row.name)])))]);
|
|
1725
|
+
};
|
|
1726
|
+
return (0, react.createElement)("div", {}, [
|
|
1727
|
+
(0, react.createElement)("div", { style: s.muted }, t("toolsFourHint")),
|
|
1728
|
+
notice ? (0, react.createElement)("div", { style: notice.kind === "ok" ? s.noticeOk : s.noticeWarn }, notice.text) : null,
|
|
1729
|
+
PART_ORDER.map(renderPhasePart),
|
|
1730
|
+
(0, react.createElement)("div", {
|
|
1731
|
+
style: s.injectBox,
|
|
1732
|
+
onDragOver: (e) => zoneDragOver(e, "all"),
|
|
1733
|
+
onDrop: (e) => zoneDrop(e, "all"),
|
|
1734
|
+
...zoneStyle("all") ?? {}
|
|
1735
|
+
}, [
|
|
1736
|
+
(0, react.createElement)("div", { style: s.rowTitle }, [t("allToolsTitle"), (0, react.createElement)("span", { style: s.orderTag }, `${allTools.length}`)]),
|
|
1737
|
+
allTools.length === 0 ? (0, react.createElement)("div", { style: s.muted }, t("empty")) : null,
|
|
1738
|
+
(0, react.createElement)("div", { style: s.toolWrap }, allTools.map((tool) => (0, react.createElement)("span", {
|
|
1739
|
+
key: tool.name,
|
|
1740
|
+
style: {
|
|
1741
|
+
...s.toolChip,
|
|
1742
|
+
cursor: "copy"
|
|
1743
|
+
},
|
|
1744
|
+
title: tool.description.slice(0, 120),
|
|
1745
|
+
draggable: true,
|
|
1746
|
+
onDragStart: (e) => startDrag(e, { kind: "all" }, tool.name),
|
|
1747
|
+
onDragEnd: endDrag
|
|
1748
|
+
}, tool.name)))
|
|
1749
|
+
])
|
|
1750
|
+
]);
|
|
1751
|
+
}
|
|
1752
|
+
//#endregion
|
|
1753
|
+
//#region src/client/preset-io.ts
|
|
1754
|
+
/** 页面是否运行在 Tauri(v1 标记或 v2 桥)webview 内。 */
|
|
1755
|
+
function isTauriEnv(win) {
|
|
1756
|
+
if (win === null || win === void 0) return false;
|
|
1757
|
+
const w = win;
|
|
1758
|
+
return w.__TAURI_INTERNALS__ !== void 0 || w.__TAURI__ !== void 0;
|
|
1759
|
+
}
|
|
1760
|
+
/** 取可用的 Tauri v2 invoke 桥;不可用时返回 null。 */
|
|
1761
|
+
function invokeOf(win) {
|
|
1762
|
+
if (!isTauriEnv(win)) return null;
|
|
1763
|
+
const internals = win.__TAURI_INTERNALS__;
|
|
1764
|
+
return internals && typeof internals.invoke === "function" ? internals.invoke : null;
|
|
1765
|
+
}
|
|
1766
|
+
const JSON_FILTERS = [{
|
|
1767
|
+
name: "JSON",
|
|
1768
|
+
extensions: ["json"]
|
|
1769
|
+
}];
|
|
1770
|
+
/**
|
|
1771
|
+
* 原生保存对话框 + 写文件。`cancelled` = 用户关闭了对话框(调用方绝不
|
|
1772
|
+
* 得再回退到下载);`unavailable` = 宿主无法提供 dialog/fs 插件(插件
|
|
1773
|
+
* 缺失或被 ACL 拦截),调用方应当回退。
|
|
1774
|
+
*/
|
|
1775
|
+
async function tauriSaveText(invoke, defaultName, text) {
|
|
1776
|
+
try {
|
|
1777
|
+
const path = await invoke("plugin:dialog|save", {
|
|
1778
|
+
defaultPath: defaultName,
|
|
1779
|
+
filters: JSON_FILTERS
|
|
1780
|
+
});
|
|
1781
|
+
if (typeof path !== "string" || path === "") return { kind: "cancelled" };
|
|
1782
|
+
await invoke("plugin:fs|write_text_file", {
|
|
1783
|
+
path,
|
|
1784
|
+
contents: text
|
|
1785
|
+
});
|
|
1786
|
+
return { kind: "saved" };
|
|
1787
|
+
} catch {
|
|
1788
|
+
return { kind: "unavailable" };
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
/** 原生打开对话框 + 读文件。`cancelled` = 用户关闭了对话框。 */
|
|
1792
|
+
async function tauriOpenText(invoke) {
|
|
1793
|
+
try {
|
|
1794
|
+
const picked = await invoke("plugin:dialog|open", {
|
|
1795
|
+
multiple: false,
|
|
1796
|
+
directory: false,
|
|
1797
|
+
filters: JSON_FILTERS
|
|
1798
|
+
});
|
|
1799
|
+
const path = Array.isArray(picked) ? picked[0] : picked;
|
|
1800
|
+
if (typeof path !== "string" || path === "") return { kind: "cancelled" };
|
|
1801
|
+
const text = await invoke("plugin:fs|read_text_file", { path });
|
|
1802
|
+
if (typeof text !== "string") return { kind: "unavailable" };
|
|
1803
|
+
return {
|
|
1804
|
+
kind: "text",
|
|
1805
|
+
text
|
|
1806
|
+
};
|
|
1807
|
+
} catch {
|
|
1808
|
+
return { kind: "unavailable" };
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
/** Web 回退导出:Blob + object URL + <a download> + revoke。 */
|
|
1812
|
+
function webDownload(target, filename, text) {
|
|
1813
|
+
const blob = target.makeBlob(text);
|
|
1814
|
+
const url = target.objectUrl(blob);
|
|
1815
|
+
const anchor = target.makeAnchor();
|
|
1816
|
+
anchor.href = url;
|
|
1817
|
+
anchor.download = filename;
|
|
1818
|
+
anchor.click();
|
|
1819
|
+
target.revoke(url);
|
|
1820
|
+
}
|
|
1821
|
+
/** 把单个预设编码为导出文件的 JSON 格式(单对象格式)。 */
|
|
1822
|
+
function encodePresetExport(preset) {
|
|
1823
|
+
return JSON.stringify({
|
|
1824
|
+
name: preset.name,
|
|
1825
|
+
data: preset.data
|
|
1826
|
+
}, null, 2);
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* 解析导出的预设文件:单个 { name, data } 对象或对象数组。空内容 /
|
|
1830
|
+
* 非法 JSON 抛普通 Error,由调用方给出友好提示;条目级的过滤在
|
|
1831
|
+
* addImportedPresets 里进行。
|
|
1832
|
+
*/
|
|
1833
|
+
function decodePresetExport(text) {
|
|
1834
|
+
const trimmed = String(text).trim();
|
|
1835
|
+
if (trimmed === "") throw new Error("EMPTY_EXPORT");
|
|
1836
|
+
return JSON.parse(trimmed);
|
|
1837
|
+
}
|
|
1838
|
+
function browserDownloadTarget() {
|
|
1839
|
+
const g = globalThis;
|
|
1840
|
+
const doc = g.document;
|
|
1841
|
+
const url = g.URL;
|
|
1842
|
+
return {
|
|
1843
|
+
makeAnchor: () => doc.createElement("a"),
|
|
1844
|
+
makeBlob: (text) => new Blob([text], { type: "application/json" }),
|
|
1845
|
+
objectUrl: (blob) => url.createObjectURL(blob),
|
|
1846
|
+
revoke: (u) => url.revokeObjectURL(u)
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
/** 为当前 window 构建真实环境(在 Node 里调用也安全)。 */
|
|
1850
|
+
function makeIoEnv(win) {
|
|
1851
|
+
const invoke = invokeOf(win !== void 0 ? win : typeof window !== "undefined" ? window : void 0);
|
|
1852
|
+
const tauri = invoke !== null;
|
|
1853
|
+
return {
|
|
1854
|
+
tauri,
|
|
1855
|
+
saveText: (name, text) => tauri ? tauriSaveText(invoke, name, text) : Promise.resolve({ kind: "unavailable" }),
|
|
1856
|
+
openText: () => tauri ? tauriOpenText(invoke) : Promise.resolve({ kind: "unavailable" }),
|
|
1857
|
+
download: (name, text) => webDownload(browserDownloadTarget(), name, text)
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* 导出一个预设:宿主能提供原生对话框时走 tauri 保存(cancelled = 用户
|
|
1862
|
+
* 拒绝,绝不回退下载);否则走 Web 下载。
|
|
1863
|
+
*/
|
|
1864
|
+
async function exportPresetFile(preset, io) {
|
|
1865
|
+
const env = io ?? makeIoEnv();
|
|
1866
|
+
try {
|
|
1867
|
+
const text = encodePresetExport(preset);
|
|
1868
|
+
const filename = presetExportFilename(preset.name);
|
|
1869
|
+
if (env.tauri) {
|
|
1870
|
+
const out = await env.saveText(filename, text);
|
|
1871
|
+
if (out.kind === "saved") return {
|
|
1872
|
+
ok: true,
|
|
1873
|
+
via: "tauri"
|
|
1874
|
+
};
|
|
1875
|
+
if (out.kind === "cancelled") return {
|
|
1876
|
+
ok: false,
|
|
1877
|
+
via: "tauri",
|
|
1878
|
+
cancelled: true
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
env.download(filename, text);
|
|
1882
|
+
return {
|
|
1883
|
+
ok: true,
|
|
1884
|
+
via: "browser"
|
|
1885
|
+
};
|
|
1886
|
+
} catch (e) {
|
|
1887
|
+
return {
|
|
1888
|
+
ok: false,
|
|
1889
|
+
via: "browser",
|
|
1890
|
+
message: e instanceof Error ? e.message : String(e)
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* 经原生 tauri 打开对话框导入一个预设;返回 `unavailable` 时由调用方
|
|
1896
|
+
* 点击自己的隐藏文件输入(Web 回退)。
|
|
1897
|
+
*/
|
|
1898
|
+
async function importPresetFile(io) {
|
|
1899
|
+
const env = io ?? makeIoEnv();
|
|
1900
|
+
try {
|
|
1901
|
+
if (env.tauri) {
|
|
1902
|
+
const out = await env.openText();
|
|
1903
|
+
if (out.kind === "text") return {
|
|
1904
|
+
kind: "text",
|
|
1905
|
+
text: out.text,
|
|
1906
|
+
via: "tauri"
|
|
1907
|
+
};
|
|
1908
|
+
if (out.kind === "cancelled") return { kind: "cancelled" };
|
|
1909
|
+
}
|
|
1910
|
+
} catch {}
|
|
1911
|
+
return { kind: "unavailable" };
|
|
836
1912
|
}
|
|
837
1913
|
//#endregion
|
|
838
1914
|
//#region src/client/PresetsTab.tsx
|
|
839
|
-
/**
|
|
840
|
-
|
|
1915
|
+
/** 配置 Tab:两种「预设」分开 ——
|
|
1916
|
+
* - 保存当前配置:本插件内的配置快照库(config.presets),可应用 / 导出 / 导入,
|
|
1917
|
+
* 不会变成可选的 agent 预设;
|
|
1918
|
+
* - 存为 agent 预设:整体 fork 当前编辑目标的预设目录,并把当前定制写进它的
|
|
1919
|
+
* 覆盖项,新预设随即出现在顶部的编辑目标选择器里。
|
|
1920
|
+
* 导入导出经 preset-io 双端适配:Tauri 2 桌面走原生对话框,Web 走
|
|
1921
|
+
* 下载 / 文件选择;成功 / 失败 / 取消都有面板内消息条提示。 */
|
|
1922
|
+
function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveAsPreset, forkSource }) {
|
|
841
1923
|
const presets = cfg.presets ?? [];
|
|
842
1924
|
const [name, setName] = (0, react.useState)("");
|
|
1925
|
+
const [agentName, setAgentName] = (0, react.useState)("");
|
|
1926
|
+
const [creating, setCreating] = (0, react.useState)(false);
|
|
1927
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
843
1928
|
const fileRef = (0, react.useRef)(null);
|
|
844
|
-
const
|
|
1929
|
+
const noticeTimer = (0, react.useRef)(null);
|
|
1930
|
+
(0, react.useEffect)(() => () => {
|
|
1931
|
+
if (noticeTimer.current !== null) clearTimeout(noticeTimer.current);
|
|
1932
|
+
}, []);
|
|
1933
|
+
const show = (kind, text) => {
|
|
1934
|
+
if (noticeTimer.current !== null) clearTimeout(noticeTimer.current);
|
|
1935
|
+
setNotice({
|
|
1936
|
+
kind,
|
|
1937
|
+
text
|
|
1938
|
+
});
|
|
1939
|
+
noticeTimer.current = setTimeout(() => setNotice(null), 4e3);
|
|
1940
|
+
};
|
|
1941
|
+
const blockedNames = new Set(cfg.sections ?? []);
|
|
1942
|
+
const assemblyNames = /* @__PURE__ */ new Set();
|
|
1943
|
+
for (const key of PART_ORDER) for (const sec of phases?.[key]?.baseSections ?? []) assemblyNames.add(sec.name);
|
|
1944
|
+
const mergedAll = mergeSections(inv, cfg, blockedNames);
|
|
1945
|
+
const merged = assemblyNames.size === 0 ? mergedAll : mergedAll.filter((sec) => sec.source === "custom" || assemblyNames.has(sec.name));
|
|
845
1946
|
const currentNames = new Set(merged.map((sec) => sec.name));
|
|
846
1947
|
const saveCurrent = () => {
|
|
847
1948
|
const presetName = name.trim() || `${t("preset")} ${presets.length + 1}`;
|
|
848
1949
|
const data = buildPresetData(cfg, merged);
|
|
849
|
-
|
|
1950
|
+
writeGlobal("presets", [...presets, {
|
|
850
1951
|
id: genId(),
|
|
851
1952
|
name: presetName,
|
|
852
1953
|
data
|
|
@@ -854,41 +1955,72 @@ window.__ModuleLoader__.load({
|
|
|
854
1955
|
setName("");
|
|
855
1956
|
};
|
|
856
1957
|
const applyPreset = (preset) => {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
scope.set("replace", patch.replace);
|
|
860
|
-
scope.set("sections", patch.sections);
|
|
861
|
-
scope.set("tools", patch.tools);
|
|
862
|
-
scope.set("activePreset", preset.id);
|
|
1958
|
+
writePatch(applyPresetData(preset.data, cfg, currentNames));
|
|
1959
|
+
writeGlobal("activePreset", preset.id);
|
|
863
1960
|
};
|
|
864
1961
|
const deletePreset = (id) => {
|
|
865
1962
|
const next = removePreset(presets, id, cfg.activePreset);
|
|
866
|
-
|
|
867
|
-
if (next.activeId === void 0 && cfg.activePreset === id)
|
|
868
|
-
};
|
|
869
|
-
const exportPreset = (preset) => {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1963
|
+
writeGlobal("presets", next.presets);
|
|
1964
|
+
if (next.activeId === void 0 && cfg.activePreset === id) writeGlobal("activePreset", void 0);
|
|
1965
|
+
};
|
|
1966
|
+
const exportPreset = async (preset) => {
|
|
1967
|
+
try {
|
|
1968
|
+
const res = await exportPresetFile(preset);
|
|
1969
|
+
if (res.ok) show("ok", t("exportOk"));
|
|
1970
|
+
else if (res.cancelled) show("ok", t("exportCancel"));
|
|
1971
|
+
else show("error", `${t("exportFail")}${res.message ? ": " + res.message : ""}`);
|
|
1972
|
+
} catch (e) {
|
|
1973
|
+
show("error", `${t("exportFail")}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
const importParsed = (text) => {
|
|
1977
|
+
try {
|
|
1978
|
+
const parsed = decodePresetExport(text);
|
|
1979
|
+
const next = addImportedPresets(presets, parsed, genId);
|
|
1980
|
+
const added = next.length - presets.length;
|
|
1981
|
+
if (added <= 0) show("ok", t("importNone"));
|
|
1982
|
+
else {
|
|
1983
|
+
writeGlobal("presets", next);
|
|
1984
|
+
show("ok", `${t("importOk")} (+${added})`);
|
|
1985
|
+
}
|
|
1986
|
+
} catch (e) {
|
|
1987
|
+
show("error", `${t("importFail")}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
const importPreset = async () => {
|
|
1991
|
+
try {
|
|
1992
|
+
const res = await importPresetFile();
|
|
1993
|
+
if (res.kind === "text") {
|
|
1994
|
+
importParsed(res.text);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
if (res.kind === "cancelled") {
|
|
1998
|
+
show("ok", t("importCancel"));
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
fileRef.current?.click();
|
|
2002
|
+
} catch (e) {
|
|
2003
|
+
show("error", `${t("importFail")}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2004
|
+
}
|
|
877
2005
|
};
|
|
878
2006
|
const onImportFile = (e) => {
|
|
879
2007
|
const file = e.target.files?.[0];
|
|
2008
|
+
e.target.value = "";
|
|
880
2009
|
if (!file) return;
|
|
881
2010
|
const reader = new FileReader();
|
|
882
|
-
reader.onload = () =>
|
|
883
|
-
|
|
884
|
-
const parsed = JSON.parse(String(reader.result));
|
|
885
|
-
scope.set("presets", addImportedPresets(presets, parsed, genId));
|
|
886
|
-
} catch {}
|
|
887
|
-
};
|
|
2011
|
+
reader.onload = () => importParsed(String(reader.result));
|
|
2012
|
+
reader.onerror = () => show("error", t("importFail"));
|
|
888
2013
|
reader.readAsText(file);
|
|
889
|
-
|
|
2014
|
+
};
|
|
2015
|
+
const createAgentPreset = async () => {
|
|
2016
|
+
const trimmed = agentName.trim();
|
|
2017
|
+
if (trimmed.length === 0 || creating) return;
|
|
2018
|
+
setCreating(true);
|
|
2019
|
+
if (await saveAsPreset(trimmed)) setAgentName("");
|
|
2020
|
+
setCreating(false);
|
|
890
2021
|
};
|
|
891
2022
|
return (0, react.createElement)("div", { style: s.list }, [
|
|
2023
|
+
notice ? (0, react.createElement)("div", { style: notice.kind === "ok" ? s.noticeOk : s.error }, notice.text) : null,
|
|
892
2024
|
(0, react.createElement)("div", { style: s.injectBox }, [(0, react.createElement)("div", { style: s.rowTitle }, t("savePreset")), (0, react.createElement)("div", { style: s.injectRow }, [(0, react.createElement)("input", {
|
|
893
2025
|
style: {
|
|
894
2026
|
...s.input,
|
|
@@ -901,9 +2033,30 @@ window.__ModuleLoader__.load({
|
|
|
901
2033
|
style: s.mini,
|
|
902
2034
|
onClick: saveCurrent
|
|
903
2035
|
}, t("save"))])]),
|
|
2036
|
+
(0, react.createElement)("div", { style: s.injectBox }, [
|
|
2037
|
+
(0, react.createElement)("div", { style: s.rowTitle }, t("saveAsPresetCard")),
|
|
2038
|
+
(0, react.createElement)("div", { style: s.muted }, t("saveAsPresetHint", { name: forkSource ?? t("forkSourceDefault") })),
|
|
2039
|
+
(0, react.createElement)("div", { style: s.injectRow }, [(0, react.createElement)("input", {
|
|
2040
|
+
style: {
|
|
2041
|
+
...s.input,
|
|
2042
|
+
flex: 1
|
|
2043
|
+
},
|
|
2044
|
+
placeholder: t("agentPresetName"),
|
|
2045
|
+
value: agentName,
|
|
2046
|
+
onChange: (e) => setAgentName(e.target.value)
|
|
2047
|
+
}), (0, react.createElement)("button", {
|
|
2048
|
+
style: s.mini,
|
|
2049
|
+
disabled: creating || agentName.trim().length === 0,
|
|
2050
|
+
onClick: () => {
|
|
2051
|
+
createAgentPreset();
|
|
2052
|
+
}
|
|
2053
|
+
}, t("saveAsPreset"))])
|
|
2054
|
+
]),
|
|
904
2055
|
(0, react.createElement)("div", { style: s.injectBox }, [(0, react.createElement)("div", { style: s.rowTitle }, t("importPreset")), (0, react.createElement)("div", { style: s.injectRow }, [(0, react.createElement)("button", {
|
|
905
2056
|
style: s.mini,
|
|
906
|
-
onClick: () =>
|
|
2057
|
+
onClick: () => {
|
|
2058
|
+
importPreset();
|
|
2059
|
+
}
|
|
907
2060
|
}, t("import")), (0, react.createElement)("input", {
|
|
908
2061
|
ref: fileRef,
|
|
909
2062
|
type: "file",
|
|
@@ -925,7 +2078,9 @@ window.__ModuleLoader__.load({
|
|
|
925
2078
|
}, t("apply")),
|
|
926
2079
|
(0, react.createElement)("button", {
|
|
927
2080
|
style: s.mini,
|
|
928
|
-
onClick: () =>
|
|
2081
|
+
onClick: () => {
|
|
2082
|
+
exportPreset(preset);
|
|
2083
|
+
}
|
|
929
2084
|
}, t("export")),
|
|
930
2085
|
(0, react.createElement)("button", {
|
|
931
2086
|
style: s.mini,
|
|
@@ -935,15 +2090,12 @@ window.__ModuleLoader__.load({
|
|
|
935
2090
|
})
|
|
936
2091
|
]);
|
|
937
2092
|
}
|
|
938
|
-
function genId() {
|
|
939
|
-
return "p_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
940
|
-
}
|
|
941
2093
|
//#endregion
|
|
942
2094
|
//#region src/client/PreviewTools.tsx
|
|
943
|
-
/**
|
|
944
|
-
*
|
|
945
|
-
*
|
|
946
|
-
|
|
2095
|
+
/** 工具预览:当前定制(黑名单过滤之后)最终对模型可见的工具目录,
|
|
2096
|
+
* 只读、清单式呈现:列出即代表模型看得见 —— 这里没有「已屏蔽」状态。
|
|
2097
|
+
* 本视图绝不修改黑名单配置。 */
|
|
2098
|
+
/** 把字符串形态的条目补全为 { name, description: '' }。 */
|
|
947
2099
|
function norm(tool) {
|
|
948
2100
|
return typeof tool === "string" ? {
|
|
949
2101
|
name: tool,
|
|
@@ -962,76 +2114,84 @@ window.__ModuleLoader__.load({
|
|
|
962
2114
|
}
|
|
963
2115
|
//#endregion
|
|
964
2116
|
//#region src/client/PreviewTab.tsx
|
|
965
|
-
/**
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
|
|
969
|
-
|
|
2117
|
+
/** 预览 Tab:两个只读子视图 —— 所有插件过滤后的最终装配系统提示词,
|
|
2118
|
+
* 以及当前定制(黑名单过滤之后)将注入的工具目录视图。
|
|
2119
|
+
* 两个视图都不修改配置。预览目标跟随面板顶部的编辑目标(`target`);
|
|
2120
|
+
* 数据直接来自 Panel 并行拉取的三阶段装配(`phases`)—— 与提示词/
|
|
2121
|
+
* 工具 Tab 同源(同一份 base + post 视图),杜绝三个视图数据不一致。
|
|
2122
|
+
* 阶段按钮恒为引导期 / 常驻期 / 压缩受控期三个(与另两个 Tab 同源同形)。 */
|
|
2123
|
+
function PreviewTab({ t, active, refreshId, target, version, phases }) {
|
|
970
2124
|
const [sub, setSub] = (0, react.useState)("prompt");
|
|
971
|
-
const [
|
|
972
|
-
const [
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
inFlight.current = true;
|
|
981
|
-
setData(null);
|
|
982
|
-
fetch(PREVIEW_URL, { cache: "no-store" }).then((r) => r.json()).then((body) => {
|
|
983
|
-
if (body && body.ok === false) throw new Error(body.error ?? "preview failed");
|
|
984
|
-
setData(body);
|
|
985
|
-
setError(null);
|
|
986
|
-
}).catch((e) => setError(String(e instanceof Error ? e.message : e))).finally(() => {
|
|
987
|
-
inFlight.current = false;
|
|
988
|
-
if (pending.current) {
|
|
989
|
-
pending.current = false;
|
|
990
|
-
load();
|
|
991
|
-
}
|
|
992
|
-
});
|
|
993
|
-
};
|
|
994
|
-
(0, react.useEffect)(load, []);
|
|
995
|
-
(0, react.useEffect)(() => scope.subscribe(load), [scope]);
|
|
2125
|
+
const [phase, setPhase] = (0, react.useState)("bootstrap");
|
|
2126
|
+
const data = phases?.[phase] ?? null;
|
|
2127
|
+
const lossNote = data === null || data === void 0 ? null : data.takenOverBy !== void 0 ? t("sectionsTakenOver", { name: data.takenOverBy }) : data.lostSections !== void 0 ? t("sectionsLost", {
|
|
2128
|
+
emitted: data.lostSections.emitted,
|
|
2129
|
+
survived: data.lostSections.survived
|
|
2130
|
+
}) : null;
|
|
2131
|
+
const stageLabel = (p) => p === "bootstrap" ? t("phaseStageGuide") : p === "compaction" ? t("phaseStageControlled") : t("phaseStageResident");
|
|
996
2132
|
return (0, react.createElement)("div", { style: s.list }, [
|
|
997
|
-
(0, react.createElement)("div", { style: s.bar }, [
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
2133
|
+
(0, react.createElement)("div", { style: s.bar }, [
|
|
2134
|
+
(0, react.createElement)("button", {
|
|
2135
|
+
style: sub === "prompt" ? s.tabActive : s.tab,
|
|
2136
|
+
onClick: () => setSub("prompt")
|
|
2137
|
+
}, t("previewPrompt")),
|
|
2138
|
+
(0, react.createElement)("button", {
|
|
2139
|
+
style: sub === "tools" ? s.tabActive : s.tab,
|
|
2140
|
+
onClick: () => setSub("tools")
|
|
2141
|
+
}, t("previewTools")),
|
|
2142
|
+
(0, react.createElement)("div", { style: {
|
|
2143
|
+
marginLeft: "auto",
|
|
2144
|
+
display: "flex",
|
|
2145
|
+
gap: 4
|
|
2146
|
+
} }, PART_ORDER.map((key) => (0, react.createElement)("button", {
|
|
2147
|
+
key,
|
|
2148
|
+
style: phase === key ? s.tabActive : s.tab,
|
|
2149
|
+
onClick: () => setPhase(key)
|
|
2150
|
+
}, stageLabel(key))))
|
|
2151
|
+
]),
|
|
2152
|
+
lossNote !== null ? (0, react.createElement)("div", { style: s.noticeWarn }, lossNote) : null,
|
|
2153
|
+
data !== null && data.scopeResolved === false ? (0, react.createElement)("div", { style: s.noticeWarn }, t("scopeFallback")) : null,
|
|
2154
|
+
data !== null && data.degraded === true ? (0, react.createElement)("div", { style: s.noticeWarn }, t("previewDegraded")) : null,
|
|
2155
|
+
phases === null ? (0, react.createElement)("div", { style: s.muted }, t("loading")) : null,
|
|
2156
|
+
phases !== null && data === null ? (0, react.createElement)("div", { style: s.error }, t("previewFail")) : null,
|
|
2157
|
+
sub === "prompt" ? data ? [(0, react.createElement)("div", { style: s.rowTitle }, [(0, react.createElement)("span", { style: s.muted }, t("previewHint")), (0, react.createElement)("span", { style: s.orderTag }, `${data.sections.length} ${t("previewSections")}`)]), (0, react.createElement)("pre", { style: s.previewText }, data.text || t("empty"))] : null : data ? [(0, react.createElement)("div", { style: s.rowTitle }, [(0, react.createElement)("span", { style: s.muted }, t("previewToolsHint")), (0, react.createElement)("span", { style: s.orderTag }, `${data.tools.length} / ${data.registryTotal ?? "?"} ${t("previewToolsCount")}`)]), (0, react.createElement)(PreviewTools, {
|
|
1007
2158
|
tools: data.tools,
|
|
1008
2159
|
t
|
|
1009
|
-
}) : null
|
|
2160
|
+
})] : null
|
|
1010
2161
|
]);
|
|
1011
2162
|
}
|
|
1012
2163
|
//#endregion
|
|
1013
2164
|
//#region src/client/index.ts
|
|
1014
2165
|
/**
|
|
1015
|
-
* dsh-prompt-customizer —
|
|
2166
|
+
* dsh-prompt-customizer — 浏览器端(TypeScript 源码)。
|
|
1016
2167
|
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
* bound settings scope (`prompt-customizer` namespace) and apply live.
|
|
2168
|
+
* 注册一个「提示词定制」设置分区(侧边栏入口)及插件设置卡片。面板从宿主
|
|
2169
|
+
* 的 `/api/prompt-customizer/inventory` 拉取当前生效的段 / 工具清单,让用户
|
|
2170
|
+
* 屏蔽、替换、注入提示词段并隐藏工具。所有写入都走绑定的设置作用域
|
|
2171
|
+
* (`prompt-customizer` 命名空间),实时生效。
|
|
1022
2172
|
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
2173
|
+
* 由 tsdown 打包为 client/client.js(__ModuleLoader__ factory bundle);
|
|
2174
|
+
* 唯一的外部依赖是 loader 模块表中的 react 入口。
|
|
1025
2175
|
*/
|
|
1026
2176
|
const NS = "prompt-customizer";
|
|
1027
2177
|
const INVENTORY_URL = "/api/prompt-customizer/inventory";
|
|
2178
|
+
const CONFIG_SET_URL = "/api/prompt-customizer/config/set";
|
|
2179
|
+
const CONFIG_UNSET_URL = "/api/prompt-customizer/config/unset";
|
|
2180
|
+
const CONFIG_APPLY_URL = "/api/prompt-customizer/config/apply";
|
|
2181
|
+
const PRESETS_CREATE_URL = "/api/prompt-customizer/presets";
|
|
2182
|
+
const PREVIEW_URL = "/api/prompt-customizer/preview";
|
|
2183
|
+
/** 三套名义装配的共享布局:顺序固定,refresh 按此并行拉取。 */
|
|
2184
|
+
const VIEW_KEYS = [
|
|
2185
|
+
"bootstrap",
|
|
2186
|
+
"compaction",
|
|
2187
|
+
"active"
|
|
2188
|
+
];
|
|
1028
2189
|
const name = "prompt-customizer";
|
|
1029
2190
|
const inject = ["slots", "locale"];
|
|
1030
2191
|
function apply(ctx) {
|
|
1031
2192
|
ctx.effect(() => ctx.locale.register(NS, DICT), "prompt-customizer: locale");
|
|
1032
2193
|
const t = ctx.locale.bind(NS);
|
|
1033
|
-
ctx.inject(["
|
|
1034
|
-
const scope = scoped.settingsScope.bind({ namespace: NS });
|
|
2194
|
+
ctx.inject(["slots"], (scoped) => {
|
|
1035
2195
|
scoped.slots.inject("settings.section", () => scoped.slots.register({
|
|
1036
2196
|
name: "settings.section",
|
|
1037
2197
|
id: "prompt-customizer",
|
|
@@ -1039,77 +2199,260 @@ window.__ModuleLoader__.load({
|
|
|
1039
2199
|
label: () => t("nav"),
|
|
1040
2200
|
locale: NS,
|
|
1041
2201
|
inject: () => ({ t })
|
|
1042
|
-
}, () => (0, react.createElement)(Panel, {
|
|
1043
|
-
scope,
|
|
1044
|
-
t
|
|
1045
|
-
})));
|
|
2202
|
+
}, () => (0, react.createElement)(Panel, { t })));
|
|
1046
2203
|
});
|
|
1047
2204
|
}
|
|
1048
|
-
function Panel({
|
|
1049
|
-
const [
|
|
2205
|
+
function Panel({ t }) {
|
|
2206
|
+
const [cfg, setCfg] = (0, react.useState)(null);
|
|
1050
2207
|
const [inv, setInv] = (0, react.useState)(null);
|
|
2208
|
+
const [phases, setPhases] = (0, react.useState)(null);
|
|
2209
|
+
const [agentPresets, setAgentPresets] = (0, react.useState)([]);
|
|
1051
2210
|
const [tab, setTab] = (0, react.useState)("sections");
|
|
1052
2211
|
const [error, setError] = (0, react.useState)(null);
|
|
1053
|
-
(0, react.
|
|
2212
|
+
const [refreshId, setRefreshId] = (0, react.useState)(0);
|
|
2213
|
+
const [target, setTarget] = (0, react.useState)(void 0);
|
|
2214
|
+
const [version, setVersion] = (0, react.useState)(0);
|
|
2215
|
+
const [draft, setDraft] = (0, react.useState)(null);
|
|
2216
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
2217
|
+
const [flash, setFlash] = (0, react.useState)(null);
|
|
2218
|
+
const [flashKind, setFlashKind] = (0, react.useState)("ok");
|
|
2219
|
+
const flashTimer = (0, react.useRef)(null);
|
|
2220
|
+
const load = () => {
|
|
2221
|
+
fetch(`/api/prompt-customizer/config?t=${Date.now()}`).then((r) => r.json()).then((body) => {
|
|
2222
|
+
if (body?.ok !== true) throw new Error(body?.error ?? "config failed");
|
|
2223
|
+
setCfg(body.config);
|
|
2224
|
+
setError(null);
|
|
2225
|
+
}).catch((e) => setError(String(e instanceof Error ? e.message : e)));
|
|
2226
|
+
};
|
|
2227
|
+
(0, react.useEffect)(load, []);
|
|
1054
2228
|
const refresh = () => {
|
|
1055
|
-
|
|
1056
|
-
|
|
2229
|
+
const qs = target ? `?scope=${encodeURIComponent(target)}` : "";
|
|
2230
|
+
const params = (phase) => `${qs}${qs ? "&" : "?"}phase=${phase}&t=${Date.now()}`;
|
|
2231
|
+
const grab = (phase) => fetch(PREVIEW_URL + params(phase)).then((r) => r.json()).then((body) => body?.ok === false ? null : body).catch(() => null);
|
|
2232
|
+
Promise.all([...VIEW_KEYS.map(grab), fetch(INVENTORY_URL).then((r) => r.json())]).then(([boot, comp, act, inventoryData]) => {
|
|
2233
|
+
setPhases({
|
|
2234
|
+
bootstrap: boot,
|
|
2235
|
+
compaction: comp,
|
|
2236
|
+
active: act
|
|
2237
|
+
});
|
|
2238
|
+
setInv(inventoryData);
|
|
1057
2239
|
setError(null);
|
|
2240
|
+
setRefreshId((n) => n + 1);
|
|
1058
2241
|
}).catch((e) => setError(String(e instanceof Error ? e.message : e)));
|
|
1059
2242
|
};
|
|
1060
|
-
(0, react.useEffect)(refresh, []);
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
2243
|
+
(0, react.useEffect)(refresh, [target, version]);
|
|
2244
|
+
const fetchPresets = () => {
|
|
2245
|
+
fetch(`/api/prompt-customizer/agent-presets?t=${Date.now()}`).then((r) => r.json()).then((body) => setAgentPresets(Array.isArray(body?.presets) ? body.presets : [])).catch(() => setAgentPresets([]));
|
|
2246
|
+
};
|
|
2247
|
+
(0, react.useEffect)(() => {
|
|
2248
|
+
fetchPresets();
|
|
2249
|
+
}, []);
|
|
2250
|
+
if (cfg === null) return (0, react.createElement)("div", { style: s.muted }, t("loading"));
|
|
2251
|
+
const showFlash = (text, kind = "ok") => {
|
|
2252
|
+
if (flashTimer.current !== null) clearTimeout(flashTimer.current);
|
|
2253
|
+
setFlash(text);
|
|
2254
|
+
setFlashKind(kind);
|
|
2255
|
+
flashTimer.current = setTimeout(() => setFlash(null), 3200);
|
|
2256
|
+
};
|
|
2257
|
+
const base = editView(cfg, target);
|
|
2258
|
+
const view = draft ? {
|
|
2259
|
+
...base,
|
|
2260
|
+
sections: draft.sections ?? base.sections,
|
|
2261
|
+
sectionsBootstrap: draft.sectionsBootstrap ?? base.sectionsBootstrap,
|
|
2262
|
+
sectionsCompaction: draft.sectionsCompaction ?? base.sectionsCompaction,
|
|
2263
|
+
replace: draft.replace ?? base.replace,
|
|
2264
|
+
inject: draft.inject ?? base.inject,
|
|
2265
|
+
tools: draft.tools ?? base.tools
|
|
2266
|
+
} : base;
|
|
2267
|
+
const writeField = (field, value) => {
|
|
2268
|
+
fetch(value === void 0 ? CONFIG_UNSET_URL : CONFIG_SET_URL, {
|
|
2269
|
+
method: "POST",
|
|
2270
|
+
headers: { "content-type": "application/json" },
|
|
2271
|
+
body: JSON.stringify({
|
|
2272
|
+
field,
|
|
2273
|
+
value
|
|
2274
|
+
})
|
|
2275
|
+
}).then((r) => r.json()).then((body) => {
|
|
2276
|
+
if (body?.ok !== true) throw new Error(body?.error ?? "write failed");
|
|
2277
|
+
setCfg(body.config);
|
|
2278
|
+
setError(null);
|
|
2279
|
+
setVersion((n) => n + 1);
|
|
2280
|
+
}).catch((e) => setError(String(e instanceof Error ? e.message : e)));
|
|
2281
|
+
};
|
|
2282
|
+
const edit = (field, value) => {
|
|
2283
|
+
setDraft((d) => ({
|
|
2284
|
+
...d ?? { dirty: false },
|
|
2285
|
+
[field]: value,
|
|
2286
|
+
dirty: true
|
|
2287
|
+
}));
|
|
2288
|
+
};
|
|
2289
|
+
const save = () => {
|
|
2290
|
+
if (draft === null || saving) return;
|
|
2291
|
+
const EDITED_FIELDS = [
|
|
2292
|
+
"sections",
|
|
2293
|
+
"sectionsBootstrap",
|
|
2294
|
+
"sectionsCompaction",
|
|
2295
|
+
"replace",
|
|
2296
|
+
"inject",
|
|
2297
|
+
"tools"
|
|
2298
|
+
];
|
|
2299
|
+
const patch = {};
|
|
2300
|
+
for (const field of EDITED_FIELDS) if (Object.hasOwn(draft, field)) patch[field] = draft[field];
|
|
2301
|
+
setSaving(true);
|
|
2302
|
+
fetch(CONFIG_APPLY_URL, {
|
|
2303
|
+
method: "POST",
|
|
2304
|
+
headers: { "content-type": "application/json" },
|
|
2305
|
+
body: JSON.stringify({
|
|
2306
|
+
target,
|
|
2307
|
+
patch
|
|
2308
|
+
})
|
|
2309
|
+
}).then((r) => r.json()).then((body) => {
|
|
2310
|
+
if (body?.ok !== true) throw new Error(body?.error ?? "save failed");
|
|
2311
|
+
setCfg(body.config);
|
|
2312
|
+
setDraft(null);
|
|
2313
|
+
setError(null);
|
|
2314
|
+
setVersion((n) => n + 1);
|
|
2315
|
+
showFlash(t("saveOk"));
|
|
2316
|
+
}).catch((e) => setError(`${t("saveFail")}: ${e instanceof Error ? e.message : String(e)}`)).finally(() => setSaving(false));
|
|
2317
|
+
};
|
|
2318
|
+
const writePatch = (patch) => {
|
|
2319
|
+
fetch(CONFIG_APPLY_URL, {
|
|
2320
|
+
method: "POST",
|
|
2321
|
+
headers: { "content-type": "application/json" },
|
|
2322
|
+
body: JSON.stringify({
|
|
2323
|
+
target,
|
|
2324
|
+
patch
|
|
2325
|
+
})
|
|
2326
|
+
}).then((r) => r.json()).then((body) => {
|
|
2327
|
+
if (body?.ok !== true) throw new Error(body?.error ?? "apply failed");
|
|
2328
|
+
setCfg(body.config);
|
|
2329
|
+
setError(null);
|
|
2330
|
+
setVersion((n) => n + 1);
|
|
2331
|
+
}).catch((e) => setError(String(e instanceof Error ? e.message : e)));
|
|
2332
|
+
};
|
|
2333
|
+
const writeGlobal = writeField;
|
|
2334
|
+
const writeGlobalField = writeField;
|
|
2335
|
+
const switchTab = (next) => {
|
|
2336
|
+
if ((tab === "sections" || tab === "tools") && (next === "presets" || next === "preview")) {
|
|
2337
|
+
if (draft?.dirty && !window.confirm(t("discardConfirm"))) return;
|
|
2338
|
+
setDraft(null);
|
|
1069
2339
|
}
|
|
2340
|
+
setTab(next);
|
|
2341
|
+
};
|
|
2342
|
+
const switchTarget = (next) => {
|
|
2343
|
+
if (draft?.dirty && !window.confirm(t("discardConfirm"))) return;
|
|
2344
|
+
setDraft(null);
|
|
2345
|
+
setTarget(next);
|
|
2346
|
+
};
|
|
2347
|
+
const saveAsPreset = (presetName) => {
|
|
2348
|
+
const name = presetName.trim();
|
|
2349
|
+
if (name.length === 0) return Promise.resolve(false);
|
|
2350
|
+
return fetch(PRESETS_CREATE_URL, {
|
|
2351
|
+
method: "POST",
|
|
2352
|
+
headers: { "content-type": "application/json" },
|
|
2353
|
+
body: JSON.stringify({
|
|
2354
|
+
name,
|
|
2355
|
+
from: target,
|
|
2356
|
+
config: {
|
|
2357
|
+
sections: view.sections ?? [],
|
|
2358
|
+
sectionsBootstrap: view.sectionsBootstrap ?? [],
|
|
2359
|
+
sectionsCompaction: view.sectionsCompaction ?? [],
|
|
2360
|
+
replace: view.replace ?? {},
|
|
2361
|
+
inject: view.inject ?? [],
|
|
2362
|
+
tools: view.tools ?? {}
|
|
2363
|
+
}
|
|
2364
|
+
})
|
|
2365
|
+
}).then((r) => r.json()).then((body) => {
|
|
2366
|
+
if (body?.ok !== true) throw new Error(body?.error ?? t("saveAsPresetFail"));
|
|
2367
|
+
load();
|
|
2368
|
+
fetchPresets();
|
|
2369
|
+
showFlash(t("saveAsPresetOk"), "ok");
|
|
2370
|
+
return true;
|
|
2371
|
+
}).catch((e) => {
|
|
2372
|
+
showFlash(`${t("saveAsPresetFail")}:${String(e instanceof Error ? e.message : e)}`, "err");
|
|
2373
|
+
return false;
|
|
2374
|
+
});
|
|
1070
2375
|
};
|
|
1071
2376
|
return (0, react.createElement)("div", { style: s.root }, [
|
|
1072
2377
|
(0, react.createElement)("div", { style: s.bar }, [
|
|
1073
2378
|
(0, react.createElement)("button", {
|
|
1074
2379
|
style: tab === "sections" ? s.tabActive : s.tab,
|
|
1075
|
-
onClick: () =>
|
|
2380
|
+
onClick: () => switchTab("sections")
|
|
1076
2381
|
}, t("tabsSections")),
|
|
1077
2382
|
(0, react.createElement)("button", {
|
|
1078
2383
|
style: tab === "tools" ? s.tabActive : s.tab,
|
|
1079
|
-
onClick: () =>
|
|
2384
|
+
onClick: () => switchTab("tools")
|
|
1080
2385
|
}, t("tabsTools")),
|
|
1081
2386
|
(0, react.createElement)("button", {
|
|
1082
2387
|
style: tab === "presets" ? s.tabActive : s.tab,
|
|
1083
|
-
onClick: () =>
|
|
2388
|
+
onClick: () => switchTab("presets")
|
|
1084
2389
|
}, t("tabsPresets")),
|
|
1085
2390
|
(0, react.createElement)("button", {
|
|
1086
2391
|
style: tab === "preview" ? s.tabActive : s.tab,
|
|
1087
|
-
onClick: () =>
|
|
2392
|
+
onClick: () => switchTab("preview")
|
|
1088
2393
|
}, t("tabsPreview")),
|
|
2394
|
+
(0, react.createElement)("select", {
|
|
2395
|
+
style: {
|
|
2396
|
+
...s.input,
|
|
2397
|
+
marginLeft: 6
|
|
2398
|
+
},
|
|
2399
|
+
value: target ?? "",
|
|
2400
|
+
onChange: (e) => switchTarget(e.target.value || void 0),
|
|
2401
|
+
title: t("targetHint")
|
|
2402
|
+
}, [(0, react.createElement)("option", {
|
|
2403
|
+
value: "",
|
|
2404
|
+
style: s.option
|
|
2405
|
+
}, t("targetGlobal")), ...agentPresets.map((p) => (0, react.createElement)("option", {
|
|
2406
|
+
key: p.id,
|
|
2407
|
+
value: p.id,
|
|
2408
|
+
style: s.option
|
|
2409
|
+
}, `${p.name}${p.broken ? ` (${t("broken")})` : ""}`))]),
|
|
2410
|
+
(0, react.createElement)("button", {
|
|
2411
|
+
style: draft?.dirty ? s.saveBtnDirty : s.saveBtn,
|
|
2412
|
+
disabled: !draft?.dirty || saving,
|
|
2413
|
+
onClick: save
|
|
2414
|
+
}, t("save")),
|
|
1089
2415
|
(0, react.createElement)("button", {
|
|
1090
2416
|
style: s.refresh,
|
|
1091
2417
|
onClick: refresh
|
|
1092
2418
|
}, t("refresh"))
|
|
1093
2419
|
]),
|
|
1094
2420
|
error ? (0, react.createElement)("div", { style: s.error }, String(error)) : null,
|
|
2421
|
+
flash ? (0, react.createElement)("div", { style: flashKind === "err" ? s.error : s.noticeOk }, flash) : null,
|
|
2422
|
+
target && agentPresets.find((p) => p.id === target)?.broken ? (0, react.createElement)("div", { style: s.error }, t("brokenPreset")) : null,
|
|
2423
|
+
inv?.scopeResolved === false ? (0, react.createElement)("div", { style: s.noticeWarn }, t("scopeFallback")) : null,
|
|
1095
2424
|
tab === "sections" ? (0, react.createElement)(SectionsTab, {
|
|
1096
|
-
cfg,
|
|
2425
|
+
cfg: view,
|
|
1097
2426
|
inv,
|
|
1098
|
-
|
|
1099
|
-
|
|
2427
|
+
phases,
|
|
2428
|
+
target,
|
|
2429
|
+
globalSections: cfg.sections,
|
|
2430
|
+
ownedSections: target ? cfg.overrides?.[target]?.sections : void 0,
|
|
2431
|
+
t,
|
|
2432
|
+
write: edit,
|
|
2433
|
+
writeGlobalField
|
|
1100
2434
|
}) : tab === "tools" ? (0, react.createElement)(ToolsTab, {
|
|
1101
|
-
cfg,
|
|
2435
|
+
cfg: view,
|
|
1102
2436
|
inv,
|
|
1103
|
-
|
|
1104
|
-
t
|
|
2437
|
+
phases,
|
|
2438
|
+
t,
|
|
2439
|
+
write: edit
|
|
1105
2440
|
}) : tab === "presets" ? (0, react.createElement)(PresetsTab, {
|
|
1106
|
-
cfg,
|
|
2441
|
+
cfg: view,
|
|
1107
2442
|
inv,
|
|
1108
|
-
|
|
1109
|
-
t
|
|
2443
|
+
phases,
|
|
2444
|
+
t,
|
|
2445
|
+
writePatch,
|
|
2446
|
+
writeGlobal,
|
|
2447
|
+
saveAsPreset,
|
|
2448
|
+
forkSource: target ? agentPresets.find((p) => p.id === target)?.name ?? target : void 0
|
|
1110
2449
|
}) : (0, react.createElement)(PreviewTab, {
|
|
1111
|
-
|
|
1112
|
-
|
|
2450
|
+
t,
|
|
2451
|
+
active: tab === "preview",
|
|
2452
|
+
refreshId,
|
|
2453
|
+
target,
|
|
2454
|
+
version,
|
|
2455
|
+
phases
|
|
1113
2456
|
})
|
|
1114
2457
|
]);
|
|
1115
2458
|
}
|