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/lib/config.mjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// lib/config.mjs — dsh-soul 配置层(纯函数模块,无任何依赖)
|
|
2
|
+
//
|
|
3
|
+
// 职责:
|
|
4
|
+
// - 默认配置与合法取值常量(DEFAULT_CONFIG / STYLE_VALUES / TRAIT_VALUES / LANGUAGE_VALUES)
|
|
5
|
+
// - 旧版本配置迁移(migrateConfig)
|
|
6
|
+
// - 外部输入校验(sanitizeConfig):字段白名单 + 类型断言 + 长度上限 + 枚举校验
|
|
7
|
+
//
|
|
8
|
+
// index.mjs 的所有配置写路径(HTTP 保存 / /soul 命令 / set_persona 工具 / soulConfig 服务)
|
|
9
|
+
// 共用同一套校验;本模块可被 scripts/verify-config.mjs 独立加载测试。
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_CONFIG = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
nickname: '',
|
|
14
|
+
// 关于你:用户职业 / 用户介绍
|
|
15
|
+
occupation: '',
|
|
16
|
+
bio: '',
|
|
17
|
+
// 回复风格和语调(v0.2.0 起合并为单一选项)
|
|
18
|
+
style: 'professional',
|
|
19
|
+
// 特质:标题和列表 / 表情符号(default=默认,more=增强,less=减弱)
|
|
20
|
+
headingLists: 'default',
|
|
21
|
+
emoji: 'default',
|
|
22
|
+
language: 'zh',
|
|
23
|
+
customInstructions: ''
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 合法的回复风格和语调取值(供工具参数校验)
|
|
27
|
+
export const STYLE_VALUES = ['professional', 'casual', 'humorous', 'roast', 'efficient']
|
|
28
|
+
|
|
29
|
+
// 特质合法取值
|
|
30
|
+
export const TRAIT_VALUES = ['default', 'more', 'less']
|
|
31
|
+
|
|
32
|
+
// 输出语言合法取值
|
|
33
|
+
export const LANGUAGE_VALUES = ['zh', 'en']
|
|
34
|
+
|
|
35
|
+
// 文本字段长度上限:防止超长文本撑爆 system prompt(同时缩小提示词注入面)。
|
|
36
|
+
// 超限一律拒绝、不做静默截断,避免改写用户输入。
|
|
37
|
+
export const FIELD_LIMITS = {
|
|
38
|
+
nickname: 50,
|
|
39
|
+
occupation: 50,
|
|
40
|
+
bio: 500,
|
|
41
|
+
customInstructions: 2000
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 需做长度校验的自由文本字段
|
|
45
|
+
const TEXT_FIELDS = ['nickname', 'occupation', 'bio', 'customInstructions']
|
|
46
|
+
|
|
47
|
+
// v0.1.x 配置使用 style + tone 两个字段;v0.2.0 起合并为单一 style(回复风格和语调)。
|
|
48
|
+
// 迁移映射:组合命中用组合表,否则退回旧 style 表,再否则用默认值。
|
|
49
|
+
const LEGACY_STYLE_TONE_MAP = {
|
|
50
|
+
'professional+formal': 'professional',
|
|
51
|
+
'casual+neutral': 'casual',
|
|
52
|
+
'humorous+informal': 'humorous'
|
|
53
|
+
}
|
|
54
|
+
const LEGACY_STYLE_MAP = {
|
|
55
|
+
professional: 'professional',
|
|
56
|
+
casual: 'casual',
|
|
57
|
+
friendly: 'casual',
|
|
58
|
+
humorous: 'humorous',
|
|
59
|
+
academic: 'professional'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 迁移并清洗磁盘上的历史配置:补齐默认值、合并旧 style+tone、丢弃废弃字段、
|
|
63
|
+
// 特质脏数据回退为默认值。
|
|
64
|
+
export function migrateConfig(raw) {
|
|
65
|
+
const config = { ...DEFAULT_CONFIG, ...raw }
|
|
66
|
+
if (raw && typeof raw.style === 'string' && typeof raw.tone === 'string') {
|
|
67
|
+
const combo = `${raw.style}+${raw.tone}`
|
|
68
|
+
config.style = LEGACY_STYLE_TONE_MAP[combo] || LEGACY_STYLE_MAP[raw.style] || DEFAULT_CONFIG.style
|
|
69
|
+
delete config.tone
|
|
70
|
+
}
|
|
71
|
+
// 人设预设 / 示例指令功能已在 v0.2.0 移除,丢弃历史残留字段
|
|
72
|
+
delete config.presets
|
|
73
|
+
delete config.tone
|
|
74
|
+
delete config.examples
|
|
75
|
+
// 特质字段校验:脏数据回退为默认值
|
|
76
|
+
if (!TRAIT_VALUES.includes(config.headingLists)) config.headingLists = 'default'
|
|
77
|
+
if (!TRAIT_VALUES.includes(config.emoji)) config.emoji = 'default'
|
|
78
|
+
return config
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 校验外部传入的配置补丁,返回 { patch, errors }:
|
|
82
|
+
// - patch:通过校验的字段补丁(仅含白名单字段),调用方将其合并到当前配置
|
|
83
|
+
// - errors:被拒绝字段的原因表(字段名 -> 说明);非空时调用方应整单拒绝本次写入
|
|
84
|
+
//
|
|
85
|
+
// 规则:
|
|
86
|
+
// - 白名单:未知字段静默丢弃,不再落盘
|
|
87
|
+
// - 类型:enabled 必须为布尔;文本字段必须为字符串;枚举字段必须命中合法取值
|
|
88
|
+
// - 长度:文本字段超过 FIELD_LIMITS 即拒绝(不截断)
|
|
89
|
+
// - 文本字段保存前 trim 首尾空白,空串视为清除该字段
|
|
90
|
+
export function sanitizeConfig(raw) {
|
|
91
|
+
const patch = {}
|
|
92
|
+
const errors = {}
|
|
93
|
+
|
|
94
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
95
|
+
return { patch, errors: { _: '配置必须是一个 JSON 对象' } }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if ('enabled' in raw) {
|
|
99
|
+
if (typeof raw.enabled === 'boolean') {
|
|
100
|
+
patch.enabled = raw.enabled
|
|
101
|
+
} else {
|
|
102
|
+
errors.enabled = '必须为布尔值(true / false)'
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const key of TEXT_FIELDS) {
|
|
107
|
+
if (!(key in raw)) continue
|
|
108
|
+
const value = raw[key]
|
|
109
|
+
if (typeof value !== 'string') {
|
|
110
|
+
errors[key] = '必须为字符串'
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
const text = value.trim()
|
|
114
|
+
if (text.length > FIELD_LIMITS[key]) {
|
|
115
|
+
errors[key] = `长度超过上限 ${FIELD_LIMITS[key]} 字符(当前 ${text.length})`
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
patch[key] = text
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ENUM_CHECKS = [
|
|
122
|
+
['style', STYLE_VALUES, '回复风格和语调'],
|
|
123
|
+
['headingLists', TRAIT_VALUES, '特质·标题和列表'],
|
|
124
|
+
['emoji', TRAIT_VALUES, '特质·表情符号'],
|
|
125
|
+
['language', LANGUAGE_VALUES, '输出语言']
|
|
126
|
+
]
|
|
127
|
+
for (const [key, values, label] of ENUM_CHECKS) {
|
|
128
|
+
if (!(key in raw)) continue
|
|
129
|
+
const value = raw[key]
|
|
130
|
+
if (typeof value === 'string' && values.includes(value)) {
|
|
131
|
+
patch[key] = value
|
|
132
|
+
} else {
|
|
133
|
+
errors[key] = `${label}必须是 ${values.join(' / ')} 之一`
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { patch, errors }
|
|
138
|
+
}
|
package/package.json
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-soul",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "为 DeepSeek Harness 添加个性化设置",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
8
|
+
"dsh-soul",
|
|
8
9
|
"deepseek",
|
|
9
10
|
"deepseek-harness",
|
|
10
11
|
"personalization",
|
|
11
12
|
"persona",
|
|
12
13
|
"system-prompt",
|
|
13
|
-
"ai-agent",
|
|
14
14
|
"ai-assistant",
|
|
15
15
|
"customization"
|
|
16
16
|
],
|
|
17
17
|
"type": "module",
|
|
18
18
|
"main": "index.mjs",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"verify": "node scripts/verify-config.mjs"
|
|
21
|
+
},
|
|
19
22
|
"license": "MIT",
|
|
20
23
|
"repository": {
|
|
21
24
|
"type": "git",
|
|
@@ -48,8 +51,10 @@
|
|
|
48
51
|
},
|
|
49
52
|
"files": [
|
|
50
53
|
"index.mjs",
|
|
54
|
+
"lib",
|
|
51
55
|
"client",
|
|
52
56
|
"cordis.patch.yml",
|
|
57
|
+
"scripts",
|
|
53
58
|
"README.md",
|
|
54
59
|
"README_EN.md"
|
|
55
60
|
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// scripts/verify-config.mjs — 配置层纯函数自检(npm run verify)
|
|
2
|
+
//
|
|
3
|
+
// 覆盖 lib/config.mjs 的 migrateConfig / sanitizeConfig:
|
|
4
|
+
// - 旧版本 style+tone 迁移、废弃字段清理、特质脏数据回退
|
|
5
|
+
// - 白名单 / 类型 / 长度 / 枚举校验的接受与拒绝路径
|
|
6
|
+
// 零依赖,直接 `node scripts/verify-config.mjs` 运行。
|
|
7
|
+
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_CONFIG,
|
|
11
|
+
FIELD_LIMITS,
|
|
12
|
+
migrateConfig,
|
|
13
|
+
sanitizeConfig
|
|
14
|
+
} from '../lib/config.mjs'
|
|
15
|
+
|
|
16
|
+
let passed = 0
|
|
17
|
+
function check(name, fn) {
|
|
18
|
+
fn()
|
|
19
|
+
passed++
|
|
20
|
+
console.log(` ✓ ${name}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log('migrateConfig')
|
|
24
|
+
|
|
25
|
+
check('非对象输入返回默认配置', () => {
|
|
26
|
+
assert.deepEqual(migrateConfig(null), DEFAULT_CONFIG)
|
|
27
|
+
assert.deepEqual(migrateConfig(undefined), DEFAULT_CONFIG)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
check('正常配置原样保留并补齐默认值', () => {
|
|
31
|
+
const config = migrateConfig({ enabled: false, nickname: '小明', style: 'roast', language: 'en' })
|
|
32
|
+
assert.equal(config.enabled, false)
|
|
33
|
+
assert.equal(config.nickname, '小明')
|
|
34
|
+
assert.equal(config.style, 'roast')
|
|
35
|
+
assert.equal(config.language, 'en')
|
|
36
|
+
assert.equal(config.headingLists, 'default')
|
|
37
|
+
assert.equal(config.emoji, 'default')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
check('v0.1.x style+tone 组合迁移', () => {
|
|
41
|
+
assert.equal(migrateConfig({ style: 'professional', tone: 'formal' }).style, 'professional')
|
|
42
|
+
assert.equal(migrateConfig({ style: 'casual', tone: 'neutral' }).style, 'casual')
|
|
43
|
+
assert.equal(migrateConfig({ style: 'humorous', tone: 'informal' }).style, 'humorous')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
check('v0.1.x 旧 style 名迁移(friendly→casual,academic→professional)', () => {
|
|
47
|
+
assert.equal(migrateConfig({ style: 'friendly', tone: 'neutral' }).style, 'casual')
|
|
48
|
+
assert.equal(migrateConfig({ style: 'academic', tone: 'formal' }).style, 'professional')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
check('废弃字段 tone / presets / examples 清理', () => {
|
|
52
|
+
const config = migrateConfig({ nickname: 'x', tone: 'formal', presets: [1], examples: 'y' })
|
|
53
|
+
assert.equal('tone' in config, false)
|
|
54
|
+
assert.equal('presets' in config, false)
|
|
55
|
+
assert.equal('examples' in config, false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
check('特质脏数据回退为默认值', () => {
|
|
59
|
+
const config = migrateConfig({ headingLists: 'always', emoji: 1 })
|
|
60
|
+
assert.equal(config.headingLists, 'default')
|
|
61
|
+
assert.equal(config.emoji, 'default')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
console.log('sanitizeConfig')
|
|
65
|
+
|
|
66
|
+
check('合法补丁全部通过', () => {
|
|
67
|
+
const { patch, errors } = sanitizeConfig({
|
|
68
|
+
enabled: false,
|
|
69
|
+
nickname: '小明',
|
|
70
|
+
occupation: '工程师',
|
|
71
|
+
bio: '写代码的',
|
|
72
|
+
style: 'humorous',
|
|
73
|
+
headingLists: 'more',
|
|
74
|
+
emoji: 'less',
|
|
75
|
+
language: 'en',
|
|
76
|
+
customInstructions: '保持简洁'
|
|
77
|
+
})
|
|
78
|
+
assert.deepEqual(errors, {})
|
|
79
|
+
assert.equal(patch.enabled, false)
|
|
80
|
+
assert.equal(patch.nickname, '小明')
|
|
81
|
+
assert.equal(patch.occupation, '工程师')
|
|
82
|
+
assert.equal(patch.style, 'humorous')
|
|
83
|
+
assert.equal(patch.headingLists, 'more')
|
|
84
|
+
assert.equal(patch.emoji, 'less')
|
|
85
|
+
assert.equal(patch.language, 'en')
|
|
86
|
+
assert.equal(patch.customInstructions, '保持简洁')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
check('未知字段静默丢弃(不进入 patch,也不报错)', () => {
|
|
90
|
+
const { patch, errors } = sanitizeConfig({ nickname: 'x', hacked: 'junk', presets: [1] })
|
|
91
|
+
assert.deepEqual(errors, {})
|
|
92
|
+
assert.deepEqual(patch, { nickname: 'x' })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
check('类型错误被拒绝:enabled 非布尔、文本非字符串', () => {
|
|
96
|
+
const { errors } = sanitizeConfig({ enabled: 'yes', nickname: 123 })
|
|
97
|
+
assert.ok(errors.enabled)
|
|
98
|
+
assert.ok(errors.nickname)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
check('枚举错误被拒绝:style / headingLists / emoji / language', () => {
|
|
102
|
+
const { errors } = sanitizeConfig({ style: 'hacker', headingLists: 'always', emoji: 'max', language: 'jp' })
|
|
103
|
+
assert.ok(errors.style)
|
|
104
|
+
assert.ok(errors.headingLists)
|
|
105
|
+
assert.ok(errors.emoji)
|
|
106
|
+
assert.ok(errors.language)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
check('超长文本被拒绝且不截断', () => {
|
|
110
|
+
const { errors, patch } = sanitizeConfig({
|
|
111
|
+
nickname: '名'.repeat(FIELD_LIMITS.nickname + 1),
|
|
112
|
+
customInstructions: 'a'.repeat(FIELD_LIMITS.customInstructions + 1)
|
|
113
|
+
})
|
|
114
|
+
assert.ok(errors.nickname)
|
|
115
|
+
assert.ok(errors.customInstructions)
|
|
116
|
+
assert.equal('nickname' in patch, false)
|
|
117
|
+
assert.equal('customInstructions' in patch, false)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
check('长度上限边界值通过', () => {
|
|
121
|
+
const { errors } = sanitizeConfig({
|
|
122
|
+
nickname: '名'.repeat(FIELD_LIMITS.nickname),
|
|
123
|
+
bio: 'b'.repeat(FIELD_LIMITS.bio),
|
|
124
|
+
customInstructions: 'a'.repeat(FIELD_LIMITS.customInstructions)
|
|
125
|
+
})
|
|
126
|
+
assert.deepEqual(errors, {})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
check('文本字段首尾空白被 trim,空白串视为清除', () => {
|
|
130
|
+
const { patch, errors } = sanitizeConfig({ nickname: ' 小明 ', bio: ' ' })
|
|
131
|
+
assert.deepEqual(errors, {})
|
|
132
|
+
assert.equal(patch.nickname, '小明')
|
|
133
|
+
assert.equal(patch.bio, '')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
check('非对象输入整体拒绝', () => {
|
|
137
|
+
assert.ok(sanitizeConfig([1, 2]).errors._)
|
|
138
|
+
assert.ok(sanitizeConfig('x').errors._)
|
|
139
|
+
assert.ok(sanitizeConfig(null).errors._)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
console.log(`\n全部通过:${passed} 项检查`)
|