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/src/client/index.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-prompt-customizer —
|
|
2
|
+
* dsh-prompt-customizer — 浏览器端(TypeScript 源码)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* bound settings scope (`prompt-customizer` namespace) and apply live.
|
|
4
|
+
* 注册一个「提示词定制」设置分区(侧边栏入口)及插件设置卡片。面板从宿主
|
|
5
|
+
* 的 `/api/prompt-customizer/inventory` 拉取当前生效的段 / 工具清单,让用户
|
|
6
|
+
* 屏蔽、替换、注入提示词段并隐藏工具。所有写入都走绑定的设置作用域
|
|
7
|
+
* (`prompt-customizer` 命名空间),实时生效。
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 由 tsdown 打包为 client/client.js(__ModuleLoader__ factory bundle);
|
|
10
|
+
* 唯一的外部依赖是 loader 模块表中的 react 入口。
|
|
12
11
|
*/
|
|
13
|
-
import { createElement as h, useEffect, useState, type ReactElement } from 'react'
|
|
12
|
+
import { createElement as h, useEffect, useRef, useState, type ReactElement } from 'react'
|
|
14
13
|
import { DICT, type Translate } from './locales.ts'
|
|
15
14
|
import { s } from './styles.ts'
|
|
16
|
-
import type { Config, Inventory,
|
|
15
|
+
import type { AgentPresetInfo, Config, Inventory, PhaseViewKey, Preview } from './types.ts'
|
|
16
|
+
import { editView, type ConfigPatch } from './presets.ts'
|
|
17
17
|
import { SectionsTab } from './SectionsTab.tsx'
|
|
18
18
|
import { ToolsTab } from './ToolsTab.tsx'
|
|
19
19
|
import { PresetsTab } from './PresetsTab.tsx'
|
|
@@ -21,25 +21,35 @@ import { PreviewTab } from './PreviewTab.tsx'
|
|
|
21
21
|
|
|
22
22
|
const NS = 'prompt-customizer'
|
|
23
23
|
const INVENTORY_URL = '/api/prompt-customizer/inventory'
|
|
24
|
+
const AGENT_PRESETS_URL = '/api/prompt-customizer/agent-presets'
|
|
25
|
+
const CONFIG_URL = '/api/prompt-customizer/config'
|
|
26
|
+
const CONFIG_SET_URL = '/api/prompt-customizer/config/set'
|
|
27
|
+
const CONFIG_UNSET_URL = '/api/prompt-customizer/config/unset'
|
|
28
|
+
const CONFIG_APPLY_URL = '/api/prompt-customizer/config/apply'
|
|
29
|
+
const PRESETS_CREATE_URL = '/api/prompt-customizer/presets'
|
|
30
|
+
const PREVIEW_URL = '/api/prompt-customizer/preview'
|
|
31
|
+
|
|
32
|
+
/** 三阶段预览装配(模型视角):提示词/工具/预览 Tab 的统一数据源。 */
|
|
33
|
+
type PhaseViews = Record<PhaseViewKey, Preview | null>
|
|
34
|
+
|
|
35
|
+
/** 三套名义装配的共享布局:顺序固定,refresh 按此并行拉取。 */
|
|
36
|
+
const VIEW_KEYS: PhaseViewKey[] = ['bootstrap', 'compaction', 'active']
|
|
24
37
|
|
|
25
38
|
export const name = 'prompt-customizer'
|
|
26
39
|
export const inject = ['slots', 'locale']
|
|
27
40
|
|
|
28
|
-
/**
|
|
41
|
+
/** 本插件用到的客户端 cordis 上下文的最小子集。 */
|
|
29
42
|
interface ClientContext {
|
|
30
43
|
effect(callback: () => unknown, label?: string): void
|
|
31
44
|
locale: {
|
|
32
45
|
register(namespace: string, dicts: { zh: Record<string, string>; en: Record<string, string> }): unknown
|
|
33
46
|
bind(namespace: string): Translate
|
|
34
47
|
}
|
|
35
|
-
inject(services: string[], callback: (scoped:
|
|
48
|
+
inject(services: string[], callback: (scoped: SlotsHost) => void): void
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
/**
|
|
39
|
-
interface
|
|
40
|
-
settingsScope: {
|
|
41
|
-
bind(options: { namespace: string }): SettingsScope
|
|
42
|
-
}
|
|
51
|
+
/** 设置分区需要的宿主能力面(插槽注册)。 */
|
|
52
|
+
interface SlotsHost {
|
|
43
53
|
slots: {
|
|
44
54
|
inject(name: string, register: () => unknown): void
|
|
45
55
|
register(meta: Record<string, unknown>, render: () => unknown): unknown
|
|
@@ -50,14 +60,10 @@ export function apply(ctx: ClientContext): void {
|
|
|
50
60
|
ctx.effect(() => ctx.locale.register(NS, DICT), 'prompt-customizer: locale')
|
|
51
61
|
const t = ctx.locale.bind(NS)
|
|
52
62
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
// it stays hidden instead of unmounting the whole plugin.
|
|
58
|
-
ctx.inject(['settingsScope'], (scoped) => {
|
|
59
|
-
const scope = scoped.settingsScope.bind({ namespace: NS })
|
|
60
|
-
|
|
63
|
+
// 注册为独立的设置分区(侧边栏菜单项),面板只出现在「提示词定制」下,
|
|
64
|
+
// 而不是作为卡片塞进「插件」设置列表。配置读写走插件自有路由(宿主端的
|
|
65
|
+
// config.yaml),不再依赖 settings 传输层。
|
|
66
|
+
ctx.inject(['slots'], (scoped) => {
|
|
61
67
|
scoped.slots.inject('settings.section', () => scoped.slots.register({
|
|
62
68
|
name: 'settings.section',
|
|
63
69
|
id: 'prompt-customizer',
|
|
@@ -65,51 +71,330 @@ export function apply(ctx: ClientContext): void {
|
|
|
65
71
|
label: () => t('nav'),
|
|
66
72
|
locale: NS,
|
|
67
73
|
inject: () => ({ t }),
|
|
68
|
-
}, () => h(Panel, {
|
|
74
|
+
}, () => h(Panel, { t })))
|
|
69
75
|
})
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
// ──
|
|
78
|
+
// ── 根面板 ────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 面板根组件:拉取配置与清单、维护 Tab 状态,把各 Tab 子视图装配起来。
|
|
82
|
+
* 配置来自插件自有 /config 路由;清单来自 /inventory,可通过「刷新」按钮
|
|
83
|
+
* 手动重取。提示词/工具 Tab 的编辑只改内存草稿,由「保存」按钮经
|
|
84
|
+
* /config/apply 一次写盘(写错的配置不点保存就不会进文件)。
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/** 可编辑的配置字段(含每阶段独立段屏蔽名单)。 */
|
|
88
|
+
type EditField = 'sections' | 'sectionsBootstrap' | 'sectionsCompaction' | 'replace' | 'inject' | 'tools'
|
|
89
|
+
|
|
90
|
+
/** 编辑域草稿:提示词/工具 Tab 共享的未保存字段 + 脏标记。
|
|
91
|
+
* 只记录用户实际编辑过的字段(首次编辑只写那一项),保存时也只提交这些
|
|
92
|
+
* 字段 —— 未编辑的字段继续回落全局 / 继承,避免一次保存冻结全部继承值。 */
|
|
93
|
+
interface EditDraft {
|
|
94
|
+
sections?: Config['sections']
|
|
95
|
+
sectionsBootstrap?: Config['sectionsBootstrap']
|
|
96
|
+
sectionsCompaction?: Config['sectionsCompaction']
|
|
97
|
+
replace?: Config['replace']
|
|
98
|
+
inject?: Config['inject']
|
|
99
|
+
tools?: Config['tools']
|
|
100
|
+
dirty: boolean
|
|
101
|
+
}
|
|
73
102
|
|
|
74
|
-
function Panel({
|
|
75
|
-
const [
|
|
103
|
+
function Panel({ t }: { t: Translate }): ReactElement {
|
|
104
|
+
const [cfg, setCfg] = useState<Config | null>(null)
|
|
76
105
|
const [inv, setInv] = useState<Inventory | null>(null)
|
|
106
|
+
// 三阶段预览装配:模型视角的唯一数据源(加载中为 null)。
|
|
107
|
+
const [phases, setPhases] = useState<PhaseViews | null>(null)
|
|
108
|
+
const [agentPresets, setAgentPresets] = useState<AgentPresetInfo[]>([])
|
|
77
109
|
const [tab, setTab] = useState<'sections' | 'tools' | 'presets' | 'preview'>('sections')
|
|
78
110
|
const [error, setError] = useState<string | null>(null)
|
|
111
|
+
const [refreshId, setRefreshId] = useState(0)
|
|
112
|
+
// 编辑目标:undefined = 全局默认;字符串 = agent 预设 id(字段级 override)。
|
|
113
|
+
const [target, setTarget] = useState<string | undefined>(undefined)
|
|
114
|
+
// 配置写入计数:每次成功写配置后 +1,驱动 PreviewTab 重载。
|
|
115
|
+
// (以下 hooks 必须在任何提前 return 之前声明 —— hooks 顺序不能随加载状态变化。)
|
|
116
|
+
const [version, setVersion] = useState(0)
|
|
117
|
+
// 编辑域草稿:null = 无草稿(Tab 显示磁盘值)。提示词 ↔ 工具切换保留;
|
|
118
|
+
// 切到预设/预览 Tab 或切换编辑目标时丢弃(脏草稿先经确认)。
|
|
119
|
+
const [draft, setDraft] = useState<EditDraft | null>(null)
|
|
120
|
+
const [saving, setSaving] = useState(false)
|
|
121
|
+
const [flash, setFlash] = useState<string | null>(null)
|
|
122
|
+
const [flashKind, setFlashKind] = useState<'ok' | 'err'>('ok')
|
|
123
|
+
const flashTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
79
124
|
|
|
80
|
-
|
|
125
|
+
const load = (): void => {
|
|
126
|
+
fetch(CONFIG_URL + `?t=${Date.now()}`)
|
|
127
|
+
.then((r) => r.json())
|
|
128
|
+
.then((body) => {
|
|
129
|
+
if (body?.ok !== true) throw new Error(body?.error ?? 'config failed')
|
|
130
|
+
setCfg(body.config as Config)
|
|
131
|
+
setError(null)
|
|
132
|
+
})
|
|
133
|
+
.catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))
|
|
134
|
+
}
|
|
135
|
+
useEffect(load, [])
|
|
81
136
|
|
|
82
137
|
const refresh = (): void => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
138
|
+
const qs = target ? `?scope=${encodeURIComponent(target)}` : ''
|
|
139
|
+
// 三阶段预览并行拉取:提示词 / 工具 / 预览三个 Tab 全部以这套装配结果
|
|
140
|
+
// 为唯一数据源 —— 静态清单只是注册表视角(不含伪 agent 阶段裁剪、
|
|
141
|
+
// pre-step 注入等运行时规则),与它保持一致才是"所见即模型所见"。
|
|
142
|
+
// 三个阶段部分恒定显示(预设没有某个阶段时该部分只是空的),
|
|
143
|
+
// 不再按 (段, 工具) 签名去重折叠。
|
|
144
|
+
const params = (phase: string): string =>
|
|
145
|
+
`${qs}${qs ? '&' : '?'}phase=${phase}&t=${Date.now()}`
|
|
146
|
+
const grab = (phase: string): Promise<Preview | null> =>
|
|
147
|
+
fetch(PREVIEW_URL + params(phase))
|
|
148
|
+
.then((r) => r.json())
|
|
149
|
+
.then((body: Preview) => (body?.ok === false ? null : body))
|
|
150
|
+
.catch(() => null)
|
|
151
|
+
// 「本系统全部提示词/工具」面板是全局注册表,永不随编辑目标切换 ——
|
|
152
|
+
// 清单请求不带 scope,三阶段预览仍带 scope(作为该预设的真实装配)。
|
|
153
|
+
Promise.all([...VIEW_KEYS.map(grab), fetch(INVENTORY_URL).then((r) => r.json())])
|
|
154
|
+
.then(([boot, comp, act, inventoryData]) => {
|
|
155
|
+
setPhases({ bootstrap: boot, compaction: comp, active: act })
|
|
156
|
+
setInv(inventoryData as Inventory)
|
|
157
|
+
setError(null)
|
|
158
|
+
setRefreshId((n) => n + 1)
|
|
159
|
+
})
|
|
86
160
|
.catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))
|
|
87
161
|
}
|
|
88
|
-
|
|
162
|
+
// 挂载时与切换编辑目标后:清单与三阶段预览切到对应 scope。
|
|
163
|
+
// version 入依赖:保存/写配置成功后 +1,驱动三阶段预览重新拉取
|
|
164
|
+
// (PreviewTab 的数据源是 phases,不重拉就会一直显示保存前的装配)。
|
|
165
|
+
useEffect(refresh, [target, version])
|
|
166
|
+
|
|
167
|
+
// 枚举已安装的 agent 预设(roster 实时读取),给目标选择器与预览选择器供货。
|
|
168
|
+
// 抽成函数:保存预设成功后也要立即刷新目标下拉。
|
|
169
|
+
const fetchPresets = (): void => {
|
|
170
|
+
fetch(AGENT_PRESETS_URL + `?t=${Date.now()}`)
|
|
171
|
+
.then((r) => r.json())
|
|
172
|
+
.then((body) => setAgentPresets(Array.isArray(body?.presets) ? body.presets as AgentPresetInfo[] : []))
|
|
173
|
+
.catch(() => setAgentPresets([]))
|
|
174
|
+
}
|
|
175
|
+
useEffect(() => { fetchPresets() }, [])
|
|
89
176
|
|
|
90
|
-
if (
|
|
177
|
+
if (cfg === null) {
|
|
91
178
|
return h('div', { style: s.muted }, t('loading'))
|
|
92
179
|
}
|
|
93
180
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
181
|
+
// 保存成功/失败后的短促闪示消息(3 秒自动消失;err 用红色样式)。
|
|
182
|
+
const showFlash = (text: string, kind: 'ok' | 'err' = 'ok'): void => {
|
|
183
|
+
if (flashTimer.current !== null) clearTimeout(flashTimer.current)
|
|
184
|
+
setFlash(text)
|
|
185
|
+
setFlashKind(kind)
|
|
186
|
+
flashTimer.current = setTimeout(() => setFlash(null), 3200)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 编辑视图:草稿存在时以草稿覆盖对应字段(提示词/工具 Tab 的共享编辑域)。
|
|
190
|
+
// 草稿只含用户实际编辑过的字段,未编辑字段回落基线 —— 绝不把继承值
|
|
191
|
+
// 冻结进 override。
|
|
192
|
+
const base = editView(cfg, target)
|
|
193
|
+
const view: Config = draft
|
|
194
|
+
? {
|
|
195
|
+
...base,
|
|
196
|
+
sections: draft.sections ?? base.sections,
|
|
197
|
+
sectionsBootstrap: draft.sectionsBootstrap ?? base.sectionsBootstrap,
|
|
198
|
+
sectionsCompaction: draft.sectionsCompaction ?? base.sectionsCompaction,
|
|
199
|
+
replace: draft.replace ?? base.replace,
|
|
200
|
+
inject: draft.inject ?? base.inject,
|
|
201
|
+
tools: draft.tools ?? base.tools,
|
|
202
|
+
}
|
|
203
|
+
: base
|
|
204
|
+
|
|
205
|
+
const writeField = (field: string, value: unknown): void => {
|
|
206
|
+
const url = value === undefined ? CONFIG_UNSET_URL : CONFIG_SET_URL
|
|
207
|
+
fetch(url, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'content-type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({ field, value }),
|
|
211
|
+
})
|
|
212
|
+
.then((r) => r.json())
|
|
213
|
+
.then((body) => {
|
|
214
|
+
if (body?.ok !== true) throw new Error(body?.error ?? 'write failed')
|
|
215
|
+
setCfg(body.config as Config)
|
|
216
|
+
setError(null)
|
|
217
|
+
setVersion((n) => n + 1)
|
|
218
|
+
})
|
|
219
|
+
.catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 提示词/工具 Tab 的写入:只改内存草稿,不落盘。草稿只记录被编辑的
|
|
223
|
+
// 这一个字段(不携带其余字段的继承值)—— 保存时也只会提交编辑过的
|
|
224
|
+
// 字段,未编辑字段继续回落全局 / 继承。target 缺省时写全局顶层;
|
|
225
|
+
// 预设目标由 /config/apply 写 overrides[id](字段级接管语义在保存时生效)。
|
|
226
|
+
const edit = (field: EditField, value: unknown): void => {
|
|
227
|
+
setDraft((d) => ({
|
|
228
|
+
...(d ?? { dirty: false }),
|
|
229
|
+
[field]: value,
|
|
230
|
+
dirty: true,
|
|
231
|
+
}))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 统一保存:草稿里实际编辑过的字段一次 POST 到 /config/apply,成功后清
|
|
235
|
+
// 草稿并采纳最新配置。失败时保留草稿(改完可以再试,不会丢)。
|
|
236
|
+
const save = (): void => {
|
|
237
|
+
if (draft === null || saving) return
|
|
238
|
+
const EDITED_FIELDS = ['sections', 'sectionsBootstrap', 'sectionsCompaction', 'replace', 'inject', 'tools'] as const
|
|
239
|
+
const patch: Record<string, unknown> = {}
|
|
240
|
+
for (const field of EDITED_FIELDS) {
|
|
241
|
+
if (Object.hasOwn(draft, field)) patch[field] = draft[field]
|
|
242
|
+
}
|
|
243
|
+
setSaving(true)
|
|
244
|
+
fetch(CONFIG_APPLY_URL, {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
headers: { 'content-type': 'application/json' },
|
|
247
|
+
body: JSON.stringify({ target, patch }),
|
|
248
|
+
})
|
|
249
|
+
.then((r) => r.json())
|
|
250
|
+
.then((body) => {
|
|
251
|
+
if (body?.ok !== true) throw new Error(body?.error ?? 'save failed')
|
|
252
|
+
setCfg(body.config as Config)
|
|
253
|
+
setDraft(null)
|
|
254
|
+
setError(null)
|
|
255
|
+
setVersion((n) => n + 1)
|
|
256
|
+
showFlash(t('saveOk'))
|
|
257
|
+
})
|
|
258
|
+
.catch((e: unknown) => setError(`${t('saveFail')}: ${e instanceof Error ? e.message : String(e)}`))
|
|
259
|
+
.finally(() => setSaving(false))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 预设 Tab 的「应用」:显式意图直接落盘(不经草稿),完整补丁一次写入
|
|
263
|
+
// 当前编辑目标(含每阶段独立名单与阶段工具目录)。调用点已在离开编辑域时
|
|
264
|
+
// 清空草稿,不会有并发草稿写盘。
|
|
265
|
+
const writePatch = (patch: ConfigPatch): void => {
|
|
266
|
+
fetch(CONFIG_APPLY_URL, {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: { 'content-type': 'application/json' },
|
|
269
|
+
body: JSON.stringify({ target, patch }),
|
|
270
|
+
})
|
|
271
|
+
.then((r) => r.json())
|
|
272
|
+
.then((body) => {
|
|
273
|
+
if (body?.ok !== true) throw new Error(body?.error ?? 'apply failed')
|
|
274
|
+
setCfg(body.config as Config)
|
|
275
|
+
setError(null)
|
|
276
|
+
setVersion((n) => n + 1)
|
|
277
|
+
})
|
|
278
|
+
.catch((e: unknown) => setError(String(e instanceof Error ? e.message : e)))
|
|
279
|
+
}
|
|
280
|
+
// 预设库(presets / activePreset)永远保持在全局字段,不分作用域。
|
|
281
|
+
const writeGlobal = writeField
|
|
282
|
+
// 提示词 Tab 解除继承自全局的屏蔽时使用的全局通道(与 writeGlobal 同源,
|
|
283
|
+
// 收窄了字段类型以匹配 SectionsTab 的 prop 签名)。
|
|
284
|
+
const writeGlobalField = writeField as (field: 'sections', value: unknown) => void
|
|
285
|
+
|
|
286
|
+
// 切换 Tab:提示词 ↔ 工具是同一编辑域,草稿保留;切到预设/预览即离开
|
|
287
|
+
// 编辑域,脏草稿经确认后丢弃。
|
|
288
|
+
const switchTab = (next: 'sections' | 'tools' | 'presets' | 'preview'): void => {
|
|
289
|
+
const leaving = (tab === 'sections' || tab === 'tools') && (next === 'presets' || next === 'preview')
|
|
290
|
+
if (leaving) {
|
|
291
|
+
if (draft?.dirty && !window.confirm(t('discardConfirm'))) return
|
|
292
|
+
setDraft(null)
|
|
293
|
+
}
|
|
294
|
+
setTab(next)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 切换编辑目标:草稿基线随目标变化,脏草稿先经确认再丢弃。
|
|
298
|
+
const switchTarget = (next: string | undefined): void => {
|
|
299
|
+
if (draft?.dirty && !window.confirm(t('discardConfirm'))) return
|
|
300
|
+
setDraft(null)
|
|
301
|
+
setTarget(next)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 「存为预设」:把当前编辑内容(含未保存草稿)fork 成一个新的 agent 预设 ——
|
|
305
|
+
// 宿主 authoring API 整体复制来源预设目录(组成文件 / 伴生 .mjs / 技能目录),
|
|
306
|
+
// 同时把当前配置写进 overrides[name]。来源 = 当前编辑目标;全局目标由服务端
|
|
307
|
+
// 回落到 roster 默认预设。名字非法 / 同名由服务端报错。
|
|
308
|
+
// 返回是否成功,让配置 Tab 用面板内消息条提示(不再用 window.prompt / 顶部按钮)。
|
|
309
|
+
const saveAsPreset = (presetName: string): Promise<boolean> => {
|
|
310
|
+
const name = presetName.trim()
|
|
311
|
+
if (name.length === 0) return Promise.resolve(false)
|
|
312
|
+
return fetch(PRESETS_CREATE_URL, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: { 'content-type': 'application/json' },
|
|
315
|
+
body: JSON.stringify({
|
|
316
|
+
name,
|
|
317
|
+
from: target,
|
|
318
|
+
config: {
|
|
319
|
+
sections: view.sections ?? [],
|
|
320
|
+
sectionsBootstrap: view.sectionsBootstrap ?? [],
|
|
321
|
+
sectionsCompaction: view.sectionsCompaction ?? [],
|
|
322
|
+
replace: view.replace ?? {},
|
|
323
|
+
inject: view.inject ?? [],
|
|
324
|
+
tools: view.tools ?? {},
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
})
|
|
328
|
+
.then((r) => r.json())
|
|
329
|
+
.then((body) => {
|
|
330
|
+
if (body?.ok !== true) throw new Error(body?.error ?? t('saveAsPresetFail'))
|
|
331
|
+
load()
|
|
332
|
+
fetchPresets()
|
|
333
|
+
showFlash(t('saveAsPresetOk'), 'ok')
|
|
334
|
+
return true
|
|
335
|
+
})
|
|
336
|
+
.catch((e: unknown) => {
|
|
337
|
+
showFlash(`${t('saveAsPresetFail')}:${String(e instanceof Error ? e.message : e)}`, 'err')
|
|
338
|
+
return false
|
|
339
|
+
})
|
|
340
|
+
}
|
|
97
341
|
|
|
98
342
|
return h('div', { style: s.root }, [
|
|
99
343
|
h('div', { style: s.bar }, [
|
|
100
|
-
h('button', { style: tab === 'sections' ? s.tabActive : s.tab, onClick: () =>
|
|
101
|
-
h('button', { style: tab === 'tools' ? s.tabActive : s.tab, onClick: () =>
|
|
102
|
-
h('button', { style: tab === 'presets' ? s.tabActive : s.tab, onClick: () =>
|
|
103
|
-
h('button', { style: tab === 'preview' ? s.tabActive : s.tab, onClick: () =>
|
|
344
|
+
h('button', { style: tab === 'sections' ? s.tabActive : s.tab, onClick: () => switchTab('sections') }, t('tabsSections')),
|
|
345
|
+
h('button', { style: tab === 'tools' ? s.tabActive : s.tab, onClick: () => switchTab('tools') }, t('tabsTools')),
|
|
346
|
+
h('button', { style: tab === 'presets' ? s.tabActive : s.tab, onClick: () => switchTab('presets') }, t('tabsPresets')),
|
|
347
|
+
h('button', { style: tab === 'preview' ? s.tabActive : s.tab, onClick: () => switchTab('preview') }, t('tabsPreview')),
|
|
348
|
+
h('select', {
|
|
349
|
+
style: { ...s.input, marginLeft: 6 },
|
|
350
|
+
value: target ?? '',
|
|
351
|
+
onChange: (e: { target: { value: string } }) => switchTarget(e.target.value || undefined),
|
|
352
|
+
title: t('targetHint'),
|
|
353
|
+
}, [
|
|
354
|
+
h('option', { value: '', style: s.option }, t('targetGlobal')),
|
|
355
|
+
...agentPresets.map((p) => h('option', { key: p.id, value: p.id, style: s.option },
|
|
356
|
+
`${p.name}${p.broken ? ` (${t('broken')})` : ''}`)),
|
|
357
|
+
]),
|
|
358
|
+
h('button', {
|
|
359
|
+
style: draft?.dirty ? s.saveBtnDirty : s.saveBtn,
|
|
360
|
+
disabled: !draft?.dirty || saving,
|
|
361
|
+
onClick: save,
|
|
362
|
+
}, t('save')),
|
|
104
363
|
h('button', { style: s.refresh, onClick: refresh }, t('refresh')),
|
|
105
364
|
]),
|
|
106
365
|
error ? h('div', { style: s.error }, String(error)) : null,
|
|
366
|
+
flash ? h('div', { style: flashKind === 'err' ? s.error : s.noticeOk }, flash) : null,
|
|
367
|
+
target && agentPresets.find((p) => p.id === target)?.broken
|
|
368
|
+
? h('div', { style: s.error }, t('brokenPreset'))
|
|
369
|
+
: null,
|
|
370
|
+
inv?.scopeResolved === false
|
|
371
|
+
? h('div', { style: s.noticeWarn }, t('scopeFallback'))
|
|
372
|
+
: null,
|
|
107
373
|
tab === 'sections'
|
|
108
|
-
? h(SectionsTab, {
|
|
374
|
+
? h(SectionsTab, {
|
|
375
|
+
cfg: view,
|
|
376
|
+
inv,
|
|
377
|
+
phases,
|
|
378
|
+
target,
|
|
379
|
+
globalSections: cfg.sections,
|
|
380
|
+
ownedSections: target ? cfg.overrides?.[target]?.sections : undefined,
|
|
381
|
+
t,
|
|
382
|
+
write: edit,
|
|
383
|
+
writeGlobalField,
|
|
384
|
+
})
|
|
109
385
|
: tab === 'tools'
|
|
110
|
-
? h(ToolsTab, { cfg, inv,
|
|
386
|
+
? h(ToolsTab, { cfg: view, inv, phases, t, write: edit })
|
|
111
387
|
: tab === 'presets'
|
|
112
|
-
? h(PresetsTab, {
|
|
113
|
-
|
|
388
|
+
? h(PresetsTab, {
|
|
389
|
+
cfg: view,
|
|
390
|
+
inv,
|
|
391
|
+
phases,
|
|
392
|
+
t,
|
|
393
|
+
writePatch,
|
|
394
|
+
writeGlobal,
|
|
395
|
+
saveAsPreset,
|
|
396
|
+
forkSource: target ? (agentPresets.find((p) => p.id === target)?.name ?? target) : undefined,
|
|
397
|
+
})
|
|
398
|
+
: h(PreviewTab, { t, active: tab === 'preview', refreshId, target, version, phases }),
|
|
114
399
|
])
|
|
115
400
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -6,7 +6,7 @@ export const DICT = {
|
|
|
6
6
|
unavailable: '当前环境未启用本命名空间。',
|
|
7
7
|
tabsSections: '提示词',
|
|
8
8
|
tabsTools: '工具',
|
|
9
|
-
tabsPresets: '
|
|
9
|
+
tabsPresets: '配置',
|
|
10
10
|
tabsPreview: '预览',
|
|
11
11
|
refresh: '刷新',
|
|
12
12
|
blockedOff: '未屏蔽',
|
|
@@ -16,6 +16,7 @@ export const DICT = {
|
|
|
16
16
|
system: '系统',
|
|
17
17
|
replace: '编辑',
|
|
18
18
|
clearReplace: '清除',
|
|
19
|
+
clearInput: '清空',
|
|
19
20
|
injectNew: '注入新段',
|
|
20
21
|
name: '名称',
|
|
21
22
|
order: '顺序',
|
|
@@ -35,16 +36,79 @@ export const DICT = {
|
|
|
35
36
|
previewSections: '段',
|
|
36
37
|
previewTools: '工具预览',
|
|
37
38
|
previewToolCount: '个工具',
|
|
38
|
-
savePreset: '
|
|
39
|
-
presetName: '
|
|
39
|
+
savePreset: '保存当前配置',
|
|
40
|
+
presetName: '配置名称',
|
|
41
|
+
saveAsPresetCard: '存为 agent 预设',
|
|
42
|
+
saveAsPresetHint: '整体复制「{name}」这个预设的组成为新预设(组成文件与伴生脚本一并带走),再把当前定制写进它的覆盖项 —— 新预设出现在顶部编辑目标里。',
|
|
43
|
+
agentPresetName: '新预设名',
|
|
44
|
+
forkSourceDefault: '默认预设',
|
|
45
|
+
saveAsPresetOk: '预设已创建,可在顶部的编辑目标选择器里选到它',
|
|
46
|
+
saveAsPresetFail: '保存预设失败',
|
|
40
47
|
save: '保存',
|
|
41
|
-
importPreset: '
|
|
48
|
+
importPreset: '导入配置',
|
|
42
49
|
import: '导入',
|
|
43
50
|
apply: '应用',
|
|
44
51
|
export: '导出',
|
|
45
52
|
delete: '删除',
|
|
53
|
+
restore: '还原',
|
|
46
54
|
active: '使用中',
|
|
47
55
|
preset: '预设',
|
|
56
|
+
ioFailed: '导入/导出失败',
|
|
57
|
+
importInvalid: '无效的配置文件',
|
|
58
|
+
targetGlobal: '目标:全局默认',
|
|
59
|
+
targetHint: '选择编辑目标 —— 选中某个 agent 预设后,改动只对该预设生效(字段级覆盖)。',
|
|
60
|
+
broken: '损坏',
|
|
61
|
+
brokenPreset: '该 agent 预设当前无法挂载,仍可编辑其定制(修复预设后自动生效)。',
|
|
62
|
+
phaseBootstrap: '仅引导阶段',
|
|
63
|
+
phaseActive: '晋级后',
|
|
64
|
+
phaseCompaction: '压缩后',
|
|
65
|
+
// agent 周期三阶段的展示命名(引导期 → 常驻期 → 压缩受控期)。
|
|
66
|
+
phaseStageGuide: '引导期',
|
|
67
|
+
phaseStageResident: '常驻期',
|
|
68
|
+
phaseStageControlled: '压缩受控期',
|
|
69
|
+
toolsPartGuide: '引导期工具',
|
|
70
|
+
toolsPartResident: '常驻期工具',
|
|
71
|
+
toolsPartControlled: '压缩受控期工具',
|
|
72
|
+
allToolsTitle: '本系统全部工具',
|
|
73
|
+
sectionsPartGuide: '引导期提示词',
|
|
74
|
+
sectionsPartResident: '常驻期提示词',
|
|
75
|
+
sectionsPartControlled: '压缩受控期提示词',
|
|
76
|
+
allSectionsTitle: '本系统全部提示词',
|
|
77
|
+
toolsFourHint: '三个阶段各一份名单,互不继承:勾选 = 该阶段是否对模型可见;从「全部」拖入某阶段 = 让这一个工具在该阶段出现(若该阶段默认裁掉了它,就加回去);在两个阶段之间拖动 = 复制(源阶段不动,目标阶段多一份,隐藏/启用状态一起带过去);拖回「全部」= 从该阶段拿掉它。每个动作只影响被拖的那一个工具。',
|
|
78
|
+
sectionsFourHint: '拖拽双向:把「全部」里的段拖入上方阶段 = 加入该阶段并默认开启;从阶段拖回「全部」= 从该阶段移除。屏蔽段与普通段完全一样可拖拽 / 编辑,只是不注入模型。',
|
|
79
|
+
dragHint: '拖入 = 让该工具在这个阶段出现',
|
|
80
|
+
phaseShortGuide: '引',
|
|
81
|
+
phaseShortResident: '常',
|
|
82
|
+
phaseShortControlled: '压',
|
|
83
|
+
poolMarksHint: '每个名字后的三个标记 = 该工具在引导期 / 常驻期 / 压缩受控期的状态:绿=该阶段可见(或已加回),灰=该阶段已隐藏,暗=不在该阶段、也没加回。',
|
|
84
|
+
toolAddedTag: '加回',
|
|
85
|
+
toolAdded: '已把「{name}」加回{phase}:该阶段默认裁掉了它,保存后它会重新出现在该阶段(当前预设注册表里有它)。',
|
|
86
|
+
toolNotInRegistry: '「{name}」不在本预设的注册表里(它是别的预设独有的工具),当前预设没有它的定义,加不进来。',
|
|
87
|
+
toolCopied: '已把「{name}」复制到{to}:{from}那份保持不动,隐藏/启用状态一起带过去。',
|
|
88
|
+
toolShown: '「{name}」已在{to}显示。',
|
|
89
|
+
toolHiddenIn: '「{name}」已在{to}隐藏。',
|
|
90
|
+
sectionsTakenOver: '该目标的最终系统提示词由「{name}」段整段接管:宿主在装配瀑布流之后把 sections 强制还原为那一条段,因此本插件的段级屏蔽 / 替换 / 注入 / 排序都不会进入模型看到的提示词(工具过滤仍然生效)。要让段级定制生效,需在该 agent 预设里关掉 persona 的 complete。',
|
|
91
|
+
sectionsLost: '本插件在该阶段产出的 {emitted} 个段里,只有 {survived} 个进入最终提示词:其余被下游装配规则丢弃,段级定制不会完全生效(工具过滤不受影响)。',
|
|
92
|
+
sectionBlockedStrip: '(本阶段已屏蔽,模型不可见)',
|
|
93
|
+
phaseSuppressed: '被抑制(不在任何阶段的装配中)',
|
|
94
|
+
sectionDynamicNoAdd: '「{name}」是运行时才生成的动态段,没法预先加进某个阶段(它的内容只有装配时才知道)。',
|
|
95
|
+
exportOk: '导出成功',
|
|
96
|
+
exportCancel: '已取消导出',
|
|
97
|
+
exportFail: '导出失败',
|
|
98
|
+
importOk: '导入完成',
|
|
99
|
+
importNone: '没有新增预设(同名已存在或文件为空)',
|
|
100
|
+
importCancel: '已取消导入',
|
|
101
|
+
importFail: '导入失败',
|
|
102
|
+
saveOk: '已保存',
|
|
103
|
+
saveFail: '保存失败',
|
|
104
|
+
discardConfirm: '有未保存的更改,离开将丢弃。确定离开吗?',
|
|
105
|
+
scopeFallback: '该 agent 预设的常驻 scope 未能挂载,以下显示的是全局层内容(并非该预设的原生段与工具)。',
|
|
106
|
+
previewDegraded: '伪会话触发了某个预设插件的异常,本次预览已降级为无会话装配——预设原生的阶段裁剪规则未参与运行。',
|
|
107
|
+
previewFail: '该阶段装配失败(scope 挂载失败或预设插件异常),请切换目标或查看服务日志。',
|
|
108
|
+
phaseVisible: '阶段可见',
|
|
109
|
+
previewToolsHint: '模型实际可见的工具目录(按所选阶段运行预设的全部装配规则,含原生阶段裁剪):',
|
|
110
|
+
previewToolsCount: '注册表',
|
|
111
|
+
saveAsPreset: '存为预设',
|
|
48
112
|
},
|
|
49
113
|
en: {
|
|
50
114
|
nav: 'Prompt Customizer',
|
|
@@ -62,6 +126,7 @@ export const DICT = {
|
|
|
62
126
|
system: 'System',
|
|
63
127
|
replace: 'Replace',
|
|
64
128
|
clearReplace: 'Clear',
|
|
129
|
+
clearInput: 'Clear',
|
|
65
130
|
injectNew: 'Inject section',
|
|
66
131
|
name: 'Name',
|
|
67
132
|
order: 'Order',
|
|
@@ -81,17 +146,80 @@ export const DICT = {
|
|
|
81
146
|
previewSections: 'sections',
|
|
82
147
|
previewTools: 'Tools preview',
|
|
83
148
|
previewToolCount: 'tools',
|
|
84
|
-
savePreset: 'Save current
|
|
85
|
-
presetName: '
|
|
149
|
+
savePreset: 'Save current config',
|
|
150
|
+
presetName: 'Config name',
|
|
151
|
+
saveAsPresetCard: 'Save as agent preset',
|
|
152
|
+
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.',
|
|
153
|
+
agentPresetName: 'New preset name',
|
|
154
|
+
forkSourceDefault: 'the default preset',
|
|
155
|
+
saveAsPresetOk: 'Preset created — pick it in the target selector above',
|
|
156
|
+
saveAsPresetFail: 'Failed to save preset',
|
|
86
157
|
save: 'Save',
|
|
87
158
|
importPreset: 'Import preset',
|
|
88
159
|
import: 'Import',
|
|
89
160
|
apply: 'Apply',
|
|
90
161
|
export: 'Export',
|
|
91
162
|
delete: 'Delete',
|
|
163
|
+
restore: 'Restore',
|
|
92
164
|
active: 'Active',
|
|
93
165
|
preset: 'Preset',
|
|
166
|
+
ioFailed: 'Import/export failed',
|
|
167
|
+
importInvalid: 'Invalid preset file',
|
|
168
|
+
targetGlobal: 'Target: global default',
|
|
169
|
+
targetHint: 'Pick an edit target — changes apply only to the chosen agent preset (field-level override).',
|
|
170
|
+
broken: 'broken',
|
|
171
|
+
brokenPreset: 'This agent preset cannot mount right now; its customization is still editable and applies once fixed.',
|
|
172
|
+
phaseBootstrap: 'Bootstrap only',
|
|
173
|
+
phaseActive: 'After promotion',
|
|
174
|
+
phaseCompaction: 'Post-compaction',
|
|
175
|
+
// agent 周期三阶段的展示命名(引导期 → 常驻期 → 压缩受控期)。
|
|
176
|
+
phaseStageGuide: 'Guide period',
|
|
177
|
+
phaseStageResident: 'Resident period',
|
|
178
|
+
phaseStageControlled: 'Compaction-controlled period',
|
|
179
|
+
toolsPartGuide: 'Guide-period tools',
|
|
180
|
+
toolsPartResident: 'Resident-period tools',
|
|
181
|
+
toolsPartControlled: 'Compaction-controlled tools',
|
|
182
|
+
allToolsTitle: 'All system tools',
|
|
183
|
+
sectionsPartGuide: 'Guide-period sections',
|
|
184
|
+
sectionsPartResident: 'Resident-period sections',
|
|
185
|
+
sectionsPartControlled: 'Compaction-controlled sections',
|
|
186
|
+
allSectionsTitle: 'All system sections',
|
|
187
|
+
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.',
|
|
188
|
+
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.',
|
|
189
|
+
dragHint: 'Drag in to make this tool appear in this period',
|
|
190
|
+
phaseShortGuide: 'G',
|
|
191
|
+
phaseShortResident: 'R',
|
|
192
|
+
phaseShortControlled: 'C',
|
|
193
|
+
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.',
|
|
194
|
+
toolAddedTag: 'added',
|
|
195
|
+
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).',
|
|
196
|
+
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.',
|
|
197
|
+
toolCopied: 'Copied "{name}" to {to}: the copy in {from} is left untouched, and its hidden/enabled state carries over.',
|
|
198
|
+
toolShown: '"{name}" is now visible in {to}.',
|
|
199
|
+
toolHiddenIn: '"{name}" is now hidden in {to}.',
|
|
200
|
+
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.',
|
|
201
|
+
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).',
|
|
202
|
+
sectionBlockedStrip: ' (blocked in this period; invisible to the model)',
|
|
203
|
+
phaseSuppressed: 'Suppressed (absent from every phase assembly)',
|
|
204
|
+
sectionDynamicNoAdd: '"{name}" is generated at runtime — it cannot be added to a phase ahead of time (its content is only known during assembly).',
|
|
205
|
+
exportOk: 'Exported',
|
|
206
|
+
exportCancel: 'Export cancelled',
|
|
207
|
+
exportFail: 'Export failed',
|
|
208
|
+
importOk: 'Import complete',
|
|
209
|
+
importNone: 'Nothing imported (duplicate names or empty file)',
|
|
210
|
+
importCancel: 'Import cancelled',
|
|
211
|
+
importFail: 'Import failed',
|
|
212
|
+
saveOk: 'Saved',
|
|
213
|
+
saveFail: 'Save failed',
|
|
214
|
+
discardConfirm: 'You have unsaved changes; leaving will discard them. Leave anyway?',
|
|
215
|
+
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).",
|
|
216
|
+
previewDegraded: 'A preset plugin threw on the synthetic session; this preview fell back to an agent-less assembly — native phase rules did not run.',
|
|
217
|
+
previewFail: 'Assembly for this phase failed (scope mount error or a preset plugin threw); switch target or check the server log.',
|
|
218
|
+
phaseVisible: 'visible in phase',
|
|
219
|
+
previewToolsHint: 'Tool catalog as the model actually sees it (all assembly rules run for the selected phase, including native phase narrowing):',
|
|
220
|
+
previewToolsCount: 'of registry',
|
|
221
|
+
saveAsPreset: 'Save as preset',
|
|
94
222
|
},
|
|
95
223
|
} as const
|
|
96
224
|
|
|
97
|
-
export type Translate = (key: string) => string
|
|
225
|
+
export type Translate = (key: string, params?: Record<string, string | number>) => string
|