dsh-soul 0.4.0 → 0.5.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.md +21 -8
- package/README_EN.md +21 -8
- package/client/index.mjs +245 -99
- package/index.mjs +446 -52
- package/lib/config.mjs +48 -1
- package/package.json +1 -1
- package/scripts/verify-config.mjs +39 -1
package/client/index.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// 在设置页面添加「个性化」栏目:
|
|
4
4
|
// - 启用/禁用个性化设置
|
|
5
5
|
// - 「关于你」(昵称/职业/介绍)、回复风格和语调、特质、输出语言、自定义指令
|
|
6
|
-
// -
|
|
6
|
+
// - 人设预设:保存当前为预设、一键使用(★ 标记当前匹配项)、删除
|
|
7
|
+
// - Agent 工具:set_persona 确认模式开关
|
|
8
|
+
// - dirty 检测(无改动禁用保存)、统一 toast 提示
|
|
7
9
|
// - 查看当前生效提示词(只读)与字符数
|
|
8
10
|
//
|
|
9
11
|
// 纯 JS + React jsx-runtime 手写(不依赖 JSX 构建),开箱即用。
|
|
@@ -51,10 +53,15 @@ window.__ModuleLoader__.load({
|
|
|
51
53
|
emoji: 'default',
|
|
52
54
|
language: 'zh',
|
|
53
55
|
customInstructions: '',
|
|
56
|
+
// set_persona 确认模式(v0.5.0)
|
|
57
|
+
requireToolConfirmation: false,
|
|
58
|
+
// 人设预设库与当前匹配项(GET /api/soul/personas 带回)
|
|
59
|
+
personas: null,
|
|
60
|
+
activePersona: null,
|
|
54
61
|
loading: false,
|
|
55
62
|
saving: false,
|
|
56
63
|
error: null,
|
|
57
|
-
//
|
|
64
|
+
// 最近一次保存实际变化的字段名数组(来自写路径的 changed)
|
|
58
65
|
lastChanged: null
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -94,29 +101,28 @@ window.__ModuleLoader__.load({
|
|
|
94
101
|
this.disposed = false
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
// 把服务端返回的完整配置映射进 store
|
|
105
|
+
applyConfig(s, config) {
|
|
106
|
+
s.enabled = config.enabled
|
|
107
|
+
s.nickname = config.nickname || ''
|
|
108
|
+
s.occupation = config.occupation || ''
|
|
109
|
+
s.bio = config.bio || ''
|
|
110
|
+
s.style = config.style
|
|
111
|
+
s.headingLists = config.headingLists || 'default'
|
|
112
|
+
s.emoji = config.emoji || 'default'
|
|
113
|
+
s.language = config.language || 'zh'
|
|
114
|
+
s.customInstructions = config.customInstructions
|
|
115
|
+
s.requireToolConfirmation = config.requireToolConfirmation === true
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
async loadConfig() {
|
|
98
119
|
if (this.disposed) return
|
|
99
120
|
this.store.update(s => { s.loading = true; s.error = null })
|
|
100
121
|
|
|
101
122
|
try {
|
|
102
|
-
const
|
|
103
|
-
const response = await this.fetcher(url)
|
|
104
|
-
const payload = await response.json().catch(() => ({}))
|
|
105
|
-
|
|
106
|
-
if (!response.ok || payload.ok !== true) {
|
|
107
|
-
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
108
|
-
}
|
|
109
|
-
|
|
123
|
+
const payload = await this.postJSON('/api/soul/config', null, { method: 'GET' })
|
|
110
124
|
this.store.update(s => {
|
|
111
|
-
s
|
|
112
|
-
s.nickname = payload.config.nickname || ''
|
|
113
|
-
s.occupation = payload.config.occupation || ''
|
|
114
|
-
s.bio = payload.config.bio || ''
|
|
115
|
-
s.style = payload.config.style
|
|
116
|
-
s.headingLists = payload.config.headingLists || 'default'
|
|
117
|
-
s.emoji = payload.config.emoji || 'default'
|
|
118
|
-
s.language = payload.config.language || 'zh'
|
|
119
|
-
s.customInstructions = payload.config.customInstructions
|
|
125
|
+
this.applyConfig(s, payload.config)
|
|
120
126
|
s.loading = false
|
|
121
127
|
})
|
|
122
128
|
} catch (error) {
|
|
@@ -133,31 +139,13 @@ window.__ModuleLoader__.load({
|
|
|
133
139
|
this.store.update(s => { s.saving = true; s.error = null })
|
|
134
140
|
|
|
135
141
|
try {
|
|
136
|
-
const
|
|
137
|
-
const response = await this.fetcher(url, {
|
|
138
|
-
method: 'POST',
|
|
139
|
-
headers: { 'content-type': 'application/json' },
|
|
140
|
-
body: JSON.stringify(config)
|
|
141
|
-
})
|
|
142
|
-
const payload = await response.json().catch(() => ({}))
|
|
143
|
-
|
|
144
|
-
if (!response.ok || payload.ok !== true) {
|
|
145
|
-
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
146
|
-
}
|
|
147
|
-
|
|
142
|
+
const payload = await this.postJSON('/api/soul/config', config)
|
|
148
143
|
this.store.update(s => {
|
|
149
|
-
s
|
|
150
|
-
s.nickname = payload.config.nickname || ''
|
|
151
|
-
s.occupation = payload.config.occupation || ''
|
|
152
|
-
s.bio = payload.config.bio || ''
|
|
153
|
-
s.style = payload.config.style
|
|
154
|
-
s.headingLists = payload.config.headingLists || 'default'
|
|
155
|
-
s.emoji = payload.config.emoji || 'default'
|
|
156
|
-
s.language = payload.config.language || 'zh'
|
|
157
|
-
s.customInstructions = payload.config.customInstructions
|
|
144
|
+
this.applyConfig(s, payload.config)
|
|
158
145
|
s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
|
|
159
146
|
s.saving = false
|
|
160
147
|
})
|
|
148
|
+
return payload
|
|
161
149
|
} catch (error) {
|
|
162
150
|
if (this.disposed) return
|
|
163
151
|
this.store.update(s => {
|
|
@@ -172,27 +160,13 @@ window.__ModuleLoader__.load({
|
|
|
172
160
|
this.store.update(s => { s.saving = true; s.error = null })
|
|
173
161
|
|
|
174
162
|
try {
|
|
175
|
-
const
|
|
176
|
-
const response = await this.fetcher(url, { method: 'POST' })
|
|
177
|
-
const payload = await response.json().catch(() => ({}))
|
|
178
|
-
|
|
179
|
-
if (!response.ok || payload.ok !== true) {
|
|
180
|
-
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
181
|
-
}
|
|
182
|
-
|
|
163
|
+
const payload = await this.postJSON('/api/soul/config/reset')
|
|
183
164
|
this.store.update(s => {
|
|
184
|
-
s
|
|
185
|
-
s.nickname = payload.config.nickname || ''
|
|
186
|
-
s.occupation = payload.config.occupation || ''
|
|
187
|
-
s.bio = payload.config.bio || ''
|
|
188
|
-
s.style = payload.config.style
|
|
189
|
-
s.headingLists = payload.config.headingLists || 'default'
|
|
190
|
-
s.emoji = payload.config.emoji || 'default'
|
|
191
|
-
s.language = payload.config.language || 'zh'
|
|
192
|
-
s.customInstructions = payload.config.customInstructions
|
|
165
|
+
this.applyConfig(s, payload.config)
|
|
193
166
|
s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
|
|
194
167
|
s.saving = false
|
|
195
168
|
})
|
|
169
|
+
return payload
|
|
196
170
|
} catch (error) {
|
|
197
171
|
if (this.disposed) return
|
|
198
172
|
this.store.update(s => {
|
|
@@ -202,10 +176,39 @@ window.__ModuleLoader__.load({
|
|
|
202
176
|
}
|
|
203
177
|
}
|
|
204
178
|
|
|
205
|
-
//
|
|
206
|
-
async
|
|
207
|
-
|
|
208
|
-
|
|
179
|
+
// 人设预设:列表 / 保存 / 使用 / 删除
|
|
180
|
+
async fetchPersonas() {
|
|
181
|
+
return await this.postJSON('/api/soul/personas', null, { method: 'GET' })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async savePersona(name) {
|
|
185
|
+
return await this.postJSON('/api/soul/personas/save', { name })
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async usePersona(name) {
|
|
189
|
+
const payload = await this.postJSON('/api/soul/personas/use', { name })
|
|
190
|
+
if (!this.disposed) {
|
|
191
|
+
this.store.update(s => {
|
|
192
|
+
this.applyConfig(s, payload.config)
|
|
193
|
+
s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
return payload
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async deletePersona(name) {
|
|
200
|
+
return await this.postJSON('/api/soul/personas/delete', { name })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 统一请求:body 为 null 时不携带请求体(GET 用)
|
|
204
|
+
async postJSON(path, body, overrides = {}) {
|
|
205
|
+
const url = new URL(path, hostBase())
|
|
206
|
+
const init = { method: overrides.method || 'POST', ...overrides }
|
|
207
|
+
if (body !== null && body !== undefined) {
|
|
208
|
+
init.headers = { 'content-type': 'application/json' }
|
|
209
|
+
init.body = JSON.stringify(body)
|
|
210
|
+
}
|
|
211
|
+
const response = await this.fetcher(url, init)
|
|
209
212
|
const payload = await response.json().catch(() => ({}))
|
|
210
213
|
if (!response.ok || payload.ok !== true) {
|
|
211
214
|
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
@@ -213,6 +216,12 @@ window.__ModuleLoader__.load({
|
|
|
213
216
|
return payload
|
|
214
217
|
}
|
|
215
218
|
|
|
219
|
+
// 读取当前生效(已保存)的提示词:{ ok, prompt, enabled }
|
|
220
|
+
async fetchPrompt() {
|
|
221
|
+
const payload = await this.postJSON('/api/soul/prompt', null, { method: 'GET' })
|
|
222
|
+
return payload
|
|
223
|
+
}
|
|
224
|
+
|
|
216
225
|
async dispose() {
|
|
217
226
|
this.disposed = true
|
|
218
227
|
}
|
|
@@ -223,31 +232,44 @@ window.__ModuleLoader__.load({
|
|
|
223
232
|
// -------------------------------------------------------------------------
|
|
224
233
|
|
|
225
234
|
const css = [
|
|
226
|
-
'.soul-section{margin:24px 0;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-
|
|
235
|
+
'.soul-section{margin:24px 0;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}',
|
|
227
236
|
'.soul-section h3{margin:0 0 16px 0;font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
228
237
|
'.soul-field{margin-bottom:16px;width:100%}',
|
|
229
238
|
'.soul-field label{display:block;margin-bottom:6px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-secondary)}',
|
|
230
|
-
'.soul-field select,.soul-field textarea,.soul-field input[type=text]{width:100%;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-
|
|
239
|
+
'.soul-field select,.soul-field textarea,.soul-field input[type=text]{width:100%;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;box-sizing:border-box}',
|
|
240
|
+
'.soul-field select option{color:var(--dsw-alias-label-primary)}',
|
|
241
|
+
'.soul-section{color-scheme:light}',
|
|
242
|
+
'body[data-ds-dark-theme] .soul-section{color-scheme:dark}',
|
|
243
|
+
'.soul-field select{color-scheme:light}',
|
|
244
|
+
'body[data-ds-dark-theme] .soul-field select{color-scheme:dark}',
|
|
231
245
|
'.soul-field textarea{min-height:100px;resize:vertical}',
|
|
232
246
|
'.soul-field textarea.soul-textarea-sm{min-height:60px}',
|
|
233
247
|
'.soul-group-title{margin:20px 0 12px 0;padding-bottom:8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
234
|
-
'.soul-field select:focus,.soul-field textarea:focus,.soul-field input[type=text]:focus{outline:none;border-color:var(--dsw-alias-
|
|
248
|
+
'.soul-field select:focus,.soul-field textarea:focus,.soul-field input[type=text]:focus{outline:none;border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 25%, transparent)}',
|
|
235
249
|
'.soul-toggle{display:flex;align-items:center;gap:8px;margin-bottom:16px}',
|
|
236
250
|
'.soul-toggle label{margin:0;font-size:13px;color:var(--dsw-alias-label-primary)}',
|
|
237
251
|
'.soul-buttons{display:flex;gap:8px;margin-top:16px}',
|
|
238
|
-
'.soul-error{margin-top:8px;padding:8px 12px;background:var(--dsw-alias-bg-
|
|
252
|
+
'.soul-error{margin-top:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-label-error);border-radius:6px;color:var(--dsw-alias-label-error);font-size:12px;width:100%;box-sizing:border-box}',
|
|
239
253
|
'.soul-hint{position:relative;display:inline-flex;align-items:center;margin-left:6px;color:var(--dsw-alias-label-secondary);cursor:help;vertical-align:middle}',
|
|
240
254
|
'.soul-hint:hover{color:var(--dsw-alias-label-primary)}',
|
|
241
255
|
'.soul-hint-tip{position:absolute;bottom:calc(100% + 8px);left:0;padding:8px 10px;background:#ffffff;border:1px solid rgba(0,0,0,0.1);border-radius:6px;font-size:12px;font-weight:400;line-height:1.5;color:rgba(0,0,0,0.85);white-space:normal;width:max-content;max-width:260px;text-align:left;opacity:0;visibility:hidden;transition:opacity .15s ease;pointer-events:none;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,0.15)}',
|
|
242
256
|
'.soul-hint:hover .soul-hint-tip{opacity:1;visibility:visible}',
|
|
243
257
|
'.soul-status{margin-top:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
244
258
|
'.soul-dirty{color:#d46b08}',
|
|
259
|
+
'.soul-persona-row{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px dashed var(--dsw-alias-border-l2);font-size:12px}',
|
|
260
|
+
'.soul-persona-name{color:var(--dsw-alias-label-primary);font-weight:500;white-space:nowrap}',
|
|
261
|
+
'.soul-persona-meta{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary)}',
|
|
262
|
+
'.soul-persona-actions{display:flex;gap:10px;white-space:nowrap}',
|
|
263
|
+
'.soul-persona-save{display:flex;gap:8px}',
|
|
264
|
+
'.soul-persona-save input[type=text]{flex:1}',
|
|
245
265
|
'.soul-prompt-toggle{margin-top:8px}',
|
|
246
266
|
'.soul-prompt-link{background:none;border:none;padding:0;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer;text-decoration:underline}',
|
|
247
267
|
'.soul-prompt-link:hover{color:var(--dsw-alias-label-primary)}',
|
|
248
|
-
'.soul-
|
|
268
|
+
'.soul-persona-danger{color:var(--dsw-alias-label-error)}',
|
|
269
|
+
'.soul-prompt-pre{margin:8px 0 0 0;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto;font-family:Consolas,Menlo,monospace;text-align:left;width:100%;box-sizing:border-box}',
|
|
249
270
|
'.soul-toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:8px 16px;border-radius:6px;font-size:13px;font-weight:500;z-index:10000;box-shadow:0 2px 8px rgba(0,0,0,0.15);pointer-events:none;text-align:center;min-width:auto;white-space:nowrap}',
|
|
250
271
|
'.soul-toast-success{background:#f6ffed;border:1px solid #b7eb8f;color:#52c41a}',
|
|
272
|
+
'.soul-toast-error{background:#fff1f0;border:1px solid #ffa39e;color:#f5222d}',
|
|
251
273
|
'@keyframes fadeInOut{0%{opacity:0;transform:translate(-50%,-50%) scale(0.9)}15%{opacity:1;transform:translate(-50%,-50%) scale(1)}85%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(-50%,-50%) scale(0.9)}}'
|
|
252
274
|
].join('')
|
|
253
275
|
|
|
@@ -295,6 +317,13 @@ window.__ModuleLoader__.load({
|
|
|
295
317
|
'field.instructions': '自定义指令',
|
|
296
318
|
'hint.instructions': '补充角色、习惯等个性化要求。建议不要重复设置回复风格和语调类似的话术,以免与上方选项冲突。',
|
|
297
319
|
'field.instructionsPlaceholder': '输入自定义指令,可以是其他行为、回复风格和语调等偏好设置',
|
|
320
|
+
'group.personas': '人设预设',
|
|
321
|
+
'personas.save': '保存当前为预设',
|
|
322
|
+
'personas.savePlaceholder': '预设名称(1-30 字符)',
|
|
323
|
+
'personas.use': '使用',
|
|
324
|
+
'personas.delete': '删除',
|
|
325
|
+
'personas.confirmDelete': '确定删除预设「{name}」?',
|
|
326
|
+
'personas.empty': '暂无人设预设,保存当前配置后可一键切换',
|
|
298
327
|
'style.professional': '专业严谨',
|
|
299
328
|
'style.casual': '轻松自然',
|
|
300
329
|
'style.humorous': '幽默风趣',
|
|
@@ -306,6 +335,9 @@ window.__ModuleLoader__.load({
|
|
|
306
335
|
'trait.emoji.default': '默认',
|
|
307
336
|
'trait.emoji.more': '增强(使用较多表情符号)',
|
|
308
337
|
'trait.emoji.less': '减弱(尽量减少使用表情符号)',
|
|
338
|
+
'group.tool': 'Agent 工具',
|
|
339
|
+
'field.toolConfirm': '人设变更需确认',
|
|
340
|
+
'hint.toolConfirm': '开启后,Agent 通过 set_persona 工具做出的修改不会立即生效,需在会话中使用 /soul confirm 确认或 /soul reject 拒绝。',
|
|
309
341
|
'button.save': '保存设置',
|
|
310
342
|
'button.saving': '保存中...',
|
|
311
343
|
'button.reset': '重置默认',
|
|
@@ -314,6 +346,10 @@ window.__ModuleLoader__.load({
|
|
|
314
346
|
'toast.saved': '✅ 设置已保存',
|
|
315
347
|
'toast.noChanges': '✅ 配置无变化',
|
|
316
348
|
'toast.reset': '✅ 已重置为默认值',
|
|
349
|
+
'toast.personaSaved': '✅ 预设已保存',
|
|
350
|
+
'toast.personaUsed': '✅ 预设已应用',
|
|
351
|
+
'toast.personaUnchanged': 'ℹ 预设与当前配置一致',
|
|
352
|
+
'toast.personaDeleted': '✅ 预设已删除',
|
|
317
353
|
'prompt.view': '查看当前生效提示词',
|
|
318
354
|
'prompt.hide': '收起提示词',
|
|
319
355
|
'prompt.loading': '提示词加载中...',
|
|
@@ -344,6 +380,13 @@ window.__ModuleLoader__.load({
|
|
|
344
380
|
'field.instructions': 'Custom instructions',
|
|
345
381
|
'hint.instructions': 'Extra persona or habit requirements. Avoid repeating wording similar to the style & tone option above to prevent conflicts.',
|
|
346
382
|
'field.instructionsPlaceholder': 'Custom instructions — other behaviors, style or tone preferences',
|
|
383
|
+
'group.personas': 'Personas',
|
|
384
|
+
'personas.save': 'Save current as persona',
|
|
385
|
+
'personas.savePlaceholder': 'Persona name (1-30 chars)',
|
|
386
|
+
'personas.use': 'Use',
|
|
387
|
+
'personas.delete': 'Delete',
|
|
388
|
+
'personas.confirmDelete': 'Delete persona "{name}"?',
|
|
389
|
+
'personas.empty': 'No personas yet — save the current config to switch with one click',
|
|
347
390
|
'style.professional': 'Professional',
|
|
348
391
|
'style.casual': 'Casual',
|
|
349
392
|
'style.humorous': 'Humorous',
|
|
@@ -355,6 +398,9 @@ window.__ModuleLoader__.load({
|
|
|
355
398
|
'trait.emoji.default': 'Default',
|
|
356
399
|
'trait.emoji.more': 'More (frequent emoji usage)',
|
|
357
400
|
'trait.emoji.less': 'Less (minimal emoji usage)',
|
|
401
|
+
'group.tool': 'Agent tools',
|
|
402
|
+
'field.toolConfirm': 'Require confirmation for persona changes',
|
|
403
|
+
'hint.toolConfirm': 'When enabled, changes made by the agent through the set_persona tool do not apply immediately — confirm with /soul confirm or reject with /soul reject in the conversation.',
|
|
358
404
|
'button.save': 'Save Settings',
|
|
359
405
|
'button.saving': 'Saving...',
|
|
360
406
|
'button.reset': 'Reset to Default',
|
|
@@ -363,6 +409,10 @@ window.__ModuleLoader__.load({
|
|
|
363
409
|
'toast.saved': '✅ Settings saved',
|
|
364
410
|
'toast.noChanges': '✅ No changes to save',
|
|
365
411
|
'toast.reset': '✅ Reset to defaults',
|
|
412
|
+
'toast.personaSaved': '✅ Persona saved',
|
|
413
|
+
'toast.personaUsed': '✅ Persona applied',
|
|
414
|
+
'toast.personaUnchanged': 'ℹ Persona matches current config',
|
|
415
|
+
'toast.personaDeleted': '✅ Persona deleted',
|
|
366
416
|
'prompt.view': 'View the active system prompt',
|
|
367
417
|
'prompt.hide': 'Hide the prompt',
|
|
368
418
|
'prompt.loading': 'Loading prompt...',
|
|
@@ -401,7 +451,7 @@ window.__ModuleLoader__.load({
|
|
|
401
451
|
]
|
|
402
452
|
|
|
403
453
|
// 表单字段与 store 字段的一一对应(dirty 检测与保存载荷共用)
|
|
404
|
-
const FIELD_KEYS = ['enabled', 'nickname', 'occupation', 'bio', 'style', 'headingLists', 'emoji', 'language', 'customInstructions']
|
|
454
|
+
const FIELD_KEYS = ['enabled', 'nickname', 'occupation', 'bio', 'style', 'headingLists', 'emoji', 'language', 'customInstructions', 'requireToolConfirmation']
|
|
405
455
|
|
|
406
456
|
// 提示词小图标:hover 展示说明文字
|
|
407
457
|
function SoulHint(props) {
|
|
@@ -430,7 +480,7 @@ window.__ModuleLoader__.load({
|
|
|
430
480
|
// 渲染器按槽位 locale 命名空间注入 t(随语言切换更新);缺失时回退中文
|
|
431
481
|
const t = typeof props.t === 'function' ? props.t : FALLBACK_T
|
|
432
482
|
const state = useSoulController((state) => state)
|
|
433
|
-
const { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, loading, saving, error
|
|
483
|
+
const { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, requireToolConfirmation, personas, activePersona, loading, saving, error } = state
|
|
434
484
|
|
|
435
485
|
const [localEnabled, setLocalEnabled] = React.useState(enabled)
|
|
436
486
|
const [localNickname, setLocalNickname] = React.useState(nickname || '')
|
|
@@ -441,8 +491,9 @@ window.__ModuleLoader__.load({
|
|
|
441
491
|
const [localEmoji, setLocalEmoji] = React.useState(emoji)
|
|
442
492
|
const [localLanguage, setLocalLanguage] = React.useState(language || 'zh')
|
|
443
493
|
const [localInstructions, setLocalInstructions] = React.useState(customInstructions)
|
|
444
|
-
const [
|
|
445
|
-
const [
|
|
494
|
+
const [localToolConfirm, setLocalToolConfirm] = React.useState(requireToolConfirmation === true)
|
|
495
|
+
const [personaName, setPersonaName] = React.useState('')
|
|
496
|
+
const [toast, setToast] = React.useState(null)
|
|
446
497
|
const [showPrompt, setShowPrompt] = React.useState(false)
|
|
447
498
|
const [promptText, setPromptText] = React.useState('')
|
|
448
499
|
const [promptLoading, setPromptLoading] = React.useState(false)
|
|
@@ -459,27 +510,20 @@ window.__ModuleLoader__.load({
|
|
|
459
510
|
setLocalEmoji(emoji)
|
|
460
511
|
setLocalLanguage(language || 'zh')
|
|
461
512
|
setLocalInstructions(customInstructions)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// 保存成功后显示提示,2秒后自动消失
|
|
465
|
-
React.useEffect(() => {
|
|
466
|
-
if (showSuccess) {
|
|
467
|
-
const timer = setTimeout(() => setShowSuccess(false), 2000)
|
|
468
|
-
return () => clearTimeout(timer)
|
|
469
|
-
}
|
|
470
|
-
}, [showSuccess])
|
|
513
|
+
setLocalToolConfirm(requireToolConfirmation === true)
|
|
514
|
+
}, [enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, requireToolConfirmation])
|
|
471
515
|
|
|
472
|
-
//
|
|
516
|
+
// toast 自动消失
|
|
473
517
|
React.useEffect(() => {
|
|
474
|
-
if (
|
|
475
|
-
const timer = setTimeout(() =>
|
|
518
|
+
if (toast) {
|
|
519
|
+
const timer = setTimeout(() => setToast(null), 2000)
|
|
476
520
|
return () => clearTimeout(timer)
|
|
477
521
|
}
|
|
478
|
-
}, [
|
|
522
|
+
}, [toast])
|
|
479
523
|
|
|
480
524
|
// dirty 检测:本地表单与已保存配置逐字段比较
|
|
481
|
-
const savedMap = { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions }
|
|
482
|
-
const localMap = { enabled: localEnabled, nickname: localNickname, occupation: localOccupation, bio: localBio, style: localStyle, headingLists: localHeadingLists, emoji: localEmoji, language: localLanguage, customInstructions: localInstructions }
|
|
525
|
+
const savedMap = { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, requireToolConfirmation }
|
|
526
|
+
const localMap = { enabled: localEnabled, nickname: localNickname, occupation: localOccupation, bio: localBio, style: localStyle, headingLists: localHeadingLists, emoji: localEmoji, language: localLanguage, customInstructions: localInstructions, requireToolConfirmation: localToolConfirm }
|
|
483
527
|
const dirty = FIELD_KEYS.some((key) => savedMap[key] !== localMap[key])
|
|
484
528
|
|
|
485
529
|
const loadPrompt = async () => {
|
|
@@ -501,22 +545,80 @@ window.__ModuleLoader__.load({
|
|
|
501
545
|
if (next) await loadPrompt()
|
|
502
546
|
}
|
|
503
547
|
|
|
548
|
+
const loadPersonas = async () => {
|
|
549
|
+
try {
|
|
550
|
+
const payload = await controller.fetchPersonas()
|
|
551
|
+
controller.store.update((s) => {
|
|
552
|
+
s.personas = payload.personas
|
|
553
|
+
s.activePersona = payload.activeName
|
|
554
|
+
})
|
|
555
|
+
} catch {
|
|
556
|
+
// 预设列表加载失败不打断主配置界面
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 首次渲染拉取预设列表
|
|
561
|
+
React.useEffect(() => { loadPersonas() }, [])
|
|
562
|
+
|
|
504
563
|
const handleSave = async () => {
|
|
505
|
-
await controller.saveConfig(localMap)
|
|
506
|
-
|
|
564
|
+
const payload = await controller.saveConfig(localMap)
|
|
565
|
+
setToast({ text: payload && Array.isArray(payload.changed) && payload.changed.length > 0 ? t('toast.saved') : t('toast.noChanges'), kind: 'success' })
|
|
507
566
|
if (showPrompt) await loadPrompt()
|
|
508
567
|
}
|
|
509
568
|
|
|
510
569
|
const handleReset = async () => {
|
|
511
570
|
await controller.resetConfig()
|
|
512
|
-
|
|
571
|
+
setToast({ text: t('toast.reset'), kind: 'success' })
|
|
513
572
|
if (showPrompt) await loadPrompt()
|
|
514
573
|
}
|
|
515
574
|
|
|
575
|
+
const handleSavePersona = async () => {
|
|
576
|
+
const name = personaName.trim()
|
|
577
|
+
if (!name) return
|
|
578
|
+
try {
|
|
579
|
+
await controller.savePersona(name)
|
|
580
|
+
setPersonaName('')
|
|
581
|
+
await loadPersonas()
|
|
582
|
+
setToast({ text: t('toast.personaSaved'), kind: 'success' })
|
|
583
|
+
} catch (err) {
|
|
584
|
+
setToast({ text: messageOf(err), kind: 'error' })
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const handleUsePersona = async (name) => {
|
|
589
|
+
try {
|
|
590
|
+
const payload = await controller.usePersona(name)
|
|
591
|
+
await loadPersonas()
|
|
592
|
+
setToast({ text: payload && payload.unchanged ? t('toast.personaUnchanged') : t('toast.personaUsed'), kind: 'success' })
|
|
593
|
+
if (showPrompt) await loadPrompt()
|
|
594
|
+
} catch (err) {
|
|
595
|
+
setToast({ text: messageOf(err), kind: 'error' })
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const handleDeletePersona = async (name) => {
|
|
600
|
+
if (typeof globalThis.confirm === 'function' && !globalThis.confirm(t('personas.confirmDelete', { name }))) return
|
|
601
|
+
try {
|
|
602
|
+
await controller.deletePersona(name)
|
|
603
|
+
await loadPersonas()
|
|
604
|
+
setToast({ text: t('toast.personaDeleted'), kind: 'success' })
|
|
605
|
+
} catch (err) {
|
|
606
|
+
setToast({ text: messageOf(err), kind: 'error' })
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
516
610
|
const styleOptions = STYLE_VALUES.map((value) => ({ value, label: t(`style.${value}`) }))
|
|
517
611
|
const headingListsOptions = TRAIT_VALUES.map((value) => ({ value, label: t(`trait.headingLists.${value}`) }))
|
|
518
612
|
const emojiOptions = TRAIT_VALUES.map((value) => ({ value, label: t(`trait.emoji.${value}`) }))
|
|
519
|
-
const
|
|
613
|
+
const personaNames = personas ? Object.keys(personas).sort() : null
|
|
614
|
+
|
|
615
|
+
const personaRowMeta = (entry) => {
|
|
616
|
+
const parts = []
|
|
617
|
+
if (entry && STYLE_VALUES.includes(entry.style)) parts.push(t(`style.${entry.style}`))
|
|
618
|
+
else if (entry && entry.style) parts.push(entry.style)
|
|
619
|
+
if (entry && entry.nickname) parts.push(entry.nickname)
|
|
620
|
+
return parts.join(' · ')
|
|
621
|
+
}
|
|
520
622
|
|
|
521
623
|
return e('div', { className: 'soul-section' },
|
|
522
624
|
e('h3', null, t('settings.title')),
|
|
@@ -639,6 +741,52 @@ window.__ModuleLoader__.load({
|
|
|
639
741
|
onChange: (ev) => setLocalInstructions(ev.target.value),
|
|
640
742
|
placeholder: t('field.instructionsPlaceholder')
|
|
641
743
|
})
|
|
744
|
+
),
|
|
745
|
+
|
|
746
|
+
e('div', { className: 'soul-group-title' }, t('group.personas')),
|
|
747
|
+
e('div', { className: 'soul-field' },
|
|
748
|
+
personaNames === null
|
|
749
|
+
? e('div', { className: 'soul-status' }, t('status.loading'))
|
|
750
|
+
: (personaNames.length === 0
|
|
751
|
+
? e('div', { className: 'soul-status' }, t('personas.empty'))
|
|
752
|
+
: e(Fragment, null,
|
|
753
|
+
...personaNames.map((name) => {
|
|
754
|
+
const entry = personas[name] || {}
|
|
755
|
+
return e('div', { className: 'soul-persona-row', key: name },
|
|
756
|
+
e('span', { className: 'soul-persona-name' }, name === activePersona ? `★ ${name}` : name),
|
|
757
|
+
e('span', { className: 'soul-persona-meta' }, personaRowMeta(entry)),
|
|
758
|
+
e('span', { className: 'soul-persona-actions' },
|
|
759
|
+
e('button', { type: 'button', className: 'soul-prompt-link', onClick: () => handleUsePersona(name) }, t('personas.use')),
|
|
760
|
+
e('button', { type: 'button', className: 'soul-prompt-link soul-persona-danger', onClick: () => handleDeletePersona(name) }, t('personas.delete'))
|
|
761
|
+
)
|
|
762
|
+
)
|
|
763
|
+
})
|
|
764
|
+
))
|
|
765
|
+
),
|
|
766
|
+
e('div', { className: 'soul-field soul-persona-save' },
|
|
767
|
+
e('input', {
|
|
768
|
+
type: 'text',
|
|
769
|
+
value: personaName,
|
|
770
|
+
maxLength: 30,
|
|
771
|
+
onChange: (ev) => setPersonaName(ev.target.value),
|
|
772
|
+
placeholder: t('personas.savePlaceholder')
|
|
773
|
+
}),
|
|
774
|
+
e(ui.Button, {
|
|
775
|
+
onClick: handleSavePersona,
|
|
776
|
+
disabled: saving || !personaName.trim()
|
|
777
|
+
}, t('personas.save'))
|
|
778
|
+
),
|
|
779
|
+
|
|
780
|
+
e('div', { className: 'soul-group-title' }, t('group.tool')),
|
|
781
|
+
e('div', { className: 'soul-toggle' },
|
|
782
|
+
e('input', {
|
|
783
|
+
type: 'checkbox',
|
|
784
|
+
id: 'soul-toolConfirm',
|
|
785
|
+
checked: localToolConfirm,
|
|
786
|
+
onChange: (ev) => setLocalToolConfirm(ev.target.checked)
|
|
787
|
+
}),
|
|
788
|
+
e('label', { htmlFor: 'soul-toolConfirm' }, t('field.toolConfirm')),
|
|
789
|
+
e(SoulHint, { text: t('hint.toolConfirm') })
|
|
642
790
|
)
|
|
643
791
|
),
|
|
644
792
|
|
|
@@ -678,19 +826,12 @@ window.__ModuleLoader__.load({
|
|
|
678
826
|
|
|
679
827
|
loading && e('div', { className: 'soul-status' }, t('status.loading')),
|
|
680
828
|
|
|
681
|
-
|
|
682
|
-
className: 'soul-toast soul-toast-success'
|
|
829
|
+
toast && e('div', {
|
|
830
|
+
className: `soul-toast ${toast.kind === 'error' ? 'soul-toast-error' : 'soul-toast-success'}`,
|
|
683
831
|
style: {
|
|
684
832
|
animation: 'fadeInOut 2s ease-in-out'
|
|
685
833
|
}
|
|
686
|
-
},
|
|
687
|
-
|
|
688
|
-
showResetSuccess && e('div', {
|
|
689
|
-
className: 'soul-toast soul-toast-success',
|
|
690
|
-
style: {
|
|
691
|
-
animation: 'fadeInOut 2s ease-in-out'
|
|
692
|
-
}
|
|
693
|
-
}, t('toast.reset'))
|
|
834
|
+
}, toast.text)
|
|
694
835
|
)
|
|
695
836
|
}
|
|
696
837
|
|
|
@@ -708,6 +849,11 @@ window.__ModuleLoader__.load({
|
|
|
708
849
|
}, 'dsh-soul: browser lifecycle')
|
|
709
850
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-soul: dictionaries')
|
|
710
851
|
|
|
852
|
+
// 暗色适配说明:原生 <select> 的弹出选项列表由浏览器按 color-scheme 渲染。
|
|
853
|
+
// 宿主主题呈现器会把 html 的 color-scheme 与 body[data-ds-dark-theme] 随主题
|
|
854
|
+
// 投影到 document(见 dsh-client-ui-layout ThemePresenter),因此这里用纯 CSS
|
|
855
|
+
// 属性选择器跟随即可,无需 JS 监听;选项行另用主题变量显式着色兜底。
|
|
856
|
+
|
|
711
857
|
// 导航 label / 图标替换用的翻译函数(bind 返回的函数在调用时读取当前语言)
|
|
712
858
|
const navT = ctx.locale.bind(NS)
|
|
713
859
|
|