dsh-prompt-customizer 0.3.1 → 0.4.0
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 +15 -1
- package/README.md +18 -3
- package/client/client.js +154 -13
- package/client/client.js.map +1 -1
- package/lib/effective.js +49 -0
- package/lib/index.js +132 -56
- package/lib/schema.js +10 -0
- package/lib/vars.js +147 -0
- package/package.json +1 -1
- package/src/client/PresetsTab.tsx +82 -4
- package/src/client/SectionsTab.tsx +4 -1
- package/src/client/index.ts +23 -0
- package/src/client/locales.ts +38 -2
- package/src/client/preset-io.ts +6 -5
- package/src/client/types.ts +8 -0
package/README.en.md
CHANGED
|
@@ -90,6 +90,19 @@ Two things that bite:
|
|
|
90
90
|
|
|
91
91
|
The Preview tab shows the final system prompt and tool catalog after **all** plugins have filtered, switchable per phase. It reads the same assembly as the prompt and tools tabs, so what you see is what the model sees. The tools view reports "visible to the model / registry total" side by side — presets such as PTC or Code Mode wrap the whole catalog into a single `run_code`, so the registry alone misleads.
|
|
92
92
|
|
|
93
|
+
### Prompt template variables
|
|
94
|
+
|
|
95
|
+
The host registers only `provider` / `model` / `cwd` as template variables; referencing any other `{{name}}` in section text makes the whole assembly fail. This plugin adds two more families through the official extension point:
|
|
96
|
+
|
|
97
|
+
- **Built-in system facts**: `date` / `time` / `datetime` / `weekday` / `hostname` / `platform` / `arch` / `username` / `home` / `shell` / `locale` / `node_version`, computed fresh at every assembly.
|
|
98
|
+
- **Every environment variable**: each `process.env` key maps to `env_<lowercase, non-alphanumerics collapsed to _>`, e.g. `PATH` → `{{env_path}}`, `USER.NAME` → `{{env_user_name}}`.
|
|
99
|
+
|
|
100
|
+
**Blocklist (`envBlocklist`)**: environment variables often carry secrets, and anything in the prompt is sent to the model with every request, so keys matching the blocklist are never registered. The prefilled entries cover common credential-style keys (`SECRET` / `TOKEN` / `PASSWORD` / `API_KEY` / `*_DSN` and friends); entries support `*` wildcards and are case-insensitive. The environment variable blocklist card in the panel lets you add or remove entries at any time, and its collapsible list shows the variables actually registered right now. Blocklist changes take effect at the next assembly (whether you edit the panel or `config.yaml` by hand).
|
|
101
|
+
|
|
102
|
+
**Carried by export / import**: exporting a preset writes the current blocklist along with it; import merges it in by union (grow-only), consistent with the snapshot library semantics.
|
|
103
|
+
|
|
104
|
+
The cost of strict rendering: referencing an unregistered name (such as a key the blocklist blocks) makes the **whole assembly fail** rather than rendering empty — only use names that appear in the panel's variable list.
|
|
105
|
+
|
|
93
106
|
### Config snapshots and agent presets
|
|
94
107
|
|
|
95
108
|
- **Save current config** — capture the complete customization of the current scope (per-phase block lists, per-phase ordering and phase tool catalogs) as a reusable snapshot.
|
|
@@ -135,6 +148,7 @@ Code map:
|
|
|
135
148
|
| `lib/index.js` | host half: assembly hook, five HTTP routes, agent-preset fork, one-off legacy cleanup |
|
|
136
149
|
| `lib/effective.js` | pure functions: field-level override merge, phase selection (injections / tool catalogs), blacklist filtering |
|
|
137
150
|
| `lib/promotion.js` | derives `{promoted, boundary}` from durable events — compaction resets, subagents always promoted |
|
|
151
|
+
| `lib/vars.js` | prompt variables: built-in system facts + full env mapping / blocklist, lazily re-synced at the assembly entry by signature |
|
|
138
152
|
| `lib/sectionOps.mjs` | phase-state pure functions **shared by the panel and the unit tests** (which list to write, injection identity, reorder, per-phase persistence) |
|
|
139
153
|
| `lib/store.js` / `lib/catalog.js` | atomic write + last-good fallback config store; cross-preset section/tool accumulation cache |
|
|
140
154
|
| `src/client/` | the panel (React written with `h()`, no JSX sugar), bundled into `client/client.js` |
|
|
@@ -176,7 +190,7 @@ One part per phase, chip-listing the catalog entering that phase's filter, title
|
|
|
176
190
|
|
|
177
191
|
### Config tab
|
|
178
192
|
|
|
179
|
-
|
|
193
|
+
Five cards: **Save current config** (snapshot), **Save as agent preset** (fork the current target's preset directory), **Import config**, **Environment variable blocklist** (add/remove entries, inspect the variables actually registered right now), **Force override switch**. Each snapshot row is Apply / Export / Delete, and the active row carries an "in use" badge.
|
|
180
194
|
|
|
181
195
|
### Edit target and saving
|
|
182
196
|
|
package/README.md
CHANGED
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
- **注入**:每个阶段部分底部都能按名称 + 文本新建一段,带 `手动` 徽标,随时可删除。
|
|
78
78
|
- **排序**:整行拖拽(拖到某行上方/下方出现插入线)或 ↑/↓ 箭头。顺序以 0 起始的连续虚拟下标存储,不会出现重复或小数 order,也不需要手填数字。
|
|
79
79
|
- **每阶段独立**:引导期屏蔽写 `sectionsBootstrap`,压缩受控期写 `sectionsCompaction`,常驻期写 `sections`;三者的排序空间互不干扰,因此「引导期只留 5 段、常驻期全开」是可以分别表达的。
|
|
80
|
+
- **强制覆盖(`forceSections`,默认开)**:包装宿主的装配入口,最终提示词段直接从注册表原始段重建 —— 预设插件自身的阶段裁段(如 liangshen 引导期 20→1)与 persona 的 `complete: true` 整段接管都无法再改写本插件的屏蔽 / 替换 / 注入 / 排序结果;工具目录与动态上下文保持预设与宿主行为。设为 `false` 退回瀑布流内过滤(此时完整段接管仍会压制段级定制)。
|
|
80
81
|
|
|
81
82
|
### 工具目录
|
|
82
83
|
|
|
@@ -90,6 +91,19 @@
|
|
|
90
91
|
|
|
91
92
|
预览 Tab 展示经过**所有插件**过滤后的最终系统提示词与工具目录,并按阶段切换。它与提示词 / 工具 Tab 读同一份装配结果,所见即模型所见。工具视图给出「模型可见 / 注册表总数」的对照 —— PTC、Code Mode 这类预设会把整套目录包装成单个 `run_code`,只看注册表会误判。
|
|
92
93
|
|
|
94
|
+
### 提示词变量
|
|
95
|
+
|
|
96
|
+
宿主只注册 `provider` / `model` / `cwd` 三个变量,段文本里引用别的 `{{name}}` 会直接让整次装配失败。本插件经官方扩展点追加两类变量:
|
|
97
|
+
|
|
98
|
+
- **内置系统事实**:`date` / `time` / `datetime` / `weekday` / `hostname` / `platform` / `arch` / `username` / `home` / `shell` / `locale` / `node_version`,每次装配现算。
|
|
99
|
+
- **全量环境变量**:`process.env` 的每个键映射为 `env_<小写,非法字符归并为 _>`,如 `PATH` → `{{env_path}}`、`USER.NAME` → `{{env_user_name}}`。
|
|
100
|
+
|
|
101
|
+
**黑名单(`envBlocklist`)**:env 里常有密钥,进了提示词就会随请求发给模型,所以命中黑名单的键不注册。预填条目覆盖常见凭据类键(含 `SECRET` / `TOKEN` / `PASSWORD` / `API_KEY` / `*_DSN` 等),条目支持 `*` 通配、大小写不敏感;面板的环境变量黑名单卡片可随时增删,卡里的折叠列表给出当前实际注册的变量清单。黑名单变化在下次装配生效(面板改动或直接改 `config.yaml` 都一样)。
|
|
102
|
+
|
|
103
|
+
**导出 / 导入随行**:导出预设时当前黑名单随行写入文件;导入端按并集并入(只增不减),与快照库语义一致。
|
|
104
|
+
|
|
105
|
+
严格渲染的代价:段里引用了一个未注册的名字(如被黑名单挡下的键),**整段装配失败**而不是留空 —— 面板清单里没列出的名字不要用。
|
|
106
|
+
|
|
93
107
|
### 配置快照与 agent 预设
|
|
94
108
|
|
|
95
109
|
- **保存当前配置**:把当前作用域的完整定制(含每阶段屏蔽名单、每阶段排序、阶段工具目录)存成一份可复用的配置快照。
|
|
@@ -120,7 +134,7 @@
|
|
|
120
134
|
</tr>
|
|
121
135
|
<tr>
|
|
122
136
|
<td colspan="9" align="center">
|
|
123
|
-
<b>⑤ 交卷前还有一手</b>:宿主在瀑布流<b>之后</b>,若存在 <code>complete: true</code> 的段就把整份 sections 还原成那一条 ——
|
|
137
|
+
<b>⑤ 交卷前还有一手</b>:宿主在瀑布流<b>之后</b>,若存在 <code>complete: true</code> 的段就把整份 sections 还原成那一条 —— 这就是面板会对这类预设亮黄标的原因(工具目录不受影响;<code>forceSections</code> 开启时本插件已在装配入口重建 sections,接管被绕过,不再亮黄标)
|
|
124
138
|
</td>
|
|
125
139
|
</tr>
|
|
126
140
|
<tr>
|
|
@@ -135,6 +149,7 @@
|
|
|
135
149
|
| `lib/index.js` | 宿主端:装配钩子、五条 HTTP 路由、agent 预设 fork、遗留字段一次性清理 |
|
|
136
150
|
| `lib/effective.js` | 纯函数:字段级 override 合并、阶段选取(注入段 / 工具目录)、黑名单过滤 |
|
|
137
151
|
| `lib/promotion.js` | 由 durable 事件推导 `{promoted, boundary}`,压缩复位、子代理恒已晋级 |
|
|
152
|
+
| `lib/vars.js` | 提示词变量:内置系统事实 + env 全量映射 / 黑名单,装配入口按签名懒同步 |
|
|
138
153
|
| `lib/sectionOps.mjs` | **面板与单测共用**的阶段状态纯函数(名单写回目标、注入身份、重排、逐阶段持久化) |
|
|
139
154
|
| `lib/store.js` / `lib/catalog.js` | 原子写 + last-good 回落的配置存储;跨预设段/工具累积登记表 |
|
|
140
155
|
| `src/client/` | 面板(React,无 JSX 语法糖依赖,`h()` 直写),构建进 `client/client.js` |
|
|
@@ -175,7 +190,7 @@ dsh plugin --profile web add /path/to/dsh-prompt-customizer
|
|
|
175
190
|
|
|
176
191
|
### 配置 Tab
|
|
177
192
|
|
|
178
|
-
|
|
193
|
+
五张卡片:**保存当前配置**(配置快照)、**存为 agent 预设**(fork 当前目标的预设目录)、**导入配置**、**环境变量黑名单**(增删条目、查看当前实际注册的变量清单)、**强制覆盖开关**。快照列表每行是 应用 / 导出 / 删除,正在使用的那一行带「使用中」标记。另有一张**恢复初始状态**卡片:清空全部定制并关闭 `forceSections`,与卸载插件等效(无需卸载 / 重启),点击需二次确认。
|
|
179
194
|
|
|
180
195
|
### 编辑目标与保存
|
|
181
196
|
|
|
@@ -190,7 +205,7 @@ dsh plugin --profile web add /path/to/dsh-prompt-customizer
|
|
|
190
205
|
|
|
191
206
|
下面都是实测结论。面板在相关情况下会直接给出黄色警示,不需要你猜:
|
|
192
207
|
|
|
193
|
-
-
|
|
208
|
+
- **有些预设的提示词不由段组成。** 若某个段以 `complete: true` 注册(dsh-persona 的「整段接管」),宿主会在装配瀑布流**之后**把整份 sections 还原成那一条段。`forceSections`(默认开)已在装配入口绕开该机制,段级定制照常生效;只有把它设为 `false` 时,本插件的屏蔽 / 替换 / 注入 / 排序才会对这类预设无效(工具过滤不受影响),面板会写明「最终系统提示词由 `deployment:persona` 段整段接管」。
|
|
194
209
|
- **丢弃段的原因不止一种**。除了 `complete` 接管,也存在预设用自己的规则在特定阶段裁段(实测某预设引导期 20 段 → 1 段,而注册表里并没有 complete 段)。所以面板还做了一次机制无关的比对:把本插件产出的段名与最终装配逐名核对,不符时提示「产出 N 段,模型只见 M 段」。
|
|
195
210
|
- **预览的伪会话可能降级**。阶段视图由一次合成会话驱动,个别预设插件会在这种会话上抛错;此时该阶段回退为无会话装配并标注「已降级」,即那个阶段看到的是不带原生阶段裁剪的结果。
|
|
196
211
|
- **替换动态生成的段会固定其内容**。像 `app:web-surface` 这类段在装配时实时生成(嵌入当前 Web 端口等)。替换后文本冻结为你编辑时的值,不再跟随运行时变化;想保持跟随请改用屏蔽。
|
package/client/client.js
CHANGED
|
@@ -94,7 +94,7 @@ window.__ModuleLoader__.load({
|
|
|
94
94
|
toolCopied: "已把「{name}」复制到{to}:{from}那份保持不动,隐藏/启用状态一起带过去。",
|
|
95
95
|
toolShown: "「{name}」已在{to}显示。",
|
|
96
96
|
toolHiddenIn: "「{name}」已在{to}隐藏。",
|
|
97
|
-
sectionsTakenOver: "
|
|
97
|
+
sectionsTakenOver: "「{name}」段声明了 complete 整段接管(宿主在装配瀑布流之后强制还原为那一条段)。本插件默认的 forceSections 已绕开该机制,段级屏蔽 / 替换 / 注入 / 排序照常生效;此提示只会在 forceSections 关闭或强制覆盖失效时出现,开启 forceSections 即可恢复。",
|
|
98
98
|
sectionsLost: "本插件在该阶段产出的 {emitted} 个段里,只有 {survived} 个进入最终提示词:其余被下游装配规则丢弃,段级定制不会完全生效(工具过滤不受影响)。",
|
|
99
99
|
sectionBlockedStrip: "(本阶段已屏蔽,模型不可见)",
|
|
100
100
|
phaseSuppressed: "被抑制(不在任何阶段的装配中)",
|
|
@@ -109,6 +109,24 @@ window.__ModuleLoader__.load({
|
|
|
109
109
|
saveOk: "已保存",
|
|
110
110
|
saveFail: "保存失败",
|
|
111
111
|
discardConfirm: "有未保存的更改,离开将丢弃。确定离开吗?",
|
|
112
|
+
forceTitle: "强制覆盖(forceSections)",
|
|
113
|
+
forceHint: "开启后所有预设的提示词段一律以本插件为准(绕过 complete 整段接管与预设阶段裁段);关闭后退回瀑布流内过滤,预设可能压过定制。「恢复初始状态」会自动关闭它。",
|
|
114
|
+
forceOn: "已开启",
|
|
115
|
+
forceOff: "已关闭",
|
|
116
|
+
envBlockTitle: "环境变量黑名单(envBlocklist)",
|
|
117
|
+
envBlockHint: "系统环境变量(process.env)已全量注册为提示词变量:段文本里写 {{env_键名小写}} 引用(如 {{env_path}});命中黑名单的键不注册——env 里常有密钥,进了提示词就会随请求发给模型。条目支持 * 通配、大小写不敏感。内置变量 {{date}} {{time}} {{datetime}} {{weekday}} {{hostname}} {{platform}} {{arch}} {{username}} {{home}} {{shell}} {{locale}} {{node_version}} 恒可用。",
|
|
118
|
+
envBlockAdd: "键名或通配,如 GITHUB_TOKEN 或 *_KEY*",
|
|
119
|
+
envBlockAddAction: "添加",
|
|
120
|
+
envBlockEmpty: "黑名单为空:所有环境变量都会被注册",
|
|
121
|
+
envVarsTitle: "已注册的提示词变量",
|
|
122
|
+
envVarsHint: "段文本里引用 {{变量名}};引用未注册的名字会让整次渲染报错(宿主严格模式)。",
|
|
123
|
+
importBlockMerge: ",文件中的环境变量黑名单已并入(+{count} 条)",
|
|
124
|
+
resetTitle: "恢复初始状态",
|
|
125
|
+
resetHint: "清空全部定制(屏蔽 / 替换 / 注入 / 排序、阶段工具目录、配置快照、agent 预设覆盖),并关闭 forceSections —— 之后所有 AI 对话不再受本插件影响,与卸载插件等效,无需卸载或重启。此操作不可撤销;要重新启用定制,在上方打开「强制覆盖」开关即可。",
|
|
126
|
+
resetConfirm: "确定清空全部定制并恢复初始状态吗?\n\n将删除:屏蔽名单、段替换、注入段、阶段工具目录、配置快照、agent 预设覆盖;并关闭 forceSections。\n之后所有 AI 对话都不再受本插件影响(与卸载插件等效)。\n此操作不可撤销。",
|
|
127
|
+
resetAction: "清空全部定制",
|
|
128
|
+
resetOk: "已恢复初始状态:所有 AI 对话不再受本插件影响",
|
|
129
|
+
resetFail: "恢复初始状态失败",
|
|
112
130
|
scopeFallback: "该 agent 预设的常驻 scope 未能挂载,以下显示的是全局层内容(并非该预设的原生段与工具)。",
|
|
113
131
|
previewDegraded: "伪会话触发了某个预设插件的异常,本次预览已降级为无会话装配——预设原生的阶段裁剪规则未参与运行。",
|
|
114
132
|
previewFail: "该阶段装配失败(scope 挂载失败或预设插件异常),请切换目标或查看服务日志。",
|
|
@@ -203,7 +221,7 @@ window.__ModuleLoader__.load({
|
|
|
203
221
|
toolCopied: "Copied \"{name}\" to {to}: the copy in {from} is left untouched, and its hidden/enabled state carries over.",
|
|
204
222
|
toolShown: "\"{name}\" is now visible in {to}.",
|
|
205
223
|
toolHiddenIn: "\"{name}\" is now hidden in {to}.",
|
|
206
|
-
sectionsTakenOver: "The
|
|
224
|
+
sectionsTakenOver: "The \"{name}\" section declares a complete takeover (the host restores that single section after the assembly waterfall). This plugin’s forceSections (default on) bypasses that mechanism, so section-level blocking / replacement / injection / ordering still applies; this notice only appears when forceSections is off or the override fails — enable it to restore.",
|
|
207
225
|
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
226
|
sectionBlockedStrip: " (blocked in this period; invisible to the model)",
|
|
209
227
|
phaseSuppressed: "Suppressed (absent from every phase assembly)",
|
|
@@ -218,6 +236,24 @@ window.__ModuleLoader__.load({
|
|
|
218
236
|
saveOk: "Saved",
|
|
219
237
|
saveFail: "Save failed",
|
|
220
238
|
discardConfirm: "You have unsaved changes; leaving will discard them. Leave anyway?",
|
|
239
|
+
forceTitle: "Force override (forceSections)",
|
|
240
|
+
forceHint: "When on, every preset’s prompt sections always follow this plugin (bypassing complete takeover and preset phase pruning); when off, filtering falls back to the waterfall, where presets may override your customization. \"Restore initial state\" turns this off automatically.",
|
|
241
|
+
forceOn: "On",
|
|
242
|
+
forceOff: "Off",
|
|
243
|
+
envBlockTitle: "Environment variable blocklist (envBlocklist)",
|
|
244
|
+
envBlockHint: "All system environment variables (process.env) are registered as prompt variables: reference them in section text as {{env_lowercase_key}} (e.g. {{env_path}}); keys hit by the blocklist are not registered — env often carries secrets, and anything in the prompt is sent to the model. Entries support * wildcards, case-insensitive. Built-ins {{date}} {{time}} {{datetime}} {{weekday}} {{hostname}} {{platform}} {{arch}} {{username}} {{home}} {{shell}} {{locale}} {{node_version}} are always available.",
|
|
245
|
+
envBlockAdd: "Key or wildcard, e.g. GITHUB_TOKEN or *_KEY*",
|
|
246
|
+
envBlockAddAction: "Add",
|
|
247
|
+
envBlockEmpty: "Blocklist is empty: every environment variable gets registered",
|
|
248
|
+
envVarsTitle: "Registered prompt variables",
|
|
249
|
+
envVarsHint: "Reference {{name}} in section text; an unregistered name fails the whole render (host strict mode).",
|
|
250
|
+
importBlockMerge: ", the file’s env-variable blocklist was merged in (+{count} entries)",
|
|
251
|
+
resetTitle: "Restore initial state",
|
|
252
|
+
resetHint: "Clears all customization (blocking / replacement / injection / ordering, per-phase tool catalogs, config snapshots, agent-preset overrides) and turns forceSections off — every AI conversation goes back to being unaffected by this plugin, equivalent to uninstalling it, without uninstalling or restarting. This cannot be undone; to re-enable customization, turn the \"Force override\" switch above back on.",
|
|
253
|
+
resetConfirm: "Clear all customization and restore the initial state?\n\nThis will delete: block lists, section replacements, injected sections, per-phase tool catalogs, config snapshots, and agent-preset overrides; forceSections will be turned off.\nAfterwards no AI conversation is affected by this plugin (equivalent to uninstalling).\nThis cannot be undone.",
|
|
254
|
+
resetAction: "Clear all customization",
|
|
255
|
+
resetOk: "Initial state restored: no AI conversation is affected by this plugin anymore",
|
|
256
|
+
resetFail: "Failed to restore initial state",
|
|
221
257
|
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
258
|
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
259
|
previewFail: "Assembly for this phase failed (scope mount error or a preset plugin threw); switch target or check the server log.",
|
|
@@ -1091,6 +1127,7 @@ window.__ModuleLoader__.load({
|
|
|
1091
1127
|
const partNote = (key) => {
|
|
1092
1128
|
const view = phases?.[key];
|
|
1093
1129
|
if (view === void 0 || view === null) return null;
|
|
1130
|
+
if (cfg.forceSections !== false) return null;
|
|
1094
1131
|
if (view.takenOverBy !== void 0) return (0, react.createElement)("div", { style: s.noticeWarn }, t("sectionsTakenOver", { name: view.takenOverBy }));
|
|
1095
1132
|
const lost = view.lostSections;
|
|
1096
1133
|
if (lost !== void 0) return (0, react.createElement)("div", { style: s.noticeWarn }, t("sectionsLost", {
|
|
@@ -1384,7 +1421,7 @@ window.__ModuleLoader__.load({
|
|
|
1384
1421
|
}, t("save")),
|
|
1385
1422
|
(0, react.createElement)("button", {
|
|
1386
1423
|
style: s.mini,
|
|
1387
|
-
onClick: () =>
|
|
1424
|
+
onClick: () => setDraft("")
|
|
1388
1425
|
}, t("clearInput")),
|
|
1389
1426
|
!row.custom && (row.override || Object.hasOwn(cfg.replace ?? {}, row.name)) ? (0, react.createElement)("button", {
|
|
1390
1427
|
style: s.mini,
|
|
@@ -1818,11 +1855,13 @@ window.__ModuleLoader__.load({
|
|
|
1818
1855
|
anchor.click();
|
|
1819
1856
|
target.revoke(url);
|
|
1820
1857
|
}
|
|
1821
|
-
/** 把单个预设编码为导出文件的 JSON
|
|
1822
|
-
|
|
1858
|
+
/** 把单个预设编码为导出文件的 JSON 格式(单对象格式)。`envBlocklist`
|
|
1859
|
+
* 是全局环境变量黑名单的随行快照:导入端按并集并入(见 PresetsTab)。 */
|
|
1860
|
+
function encodePresetExport(preset, envBlocklist) {
|
|
1823
1861
|
return JSON.stringify({
|
|
1824
1862
|
name: preset.name,
|
|
1825
|
-
data: preset.data
|
|
1863
|
+
data: preset.data,
|
|
1864
|
+
...Array.isArray(envBlocklist) ? { envBlocklist } : {}
|
|
1826
1865
|
}, null, 2);
|
|
1827
1866
|
}
|
|
1828
1867
|
/**
|
|
@@ -1861,10 +1900,10 @@ window.__ModuleLoader__.load({
|
|
|
1861
1900
|
* 导出一个预设:宿主能提供原生对话框时走 tauri 保存(cancelled = 用户
|
|
1862
1901
|
* 拒绝,绝不回退下载);否则走 Web 下载。
|
|
1863
1902
|
*/
|
|
1864
|
-
async function exportPresetFile(preset, io) {
|
|
1903
|
+
async function exportPresetFile(preset, io, envBlocklist) {
|
|
1865
1904
|
const env = io ?? makeIoEnv();
|
|
1866
1905
|
try {
|
|
1867
|
-
const text = encodePresetExport(preset);
|
|
1906
|
+
const text = encodePresetExport(preset, envBlocklist);
|
|
1868
1907
|
const filename = presetExportFilename(preset.name);
|
|
1869
1908
|
if (env.tauri) {
|
|
1870
1909
|
const out = await env.saveText(filename, text);
|
|
@@ -1919,9 +1958,10 @@ window.__ModuleLoader__.load({
|
|
|
1919
1958
|
* 覆盖项,新预设随即出现在顶部的编辑目标选择器里。
|
|
1920
1959
|
* 导入导出经 preset-io 双端适配:Tauri 2 桌面走原生对话框,Web 走
|
|
1921
1960
|
* 下载 / 文件选择;成功 / 失败 / 取消都有面板内消息条提示。 */
|
|
1922
|
-
function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveAsPreset, forkSource }) {
|
|
1961
|
+
function PresetsTab({ cfg, inv, phases, t, writePatch, writeGlobal, saveAsPreset, forkSource, onReset, envBlocklist }) {
|
|
1923
1962
|
const presets = cfg.presets ?? [];
|
|
1924
1963
|
const [name, setName] = (0, react.useState)("");
|
|
1964
|
+
const [blockInput, setBlockInput] = (0, react.useState)("");
|
|
1925
1965
|
const [agentName, setAgentName] = (0, react.useState)("");
|
|
1926
1966
|
const [creating, setCreating] = (0, react.useState)(false);
|
|
1927
1967
|
const [notice, setNotice] = (0, react.useState)(null);
|
|
@@ -1965,7 +2005,7 @@ window.__ModuleLoader__.load({
|
|
|
1965
2005
|
};
|
|
1966
2006
|
const exportPreset = async (preset) => {
|
|
1967
2007
|
try {
|
|
1968
|
-
const res = await exportPresetFile(preset);
|
|
2008
|
+
const res = await exportPresetFile(preset, void 0, envBlocklist);
|
|
1969
2009
|
if (res.ok) show("ok", t("exportOk"));
|
|
1970
2010
|
else if (res.cancelled) show("ok", t("exportCancel"));
|
|
1971
2011
|
else show("error", `${t("exportFail")}${res.message ? ": " + res.message : ""}`);
|
|
@@ -1978,10 +2018,13 @@ window.__ModuleLoader__.load({
|
|
|
1978
2018
|
const parsed = decodePresetExport(text);
|
|
1979
2019
|
const next = addImportedPresets(presets, parsed, genId);
|
|
1980
2020
|
const added = next.length - presets.length;
|
|
1981
|
-
|
|
2021
|
+
const fresh = (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && Array.isArray(parsed.envBlocklist) ? (parsed.envBlocklist ?? []).filter((e) => typeof e === "string") : []).filter((e) => !envBlocklist.includes(e));
|
|
2022
|
+
if (fresh.length > 0) writeGlobal("envBlocklist", [...envBlocklist, ...fresh]);
|
|
2023
|
+
const mergedNote = fresh.length > 0 ? t("importBlockMerge", { count: fresh.length }) : "";
|
|
2024
|
+
if (added <= 0) show("ok", t("importNone") + mergedNote);
|
|
1982
2025
|
else {
|
|
1983
2026
|
writeGlobal("presets", next);
|
|
1984
|
-
show("ok", `${t("importOk")} (+${added})`);
|
|
2027
|
+
show("ok", `${t("importOk")} (+${added})${mergedNote}`);
|
|
1985
2028
|
}
|
|
1986
2029
|
} catch (e) {
|
|
1987
2030
|
show("error", `${t("importFail")}: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -2019,6 +2062,15 @@ window.__ModuleLoader__.load({
|
|
|
2019
2062
|
if (await saveAsPreset(trimmed)) setAgentName("");
|
|
2020
2063
|
setCreating(false);
|
|
2021
2064
|
};
|
|
2065
|
+
const removeBlockEntry = (entry) => {
|
|
2066
|
+
writeGlobal("envBlocklist", envBlocklist.filter((e) => e !== entry));
|
|
2067
|
+
};
|
|
2068
|
+
const addBlockEntry = () => {
|
|
2069
|
+
const entry = blockInput.trim();
|
|
2070
|
+
setBlockInput("");
|
|
2071
|
+
if (entry === "" || envBlocklist.includes(entry)) return;
|
|
2072
|
+
writeGlobal("envBlocklist", [...envBlocklist, entry]);
|
|
2073
|
+
};
|
|
2022
2074
|
return (0, react.createElement)("div", { style: s.list }, [
|
|
2023
2075
|
notice ? (0, react.createElement)("div", { style: notice.kind === "ok" ? s.noticeOk : s.error }, notice.text) : null,
|
|
2024
2076
|
(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", {
|
|
@@ -2064,6 +2116,79 @@ window.__ModuleLoader__.load({
|
|
|
2064
2116
|
style: { display: "none" },
|
|
2065
2117
|
onChange: onImportFile
|
|
2066
2118
|
})])]),
|
|
2119
|
+
(0, react.createElement)("div", { style: s.injectBox }, [
|
|
2120
|
+
(0, react.createElement)("div", { style: s.rowTitle }, t("forceTitle")),
|
|
2121
|
+
(0, react.createElement)("div", { style: s.muted }, t("forceHint")),
|
|
2122
|
+
(0, react.createElement)("div", { style: s.injectRow }, [(0, react.createElement)("label", { style: s.switchWrap }, [(0, react.createElement)("input", {
|
|
2123
|
+
type: "checkbox",
|
|
2124
|
+
checked: cfg.forceSections !== false,
|
|
2125
|
+
onChange: (e) => writeGlobal("forceSections", e.target.checked)
|
|
2126
|
+
}), (0, react.createElement)("span", { style: cfg.forceSections !== false ? s.badgeOk : s.badgeBlocked }, cfg.forceSections !== false ? t("forceOn") : t("forceOff"))])])
|
|
2127
|
+
]),
|
|
2128
|
+
(0, react.createElement)("div", { style: s.injectBox }, [
|
|
2129
|
+
(0, react.createElement)("div", { style: s.rowTitle }, t("envBlockTitle")),
|
|
2130
|
+
(0, react.createElement)("div", { style: s.muted }, t("envBlockHint")),
|
|
2131
|
+
(0, react.createElement)("div", { style: {
|
|
2132
|
+
...s.toolWrap,
|
|
2133
|
+
marginTop: 6
|
|
2134
|
+
} }, envBlocklist.map((entry) => (0, react.createElement)("span", {
|
|
2135
|
+
key: entry,
|
|
2136
|
+
style: {
|
|
2137
|
+
...s.toolChip,
|
|
2138
|
+
display: "inline-flex",
|
|
2139
|
+
alignItems: "center",
|
|
2140
|
+
gap: 4
|
|
2141
|
+
}
|
|
2142
|
+
}, [entry, (0, react.createElement)("span", {
|
|
2143
|
+
style: { cursor: "pointer" },
|
|
2144
|
+
title: t("delete"),
|
|
2145
|
+
onClick: () => removeBlockEntry(entry)
|
|
2146
|
+
}, "×")]))),
|
|
2147
|
+
envBlocklist.length === 0 ? (0, react.createElement)("div", { style: s.muted }, t("envBlockEmpty")) : null,
|
|
2148
|
+
(0, react.createElement)("div", { style: {
|
|
2149
|
+
...s.injectRow,
|
|
2150
|
+
marginTop: 6
|
|
2151
|
+
} }, [(0, react.createElement)("input", {
|
|
2152
|
+
style: {
|
|
2153
|
+
...s.input,
|
|
2154
|
+
flex: 1
|
|
2155
|
+
},
|
|
2156
|
+
placeholder: t("envBlockAdd"),
|
|
2157
|
+
value: blockInput,
|
|
2158
|
+
onChange: (e) => setBlockInput(e.target.value)
|
|
2159
|
+
}), (0, react.createElement)("button", {
|
|
2160
|
+
style: s.mini,
|
|
2161
|
+
disabled: blockInput.trim() === "",
|
|
2162
|
+
onClick: addBlockEntry
|
|
2163
|
+
}, t("envBlockAddAction"))]),
|
|
2164
|
+
(0, react.createElement)("details", { style: { marginTop: 6 } }, [
|
|
2165
|
+
(0, react.createElement)("summary", { style: {
|
|
2166
|
+
...s.muted,
|
|
2167
|
+
cursor: "pointer"
|
|
2168
|
+
} }, `${t("envVarsTitle")} (${inv?.variables?.length ?? 0})`),
|
|
2169
|
+
(0, react.createElement)("div", { style: {
|
|
2170
|
+
...s.muted,
|
|
2171
|
+
marginTop: 4
|
|
2172
|
+
} }, t("envVarsHint")),
|
|
2173
|
+
(0, react.createElement)("div", { style: {
|
|
2174
|
+
...s.toolWrap,
|
|
2175
|
+
marginTop: 4
|
|
2176
|
+
} }, (inv?.variables ?? []).map((name) => (0, react.createElement)("span", {
|
|
2177
|
+
key: name,
|
|
2178
|
+
style: s.toolChip
|
|
2179
|
+
}, `{{${name}}}`)))
|
|
2180
|
+
])
|
|
2181
|
+
]),
|
|
2182
|
+
(0, react.createElement)("div", { style: s.injectBox }, [
|
|
2183
|
+
(0, react.createElement)("div", { style: s.rowTitle }, t("resetTitle")),
|
|
2184
|
+
(0, react.createElement)("div", { style: s.muted }, t("resetHint")),
|
|
2185
|
+
(0, react.createElement)("div", { style: s.injectRow }, [(0, react.createElement)("button", {
|
|
2186
|
+
style: s.mini,
|
|
2187
|
+
onClick: () => {
|
|
2188
|
+
if (window.confirm(t("resetConfirm"))) onReset();
|
|
2189
|
+
}
|
|
2190
|
+
}, t("resetAction"))])
|
|
2191
|
+
]),
|
|
2067
2192
|
presets.length === 0 ? (0, react.createElement)("div", { style: s.muted }, t("empty")) : null,
|
|
2068
2193
|
presets.map((preset) => {
|
|
2069
2194
|
const active = cfg.activePreset === preset.id;
|
|
@@ -2178,6 +2303,7 @@ window.__ModuleLoader__.load({
|
|
|
2178
2303
|
const CONFIG_SET_URL = "/api/prompt-customizer/config/set";
|
|
2179
2304
|
const CONFIG_UNSET_URL = "/api/prompt-customizer/config/unset";
|
|
2180
2305
|
const CONFIG_APPLY_URL = "/api/prompt-customizer/config/apply";
|
|
2306
|
+
const CONFIG_RESET_URL = "/api/prompt-customizer/config/reset";
|
|
2181
2307
|
const PRESETS_CREATE_URL = "/api/prompt-customizer/presets";
|
|
2182
2308
|
const PREVIEW_URL = "/api/prompt-customizer/preview";
|
|
2183
2309
|
/** 三套名义装配的共享布局:顺序固定,refresh 按此并行拉取。 */
|
|
@@ -2332,6 +2458,19 @@ window.__ModuleLoader__.load({
|
|
|
2332
2458
|
};
|
|
2333
2459
|
const writeGlobal = writeField;
|
|
2334
2460
|
const writeGlobalField = writeField;
|
|
2461
|
+
const resetAll = () => {
|
|
2462
|
+
fetch(CONFIG_RESET_URL, {
|
|
2463
|
+
method: "POST",
|
|
2464
|
+
headers: { "content-type": "application/json" }
|
|
2465
|
+
}).then((r) => r.json()).then((body) => {
|
|
2466
|
+
if (body?.ok !== true) throw new Error(body?.error ?? "reset failed");
|
|
2467
|
+
setCfg(body.config);
|
|
2468
|
+
setDraft(null);
|
|
2469
|
+
setError(null);
|
|
2470
|
+
setVersion((n) => n + 1);
|
|
2471
|
+
showFlash(t("resetOk"));
|
|
2472
|
+
}).catch((e) => setError(`${t("resetFail")}: ${e instanceof Error ? e.message : String(e)}`));
|
|
2473
|
+
};
|
|
2335
2474
|
const switchTab = (next) => {
|
|
2336
2475
|
if ((tab === "sections" || tab === "tools") && (next === "presets" || next === "preview")) {
|
|
2337
2476
|
if (draft?.dirty && !window.confirm(t("discardConfirm"))) return;
|
|
@@ -2445,7 +2584,9 @@ window.__ModuleLoader__.load({
|
|
|
2445
2584
|
writePatch,
|
|
2446
2585
|
writeGlobal,
|
|
2447
2586
|
saveAsPreset,
|
|
2448
|
-
forkSource: target ? agentPresets.find((p) => p.id === target)?.name ?? target : void 0
|
|
2587
|
+
forkSource: target ? agentPresets.find((p) => p.id === target)?.name ?? target : void 0,
|
|
2588
|
+
onReset: resetAll,
|
|
2589
|
+
envBlocklist: cfg.envBlocklist ?? []
|
|
2449
2590
|
}) : (0, react.createElement)(PreviewTab, {
|
|
2450
2591
|
t,
|
|
2451
2592
|
active: tab === "preview",
|