dsh-soul 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/README_EN.md +7 -1
- package/client/index.mjs +295 -148
- package/index.mjs +298 -274
- package/lib/config.mjs +138 -0
- package/package.json +7 -2
- package/scripts/verify-config.mjs +142 -0
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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: '
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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: '
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
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,22 +759,24 @@ window.__ModuleLoader__.load({
|
|
|
614
759
|
}
|
|
615
760
|
}
|
|
616
761
|
}
|
|
617
|
-
|
|
762
|
+
|
|
618
763
|
// 延迟执行初始同步,等待设置页面渲染完成
|
|
619
|
-
|
|
620
|
-
|
|
764
|
+
let timer = setTimeout(sync, 500)
|
|
765
|
+
|
|
621
766
|
// 监听 DOM 变化(只监听子节点添加,不监听所有变化)
|
|
622
767
|
const observer = new MutationObserver(() => {
|
|
623
|
-
//
|
|
768
|
+
// 防抖:每次变化都重置计时器,停止变化 100ms 后执行
|
|
769
|
+
// (修复:此前新计时器未赋回 timer,clearTimeout 永远只清除首个 500ms
|
|
770
|
+
// 计时器,防抖实际失效,每次 DOM 变化都会调度一次 sync)
|
|
624
771
|
clearTimeout(timer)
|
|
625
|
-
setTimeout(sync, 100)
|
|
772
|
+
timer = setTimeout(sync, 100)
|
|
626
773
|
})
|
|
627
|
-
|
|
774
|
+
|
|
628
775
|
observer.observe(document.body, {
|
|
629
776
|
childList: true,
|
|
630
777
|
subtree: true
|
|
631
778
|
})
|
|
632
|
-
|
|
779
|
+
|
|
633
780
|
return () => {
|
|
634
781
|
disposed = true
|
|
635
782
|
clearTimeout(timer)
|
|
@@ -642,16 +789,16 @@ window.__ModuleLoader__.load({
|
|
|
642
789
|
})
|
|
643
790
|
}
|
|
644
791
|
}
|
|
645
|
-
|
|
792
|
+
|
|
646
793
|
// 注册图标替换效果
|
|
647
794
|
ctx.effect(() => registerSettingsNavIcon(), 'dsh-soul: settings navigation icon')
|
|
648
|
-
|
|
649
|
-
//
|
|
795
|
+
|
|
796
|
+
// 注册到设置页面(label 用 navT 实时解析;壳层在语言切换时会重新解析 label)
|
|
650
797
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
651
798
|
name: 'settings.section',
|
|
652
799
|
id: 'soul-settings',
|
|
653
800
|
order: 50,
|
|
654
|
-
label: () => '
|
|
801
|
+
label: () => navT('nav.label'),
|
|
655
802
|
locale: NS,
|
|
656
803
|
inject: () => ({
|
|
657
804
|
hooks: {
|
|
@@ -660,7 +807,7 @@ window.__ModuleLoader__.load({
|
|
|
660
807
|
controller
|
|
661
808
|
})
|
|
662
809
|
}, SoulSettings))
|
|
663
|
-
|
|
810
|
+
|
|
664
811
|
// 加载初始配置
|
|
665
812
|
controller.loadConfig()
|
|
666
813
|
}
|
|
@@ -669,4 +816,4 @@ window.__ModuleLoader__.load({
|
|
|
669
816
|
exports.inject = inject
|
|
670
817
|
return module.exports
|
|
671
818
|
},
|
|
672
|
-
})
|
|
819
|
+
})
|