dsh-soul 0.3.3 → 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 +25 -9
- package/README_EN.md +25 -9
- package/client/index.mjs +509 -218
- package/index.mjs +637 -242
- package/lib/config.mjs +48 -1
- package/package.json +1 -1
- package/scripts/verify-config.mjs +39 -1
package/client/index.mjs
CHANGED
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 在设置页面添加「个性化」栏目:
|
|
4
4
|
// - 启用/禁用个性化设置
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
5
|
+
// - 「关于你」(昵称/职业/介绍)、回复风格和语调、特质、输出语言、自定义指令
|
|
6
|
+
// - 人设预设:保存当前为预设、一键使用(★ 标记当前匹配项)、删除
|
|
7
|
+
// - Agent 工具:set_persona 确认模式开关
|
|
8
|
+
// - dirty 检测(无改动禁用保存)、统一 toast 提示
|
|
9
|
+
// - 查看当前生效提示词(只读)与字符数
|
|
7
10
|
//
|
|
8
11
|
// 纯 JS + React jsx-runtime 手写(不依赖 JSX 构建),开箱即用。
|
|
9
12
|
// 注意:dsh web 使用 React 17+ 的 jsx-runtime,children 必须放在 props.children
|
|
10
13
|
// 里;不能把 children 当第三个参数传(第三个参数是 key)。
|
|
14
|
+
//
|
|
15
|
+
// 文案全部走宿主 locale 词典:槽位声明 locale: NS 后,渲染器会把绑定本命名
|
|
16
|
+
// 空间的 t 注入组件(props.t),并在语言切换时重渲染;导航 label 由 apply 内
|
|
17
|
+
// 绑定的 navT 实时解析,壳层在语言切换时会重新解析 label。
|
|
11
18
|
|
|
12
19
|
window.__ModuleLoader__.load({
|
|
13
20
|
id: 'dsh-soul',
|
|
@@ -39,19 +46,23 @@ window.__ModuleLoader__.load({
|
|
|
39
46
|
const INITIAL = {
|
|
40
47
|
enabled: true,
|
|
41
48
|
nickname: '',
|
|
42
|
-
// 关于你:用户职业 / 用户介绍
|
|
43
49
|
occupation: '',
|
|
44
50
|
bio: '',
|
|
45
|
-
// 回复风格和语调(合并为单一选项)
|
|
46
51
|
style: 'professional',
|
|
47
|
-
// 特质:标题和列表 / 表情符号
|
|
48
52
|
headingLists: 'default',
|
|
49
53
|
emoji: 'default',
|
|
50
54
|
language: 'zh',
|
|
51
55
|
customInstructions: '',
|
|
56
|
+
// set_persona 确认模式(v0.5.0)
|
|
57
|
+
requireToolConfirmation: false,
|
|
58
|
+
// 人设预设库与当前匹配项(GET /api/soul/personas 带回)
|
|
59
|
+
personas: null,
|
|
60
|
+
activePersona: null,
|
|
52
61
|
loading: false,
|
|
53
62
|
saving: false,
|
|
54
|
-
error: null
|
|
63
|
+
error: null,
|
|
64
|
+
// 最近一次保存实际变化的字段名数组(来自写路径的 changed)
|
|
65
|
+
lastChanged: null
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
// 第三方插件不能 require('@deepseek-ai/dsh-client-runtime/client')——
|
|
@@ -90,29 +101,28 @@ window.__ModuleLoader__.load({
|
|
|
90
101
|
this.disposed = false
|
|
91
102
|
}
|
|
92
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
|
+
|
|
93
118
|
async loadConfig() {
|
|
94
119
|
if (this.disposed) return
|
|
95
120
|
this.store.update(s => { s.loading = true; s.error = null })
|
|
96
|
-
|
|
121
|
+
|
|
97
122
|
try {
|
|
98
|
-
const
|
|
99
|
-
const response = await this.fetcher(url)
|
|
100
|
-
const payload = await response.json().catch(() => ({}))
|
|
101
|
-
|
|
102
|
-
if (!response.ok || payload.ok !== true) {
|
|
103
|
-
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
104
|
-
}
|
|
105
|
-
|
|
123
|
+
const payload = await this.postJSON('/api/soul/config', null, { method: 'GET' })
|
|
106
124
|
this.store.update(s => {
|
|
107
|
-
s
|
|
108
|
-
s.nickname = payload.config.nickname || ''
|
|
109
|
-
s.occupation = payload.config.occupation || ''
|
|
110
|
-
s.bio = payload.config.bio || ''
|
|
111
|
-
s.style = payload.config.style
|
|
112
|
-
s.headingLists = payload.config.headingLists || 'default'
|
|
113
|
-
s.emoji = payload.config.emoji || 'default'
|
|
114
|
-
s.language = payload.config.language || 'zh'
|
|
115
|
-
s.customInstructions = payload.config.customInstructions
|
|
125
|
+
this.applyConfig(s, payload.config)
|
|
116
126
|
s.loading = false
|
|
117
127
|
})
|
|
118
128
|
} catch (error) {
|
|
@@ -127,32 +137,15 @@ window.__ModuleLoader__.load({
|
|
|
127
137
|
async saveConfig(config) {
|
|
128
138
|
if (this.disposed) return
|
|
129
139
|
this.store.update(s => { s.saving = true; s.error = null })
|
|
130
|
-
|
|
140
|
+
|
|
131
141
|
try {
|
|
132
|
-
const
|
|
133
|
-
const response = await this.fetcher(url, {
|
|
134
|
-
method: 'POST',
|
|
135
|
-
headers: { 'content-type': 'application/json' },
|
|
136
|
-
body: JSON.stringify(config)
|
|
137
|
-
})
|
|
138
|
-
const payload = await response.json().catch(() => ({}))
|
|
139
|
-
|
|
140
|
-
if (!response.ok || payload.ok !== true) {
|
|
141
|
-
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
142
|
-
}
|
|
143
|
-
|
|
142
|
+
const payload = await this.postJSON('/api/soul/config', config)
|
|
144
143
|
this.store.update(s => {
|
|
145
|
-
s
|
|
146
|
-
s.
|
|
147
|
-
s.occupation = payload.config.occupation || ''
|
|
148
|
-
s.bio = payload.config.bio || ''
|
|
149
|
-
s.style = payload.config.style
|
|
150
|
-
s.headingLists = payload.config.headingLists || 'default'
|
|
151
|
-
s.emoji = payload.config.emoji || 'default'
|
|
152
|
-
s.language = payload.config.language || 'zh'
|
|
153
|
-
s.customInstructions = payload.config.customInstructions
|
|
144
|
+
this.applyConfig(s, payload.config)
|
|
145
|
+
s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
|
|
154
146
|
s.saving = false
|
|
155
147
|
})
|
|
148
|
+
return payload
|
|
156
149
|
} catch (error) {
|
|
157
150
|
if (this.disposed) return
|
|
158
151
|
this.store.update(s => {
|
|
@@ -165,28 +158,15 @@ window.__ModuleLoader__.load({
|
|
|
165
158
|
async resetConfig() {
|
|
166
159
|
if (this.disposed) return
|
|
167
160
|
this.store.update(s => { s.saving = true; s.error = null })
|
|
168
|
-
|
|
161
|
+
|
|
169
162
|
try {
|
|
170
|
-
const
|
|
171
|
-
const response = await this.fetcher(url, { method: 'POST' })
|
|
172
|
-
const payload = await response.json().catch(() => ({}))
|
|
173
|
-
|
|
174
|
-
if (!response.ok || payload.ok !== true) {
|
|
175
|
-
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
176
|
-
}
|
|
177
|
-
|
|
163
|
+
const payload = await this.postJSON('/api/soul/config/reset')
|
|
178
164
|
this.store.update(s => {
|
|
179
|
-
s
|
|
180
|
-
s.
|
|
181
|
-
s.occupation = payload.config.occupation || ''
|
|
182
|
-
s.bio = payload.config.bio || ''
|
|
183
|
-
s.style = payload.config.style
|
|
184
|
-
s.headingLists = payload.config.headingLists || 'default'
|
|
185
|
-
s.emoji = payload.config.emoji || 'default'
|
|
186
|
-
s.language = payload.config.language || 'zh'
|
|
187
|
-
s.customInstructions = payload.config.customInstructions
|
|
165
|
+
this.applyConfig(s, payload.config)
|
|
166
|
+
s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
|
|
188
167
|
s.saving = false
|
|
189
168
|
})
|
|
169
|
+
return payload
|
|
190
170
|
} catch (error) {
|
|
191
171
|
if (this.disposed) return
|
|
192
172
|
this.store.update(s => {
|
|
@@ -196,6 +176,52 @@ window.__ModuleLoader__.load({
|
|
|
196
176
|
}
|
|
197
177
|
}
|
|
198
178
|
|
|
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)
|
|
212
|
+
const payload = await response.json().catch(() => ({}))
|
|
213
|
+
if (!response.ok || payload.ok !== true) {
|
|
214
|
+
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
215
|
+
}
|
|
216
|
+
return payload
|
|
217
|
+
}
|
|
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
|
+
|
|
199
225
|
async dispose() {
|
|
200
226
|
this.disposed = true
|
|
201
227
|
}
|
|
@@ -206,29 +232,47 @@ window.__ModuleLoader__.load({
|
|
|
206
232
|
// -------------------------------------------------------------------------
|
|
207
233
|
|
|
208
234
|
const css = [
|
|
209
|
-
'.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)}',
|
|
210
236
|
'.soul-section h3{margin:0 0 16px 0;font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
211
237
|
'.soul-field{margin-bottom:16px;width:100%}',
|
|
212
238
|
'.soul-field label{display:block;margin-bottom:6px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-secondary)}',
|
|
213
|
-
'.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}',
|
|
214
245
|
'.soul-field textarea{min-height:100px;resize:vertical}',
|
|
215
246
|
'.soul-field textarea.soul-textarea-sm{min-height:60px}',
|
|
216
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)}',
|
|
217
|
-
'.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)}',
|
|
218
249
|
'.soul-toggle{display:flex;align-items:center;gap:8px;margin-bottom:16px}',
|
|
219
250
|
'.soul-toggle label{margin:0;font-size:13px;color:var(--dsw-alias-label-primary)}',
|
|
220
251
|
'.soul-buttons{display:flex;gap:8px;margin-top:16px}',
|
|
221
|
-
'.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}',
|
|
222
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}',
|
|
223
254
|
'.soul-hint:hover{color:var(--dsw-alias-label-primary)}',
|
|
224
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)}',
|
|
225
256
|
'.soul-hint:hover .soul-hint-tip{opacity:1;visibility:visible}',
|
|
226
257
|
'.soul-status{margin-top:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
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}',
|
|
265
|
+
'.soul-prompt-toggle{margin-top:8px}',
|
|
266
|
+
'.soul-prompt-link{background:none;border:none;padding:0;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer;text-decoration:underline}',
|
|
267
|
+
'.soul-prompt-link:hover{color:var(--dsw-alias-label-primary)}',
|
|
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}',
|
|
227
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}',
|
|
228
271
|
'.soul-toast-success{background:#f6ffed;border:1px solid #b7eb8f;color:#52c41a}',
|
|
272
|
+
'.soul-toast-error{background:#fff1f0;border:1px solid #ffa39e;color:#f5222d}',
|
|
229
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)}}'
|
|
230
274
|
].join('')
|
|
231
|
-
|
|
275
|
+
|
|
232
276
|
const tagId = 'dsh-soul/styles.css'
|
|
233
277
|
let styleTagMissing = false
|
|
234
278
|
if (typeof document !== 'undefined') {
|
|
@@ -247,38 +291,168 @@ window.__ModuleLoader__.load({
|
|
|
247
291
|
}
|
|
248
292
|
|
|
249
293
|
// -------------------------------------------------------------------------
|
|
250
|
-
//
|
|
294
|
+
// 文案词典(键集中英完全对齐;渲染器按槽位 locale 注入 t)
|
|
251
295
|
// -------------------------------------------------------------------------
|
|
252
296
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
297
|
+
const zh = {
|
|
298
|
+
'nav.label': '个性化',
|
|
299
|
+
'settings.title': '个性化设置',
|
|
300
|
+
'toggle.enabled': '启用个性化设置',
|
|
301
|
+
'group.aboutYou': '关于你',
|
|
302
|
+
'field.nickname': '用户昵称',
|
|
303
|
+
'field.nicknamePlaceholder': '输入你的昵称,回复时会称呼你',
|
|
304
|
+
'field.occupation': '用户职业',
|
|
305
|
+
'field.occupationPlaceholder': '例如:软件工程师、学生、产品经理',
|
|
306
|
+
'field.bio': '用户介绍',
|
|
307
|
+
'field.bioPlaceholder': '简单介绍自己,让回复更贴合你的背景',
|
|
308
|
+
'group.traits': '特质',
|
|
309
|
+
'field.style': '回复风格和语调',
|
|
310
|
+
'hint.style': '设置 Agent 回复你的风格和语调。这不会影响 Agent 的功能。',
|
|
311
|
+
'field.headingLists': '标题和列表',
|
|
312
|
+
'hint.headingLists': '在回复风格和语调的基础上选择额外的自定义特质项。控制回答中标题和列表的使用程度。',
|
|
313
|
+
'field.emoji': '表情符号',
|
|
314
|
+
'hint.emoji': '在回复风格和语调的基础上选择额外的自定义特质项。控制表情符号的使用程度。',
|
|
315
|
+
'field.language': '输出语言',
|
|
316
|
+
'hint.language': '设置 Agent 回复你使用的语言,同时影响 /soul 命令的输出语言。',
|
|
317
|
+
'field.instructions': '自定义指令',
|
|
318
|
+
'hint.instructions': '补充角色、习惯等个性化要求。建议不要重复设置回复风格和语调类似的话术,以免与上方选项冲突。',
|
|
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': '暂无人设预设,保存当前配置后可一键切换',
|
|
327
|
+
'style.professional': '专业严谨',
|
|
328
|
+
'style.casual': '轻松自然',
|
|
329
|
+
'style.humorous': '幽默风趣',
|
|
330
|
+
'style.roast': '吐槽达人',
|
|
331
|
+
'style.efficient': '高效干练',
|
|
332
|
+
'trait.headingLists.default': '默认',
|
|
333
|
+
'trait.headingLists.more': '增强(采用清晰格式和列表结构)',
|
|
334
|
+
'trait.headingLists.less': '减弱(使用更多段落文本)',
|
|
335
|
+
'trait.emoji.default': '默认',
|
|
336
|
+
'trait.emoji.more': '增强(使用较多表情符号)',
|
|
337
|
+
'trait.emoji.less': '减弱(尽量减少使用表情符号)',
|
|
338
|
+
'group.tool': 'Agent 工具',
|
|
339
|
+
'field.toolConfirm': '人设变更需确认',
|
|
340
|
+
'hint.toolConfirm': '开启后,Agent 通过 set_persona 工具做出的修改不会立即生效,需在会话中使用 /soul confirm 确认或 /soul reject 拒绝。',
|
|
341
|
+
'button.save': '保存设置',
|
|
342
|
+
'button.saving': '保存中...',
|
|
343
|
+
'button.reset': '重置默认',
|
|
344
|
+
'status.loading': '加载中...',
|
|
345
|
+
'status.unsaved': '有未保存的更改',
|
|
346
|
+
'toast.saved': '✅ 设置已保存',
|
|
347
|
+
'toast.noChanges': '✅ 配置无变化',
|
|
348
|
+
'toast.reset': '✅ 已重置为默认值',
|
|
349
|
+
'toast.personaSaved': '✅ 预设已保存',
|
|
350
|
+
'toast.personaUsed': '✅ 预设已应用',
|
|
351
|
+
'toast.personaUnchanged': 'ℹ 预设与当前配置一致',
|
|
352
|
+
'toast.personaDeleted': '✅ 预设已删除',
|
|
353
|
+
'prompt.view': '查看当前生效提示词',
|
|
354
|
+
'prompt.hide': '收起提示词',
|
|
355
|
+
'prompt.loading': '提示词加载中...',
|
|
356
|
+
'prompt.empty': '(个性化已禁用,提示词为空)',
|
|
357
|
+
'prompt.chars': '当前字符数:{n}'
|
|
358
|
+
}
|
|
261
359
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
360
|
+
const en = {
|
|
361
|
+
'nav.label': 'Personalization',
|
|
362
|
+
'settings.title': 'Personalization Settings',
|
|
363
|
+
'toggle.enabled': 'Enable personalization',
|
|
364
|
+
'group.aboutYou': 'About you',
|
|
365
|
+
'field.nickname': 'Nickname',
|
|
366
|
+
'field.nicknamePlaceholder': 'Your nickname — the agent will address you by it',
|
|
367
|
+
'field.occupation': 'Occupation',
|
|
368
|
+
'field.occupationPlaceholder': 'e.g. software engineer, student, product manager',
|
|
369
|
+
'field.bio': 'Bio',
|
|
370
|
+
'field.bioPlaceholder': 'A short introduction so replies fit your background',
|
|
371
|
+
'group.traits': 'Traits',
|
|
372
|
+
'field.style': 'Reply style & tone',
|
|
373
|
+
'hint.style': "Sets the style and tone the agent uses when replying to you. It does not affect the agent's capabilities.",
|
|
374
|
+
'field.headingLists': 'Headings & lists',
|
|
375
|
+
'hint.headingLists': 'An extra trait layered on top of style & tone. Controls how much answers rely on headings and lists.',
|
|
376
|
+
'field.emoji': 'Emoji',
|
|
377
|
+
'hint.emoji': 'An extra trait layered on top of style & tone. Controls how many emojis answers use.',
|
|
378
|
+
'field.language': 'Output language',
|
|
379
|
+
'hint.language': 'Sets the language the agent replies in; also affects the /soul command output.',
|
|
380
|
+
'field.instructions': 'Custom instructions',
|
|
381
|
+
'hint.instructions': 'Extra persona or habit requirements. Avoid repeating wording similar to the style & tone option above to prevent conflicts.',
|
|
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',
|
|
390
|
+
'style.professional': 'Professional',
|
|
391
|
+
'style.casual': 'Casual',
|
|
392
|
+
'style.humorous': 'Humorous',
|
|
393
|
+
'style.roast': 'Roast Master',
|
|
394
|
+
'style.efficient': 'Efficient',
|
|
395
|
+
'trait.headingLists.default': 'Default',
|
|
396
|
+
'trait.headingLists.more': 'More (clear formatting with headings and lists)',
|
|
397
|
+
'trait.headingLists.less': 'Less (more paragraph text)',
|
|
398
|
+
'trait.emoji.default': 'Default',
|
|
399
|
+
'trait.emoji.more': 'More (frequent emoji usage)',
|
|
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.',
|
|
404
|
+
'button.save': 'Save Settings',
|
|
405
|
+
'button.saving': 'Saving...',
|
|
406
|
+
'button.reset': 'Reset to Default',
|
|
407
|
+
'status.loading': 'Loading...',
|
|
408
|
+
'status.unsaved': 'You have unsaved changes',
|
|
409
|
+
'toast.saved': '✅ Settings saved',
|
|
410
|
+
'toast.noChanges': '✅ No changes to save',
|
|
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',
|
|
416
|
+
'prompt.view': 'View the active system prompt',
|
|
417
|
+
'prompt.hide': 'Hide the prompt',
|
|
418
|
+
'prompt.loading': 'Loading prompt...',
|
|
419
|
+
'prompt.empty': '(personalization disabled — the prompt is empty)',
|
|
420
|
+
'prompt.chars': 'Current character count: {n}'
|
|
421
|
+
}
|
|
268
422
|
|
|
269
|
-
//
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
423
|
+
// 图标替换匹配的导航文案(词典中 nav.label 的全部语言取值)
|
|
424
|
+
const NAV_LABELS = new Set([zh['nav.label'], en['nav.label']])
|
|
425
|
+
|
|
426
|
+
// 组件内翻译回退:渲染器未注入 t 时使用中文词典(保证组件可独立渲染/测试)
|
|
427
|
+
function makeFallbackT(dict) {
|
|
428
|
+
return (key, params) => {
|
|
429
|
+
let text = dict[key] ?? key
|
|
430
|
+
if (params) {
|
|
431
|
+
for (const [name, value] of Object.entries(params)) {
|
|
432
|
+
text = text.split(`{${name}}`).join(String(value))
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return text
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const FALLBACK_T = makeFallbackT(zh)
|
|
275
439
|
|
|
276
|
-
//
|
|
440
|
+
// -------------------------------------------------------------------------
|
|
441
|
+
// 组件
|
|
442
|
+
// -------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
// 选项取值(label 走词典;取值与 lib/config.mjs 的合法值保持一致)
|
|
445
|
+
const STYLE_VALUES = ['professional', 'casual', 'humorous', 'roast', 'efficient']
|
|
446
|
+
const TRAIT_VALUES = ['default', 'more', 'less']
|
|
447
|
+
// 输出语言选项(两种 UI 语言下均自解释,不进词典)
|
|
277
448
|
const LANGUAGE_OPTIONS = [
|
|
278
449
|
{ value: 'zh', label: '中文' },
|
|
279
450
|
{ value: 'en', label: 'English' }
|
|
280
451
|
]
|
|
281
452
|
|
|
453
|
+
// 表单字段与 store 字段的一一对应(dirty 检测与保存载荷共用)
|
|
454
|
+
const FIELD_KEYS = ['enabled', 'nickname', 'occupation', 'bio', 'style', 'headingLists', 'emoji', 'language', 'customInstructions', 'requireToolConfirmation']
|
|
455
|
+
|
|
282
456
|
// 提示词小图标:hover 展示说明文字
|
|
283
457
|
function SoulHint(props) {
|
|
284
458
|
return e('span', { className: 'soul-hint', 'aria-label': props.text },
|
|
@@ -303,9 +477,11 @@ window.__ModuleLoader__.load({
|
|
|
303
477
|
|
|
304
478
|
function SoulSettings(props) {
|
|
305
479
|
const { useSoulController, controller } = props
|
|
480
|
+
// 渲染器按槽位 locale 命名空间注入 t(随语言切换更新);缺失时回退中文
|
|
481
|
+
const t = typeof props.t === 'function' ? props.t : FALLBACK_T
|
|
306
482
|
const state = useSoulController((state) => state)
|
|
307
|
-
const { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, loading, saving, error } = state
|
|
308
|
-
|
|
483
|
+
const { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, requireToolConfirmation, personas, activePersona, loading, saving, error } = state
|
|
484
|
+
|
|
309
485
|
const [localEnabled, setLocalEnabled] = React.useState(enabled)
|
|
310
486
|
const [localNickname, setLocalNickname] = React.useState(nickname || '')
|
|
311
487
|
const [localOccupation, setLocalOccupation] = React.useState(occupation || '')
|
|
@@ -315,9 +491,14 @@ window.__ModuleLoader__.load({
|
|
|
315
491
|
const [localEmoji, setLocalEmoji] = React.useState(emoji)
|
|
316
492
|
const [localLanguage, setLocalLanguage] = React.useState(language || 'zh')
|
|
317
493
|
const [localInstructions, setLocalInstructions] = React.useState(customInstructions)
|
|
318
|
-
const [
|
|
319
|
-
const [
|
|
320
|
-
|
|
494
|
+
const [localToolConfirm, setLocalToolConfirm] = React.useState(requireToolConfirmation === true)
|
|
495
|
+
const [personaName, setPersonaName] = React.useState('')
|
|
496
|
+
const [toast, setToast] = React.useState(null)
|
|
497
|
+
const [showPrompt, setShowPrompt] = React.useState(false)
|
|
498
|
+
const [promptText, setPromptText] = React.useState('')
|
|
499
|
+
const [promptLoading, setPromptLoading] = React.useState(false)
|
|
500
|
+
const [promptError, setPromptError] = React.useState(null)
|
|
501
|
+
|
|
321
502
|
// 同步状态
|
|
322
503
|
React.useEffect(() => {
|
|
323
504
|
setLocalEnabled(enabled)
|
|
@@ -329,48 +510,118 @@ window.__ModuleLoader__.load({
|
|
|
329
510
|
setLocalEmoji(emoji)
|
|
330
511
|
setLocalLanguage(language || 'zh')
|
|
331
512
|
setLocalInstructions(customInstructions)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
513
|
+
setLocalToolConfirm(requireToolConfirmation === true)
|
|
514
|
+
}, [enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, requireToolConfirmation])
|
|
515
|
+
|
|
516
|
+
// toast 自动消失
|
|
335
517
|
React.useEffect(() => {
|
|
336
|
-
if (
|
|
337
|
-
const timer = setTimeout(() =>
|
|
518
|
+
if (toast) {
|
|
519
|
+
const timer = setTimeout(() => setToast(null), 2000)
|
|
338
520
|
return () => clearTimeout(timer)
|
|
339
521
|
}
|
|
340
|
-
}, [
|
|
341
|
-
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
522
|
+
}, [toast])
|
|
523
|
+
|
|
524
|
+
// dirty 检测:本地表单与已保存配置逐字段比较
|
|
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 }
|
|
527
|
+
const dirty = FIELD_KEYS.some((key) => savedMap[key] !== localMap[key])
|
|
528
|
+
|
|
529
|
+
const loadPrompt = async () => {
|
|
530
|
+
setPromptLoading(true)
|
|
531
|
+
setPromptError(null)
|
|
532
|
+
try {
|
|
533
|
+
const payload = await controller.fetchPrompt()
|
|
534
|
+
setPromptText(payload.prompt || '')
|
|
535
|
+
} catch (err) {
|
|
536
|
+
setPromptError(messageOf(err))
|
|
537
|
+
} finally {
|
|
538
|
+
setPromptLoading(false)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const togglePrompt = async () => {
|
|
543
|
+
const next = !showPrompt
|
|
544
|
+
setShowPrompt(next)
|
|
545
|
+
if (next) await loadPrompt()
|
|
546
|
+
}
|
|
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
|
+
// 预设列表加载失败不打断主配置界面
|
|
347
557
|
}
|
|
348
|
-
}
|
|
349
|
-
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 首次渲染拉取预设列表
|
|
561
|
+
React.useEffect(() => { loadPersonas() }, [])
|
|
562
|
+
|
|
350
563
|
const handleSave = async () => {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
nickname: localNickname,
|
|
355
|
-
occupation: localOccupation,
|
|
356
|
-
bio: localBio,
|
|
357
|
-
style: localStyle,
|
|
358
|
-
headingLists: localHeadingLists,
|
|
359
|
-
emoji: localEmoji,
|
|
360
|
-
language: localLanguage,
|
|
361
|
-
customInstructions: localInstructions
|
|
362
|
-
})
|
|
363
|
-
setShowSuccess(true)
|
|
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' })
|
|
566
|
+
if (showPrompt) await loadPrompt()
|
|
364
567
|
}
|
|
365
|
-
|
|
568
|
+
|
|
366
569
|
const handleReset = async () => {
|
|
367
|
-
const controller = props.controller
|
|
368
570
|
await controller.resetConfig()
|
|
369
|
-
|
|
571
|
+
setToast({ text: t('toast.reset'), kind: 'success' })
|
|
572
|
+
if (showPrompt) await loadPrompt()
|
|
573
|
+
}
|
|
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
|
+
}
|
|
370
597
|
}
|
|
371
|
-
|
|
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
|
+
|
|
610
|
+
const styleOptions = STYLE_VALUES.map((value) => ({ value, label: t(`style.${value}`) }))
|
|
611
|
+
const headingListsOptions = TRAIT_VALUES.map((value) => ({ value, label: t(`trait.headingLists.${value}`) }))
|
|
612
|
+
const emojiOptions = TRAIT_VALUES.map((value) => ({ value, label: t(`trait.emoji.${value}`) }))
|
|
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
|
+
}
|
|
622
|
+
|
|
372
623
|
return e('div', { className: 'soul-section' },
|
|
373
|
-
e('h3', null, '
|
|
624
|
+
e('h3', null, t('settings.title')),
|
|
374
625
|
e('div', { className: 'soul-toggle' },
|
|
375
626
|
e('input', {
|
|
376
627
|
type: 'checkbox',
|
|
@@ -378,54 +629,54 @@ window.__ModuleLoader__.load({
|
|
|
378
629
|
checked: localEnabled,
|
|
379
630
|
onChange: (ev) => setLocalEnabled(ev.target.checked)
|
|
380
631
|
}),
|
|
381
|
-
e('label', { htmlFor: 'soul-enabled' }, '
|
|
632
|
+
e('label', { htmlFor: 'soul-enabled' }, t('toggle.enabled'))
|
|
382
633
|
),
|
|
383
|
-
|
|
634
|
+
|
|
384
635
|
localEnabled && e(Fragment, null,
|
|
385
|
-
e('div', { className: 'soul-group-title' }, '
|
|
636
|
+
e('div', { className: 'soul-group-title' }, t('group.aboutYou')),
|
|
386
637
|
e('div', { className: 'soul-field' },
|
|
387
|
-
e('label', { htmlFor: 'soul-nickname' }, '
|
|
638
|
+
e('label', { htmlFor: 'soul-nickname' }, t('field.nickname')),
|
|
388
639
|
e('input', {
|
|
389
640
|
type: 'text',
|
|
390
641
|
id: 'soul-nickname',
|
|
391
642
|
value: localNickname,
|
|
392
643
|
onChange: (ev) => setLocalNickname(ev.target.value),
|
|
393
|
-
placeholder: '
|
|
644
|
+
placeholder: t('field.nicknamePlaceholder')
|
|
394
645
|
})
|
|
395
646
|
),
|
|
396
647
|
e('div', { className: 'soul-field' },
|
|
397
|
-
e('label', { htmlFor: 'soul-occupation' }, '
|
|
648
|
+
e('label', { htmlFor: 'soul-occupation' }, t('field.occupation')),
|
|
398
649
|
e('input', {
|
|
399
650
|
type: 'text',
|
|
400
651
|
id: 'soul-occupation',
|
|
401
652
|
value: localOccupation,
|
|
402
653
|
onChange: (ev) => setLocalOccupation(ev.target.value),
|
|
403
|
-
placeholder: '
|
|
654
|
+
placeholder: t('field.occupationPlaceholder')
|
|
404
655
|
})
|
|
405
656
|
),
|
|
406
657
|
e('div', { className: 'soul-field' },
|
|
407
|
-
e('label', { htmlFor: 'soul-bio' }, '
|
|
658
|
+
e('label', { htmlFor: 'soul-bio' }, t('field.bio')),
|
|
408
659
|
e('textarea', {
|
|
409
660
|
id: 'soul-bio',
|
|
410
661
|
className: 'soul-textarea-sm',
|
|
411
662
|
value: localBio,
|
|
412
663
|
onChange: (ev) => setLocalBio(ev.target.value),
|
|
413
|
-
placeholder: '
|
|
664
|
+
placeholder: t('field.bioPlaceholder')
|
|
414
665
|
})
|
|
415
666
|
),
|
|
416
667
|
|
|
417
|
-
e('div', { className: 'soul-group-title' }, '
|
|
668
|
+
e('div', { className: 'soul-group-title' }, t('group.traits')),
|
|
418
669
|
e('div', { className: 'soul-field' },
|
|
419
670
|
e('label', { htmlFor: 'soul-style' },
|
|
420
|
-
'
|
|
421
|
-
e(SoulHint, { text: '
|
|
671
|
+
t('field.style'),
|
|
672
|
+
e(SoulHint, { text: t('hint.style') })
|
|
422
673
|
),
|
|
423
674
|
e('select', {
|
|
424
675
|
id: 'soul-style',
|
|
425
676
|
value: localStyle,
|
|
426
677
|
onChange: (ev) => setLocalStyle(ev.target.value)
|
|
427
678
|
},
|
|
428
|
-
...
|
|
679
|
+
...styleOptions.map(opt =>
|
|
429
680
|
e('option', { key: opt.value, value: opt.value }, opt.label)
|
|
430
681
|
)
|
|
431
682
|
)
|
|
@@ -433,15 +684,15 @@ window.__ModuleLoader__.load({
|
|
|
433
684
|
|
|
434
685
|
e('div', { className: 'soul-field' },
|
|
435
686
|
e('label', { htmlFor: 'soul-headingLists' },
|
|
436
|
-
'
|
|
437
|
-
e(SoulHint, { text: '
|
|
687
|
+
t('field.headingLists'),
|
|
688
|
+
e(SoulHint, { text: t('hint.headingLists') })
|
|
438
689
|
),
|
|
439
690
|
e('select', {
|
|
440
691
|
id: 'soul-headingLists',
|
|
441
692
|
value: localHeadingLists,
|
|
442
693
|
onChange: (ev) => setLocalHeadingLists(ev.target.value)
|
|
443
694
|
},
|
|
444
|
-
...
|
|
695
|
+
...headingListsOptions.map(opt =>
|
|
445
696
|
e('option', { key: opt.value, value: opt.value }, opt.label)
|
|
446
697
|
)
|
|
447
698
|
)
|
|
@@ -449,15 +700,15 @@ window.__ModuleLoader__.load({
|
|
|
449
700
|
|
|
450
701
|
e('div', { className: 'soul-field' },
|
|
451
702
|
e('label', { htmlFor: 'soul-emoji' },
|
|
452
|
-
'
|
|
453
|
-
e(SoulHint, { text: '
|
|
703
|
+
t('field.emoji'),
|
|
704
|
+
e(SoulHint, { text: t('hint.emoji') })
|
|
454
705
|
),
|
|
455
706
|
e('select', {
|
|
456
707
|
id: 'soul-emoji',
|
|
457
708
|
value: localEmoji,
|
|
458
709
|
onChange: (ev) => setLocalEmoji(ev.target.value)
|
|
459
710
|
},
|
|
460
|
-
...
|
|
711
|
+
...emojiOptions.map(opt =>
|
|
461
712
|
e('option', { key: opt.value, value: opt.value }, opt.label)
|
|
462
713
|
)
|
|
463
714
|
)
|
|
@@ -465,8 +716,8 @@ window.__ModuleLoader__.load({
|
|
|
465
716
|
|
|
466
717
|
e('div', { className: 'soul-field' },
|
|
467
718
|
e('label', { htmlFor: 'soul-language' },
|
|
468
|
-
'
|
|
469
|
-
e(SoulHint, { text: '
|
|
719
|
+
t('field.language'),
|
|
720
|
+
e(SoulHint, { text: t('hint.language') })
|
|
470
721
|
),
|
|
471
722
|
e('select', {
|
|
472
723
|
id: 'soul-language',
|
|
@@ -481,78 +732,109 @@ window.__ModuleLoader__.load({
|
|
|
481
732
|
|
|
482
733
|
e('div', { className: 'soul-field' },
|
|
483
734
|
e('label', { htmlFor: 'soul-instructions' },
|
|
484
|
-
'
|
|
485
|
-
e(SoulHint, { text: '
|
|
735
|
+
t('field.instructions'),
|
|
736
|
+
e(SoulHint, { text: t('hint.instructions') })
|
|
486
737
|
),
|
|
487
738
|
e('textarea', {
|
|
488
739
|
id: 'soul-instructions',
|
|
489
740
|
value: localInstructions,
|
|
490
741
|
onChange: (ev) => setLocalInstructions(ev.target.value),
|
|
491
|
-
placeholder: '
|
|
742
|
+
placeholder: t('field.instructionsPlaceholder')
|
|
492
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') })
|
|
493
790
|
)
|
|
494
791
|
),
|
|
495
|
-
|
|
792
|
+
|
|
496
793
|
error && e('div', { className: 'soul-error' }, error),
|
|
497
|
-
|
|
794
|
+
|
|
498
795
|
e('div', { className: 'soul-buttons' },
|
|
499
796
|
e(ui.Button, {
|
|
500
797
|
variant: 'primary',
|
|
501
798
|
onClick: handleSave,
|
|
502
|
-
disabled: saving
|
|
503
|
-
}, saving ? '
|
|
799
|
+
disabled: saving || !dirty
|
|
800
|
+
}, saving ? t('button.saving') : t('button.save')),
|
|
504
801
|
e(ui.Button, {
|
|
505
802
|
onClick: handleReset,
|
|
506
803
|
disabled: saving
|
|
507
|
-
}, '
|
|
804
|
+
}, t('button.reset'))
|
|
508
805
|
),
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
806
|
+
|
|
807
|
+
dirty && !saving && e('div', { className: 'soul-status soul-dirty' }, t('status.unsaved')),
|
|
808
|
+
|
|
809
|
+
e('div', { className: 'soul-prompt-toggle' },
|
|
810
|
+
e('button', {
|
|
811
|
+
type: 'button',
|
|
812
|
+
className: 'soul-prompt-link',
|
|
813
|
+
onClick: togglePrompt
|
|
814
|
+
}, showPrompt ? t('prompt.hide') : t('prompt.view'))
|
|
815
|
+
),
|
|
816
|
+
showPrompt && e('div', { className: 'soul-prompt-panel' },
|
|
817
|
+
promptLoading
|
|
818
|
+
? e('div', { className: 'soul-status' }, t('prompt.loading'))
|
|
819
|
+
: (promptError
|
|
820
|
+
? e('div', { className: 'soul-error' }, promptError)
|
|
821
|
+
: e(Fragment, null,
|
|
822
|
+
e('pre', { className: 'soul-prompt-pre' }, promptText || t('prompt.empty')),
|
|
823
|
+
e('div', { className: 'soul-status' }, t('prompt.chars', { n: String(promptText.length) }))
|
|
824
|
+
))
|
|
825
|
+
),
|
|
826
|
+
|
|
827
|
+
loading && e('div', { className: 'soul-status' }, t('status.loading')),
|
|
828
|
+
|
|
829
|
+
toast && e('div', {
|
|
830
|
+
className: `soul-toast ${toast.kind === 'error' ? 'soul-toast-error' : 'soul-toast-success'}`,
|
|
521
831
|
style: {
|
|
522
832
|
animation: 'fadeInOut 2s ease-in-out'
|
|
523
833
|
}
|
|
524
|
-
},
|
|
834
|
+
}, toast.text)
|
|
525
835
|
)
|
|
526
836
|
}
|
|
527
837
|
|
|
528
|
-
// -------------------------------------------------------------------------
|
|
529
|
-
// 文案
|
|
530
|
-
// -------------------------------------------------------------------------
|
|
531
|
-
|
|
532
|
-
const zh = {
|
|
533
|
-
'settings.title': '个性化设置',
|
|
534
|
-
'settings.description': '自定义Agent回复的风格和语调',
|
|
535
|
-
'button.save': '保存设置',
|
|
536
|
-
'button.reset': '重置默认',
|
|
537
|
-
'option.style': '回复风格和语调',
|
|
538
|
-
'option.instructions': '自定义指令',
|
|
539
|
-
'option.enabled': '启用个性化设置',
|
|
540
|
-
'status.loading': '加载中...',
|
|
541
|
-
'status.saving': '保存中...'
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const en = {
|
|
545
|
-
'settings.title': 'Personalization Settings',
|
|
546
|
-
'settings.description': 'Customize agent response style and tone',
|
|
547
|
-
'button.save': 'Save Settings',
|
|
548
|
-
'button.reset': 'Reset to Default',
|
|
549
|
-
'option.style': 'Response Style & Tone',
|
|
550
|
-
'option.instructions': 'Custom Instructions',
|
|
551
|
-
'option.enabled': 'Enable Personalization',
|
|
552
|
-
'status.loading': 'Loading...',
|
|
553
|
-
'status.saving': 'Saving...'
|
|
554
|
-
}
|
|
555
|
-
|
|
556
838
|
// -------------------------------------------------------------------------
|
|
557
839
|
// 插件装配
|
|
558
840
|
// -------------------------------------------------------------------------
|
|
@@ -566,32 +848,41 @@ window.__ModuleLoader__.load({
|
|
|
566
848
|
controller.dispose()
|
|
567
849
|
}, 'dsh-soul: browser lifecycle')
|
|
568
850
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-soul: dictionaries')
|
|
569
|
-
|
|
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
|
+
|
|
857
|
+
// 导航 label / 图标替换用的翻译函数(bind 返回的函数在调用时读取当前语言)
|
|
858
|
+
const navT = ctx.locale.bind(NS)
|
|
859
|
+
|
|
570
860
|
// DSH 0.1.x 不支持 settings.section 的 icon 字段
|
|
571
861
|
// 使用 DOM 操作动态替换图标(类似 dsh-better-sidebar 的方案)
|
|
572
862
|
const SOUL_NAV_MARKER = 'data-soul-settings-nav'
|
|
573
863
|
const SOUL_ICON_MARKER = 'data-soul-icon-replaced'
|
|
574
|
-
|
|
864
|
+
|
|
575
865
|
const registerSettingsNavIcon = () => {
|
|
576
866
|
let disposed = false
|
|
577
|
-
|
|
867
|
+
|
|
578
868
|
const sync = () => {
|
|
579
869
|
if (disposed) return
|
|
580
|
-
|
|
870
|
+
|
|
581
871
|
const buttons = document.querySelectorAll('[role="dialog"] nav button')
|
|
582
872
|
for (const button of buttons) {
|
|
583
|
-
|
|
873
|
+
// 匹配本栏目导航按钮:label 由 locale 词典渲染,双语均需识别
|
|
874
|
+
if (NAV_LABELS.has(button.textContent?.trim())) {
|
|
584
875
|
button.setAttribute(SOUL_NAV_MARKER, '')
|
|
585
|
-
|
|
876
|
+
|
|
586
877
|
// 检查是否已经替换过图标
|
|
587
878
|
if (button.hasAttribute(SOUL_ICON_MARKER)) continue
|
|
588
|
-
|
|
879
|
+
|
|
589
880
|
// 查找并替换图标
|
|
590
881
|
const existingIcon = button.querySelector('svg')
|
|
591
882
|
if (existingIcon) {
|
|
592
883
|
// 标记已替换
|
|
593
884
|
button.setAttribute(SOUL_ICON_MARKER, 'true')
|
|
594
|
-
|
|
885
|
+
|
|
595
886
|
// 创建星形图标(跟随主题色)
|
|
596
887
|
const starSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
597
888
|
starSvg.setAttribute('viewBox', '0 0 24 24')
|
|
@@ -602,11 +893,11 @@ window.__ModuleLoader__.load({
|
|
|
602
893
|
starSvg.setAttribute('stroke-linejoin', 'round')
|
|
603
894
|
starSvg.style.width = '16px'
|
|
604
895
|
starSvg.style.height = '16px'
|
|
605
|
-
|
|
896
|
+
|
|
606
897
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
|
607
898
|
path.setAttribute('d', 'M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z')
|
|
608
899
|
starSvg.appendChild(path)
|
|
609
|
-
|
|
900
|
+
|
|
610
901
|
existingIcon.replaceWith(starSvg)
|
|
611
902
|
}
|
|
612
903
|
} else {
|
|
@@ -614,10 +905,10 @@ window.__ModuleLoader__.load({
|
|
|
614
905
|
}
|
|
615
906
|
}
|
|
616
907
|
}
|
|
617
|
-
|
|
908
|
+
|
|
618
909
|
// 延迟执行初始同步,等待设置页面渲染完成
|
|
619
910
|
let timer = setTimeout(sync, 500)
|
|
620
|
-
|
|
911
|
+
|
|
621
912
|
// 监听 DOM 变化(只监听子节点添加,不监听所有变化)
|
|
622
913
|
const observer = new MutationObserver(() => {
|
|
623
914
|
// 防抖:每次变化都重置计时器,停止变化 100ms 后执行
|
|
@@ -626,12 +917,12 @@ window.__ModuleLoader__.load({
|
|
|
626
917
|
clearTimeout(timer)
|
|
627
918
|
timer = setTimeout(sync, 100)
|
|
628
919
|
})
|
|
629
|
-
|
|
920
|
+
|
|
630
921
|
observer.observe(document.body, {
|
|
631
922
|
childList: true,
|
|
632
923
|
subtree: true
|
|
633
924
|
})
|
|
634
|
-
|
|
925
|
+
|
|
635
926
|
return () => {
|
|
636
927
|
disposed = true
|
|
637
928
|
clearTimeout(timer)
|
|
@@ -644,16 +935,16 @@ window.__ModuleLoader__.load({
|
|
|
644
935
|
})
|
|
645
936
|
}
|
|
646
937
|
}
|
|
647
|
-
|
|
938
|
+
|
|
648
939
|
// 注册图标替换效果
|
|
649
940
|
ctx.effect(() => registerSettingsNavIcon(), 'dsh-soul: settings navigation icon')
|
|
650
|
-
|
|
651
|
-
//
|
|
941
|
+
|
|
942
|
+
// 注册到设置页面(label 用 navT 实时解析;壳层在语言切换时会重新解析 label)
|
|
652
943
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
653
944
|
name: 'settings.section',
|
|
654
945
|
id: 'soul-settings',
|
|
655
946
|
order: 50,
|
|
656
|
-
label: () => '
|
|
947
|
+
label: () => navT('nav.label'),
|
|
657
948
|
locale: NS,
|
|
658
949
|
inject: () => ({
|
|
659
950
|
hooks: {
|
|
@@ -662,7 +953,7 @@ window.__ModuleLoader__.load({
|
|
|
662
953
|
controller
|
|
663
954
|
})
|
|
664
955
|
}, SoulSettings))
|
|
665
|
-
|
|
956
|
+
|
|
666
957
|
// 加载初始配置
|
|
667
958
|
controller.loadConfig()
|
|
668
959
|
}
|
|
@@ -671,4 +962,4 @@ window.__ModuleLoader__.load({
|
|
|
671
962
|
exports.inject = inject
|
|
672
963
|
return module.exports
|
|
673
964
|
},
|
|
674
|
-
})
|
|
965
|
+
})
|