dsh-soul 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/index.mjs CHANGED
@@ -2,12 +2,17 @@
2
2
  //
3
3
  // 在设置页面添加「个性化」栏目:
4
4
  // - 启用/禁用个性化设置
5
- // - 选择回复风格和语调
6
- // - 输入自定义指令
5
+ // - 「关于你」(昵称/职业/介绍)、回复风格和语调、特质、输出语言、自定义指令
6
+ // - dirty 检测(无改动禁用保存)、保存结果提示(已保存 / 无变化)
7
+ // - 查看当前生效提示词(只读)与字符数
7
8
  //
8
9
  // 纯 JS + React jsx-runtime 手写(不依赖 JSX 构建),开箱即用。
9
10
  // 注意:dsh web 使用 React 17+ 的 jsx-runtime,children 必须放在 props.children
10
11
  // 里;不能把 children 当第三个参数传(第三个参数是 key)。
12
+ //
13
+ // 文案全部走宿主 locale 词典:槽位声明 locale: NS 后,渲染器会把绑定本命名
14
+ // 空间的 t 注入组件(props.t),并在语言切换时重渲染;导航 label 由 apply 内
15
+ // 绑定的 navT 实时解析,壳层在语言切换时会重新解析 label。
11
16
 
12
17
  window.__ModuleLoader__.load({
13
18
  id: 'dsh-soul',
@@ -39,19 +44,18 @@ window.__ModuleLoader__.load({
39
44
  const INITIAL = {
40
45
  enabled: true,
41
46
  nickname: '',
42
- // 关于你:用户职业 / 用户介绍
43
47
  occupation: '',
44
48
  bio: '',
45
- // 回复风格和语调(合并为单一选项)
46
49
  style: 'professional',
47
- // 特质:标题和列表 / 表情符号
48
50
  headingLists: 'default',
49
51
  emoji: 'default',
50
52
  language: 'zh',
51
53
  customInstructions: '',
52
54
  loading: false,
53
55
  saving: false,
54
- error: null
56
+ error: null,
57
+ // 最近一次保存实际变化的字段名数组(来自 POST /api/soul/config 的 changed)
58
+ lastChanged: null
55
59
  }
56
60
 
57
61
  // 第三方插件不能 require('@deepseek-ai/dsh-client-runtime/client')——
@@ -93,16 +97,16 @@ window.__ModuleLoader__.load({
93
97
  async loadConfig() {
94
98
  if (this.disposed) return
95
99
  this.store.update(s => { s.loading = true; s.error = null })
96
-
100
+
97
101
  try {
98
102
  const url = new URL('/api/soul/config', hostBase())
99
103
  const response = await this.fetcher(url)
100
104
  const payload = await response.json().catch(() => ({}))
101
-
105
+
102
106
  if (!response.ok || payload.ok !== true) {
103
107
  throw new Error(payload.error || `HTTP ${response.status}`)
104
108
  }
105
-
109
+
106
110
  this.store.update(s => {
107
111
  s.enabled = payload.config.enabled
108
112
  s.nickname = payload.config.nickname || ''
@@ -127,7 +131,7 @@ window.__ModuleLoader__.load({
127
131
  async saveConfig(config) {
128
132
  if (this.disposed) return
129
133
  this.store.update(s => { s.saving = true; s.error = null })
130
-
134
+
131
135
  try {
132
136
  const url = new URL('/api/soul/config', hostBase())
133
137
  const response = await this.fetcher(url, {
@@ -136,11 +140,11 @@ window.__ModuleLoader__.load({
136
140
  body: JSON.stringify(config)
137
141
  })
138
142
  const payload = await response.json().catch(() => ({}))
139
-
143
+
140
144
  if (!response.ok || payload.ok !== true) {
141
145
  throw new Error(payload.error || `HTTP ${response.status}`)
142
146
  }
143
-
147
+
144
148
  this.store.update(s => {
145
149
  s.enabled = payload.config.enabled
146
150
  s.nickname = payload.config.nickname || ''
@@ -151,6 +155,7 @@ window.__ModuleLoader__.load({
151
155
  s.emoji = payload.config.emoji || 'default'
152
156
  s.language = payload.config.language || 'zh'
153
157
  s.customInstructions = payload.config.customInstructions
158
+ s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
154
159
  s.saving = false
155
160
  })
156
161
  } catch (error) {
@@ -165,16 +170,16 @@ window.__ModuleLoader__.load({
165
170
  async resetConfig() {
166
171
  if (this.disposed) return
167
172
  this.store.update(s => { s.saving = true; s.error = null })
168
-
173
+
169
174
  try {
170
175
  const url = new URL('/api/soul/config/reset', hostBase())
171
176
  const response = await this.fetcher(url, { method: 'POST' })
172
177
  const payload = await response.json().catch(() => ({}))
173
-
178
+
174
179
  if (!response.ok || payload.ok !== true) {
175
180
  throw new Error(payload.error || `HTTP ${response.status}`)
176
181
  }
177
-
182
+
178
183
  this.store.update(s => {
179
184
  s.enabled = payload.config.enabled
180
185
  s.nickname = payload.config.nickname || ''
@@ -185,6 +190,7 @@ window.__ModuleLoader__.load({
185
190
  s.emoji = payload.config.emoji || 'default'
186
191
  s.language = payload.config.language || 'zh'
187
192
  s.customInstructions = payload.config.customInstructions
193
+ s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
188
194
  s.saving = false
189
195
  })
190
196
  } catch (error) {
@@ -196,6 +202,17 @@ window.__ModuleLoader__.load({
196
202
  }
197
203
  }
198
204
 
205
+ // 读取当前生效(已保存)的提示词:{ ok, prompt, enabled }
206
+ async fetchPrompt() {
207
+ const url = new URL('/api/soul/prompt', hostBase())
208
+ const response = await this.fetcher(url)
209
+ const payload = await response.json().catch(() => ({}))
210
+ if (!response.ok || payload.ok !== true) {
211
+ throw new Error(payload.error || `HTTP ${response.status}`)
212
+ }
213
+ return payload
214
+ }
215
+
199
216
  async dispose() {
200
217
  this.disposed = true
201
218
  }
@@ -224,11 +241,16 @@ window.__ModuleLoader__.load({
224
241
  '.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
242
  '.soul-hint:hover .soul-hint-tip{opacity:1;visibility:visible}',
226
243
  '.soul-status{margin-top:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
244
+ '.soul-dirty{color:#d46b08}',
245
+ '.soul-prompt-toggle{margin-top:8px}',
246
+ '.soul-prompt-link{background:none;border:none;padding:0;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer;text-decoration:underline}',
247
+ '.soul-prompt-link:hover{color:var(--dsw-alias-label-primary)}',
248
+ '.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-l1);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
249
  '.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
250
  '.soul-toast-success{background:#f6ffed;border:1px solid #b7eb8f;color:#52c41a}',
229
251
  '@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
252
  ].join('')
231
-
253
+
232
254
  const tagId = 'dsh-soul/styles.css'
233
255
  let styleTagMissing = false
234
256
  if (typeof document !== 'undefined') {
@@ -247,38 +269,140 @@ window.__ModuleLoader__.load({
247
269
  }
248
270
 
249
271
  // -------------------------------------------------------------------------
250
- // 组件
272
+ // 文案词典(键集中英完全对齐;渲染器按槽位 locale 注入 t)
251
273
  // -------------------------------------------------------------------------
252
274
 
253
- // 回复风格和语调(合并为单一选项)
254
- const STYLE_TONE_OPTIONS = [
255
- { value: 'professional', label: '专业严谨' },
256
- { value: 'casual', label: '轻松自然' },
257
- { value: 'humorous', label: '幽默风趣' },
258
- { value: 'roast', label: '吐槽达人' },
259
- { value: 'efficient', label: '高效干练' }
260
- ]
275
+ const zh = {
276
+ 'nav.label': '个性化',
277
+ 'settings.title': '个性化设置',
278
+ 'toggle.enabled': '启用个性化设置',
279
+ 'group.aboutYou': '关于你',
280
+ 'field.nickname': '用户昵称',
281
+ 'field.nicknamePlaceholder': '输入你的昵称,回复时会称呼你',
282
+ 'field.occupation': '用户职业',
283
+ 'field.occupationPlaceholder': '例如:软件工程师、学生、产品经理',
284
+ 'field.bio': '用户介绍',
285
+ 'field.bioPlaceholder': '简单介绍自己,让回复更贴合你的背景',
286
+ 'group.traits': '特质',
287
+ 'field.style': '回复风格和语调',
288
+ 'hint.style': '设置 Agent 回复你的风格和语调。这不会影响 Agent 的功能。',
289
+ 'field.headingLists': '标题和列表',
290
+ 'hint.headingLists': '在回复风格和语调的基础上选择额外的自定义特质项。控制回答中标题和列表的使用程度。',
291
+ 'field.emoji': '表情符号',
292
+ 'hint.emoji': '在回复风格和语调的基础上选择额外的自定义特质项。控制表情符号的使用程度。',
293
+ 'field.language': '输出语言',
294
+ 'hint.language': '设置 Agent 回复你使用的语言,同时影响 /soul 命令的输出语言。',
295
+ 'field.instructions': '自定义指令',
296
+ 'hint.instructions': '补充角色、习惯等个性化要求。建议不要重复设置回复风格和语调类似的话术,以免与上方选项冲突。',
297
+ 'field.instructionsPlaceholder': '输入自定义指令,可以是其他行为、回复风格和语调等偏好设置',
298
+ 'style.professional': '专业严谨',
299
+ 'style.casual': '轻松自然',
300
+ 'style.humorous': '幽默风趣',
301
+ 'style.roast': '吐槽达人',
302
+ 'style.efficient': '高效干练',
303
+ 'trait.headingLists.default': '默认',
304
+ 'trait.headingLists.more': '增强(采用清晰格式和列表结构)',
305
+ 'trait.headingLists.less': '减弱(使用更多段落文本)',
306
+ 'trait.emoji.default': '默认',
307
+ 'trait.emoji.more': '增强(使用较多表情符号)',
308
+ 'trait.emoji.less': '减弱(尽量减少使用表情符号)',
309
+ 'button.save': '保存设置',
310
+ 'button.saving': '保存中...',
311
+ 'button.reset': '重置默认',
312
+ 'status.loading': '加载中...',
313
+ 'status.unsaved': '有未保存的更改',
314
+ 'toast.saved': '✅ 设置已保存',
315
+ 'toast.noChanges': '✅ 配置无变化',
316
+ 'toast.reset': '✅ 已重置为默认值',
317
+ 'prompt.view': '查看当前生效提示词',
318
+ 'prompt.hide': '收起提示词',
319
+ 'prompt.loading': '提示词加载中...',
320
+ 'prompt.empty': '(个性化已禁用,提示词为空)',
321
+ 'prompt.chars': '当前字符数:{n}'
322
+ }
261
323
 
262
- // 特质:标题和列表
263
- const HEADING_LISTS_OPTIONS = [
264
- { value: 'default', label: '默认' },
265
- { value: 'more', label: '增强(采用清晰格式和列表结构)' },
266
- { value: 'less', label: '减弱(使用更多段落文本)' }
267
- ]
324
+ const en = {
325
+ 'nav.label': 'Personalization',
326
+ 'settings.title': 'Personalization Settings',
327
+ 'toggle.enabled': 'Enable personalization',
328
+ 'group.aboutYou': 'About you',
329
+ 'field.nickname': 'Nickname',
330
+ 'field.nicknamePlaceholder': 'Your nickname — the agent will address you by it',
331
+ 'field.occupation': 'Occupation',
332
+ 'field.occupationPlaceholder': 'e.g. software engineer, student, product manager',
333
+ 'field.bio': 'Bio',
334
+ 'field.bioPlaceholder': 'A short introduction so replies fit your background',
335
+ 'group.traits': 'Traits',
336
+ 'field.style': 'Reply style & tone',
337
+ 'hint.style': "Sets the style and tone the agent uses when replying to you. It does not affect the agent's capabilities.",
338
+ 'field.headingLists': 'Headings & lists',
339
+ 'hint.headingLists': 'An extra trait layered on top of style & tone. Controls how much answers rely on headings and lists.',
340
+ 'field.emoji': 'Emoji',
341
+ 'hint.emoji': 'An extra trait layered on top of style & tone. Controls how many emojis answers use.',
342
+ 'field.language': 'Output language',
343
+ 'hint.language': 'Sets the language the agent replies in; also affects the /soul command output.',
344
+ 'field.instructions': 'Custom instructions',
345
+ 'hint.instructions': 'Extra persona or habit requirements. Avoid repeating wording similar to the style & tone option above to prevent conflicts.',
346
+ 'field.instructionsPlaceholder': 'Custom instructions — other behaviors, style or tone preferences',
347
+ 'style.professional': 'Professional',
348
+ 'style.casual': 'Casual',
349
+ 'style.humorous': 'Humorous',
350
+ 'style.roast': 'Roast Master',
351
+ 'style.efficient': 'Efficient',
352
+ 'trait.headingLists.default': 'Default',
353
+ 'trait.headingLists.more': 'More (clear formatting with headings and lists)',
354
+ 'trait.headingLists.less': 'Less (more paragraph text)',
355
+ 'trait.emoji.default': 'Default',
356
+ 'trait.emoji.more': 'More (frequent emoji usage)',
357
+ 'trait.emoji.less': 'Less (minimal emoji usage)',
358
+ 'button.save': 'Save Settings',
359
+ 'button.saving': 'Saving...',
360
+ 'button.reset': 'Reset to Default',
361
+ 'status.loading': 'Loading...',
362
+ 'status.unsaved': 'You have unsaved changes',
363
+ 'toast.saved': '✅ Settings saved',
364
+ 'toast.noChanges': '✅ No changes to save',
365
+ 'toast.reset': '✅ Reset to defaults',
366
+ 'prompt.view': 'View the active system prompt',
367
+ 'prompt.hide': 'Hide the prompt',
368
+ 'prompt.loading': 'Loading prompt...',
369
+ 'prompt.empty': '(personalization disabled — the prompt is empty)',
370
+ 'prompt.chars': 'Current character count: {n}'
371
+ }
268
372
 
269
- // 特质:表情符号
270
- const EMOJI_OPTIONS = [
271
- { value: 'default', label: '默认' },
272
- { value: 'more', label: '增强(使用较多表情符号)' },
273
- { value: 'less', label: '减弱(尽量减少使用表情符号)' }
274
- ]
373
+ // 图标替换匹配的导航文案(词典中 nav.label 的全部语言取值)
374
+ const NAV_LABELS = new Set([zh['nav.label'], en['nav.label']])
275
375
 
276
- // 选项用原生语言显示,两种 UI 语言下均自解释
376
+ // 组件内翻译回退:渲染器未注入 t 时使用中文词典(保证组件可独立渲染/测试)
377
+ function makeFallbackT(dict) {
378
+ return (key, params) => {
379
+ let text = dict[key] ?? key
380
+ if (params) {
381
+ for (const [name, value] of Object.entries(params)) {
382
+ text = text.split(`{${name}}`).join(String(value))
383
+ }
384
+ }
385
+ return text
386
+ }
387
+ }
388
+ const FALLBACK_T = makeFallbackT(zh)
389
+
390
+ // -------------------------------------------------------------------------
391
+ // 组件
392
+ // -------------------------------------------------------------------------
393
+
394
+ // 选项取值(label 走词典;取值与 lib/config.mjs 的合法值保持一致)
395
+ const STYLE_VALUES = ['professional', 'casual', 'humorous', 'roast', 'efficient']
396
+ const TRAIT_VALUES = ['default', 'more', 'less']
397
+ // 输出语言选项(两种 UI 语言下均自解释,不进词典)
277
398
  const LANGUAGE_OPTIONS = [
278
399
  { value: 'zh', label: '中文' },
279
400
  { value: 'en', label: 'English' }
280
401
  ]
281
402
 
403
+ // 表单字段与 store 字段的一一对应(dirty 检测与保存载荷共用)
404
+ const FIELD_KEYS = ['enabled', 'nickname', 'occupation', 'bio', 'style', 'headingLists', 'emoji', 'language', 'customInstructions']
405
+
282
406
  // 提示词小图标:hover 展示说明文字
283
407
  function SoulHint(props) {
284
408
  return e('span', { className: 'soul-hint', 'aria-label': props.text },
@@ -303,9 +427,11 @@ window.__ModuleLoader__.load({
303
427
 
304
428
  function SoulSettings(props) {
305
429
  const { useSoulController, controller } = props
430
+ // 渲染器按槽位 locale 命名空间注入 t(随语言切换更新);缺失时回退中文
431
+ const t = typeof props.t === 'function' ? props.t : FALLBACK_T
306
432
  const state = useSoulController((state) => state)
307
- const { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, loading, saving, error } = state
308
-
433
+ const { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, loading, saving, error, lastChanged } = state
434
+
309
435
  const [localEnabled, setLocalEnabled] = React.useState(enabled)
310
436
  const [localNickname, setLocalNickname] = React.useState(nickname || '')
311
437
  const [localOccupation, setLocalOccupation] = React.useState(occupation || '')
@@ -317,7 +443,11 @@ window.__ModuleLoader__.load({
317
443
  const [localInstructions, setLocalInstructions] = React.useState(customInstructions)
318
444
  const [showSuccess, setShowSuccess] = React.useState(false)
319
445
  const [showResetSuccess, setShowResetSuccess] = React.useState(false)
320
-
446
+ const [showPrompt, setShowPrompt] = React.useState(false)
447
+ const [promptText, setPromptText] = React.useState('')
448
+ const [promptLoading, setPromptLoading] = React.useState(false)
449
+ const [promptError, setPromptError] = React.useState(null)
450
+
321
451
  // 同步状态
322
452
  React.useEffect(() => {
323
453
  setLocalEnabled(enabled)
@@ -330,7 +460,7 @@ window.__ModuleLoader__.load({
330
460
  setLocalLanguage(language || 'zh')
331
461
  setLocalInstructions(customInstructions)
332
462
  }, [enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions])
333
-
463
+
334
464
  // 保存成功后显示提示,2秒后自动消失
335
465
  React.useEffect(() => {
336
466
  if (showSuccess) {
@@ -338,7 +468,7 @@ window.__ModuleLoader__.load({
338
468
  return () => clearTimeout(timer)
339
469
  }
340
470
  }, [showSuccess])
341
-
471
+
342
472
  // 重置成功后显示提示,2秒后自动消失
343
473
  React.useEffect(() => {
344
474
  if (showResetSuccess) {
@@ -346,31 +476,50 @@ window.__ModuleLoader__.load({
346
476
  return () => clearTimeout(timer)
347
477
  }
348
478
  }, [showResetSuccess])
349
-
479
+
480
+ // 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 }
483
+ const dirty = FIELD_KEYS.some((key) => savedMap[key] !== localMap[key])
484
+
485
+ const loadPrompt = async () => {
486
+ setPromptLoading(true)
487
+ setPromptError(null)
488
+ try {
489
+ const payload = await controller.fetchPrompt()
490
+ setPromptText(payload.prompt || '')
491
+ } catch (err) {
492
+ setPromptError(messageOf(err))
493
+ } finally {
494
+ setPromptLoading(false)
495
+ }
496
+ }
497
+
498
+ const togglePrompt = async () => {
499
+ const next = !showPrompt
500
+ setShowPrompt(next)
501
+ if (next) await loadPrompt()
502
+ }
503
+
350
504
  const handleSave = async () => {
351
- const controller = props.controller
352
- await controller.saveConfig({
353
- enabled: localEnabled,
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
- })
505
+ await controller.saveConfig(localMap)
363
506
  setShowSuccess(true)
507
+ if (showPrompt) await loadPrompt()
364
508
  }
365
-
509
+
366
510
  const handleReset = async () => {
367
- const controller = props.controller
368
511
  await controller.resetConfig()
369
512
  setShowResetSuccess(true)
513
+ if (showPrompt) await loadPrompt()
370
514
  }
371
-
515
+
516
+ const styleOptions = STYLE_VALUES.map((value) => ({ value, label: t(`style.${value}`) }))
517
+ const headingListsOptions = TRAIT_VALUES.map((value) => ({ value, label: t(`trait.headingLists.${value}`) }))
518
+ const emojiOptions = TRAIT_VALUES.map((value) => ({ value, label: t(`trait.emoji.${value}`) }))
519
+ const savedToastText = Array.isArray(lastChanged) && lastChanged.length > 0 ? t('toast.saved') : t('toast.noChanges')
520
+
372
521
  return e('div', { className: 'soul-section' },
373
- e('h3', null, '个性化设置'),
522
+ e('h3', null, t('settings.title')),
374
523
  e('div', { className: 'soul-toggle' },
375
524
  e('input', {
376
525
  type: 'checkbox',
@@ -378,54 +527,54 @@ window.__ModuleLoader__.load({
378
527
  checked: localEnabled,
379
528
  onChange: (ev) => setLocalEnabled(ev.target.checked)
380
529
  }),
381
- e('label', { htmlFor: 'soul-enabled' }, '启用个性化设置')
530
+ e('label', { htmlFor: 'soul-enabled' }, t('toggle.enabled'))
382
531
  ),
383
-
532
+
384
533
  localEnabled && e(Fragment, null,
385
- e('div', { className: 'soul-group-title' }, '关于你'),
534
+ e('div', { className: 'soul-group-title' }, t('group.aboutYou')),
386
535
  e('div', { className: 'soul-field' },
387
- e('label', { htmlFor: 'soul-nickname' }, '用户昵称'),
536
+ e('label', { htmlFor: 'soul-nickname' }, t('field.nickname')),
388
537
  e('input', {
389
538
  type: 'text',
390
539
  id: 'soul-nickname',
391
540
  value: localNickname,
392
541
  onChange: (ev) => setLocalNickname(ev.target.value),
393
- placeholder: '输入你的昵称,回复时会称呼你'
542
+ placeholder: t('field.nicknamePlaceholder')
394
543
  })
395
544
  ),
396
545
  e('div', { className: 'soul-field' },
397
- e('label', { htmlFor: 'soul-occupation' }, '用户职业'),
546
+ e('label', { htmlFor: 'soul-occupation' }, t('field.occupation')),
398
547
  e('input', {
399
548
  type: 'text',
400
549
  id: 'soul-occupation',
401
550
  value: localOccupation,
402
551
  onChange: (ev) => setLocalOccupation(ev.target.value),
403
- placeholder: '例如:软件工程师、学生、产品经理'
552
+ placeholder: t('field.occupationPlaceholder')
404
553
  })
405
554
  ),
406
555
  e('div', { className: 'soul-field' },
407
- e('label', { htmlFor: 'soul-bio' }, '用户介绍'),
556
+ e('label', { htmlFor: 'soul-bio' }, t('field.bio')),
408
557
  e('textarea', {
409
558
  id: 'soul-bio',
410
559
  className: 'soul-textarea-sm',
411
560
  value: localBio,
412
561
  onChange: (ev) => setLocalBio(ev.target.value),
413
- placeholder: '简单介绍自己,让回复更贴合你的背景'
562
+ placeholder: t('field.bioPlaceholder')
414
563
  })
415
564
  ),
416
565
 
417
- e('div', { className: 'soul-group-title' }, '特质'),
566
+ e('div', { className: 'soul-group-title' }, t('group.traits')),
418
567
  e('div', { className: 'soul-field' },
419
568
  e('label', { htmlFor: 'soul-style' },
420
- '回复风格和语调',
421
- e(SoulHint, { text: '设置 Agent 回复你的风格和语调。这不会影响 Agent 的功能。' })
569
+ t('field.style'),
570
+ e(SoulHint, { text: t('hint.style') })
422
571
  ),
423
572
  e('select', {
424
573
  id: 'soul-style',
425
574
  value: localStyle,
426
575
  onChange: (ev) => setLocalStyle(ev.target.value)
427
576
  },
428
- ...STYLE_TONE_OPTIONS.map(opt =>
577
+ ...styleOptions.map(opt =>
429
578
  e('option', { key: opt.value, value: opt.value }, opt.label)
430
579
  )
431
580
  )
@@ -433,15 +582,15 @@ window.__ModuleLoader__.load({
433
582
 
434
583
  e('div', { className: 'soul-field' },
435
584
  e('label', { htmlFor: 'soul-headingLists' },
436
- '标题和列表',
437
- e(SoulHint, { text: '在回复风格和语调的基础上选择额外的自定义特质项。控制回答中标题和列表的使用程度。' })
585
+ t('field.headingLists'),
586
+ e(SoulHint, { text: t('hint.headingLists') })
438
587
  ),
439
588
  e('select', {
440
589
  id: 'soul-headingLists',
441
590
  value: localHeadingLists,
442
591
  onChange: (ev) => setLocalHeadingLists(ev.target.value)
443
592
  },
444
- ...HEADING_LISTS_OPTIONS.map(opt =>
593
+ ...headingListsOptions.map(opt =>
445
594
  e('option', { key: opt.value, value: opt.value }, opt.label)
446
595
  )
447
596
  )
@@ -449,15 +598,15 @@ window.__ModuleLoader__.load({
449
598
 
450
599
  e('div', { className: 'soul-field' },
451
600
  e('label', { htmlFor: 'soul-emoji' },
452
- '表情符号',
453
- e(SoulHint, { text: '在回复风格和语调的基础上选择额外的自定义特质项。控制表情符号的使用程度。' })
601
+ t('field.emoji'),
602
+ e(SoulHint, { text: t('hint.emoji') })
454
603
  ),
455
604
  e('select', {
456
605
  id: 'soul-emoji',
457
606
  value: localEmoji,
458
607
  onChange: (ev) => setLocalEmoji(ev.target.value)
459
608
  },
460
- ...EMOJI_OPTIONS.map(opt =>
609
+ ...emojiOptions.map(opt =>
461
610
  e('option', { key: opt.value, value: opt.value }, opt.label)
462
611
  )
463
612
  )
@@ -465,8 +614,8 @@ window.__ModuleLoader__.load({
465
614
 
466
615
  e('div', { className: 'soul-field' },
467
616
  e('label', { htmlFor: 'soul-language' },
468
- '输出语言',
469
- e(SoulHint, { text: '设置 Agent 回复你使用的语言,同时影响 /soul 命令的输出语言。' })
617
+ t('field.language'),
618
+ e(SoulHint, { text: t('hint.language') })
470
619
  ),
471
620
  e('select', {
472
621
  id: 'soul-language',
@@ -481,78 +630,70 @@ window.__ModuleLoader__.load({
481
630
 
482
631
  e('div', { className: 'soul-field' },
483
632
  e('label', { htmlFor: 'soul-instructions' },
484
- '自定义指令',
485
- e(SoulHint, { text: '补充角色、习惯等个性化要求。建议不要重复设置回复风格和语调类似的话术,以免与上方选项冲突。' })
633
+ t('field.instructions'),
634
+ e(SoulHint, { text: t('hint.instructions') })
486
635
  ),
487
636
  e('textarea', {
488
637
  id: 'soul-instructions',
489
638
  value: localInstructions,
490
639
  onChange: (ev) => setLocalInstructions(ev.target.value),
491
- placeholder: '输入自定义指令,可以是其他行为、回复风格和语调等偏好设置'
640
+ placeholder: t('field.instructionsPlaceholder')
492
641
  })
493
642
  )
494
643
  ),
495
-
644
+
496
645
  error && e('div', { className: 'soul-error' }, error),
497
-
646
+
498
647
  e('div', { className: 'soul-buttons' },
499
648
  e(ui.Button, {
500
649
  variant: 'primary',
501
650
  onClick: handleSave,
502
- disabled: saving
503
- }, saving ? '保存中...' : '保存设置'),
651
+ disabled: saving || !dirty
652
+ }, saving ? t('button.saving') : t('button.save')),
504
653
  e(ui.Button, {
505
654
  onClick: handleReset,
506
655
  disabled: saving
507
- }, '重置默认')
656
+ }, t('button.reset'))
657
+ ),
658
+
659
+ dirty && !saving && e('div', { className: 'soul-status soul-dirty' }, t('status.unsaved')),
660
+
661
+ e('div', { className: 'soul-prompt-toggle' },
662
+ e('button', {
663
+ type: 'button',
664
+ className: 'soul-prompt-link',
665
+ onClick: togglePrompt
666
+ }, showPrompt ? t('prompt.hide') : t('prompt.view'))
508
667
  ),
509
-
510
- loading && e('div', { className: 'soul-status' }, '加载中...'),
511
-
512
- showSuccess && e('div', {
668
+ showPrompt && e('div', { className: 'soul-prompt-panel' },
669
+ promptLoading
670
+ ? e('div', { className: 'soul-status' }, t('prompt.loading'))
671
+ : (promptError
672
+ ? e('div', { className: 'soul-error' }, promptError)
673
+ : e(Fragment, null,
674
+ e('pre', { className: 'soul-prompt-pre' }, promptText || t('prompt.empty')),
675
+ e('div', { className: 'soul-status' }, t('prompt.chars', { n: String(promptText.length) }))
676
+ ))
677
+ ),
678
+
679
+ loading && e('div', { className: 'soul-status' }, t('status.loading')),
680
+
681
+ showSuccess && e('div', {
513
682
  className: 'soul-toast soul-toast-success',
514
683
  style: {
515
684
  animation: 'fadeInOut 2s ease-in-out'
516
685
  }
517
- }, '✅ 设置已保存'),
518
-
519
- showResetSuccess && e('div', {
686
+ }, savedToastText),
687
+
688
+ showResetSuccess && e('div', {
520
689
  className: 'soul-toast soul-toast-success',
521
690
  style: {
522
691
  animation: 'fadeInOut 2s ease-in-out'
523
692
  }
524
- }, '✅ 已重置为默认值')
693
+ }, t('toast.reset'))
525
694
  )
526
695
  }
527
696
 
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
697
  // -------------------------------------------------------------------------
557
698
  // 插件装配
558
699
  // -------------------------------------------------------------------------
@@ -566,32 +707,36 @@ window.__ModuleLoader__.load({
566
707
  controller.dispose()
567
708
  }, 'dsh-soul: browser lifecycle')
568
709
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-soul: dictionaries')
569
-
710
+
711
+ // 导航 label / 图标替换用的翻译函数(bind 返回的函数在调用时读取当前语言)
712
+ const navT = ctx.locale.bind(NS)
713
+
570
714
  // DSH 0.1.x 不支持 settings.section 的 icon 字段
571
715
  // 使用 DOM 操作动态替换图标(类似 dsh-better-sidebar 的方案)
572
716
  const SOUL_NAV_MARKER = 'data-soul-settings-nav'
573
717
  const SOUL_ICON_MARKER = 'data-soul-icon-replaced'
574
-
718
+
575
719
  const registerSettingsNavIcon = () => {
576
720
  let disposed = false
577
-
721
+
578
722
  const sync = () => {
579
723
  if (disposed) return
580
-
724
+
581
725
  const buttons = document.querySelectorAll('[role="dialog"] nav button')
582
726
  for (const button of buttons) {
583
- if (button.textContent?.trim() === '个性化') {
727
+ // 匹配本栏目导航按钮:label 由 locale 词典渲染,双语均需识别
728
+ if (NAV_LABELS.has(button.textContent?.trim())) {
584
729
  button.setAttribute(SOUL_NAV_MARKER, '')
585
-
730
+
586
731
  // 检查是否已经替换过图标
587
732
  if (button.hasAttribute(SOUL_ICON_MARKER)) continue
588
-
733
+
589
734
  // 查找并替换图标
590
735
  const existingIcon = button.querySelector('svg')
591
736
  if (existingIcon) {
592
737
  // 标记已替换
593
738
  button.setAttribute(SOUL_ICON_MARKER, 'true')
594
-
739
+
595
740
  // 创建星形图标(跟随主题色)
596
741
  const starSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
597
742
  starSvg.setAttribute('viewBox', '0 0 24 24')
@@ -602,11 +747,11 @@ window.__ModuleLoader__.load({
602
747
  starSvg.setAttribute('stroke-linejoin', 'round')
603
748
  starSvg.style.width = '16px'
604
749
  starSvg.style.height = '16px'
605
-
750
+
606
751
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
607
752
  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
753
  starSvg.appendChild(path)
609
-
754
+
610
755
  existingIcon.replaceWith(starSvg)
611
756
  }
612
757
  } else {
@@ -614,10 +759,10 @@ window.__ModuleLoader__.load({
614
759
  }
615
760
  }
616
761
  }
617
-
762
+
618
763
  // 延迟执行初始同步,等待设置页面渲染完成
619
764
  let timer = setTimeout(sync, 500)
620
-
765
+
621
766
  // 监听 DOM 变化(只监听子节点添加,不监听所有变化)
622
767
  const observer = new MutationObserver(() => {
623
768
  // 防抖:每次变化都重置计时器,停止变化 100ms 后执行
@@ -626,12 +771,12 @@ window.__ModuleLoader__.load({
626
771
  clearTimeout(timer)
627
772
  timer = setTimeout(sync, 100)
628
773
  })
629
-
774
+
630
775
  observer.observe(document.body, {
631
776
  childList: true,
632
777
  subtree: true
633
778
  })
634
-
779
+
635
780
  return () => {
636
781
  disposed = true
637
782
  clearTimeout(timer)
@@ -644,16 +789,16 @@ window.__ModuleLoader__.load({
644
789
  })
645
790
  }
646
791
  }
647
-
792
+
648
793
  // 注册图标替换效果
649
794
  ctx.effect(() => registerSettingsNavIcon(), 'dsh-soul: settings navigation icon')
650
-
651
- // 注册到设置页面
795
+
796
+ // 注册到设置页面(label 用 navT 实时解析;壳层在语言切换时会重新解析 label)
652
797
  ctx.slots.inject('settings.section', () => ctx.slots.register({
653
798
  name: 'settings.section',
654
799
  id: 'soul-settings',
655
800
  order: 50,
656
- label: () => '个性化',
801
+ label: () => navT('nav.label'),
657
802
  locale: NS,
658
803
  inject: () => ({
659
804
  hooks: {
@@ -662,7 +807,7 @@ window.__ModuleLoader__.load({
662
807
  controller
663
808
  })
664
809
  }, SoulSettings))
665
-
810
+
666
811
  // 加载初始配置
667
812
  controller.loadConfig()
668
813
  }
@@ -671,4 +816,4 @@ window.__ModuleLoader__.load({
671
816
  exports.inject = inject
672
817
  return module.exports
673
818
  },
674
- })
819
+ })