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/index.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// index.mjs — dsh-soul host 入口
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// -
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
3
|
+
// 提示词结构:
|
|
4
|
+
// - 身份(自定义指令 → [角色设定])
|
|
5
|
+
// - 用户背景(「关于你」→ [用户背景])
|
|
6
|
+
// - 行为(风格/特质/输出语言 → [回复风格])
|
|
7
|
+
// - 执行规则([执行规则])
|
|
8
|
+
// 全部固定文案按 config.language 提供中英两套(PROMPT_TEXT 文案表)。
|
|
7
9
|
//
|
|
8
10
|
// 功能:
|
|
9
11
|
// - 支持自定义 Agent 回复风格和语调
|
|
@@ -12,11 +14,20 @@
|
|
|
12
14
|
// - 提供 HTTP API 供客户端调用
|
|
13
15
|
// - 注册斜杠命令 /soul 查看当前配置
|
|
14
16
|
// - 注入系统提示词到 Agent
|
|
17
|
+
// - 配置写入统一走写队列,返回 changed 供调用方跳过无变化刷新
|
|
15
18
|
|
|
16
19
|
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
17
20
|
import { join } from 'node:path'
|
|
18
21
|
import { homedir } from 'node:os'
|
|
19
22
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_CONFIG,
|
|
25
|
+
STYLE_VALUES,
|
|
26
|
+
TRAIT_VALUES,
|
|
27
|
+
LANGUAGE_VALUES,
|
|
28
|
+
migrateConfig,
|
|
29
|
+
sanitizeConfig
|
|
30
|
+
} from './lib/config.mjs'
|
|
20
31
|
|
|
21
32
|
const name = 'soul'
|
|
22
33
|
|
|
@@ -25,189 +36,134 @@ function configPath() {
|
|
|
25
36
|
return join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'soul-config.json')
|
|
26
37
|
}
|
|
27
38
|
|
|
28
|
-
// ====================
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
// ==================== 提示词文案与构建器 ====================
|
|
40
|
+
|
|
41
|
+
// 提示词固定文案表:所有进入 system prompt / 会话注入 / 工具返回的文本,
|
|
42
|
+
// 按输出语言(config.language)提供中英两套,键集完全对齐。
|
|
43
|
+
const PROMPT_TEXT = {
|
|
44
|
+
zh: {
|
|
45
|
+
roleHeader: '[角色设定]',
|
|
46
|
+
profileHeader: '[用户背景]',
|
|
47
|
+
profileDisclaimer: '以下信息描述的是与你对话的用户本人,不是你的身份;不要把用户的职业、技术背景或自述当成你自己的设定。',
|
|
48
|
+
profileNickname: (n) => `- 昵称:${n}(回复时使用这个称呼)`,
|
|
49
|
+
profileOccupation: (o) => `- 职业:${o}`,
|
|
50
|
+
profileBio: (b) => `- 用户自述:${b}`,
|
|
51
|
+
styleHeader: '[回复风格]',
|
|
52
|
+
styleLabel: '回复风格和语调:',
|
|
53
|
+
styles: {
|
|
54
|
+
professional: '专业严谨、注重逻辑和准确性、正式礼貌',
|
|
55
|
+
casual: '轻松自然、像朋友一样交流、中性客观',
|
|
56
|
+
humorous: '幽默风趣、适当使用比喻和玩笑、口语化',
|
|
57
|
+
roast: '吐槽达人、犀利幽默、爱调侃但无恶意、口语化',
|
|
58
|
+
efficient: '高效干练、直击要点、简洁明了、不绕弯子'
|
|
59
|
+
},
|
|
60
|
+
headingLists: {
|
|
61
|
+
more: '采用清晰的格式和列表结构组织回答,善用标题和列表',
|
|
62
|
+
less: '使用更多段落文本,减少标题和列表的使用'
|
|
63
|
+
},
|
|
64
|
+
emoji: {
|
|
65
|
+
more: '在回复中使用较多表情符号',
|
|
66
|
+
less: '尽量减少使用表情符号'
|
|
67
|
+
},
|
|
68
|
+
replyLanguage: {
|
|
69
|
+
zh: '必须使用简体中文回复',
|
|
70
|
+
en: '必须使用英语(English)回复'
|
|
71
|
+
},
|
|
72
|
+
joiner: ',',
|
|
73
|
+
rulesHeader: '[执行规则]',
|
|
74
|
+
rules: '你必须在每一条回复中严格遵守以上角色设定和回复风格;涉及用户背景时,用它来理解用户、贴合用户的需求和水平作答,而不是把它当作你自己的身份。包括但不限于:代码解释、技术问答、日常闲聊、知识科普等所有场景。违反此规则视为失败。',
|
|
75
|
+
injectUpdatedHeader: '[dsh-soul 个性化配置已更新]',
|
|
76
|
+
injectUpdatedBody: [
|
|
77
|
+
'以下是当前最新的个性化配置快照(含昵称、回复风格和语调、特质、输出语言等)。',
|
|
78
|
+
'从现在开始必须按照此配置回复,不要继续使用旧的昵称、角色、风格、语调、特质或输出语言。'
|
|
79
|
+
].join('\n'),
|
|
80
|
+
injectDisabled: [
|
|
81
|
+
'[dsh-soul 个性化配置已关闭]',
|
|
82
|
+
'从现在开始不要使用 dsh-soul 之前注入的昵称、角色、回复风格、语调、特质或输出语言配置。',
|
|
83
|
+
'请恢复使用 Agent 的默认行为。'
|
|
84
|
+
].join('\n'),
|
|
85
|
+
toolUpdated: '已更新个性化设置',
|
|
86
|
+
toolNoChanges: '没有要更改的设置',
|
|
87
|
+
toolValidationFailed: '配置校验失败:'
|
|
39
88
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
89
|
+
en: {
|
|
90
|
+
roleHeader: '[Role]',
|
|
91
|
+
profileHeader: '[User profile]',
|
|
92
|
+
profileDisclaimer: 'The information below describes the user you are talking to — it is NOT your own identity; do not treat the user\'s occupation, background or self-description as your persona.',
|
|
93
|
+
profileNickname: (n) => `- Nickname: ${n} (address the user by this name in replies)`,
|
|
94
|
+
profileOccupation: (o) => `- Occupation: ${o}`,
|
|
95
|
+
profileBio: (b) => `- Self-description: ${b}`,
|
|
96
|
+
styleHeader: '[Reply style]',
|
|
97
|
+
styleLabel: 'Reply style & tone: ',
|
|
98
|
+
styles: {
|
|
99
|
+
professional: 'Rigorous and professional, focused on logic and accuracy, formal and courteous',
|
|
100
|
+
casual: 'Relaxed and natural, chatting like a friend, neutral and objective',
|
|
101
|
+
humorous: 'Humorous, apt with analogies and jokes, colloquial',
|
|
102
|
+
roast: 'Playful roaster, sharp and witty but never mean, colloquial',
|
|
103
|
+
efficient: 'Efficient and straight to the point, concise, no beating around the bush'
|
|
104
|
+
},
|
|
105
|
+
headingLists: {
|
|
106
|
+
more: 'Organize answers with clear formatting, headings and lists',
|
|
107
|
+
less: 'Use more paragraph text; minimize headings and lists'
|
|
108
|
+
},
|
|
109
|
+
emoji: {
|
|
110
|
+
more: 'Use emojis fairly often in replies',
|
|
111
|
+
less: 'Keep emoji usage to a minimum'
|
|
112
|
+
},
|
|
113
|
+
replyLanguage: {
|
|
114
|
+
zh: '必须使用简体中文回复',
|
|
115
|
+
en: 'You must reply in English.'
|
|
116
|
+
},
|
|
117
|
+
joiner: '; ',
|
|
118
|
+
rulesHeader: '[Execution rules]',
|
|
119
|
+
rules: 'You must strictly follow the role setup and reply style above in EVERY reply; when user background is involved, use it to understand the user and tailor your answers to their needs and level — never treat it as your own identity. This covers all scenarios including code explanation, technical Q&A, casual chat and knowledge sharing. Violating this rule counts as failure.',
|
|
120
|
+
injectUpdatedHeader: '[dsh-soul] Personalization config updated',
|
|
121
|
+
injectUpdatedBody: [
|
|
122
|
+
'Below is the latest personalization snapshot (nickname, reply style & tone, traits, output language, etc.).',
|
|
123
|
+
'From now on you must reply according to this config; do not keep using the previous nickname, role, style, tone, traits or output language.'
|
|
124
|
+
].join('\n'),
|
|
125
|
+
injectDisabled: [
|
|
126
|
+
'[dsh-soul] Personalization config disabled',
|
|
127
|
+
'Stop using any nickname, role, reply style, tone, traits or output language previously injected by dsh-soul.',
|
|
128
|
+
'Return to your default behavior.'
|
|
129
|
+
].join('\n'),
|
|
130
|
+
toolUpdated: 'Personalization settings updated',
|
|
131
|
+
toolNoChanges: 'No settings to change',
|
|
132
|
+
toolValidationFailed: 'Config validation failed: '
|
|
51
133
|
}
|
|
52
134
|
}
|
|
53
135
|
|
|
54
|
-
//
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
if (config.nickname) {
|
|
58
|
-
lines.push(`- 昵称:${config.nickname}(回复时使用这个称呼)`)
|
|
59
|
-
}
|
|
60
|
-
if (config.occupation) {
|
|
61
|
-
lines.push(`- 职业:${config.occupation}`)
|
|
62
|
-
}
|
|
63
|
-
if (config.bio) {
|
|
64
|
-
lines.push(`- 用户自述:${config.bio}`)
|
|
65
|
-
}
|
|
66
|
-
return lines.join('\n')
|
|
136
|
+
// 按配置选择文案表(未知语言回退中文)
|
|
137
|
+
function promptTextOf(config) {
|
|
138
|
+
return PROMPT_TEXT[config && config.language === 'en' ? 'en' : 'zh']
|
|
67
139
|
}
|
|
68
140
|
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
roast: '吐槽达人、犀利幽默、爱调侃但无恶意、口语化',
|
|
77
|
-
efficient: '高效干练、直击要点、简洁明了、不绕弯子'
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
// 行为规则
|
|
81
|
-
rules: [
|
|
82
|
-
'在回答任何问题时,都必须体现上述身份特征',
|
|
83
|
-
'禁止使用标准AI助手的中性语气',
|
|
84
|
-
'保持角色一致性,不要跳出设定'
|
|
85
|
-
],
|
|
86
|
-
|
|
87
|
-
// 回复语言指令(同时决定 /soul 命令输出语言)
|
|
88
|
-
languages: {
|
|
89
|
-
zh: '必须使用简体中文回复',
|
|
90
|
-
en: '必须使用英语(English)回复'
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
// 特质:标题和列表的使用程度
|
|
94
|
-
headingLists: {
|
|
95
|
-
more: '采用清晰的格式和列表结构组织回答,善用标题和列表',
|
|
96
|
-
less: '使用更多段落文本,减少标题和列表的使用'
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
// 特质:表情符号的使用程度
|
|
100
|
-
emoji: {
|
|
101
|
-
more: '在回复中使用较多表情符号',
|
|
102
|
-
less: '尽量减少使用表情符号'
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
// 构建行为描述
|
|
106
|
-
build(config) {
|
|
107
|
-
const parts = []
|
|
108
|
-
|
|
109
|
-
// 回复风格和语调
|
|
110
|
-
if (config.style && this.styles[config.style]) {
|
|
111
|
-
parts.push(`回复风格和语调:${this.styles[config.style]}`)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 特质:标题和列表
|
|
115
|
-
if (config.headingLists && this.headingLists[config.headingLists]) {
|
|
116
|
-
parts.push(this.headingLists[config.headingLists])
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 特质:表情符号
|
|
120
|
-
if (config.emoji && this.emoji[config.emoji]) {
|
|
121
|
-
parts.push(this.emoji[config.emoji])
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 回复语言
|
|
125
|
-
if (config.language && this.languages[config.language]) {
|
|
126
|
-
parts.push(this.languages[config.language])
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return parts.join(',')
|
|
130
|
-
}
|
|
141
|
+
// 用户背景块(「关于你」):描述对话用户本人,不属于 Agent 的角色设定
|
|
142
|
+
function buildUserProfile(config, T) {
|
|
143
|
+
const lines = []
|
|
144
|
+
if (config.nickname) lines.push(T.profileNickname(config.nickname))
|
|
145
|
+
if (config.occupation) lines.push(T.profileOccupation(config.occupation))
|
|
146
|
+
if (config.bio) lines.push(T.profileBio(config.bio))
|
|
147
|
+
return lines.join('\n')
|
|
131
148
|
}
|
|
132
149
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
// 构建风格描述
|
|
144
|
-
build(config) {
|
|
145
|
-
const parts = []
|
|
146
|
-
|
|
147
|
-
// 根据风格选择格式
|
|
148
|
-
if (config.style === 'casual') {
|
|
149
|
-
parts.push('使用轻松的语气')
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return parts.join(',')
|
|
153
|
-
}
|
|
150
|
+
// 行为块:回复风格和语调 + 特质 + 输出语言
|
|
151
|
+
function buildBehavior(config, T) {
|
|
152
|
+
const parts = []
|
|
153
|
+
if (config.style && T.styles[config.style]) parts.push(`${T.styleLabel}${T.styles[config.style]}`)
|
|
154
|
+
if (config.headingLists && T.headingLists[config.headingLists]) parts.push(T.headingLists[config.headingLists])
|
|
155
|
+
if (config.emoji && T.emoji[config.emoji]) parts.push(T.emoji[config.emoji])
|
|
156
|
+
if (config.language && T.replyLanguage[config.language]) parts.push(T.replyLanguage[config.language])
|
|
157
|
+
return parts.join(T.joiner)
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
// ==================== 配置管理 ====================
|
|
157
161
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
enabled: true,
|
|
161
|
-
nickname: '',
|
|
162
|
-
// 关于你:用户职业 / 用户介绍
|
|
163
|
-
occupation: '',
|
|
164
|
-
bio: '',
|
|
165
|
-
// 回复风格和语调(v0.2.0 起合并为单一选项)
|
|
166
|
-
style: 'professional',
|
|
167
|
-
// 特质:标题和列表 / 表情符号(default=默认,more=增强,less=减弱)
|
|
168
|
-
headingLists: 'default',
|
|
169
|
-
emoji: 'default',
|
|
170
|
-
language: 'zh',
|
|
171
|
-
customInstructions: ''
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// 合法的回复风格和语调取值(供工具参数校验)
|
|
175
|
-
const STYLE_VALUES = ['professional', 'casual', 'humorous', 'roast', 'efficient']
|
|
162
|
+
// 默认配置、合法取值(STYLE_VALUES / TRAIT_VALUES / LANGUAGE_VALUES)、
|
|
163
|
+
// 旧版本迁移(migrateConfig)与输入校验(sanitizeConfig)见 lib/config.mjs。
|
|
176
164
|
|
|
177
|
-
//
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
// v0.1.x 配置使用 style + tone 两个字段;v0.2.0 起合并为单一 style(回复风格和语调)。
|
|
181
|
-
// 迁移映射:组合命中用组合表,否则退回旧 style 表,再否则用默认值。
|
|
182
|
-
const LEGACY_STYLE_TONE_MAP = {
|
|
183
|
-
'professional+formal': 'professional',
|
|
184
|
-
'casual+neutral': 'casual',
|
|
185
|
-
'humorous+informal': 'humorous'
|
|
186
|
-
}
|
|
187
|
-
const LEGACY_STYLE_MAP = {
|
|
188
|
-
professional: 'professional',
|
|
189
|
-
casual: 'casual',
|
|
190
|
-
friendly: 'casual',
|
|
191
|
-
humorous: 'humorous',
|
|
192
|
-
academic: 'professional'
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function migrateConfig(raw) {
|
|
196
|
-
const config = { ...DEFAULT_CONFIG, ...raw }
|
|
197
|
-
if (raw && typeof raw.style === 'string' && typeof raw.tone === 'string') {
|
|
198
|
-
const combo = `${raw.style}+${raw.tone}`
|
|
199
|
-
config.style = LEGACY_STYLE_TONE_MAP[combo] || LEGACY_STYLE_MAP[raw.style] || DEFAULT_CONFIG.style
|
|
200
|
-
delete config.tone
|
|
201
|
-
}
|
|
202
|
-
// 人设预设 / 示例指令功能已在 v0.2.0 移除,丢弃历史残留字段
|
|
203
|
-
delete config.presets
|
|
204
|
-
delete config.tone
|
|
205
|
-
delete config.examples
|
|
206
|
-
// 特质字段校验:脏数据回退为默认值
|
|
207
|
-
if (!TRAIT_VALUES.includes(config.headingLists)) config.headingLists = 'default'
|
|
208
|
-
if (!TRAIT_VALUES.includes(config.emoji)) config.emoji = 'default'
|
|
209
|
-
return config
|
|
210
|
-
}
|
|
165
|
+
// POST /api/soul/config 请求体大小上限:合法配置远小于该值,仅用于拦截异常请求
|
|
166
|
+
const MAX_BODY_BYTES = 64 * 1024
|
|
211
167
|
|
|
212
168
|
// 内存中的配置缓存
|
|
213
169
|
let configCache = null
|
|
@@ -239,48 +195,89 @@ async function saveConfig(config) {
|
|
|
239
195
|
return clean
|
|
240
196
|
}
|
|
241
197
|
|
|
198
|
+
// ==================== 配置写入队列 ====================
|
|
199
|
+
|
|
200
|
+
// 所有配置写路径(HTTP 保存 / /soul 命令 / set_persona 工具 / soulConfig 服务)
|
|
201
|
+
// 统一经由该队列串行执行「读—改—写」,避免并发写入时各方都基于同一份旧配置合并、
|
|
202
|
+
// 后写覆盖前写(丢更新)。队列按提交顺序执行任务。
|
|
203
|
+
let configWriteQueue = Promise.resolve()
|
|
204
|
+
|
|
205
|
+
function enqueueConfigWrite(task) {
|
|
206
|
+
const run = configWriteQueue.then(task)
|
|
207
|
+
// 队列吞掉单次任务的失败,保证后续写入不会被一次磁盘错误卡死
|
|
208
|
+
configWriteQueue = run.then(() => {}, () => {})
|
|
209
|
+
return run
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 在写队列中执行一次配置更新:mutate 收到当前配置的浅拷贝,返回新配置对象。
|
|
213
|
+
// 注意 mutate 必须在队列任务内完成全部读取与合并,不要在队列外提前读取配置。
|
|
214
|
+
// 返回 { config, changed }:changed 为实际发生变化的字段名数组,
|
|
215
|
+
// 调用方在其为空时应跳过提示词刷新与会话注入(配置没有变化)。
|
|
216
|
+
async function commitConfig(mutate) {
|
|
217
|
+
return enqueueConfigWrite(async () => {
|
|
218
|
+
const current = await loadConfig()
|
|
219
|
+
const next = mutate({ ...current })
|
|
220
|
+
const changed = diffKeys(current, next)
|
|
221
|
+
const config = await saveConfig(next)
|
|
222
|
+
return { config, changed }
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 比较两份配置的差异字段名(浅比较;soul 配置为扁平对象)
|
|
227
|
+
function diffKeys(a, b) {
|
|
228
|
+
const changed = []
|
|
229
|
+
for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
230
|
+
if (a[key] !== b[key]) changed.push(key)
|
|
231
|
+
}
|
|
232
|
+
return changed
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 将字段级校验错误汇总为一行文本(HTTP 错误信息 / 工具返回消息共用)
|
|
236
|
+
function formatFieldErrors(errors) {
|
|
237
|
+
return Object.entries(errors)
|
|
238
|
+
.map(([field, reason]) => `${field}: ${reason}`)
|
|
239
|
+
.join(';')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
242
|
// ==================== 提示词编译器 ====================
|
|
243
243
|
|
|
244
|
-
// Prompt Compiler
|
|
244
|
+
// Prompt Compiler:将身份(自定义指令)、用户背景与行为(风格/特质/语言)
|
|
245
|
+
// 按 config.language 对应的文案表编译为 system prompt。
|
|
245
246
|
function compilePrompt(config) {
|
|
246
247
|
if (!config.enabled) {
|
|
247
248
|
return ''
|
|
248
249
|
}
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
|
|
250
|
+
|
|
251
|
+
const T = promptTextOf(config)
|
|
252
|
+
const identity = (config.customInstructions || '').trim()
|
|
253
|
+
const profile = buildUserProfile(config, T)
|
|
254
|
+
const behavior = buildBehavior(config, T)
|
|
255
|
+
|
|
255
256
|
// 如果没有任何配置,返回空
|
|
256
257
|
if (!identity && !behavior && !profile) {
|
|
257
258
|
return ''
|
|
258
259
|
}
|
|
259
|
-
|
|
260
|
-
// 编译提示词
|
|
260
|
+
|
|
261
261
|
const parts = []
|
|
262
|
-
|
|
262
|
+
|
|
263
263
|
// 身份层
|
|
264
264
|
if (identity) {
|
|
265
|
-
parts.push(
|
|
265
|
+
parts.push(`${T.roleHeader} ${identity}`)
|
|
266
266
|
}
|
|
267
|
-
|
|
268
|
-
//
|
|
267
|
+
|
|
268
|
+
// 用户背景块:明确声明描述的是用户本人,防止模型把用户信息当成自己的身份
|
|
269
269
|
if (profile) {
|
|
270
|
-
parts.push(
|
|
271
|
-
'[用户背景] 以下信息描述的是与你对话的用户本人,不是你的身份;不要把用户的职业、技术背景或自述当成你自己的设定。',
|
|
272
|
-
profile
|
|
273
|
-
].join('\n'))
|
|
270
|
+
parts.push(`${T.profileHeader} ${T.profileDisclaimer}\n${profile}`)
|
|
274
271
|
}
|
|
275
|
-
|
|
272
|
+
|
|
276
273
|
// 行为层
|
|
277
274
|
if (behavior) {
|
|
278
|
-
parts.push(
|
|
275
|
+
parts.push(`${T.styleHeader} ${behavior}`)
|
|
279
276
|
}
|
|
280
|
-
|
|
277
|
+
|
|
281
278
|
// 执行规则
|
|
282
|
-
parts.push(`
|
|
283
|
-
|
|
279
|
+
parts.push(`${T.rulesHeader} ${T.rules}`)
|
|
280
|
+
|
|
284
281
|
return parts.join('\n')
|
|
285
282
|
}
|
|
286
283
|
|
|
@@ -295,26 +292,16 @@ function injectPromptToAllAgents(ctx, config) {
|
|
|
295
292
|
return
|
|
296
293
|
}
|
|
297
294
|
|
|
295
|
+
const T = promptTextOf(config)
|
|
298
296
|
const prompt = compilePrompt(config)
|
|
299
|
-
const
|
|
300
|
-
'[dsh-soul 个性化配置已关闭]',
|
|
301
|
-
'从现在开始不要使用 dsh-soul 之前注入的昵称、角色、回复风格、语调、特质或输出语言配置。',
|
|
302
|
-
'请恢复使用 Agent 的默认行为。'
|
|
303
|
-
].join('\n')
|
|
304
|
-
const snapshotText = prompt || disabledText
|
|
297
|
+
const snapshotText = prompt || T.injectDisabled
|
|
305
298
|
|
|
306
299
|
for (const agent of agents.list()) {
|
|
307
300
|
try {
|
|
308
301
|
agent.inject(createUserMessage({
|
|
309
302
|
content: [{
|
|
310
303
|
type: 'text',
|
|
311
|
-
text: prompt ?
|
|
312
|
-
'[dsh-soul 个性化配置已更新]',
|
|
313
|
-
'以下是当前最新的个性化配置快照(含昵称、回复风格和语调、特质、输出语言等)。',
|
|
314
|
-
'从现在开始必须按照此配置回复,不要继续使用旧的昵称、角色、风格、语调、特质或输出语言。',
|
|
315
|
-
'',
|
|
316
|
-
prompt
|
|
317
|
-
].join('\n') : snapshotText
|
|
304
|
+
text: prompt ? `${T.injectUpdatedHeader}\n${T.injectUpdatedBody}\n\n${prompt}` : snapshotText
|
|
318
305
|
}],
|
|
319
306
|
source: {
|
|
320
307
|
kind: 'plugin',
|
|
@@ -361,16 +348,50 @@ function registerRoutes(ctx) {
|
|
|
361
348
|
const config = await loadConfig()
|
|
362
349
|
send(200, { ok: true, config })
|
|
363
350
|
} else if (req.method === 'POST') {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
351
|
+
// 读取请求体:超过大小上限则丢弃内容但继续读完剩余数据,
|
|
352
|
+
// 避免连接中残留未读字节污染 keep-alive
|
|
353
|
+
const chunks = []
|
|
354
|
+
let size = 0
|
|
355
|
+
let oversized = false
|
|
356
|
+
for await (const chunk of req) {
|
|
357
|
+
size += chunk.length
|
|
358
|
+
if (size > MAX_BODY_BYTES) {
|
|
359
|
+
oversized = true
|
|
360
|
+
chunks.length = 0
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
chunks.push(chunk)
|
|
364
|
+
}
|
|
365
|
+
if (oversized) {
|
|
366
|
+
send(413, { ok: false, error: `请求体超过大小上限 ${MAX_BODY_BYTES} 字节` })
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// JSON 解析失败属于客户端错误:返回 400 而非 500
|
|
371
|
+
let body
|
|
372
|
+
try {
|
|
373
|
+
body = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
374
|
+
} catch (err) {
|
|
375
|
+
send(400, { ok: false, error: '请求体不是合法的 JSON:' + String(err.message || err) })
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 输入校验:字段白名单 + 类型 + 长度上限 + 枚举;存在错误时整单拒绝
|
|
380
|
+
const { patch, errors } = sanitizeConfig(body)
|
|
381
|
+
if (Object.keys(errors).length > 0) {
|
|
382
|
+
send(400, { ok: false, error: '配置校验失败:' + formatFieldErrors(errors), errors })
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 写队列内「读—改—写」,只合并通过校验的字段(未知字段不落盘)
|
|
387
|
+
const { config: updated, changed } = await commitConfig(current => ({ ...current, ...patch }))
|
|
388
|
+
|
|
389
|
+
// 配置有实际变化才更新系统提示词并注入会话,避免无变化保存堆积注入消息
|
|
390
|
+
if (changed.length > 0) {
|
|
391
|
+
refreshPromptAndInject(ctx, updated)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
send(200, { ok: true, config: updated, changed })
|
|
374
395
|
} else {
|
|
375
396
|
send(405, { ok: false, error: 'Method not allowed' })
|
|
376
397
|
}
|
|
@@ -413,12 +434,14 @@ function registerRoutes(ctx) {
|
|
|
413
434
|
}
|
|
414
435
|
|
|
415
436
|
try {
|
|
416
|
-
const config = await
|
|
437
|
+
const { config, changed } = await commitConfig(() => ({ ...DEFAULT_CONFIG }))
|
|
417
438
|
|
|
418
|
-
//
|
|
419
|
-
|
|
439
|
+
// 配置有实际变化才更新系统提示词并注入会话
|
|
440
|
+
if (changed.length > 0) {
|
|
441
|
+
refreshPromptAndInject(ctx, config)
|
|
442
|
+
}
|
|
420
443
|
|
|
421
|
-
send(200, { ok: true, config })
|
|
444
|
+
send(200, { ok: true, config, changed })
|
|
422
445
|
} catch (err) {
|
|
423
446
|
send(500, { ok: false, error: String(err.message || err) })
|
|
424
447
|
}
|
|
@@ -538,29 +561,27 @@ function registerCommands(ctx) {
|
|
|
538
561
|
text: `${t.showTitle}\n\n${t.statusLabel}${t.colon}${status}\n${t.nicknameLabel}${t.colon}${nickname}\n${t.occupationLabel}${t.colon}${occupation}\n${t.bioLabel}${t.colon}${bio}\n${t.styleLabel}${t.colon}${styleName}\n${t.traitsLabel}${t.colon}${t.headingListsLabel}=${hlName},${t.emojiLabel}=${emojiName}\n${t.instructionsLabel}${t.colon}${instructions}\n\n${t.help}`
|
|
539
562
|
}
|
|
540
563
|
} else if (args === 'reset') {
|
|
541
|
-
const updated = await
|
|
542
|
-
|
|
543
|
-
refreshPromptAndInject(ctx, updated)
|
|
564
|
+
const { config: updated, changed } = await commitConfig(() => ({ ...DEFAULT_CONFIG }))
|
|
565
|
+
if (changed.length > 0) refreshPromptAndInject(ctx, updated)
|
|
544
566
|
return { kind: 'success', text: t.resetDone }
|
|
545
567
|
} else if (args === 'enable') {
|
|
546
|
-
|
|
547
|
-
const updated = await
|
|
548
|
-
|
|
549
|
-
refreshPromptAndInject(ctx, updated)
|
|
568
|
+
// 写队列内「读—改—写」,不在队列外改写内存缓存对象;无变化时跳过注入
|
|
569
|
+
const { config: updated, changed } = await commitConfig(c => ({ ...c, enabled: true }))
|
|
570
|
+
if (changed.length > 0) refreshPromptAndInject(ctx, updated)
|
|
550
571
|
return { kind: 'success', text: t.enableDone }
|
|
551
572
|
} else if (args === 'disable') {
|
|
552
|
-
config
|
|
553
|
-
|
|
554
|
-
// 更新系统提示词,并注入所有活动会话
|
|
555
|
-
refreshPromptAndInject(ctx, updated)
|
|
573
|
+
const { config: updated, changed } = await commitConfig(c => ({ ...c, enabled: false }))
|
|
574
|
+
if (changed.length > 0) refreshPromptAndInject(ctx, updated)
|
|
556
575
|
return { kind: 'success', text: t.disableDone }
|
|
557
576
|
} else {
|
|
558
|
-
// 设置昵称(保留原始大小写,不做 toLowerCase
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
577
|
+
// 设置昵称(保留原始大小写,不做 toLowerCase);与 HTTP 保存共用同一套校验
|
|
578
|
+
const { patch, errors } = sanitizeConfig({ nickname: raw })
|
|
579
|
+
if (errors.nickname) {
|
|
580
|
+
return { kind: 'error', text: t.failed(errors.nickname) }
|
|
581
|
+
}
|
|
582
|
+
const { config: updated, changed } = await commitConfig(c => ({ ...c, nickname: patch.nickname }))
|
|
583
|
+
if (changed.length > 0) refreshPromptAndInject(ctx, updated)
|
|
584
|
+
return { kind: 'success', text: t.nicknameDone(patch.nickname) }
|
|
564
585
|
}
|
|
565
586
|
} catch (err) {
|
|
566
587
|
return { kind: 'error', text: commandMessages(await loadConfig()).failed(err.message) }
|
|
@@ -643,7 +664,7 @@ function registerTools(ctx) {
|
|
|
643
664
|
},
|
|
644
665
|
language: {
|
|
645
666
|
type: 'string',
|
|
646
|
-
enum:
|
|
667
|
+
enum: LANGUAGE_VALUES,
|
|
647
668
|
description: '回复语言:zh=简体中文,en=English。'
|
|
648
669
|
},
|
|
649
670
|
headingLists: {
|
|
@@ -685,39 +706,37 @@ function registerTools(ctx) {
|
|
|
685
706
|
},
|
|
686
707
|
async execute(args) {
|
|
687
708
|
try {
|
|
688
|
-
|
|
689
|
-
const patch =
|
|
690
|
-
if (
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (typeof args.occupation === 'string' && args.occupation !== current.occupation) {
|
|
694
|
-
patch.occupation = args.occupation
|
|
695
|
-
}
|
|
696
|
-
if (typeof args.bio === 'string' && args.bio !== current.bio) {
|
|
697
|
-
patch.bio = args.bio
|
|
698
|
-
}
|
|
699
|
-
if (typeof args.style === 'string' && STYLE_VALUES.includes(args.style) && args.style !== current.style) {
|
|
700
|
-
patch.style = args.style
|
|
701
|
-
}
|
|
702
|
-
if (typeof args.language === 'string' && ['zh', 'en'].includes(args.language) && args.language !== current.language) {
|
|
703
|
-
patch.language = args.language
|
|
704
|
-
}
|
|
705
|
-
if (typeof args.headingLists === 'string' && TRAIT_VALUES.includes(args.headingLists) && args.headingLists !== current.headingLists) {
|
|
706
|
-
patch.headingLists = args.headingLists
|
|
707
|
-
}
|
|
708
|
-
if (typeof args.emoji === 'string' && TRAIT_VALUES.includes(args.emoji) && args.emoji !== current.emoji) {
|
|
709
|
-
patch.emoji = args.emoji
|
|
710
|
-
}
|
|
711
|
-
if (typeof args.customInstructions === 'string' && args.customInstructions !== current.customInstructions) {
|
|
712
|
-
patch.customInstructions = args.customInstructions
|
|
709
|
+
// 与 HTTP 保存共用同一套校验(白名单 / 类型 / 长度 / 枚举)
|
|
710
|
+
const { patch, errors } = sanitizeConfig(args)
|
|
711
|
+
if (Object.keys(errors).length > 0) {
|
|
712
|
+
const T0 = promptTextOf(await loadConfig())
|
|
713
|
+
return { ok: false, message: T0.toolValidationFailed + formatFieldErrors(errors) }
|
|
713
714
|
}
|
|
714
715
|
if (Object.keys(patch).length === 0) {
|
|
715
|
-
|
|
716
|
+
const T0 = promptTextOf(await loadConfig())
|
|
717
|
+
return { ok: true, message: T0.toolNoChanges }
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// 写队列内合并,避免与其他写入方(Web UI、/soul 命令)竞态;
|
|
721
|
+
// changed 由队列任务统一 diff 得出
|
|
722
|
+
const { config: updated, changed } = await commitConfig((current) => {
|
|
723
|
+
const next = { ...current }
|
|
724
|
+
for (const key of Object.keys(patch)) {
|
|
725
|
+
next[key] = patch[key]
|
|
726
|
+
}
|
|
727
|
+
return next
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
if (changed.length === 0) {
|
|
731
|
+
return { ok: true, message: promptTextOf(updated).toolNoChanges }
|
|
716
732
|
}
|
|
717
|
-
|
|
718
|
-
await saveConfig(updated)
|
|
733
|
+
|
|
719
734
|
refreshPromptAndInject(ctx, updated)
|
|
720
|
-
return {
|
|
735
|
+
return {
|
|
736
|
+
ok: true,
|
|
737
|
+
message: promptTextOf(updated).toolUpdated,
|
|
738
|
+
changes: Object.fromEntries(changed.map(key => [key, updated[key]]))
|
|
739
|
+
}
|
|
721
740
|
} catch (err) {
|
|
722
741
|
return { ok: false, message: String(err.message || err) }
|
|
723
742
|
}
|
|
@@ -741,16 +760,21 @@ export async function apply(ctx) {
|
|
|
741
760
|
return await loadConfig()
|
|
742
761
|
},
|
|
743
762
|
async updateConfig(newConfig) {
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
763
|
+
// 与 HTTP 保存共用同一套校验;失败时抛错给服务调用方;返回更新后的配置
|
|
764
|
+
const { patch, errors } = sanitizeConfig(newConfig)
|
|
765
|
+
if (Object.keys(errors).length > 0) {
|
|
766
|
+
throw new Error('配置校验失败:' + formatFieldErrors(errors))
|
|
767
|
+
}
|
|
768
|
+
const { config } = await commitConfig(current => ({ ...current, ...patch }))
|
|
769
|
+
return config
|
|
747
770
|
},
|
|
748
771
|
async getSystemPrompt() {
|
|
749
772
|
const config = await loadConfig()
|
|
750
773
|
return compilePrompt(config)
|
|
751
774
|
},
|
|
752
775
|
async resetConfig() {
|
|
753
|
-
|
|
776
|
+
const { config } = await commitConfig(() => ({ ...DEFAULT_CONFIG }))
|
|
777
|
+
return config
|
|
754
778
|
}
|
|
755
779
|
})
|
|
756
780
|
|