dsh-soul 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.mjs CHANGED
@@ -20,7 +20,9 @@ export const DEFAULT_CONFIG = {
20
20
  headingLists: 'default',
21
21
  emoji: 'default',
22
22
  language: 'zh',
23
- customInstructions: ''
23
+ customInstructions: '',
24
+ // set_persona 工具确认模式:开启后 Agent 的人设修改返回待确认提议而不落盘
25
+ requireToolConfirmation: false
24
26
  }
25
27
 
26
28
  // 合法的回复风格和语调取值(供工具参数校验)
@@ -44,6 +46,39 @@ export const FIELD_LIMITS = {
44
46
  // 需做长度校验的自由文本字段
45
47
  const TEXT_FIELDS = ['nickname', 'occupation', 'bio', 'customInstructions']
46
48
 
49
+ // ==================== 人设预设 ====================
50
+
51
+ // 人设预设覆盖的配置字段(enabled 为全局开关、不进入预设)
52
+ export const PERSONA_FIELDS = ['nickname', 'occupation', 'bio', 'style', 'headingLists', 'emoji', 'language', 'customInstructions']
53
+
54
+ // 人设预设名称上限
55
+ export const PERSONA_NAME_MAX = 30
56
+
57
+ // 禁用作预设名的保留键(防原型污染)
58
+ const FORBIDDEN_PERSONA_NAMES = new Set(['__proto__', 'constructor', 'prototype'])
59
+
60
+ // 校验并归一化人设预设名称:trim 后 1..PERSONA_NAME_MAX 字符;非法返回 null
61
+ export function sanitizePersonaName(raw) {
62
+ if (typeof raw !== 'string') return null
63
+ const name = raw.trim()
64
+ if (!name || name.length > PERSONA_NAME_MAX) return null
65
+ if (FORBIDDEN_PERSONA_NAMES.has(name)) return null
66
+ return name
67
+ }
68
+
69
+ // 归一化磁盘上的预设库:只保留普通对象条目,剔除保留键与非对象条目
70
+ export function normalizePersonas(raw) {
71
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
72
+ const out = {}
73
+ for (const key of Object.keys(raw)) {
74
+ if (FORBIDDEN_PERSONA_NAMES.has(key)) continue
75
+ const entry = raw[key]
76
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue
77
+ out[key] = { ...entry }
78
+ }
79
+ return out
80
+ }
81
+
47
82
  // v0.1.x 配置使用 style + tone 两个字段;v0.2.0 起合并为单一 style(回复风格和语调)。
48
83
  // 迁移映射:组合命中用组合表,否则退回旧 style 表,再否则用默认值。
49
84
  const LEGACY_STYLE_TONE_MAP = {
@@ -75,6 +110,10 @@ export function migrateConfig(raw) {
75
110
  // 特质字段校验:脏数据回退为默认值
76
111
  if (!TRAIT_VALUES.includes(config.headingLists)) config.headingLists = 'default'
77
112
  if (!TRAIT_VALUES.includes(config.emoji)) config.emoji = 'default'
113
+ // 确认模式:非布尔脏数据回退为默认值
114
+ if (typeof config.requireToolConfirmation !== 'boolean') config.requireToolConfirmation = DEFAULT_CONFIG.requireToolConfirmation
115
+ // 人设预设库归一化:剔除危险键与非对象条目(磁盘无该字段时保持缺席)
116
+ if ('personas' in config) config.personas = normalizePersonas(config.personas)
78
117
  return config
79
118
  }
80
119
 
@@ -103,6 +142,14 @@ export function sanitizeConfig(raw) {
103
142
  }
104
143
  }
105
144
 
145
+ if ('requireToolConfirmation' in raw) {
146
+ if (typeof raw.requireToolConfirmation === 'boolean') {
147
+ patch.requireToolConfirmation = raw.requireToolConfirmation
148
+ } else {
149
+ errors.requireToolConfirmation = '必须为布尔值(true / false)'
150
+ }
151
+ }
152
+
106
153
  for (const key of TEXT_FIELDS) {
107
154
  if (!(key in raw)) continue
108
155
  const value = raw[key]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-soul",
3
- "version": "0.3.3",
3
+ "version": "0.5.0",
4
4
  "description": "为 DeepSeek Harness 添加个性化设置",
5
5
  "keywords": [
6
6
  "dsh",
@@ -10,7 +10,9 @@ import {
10
10
  DEFAULT_CONFIG,
11
11
  FIELD_LIMITS,
12
12
  migrateConfig,
13
- sanitizeConfig
13
+ normalizePersonas,
14
+ sanitizeConfig,
15
+ sanitizePersonaName
14
16
  } from '../lib/config.mjs'
15
17
 
16
18
  let passed = 0
@@ -139,4 +141,40 @@ check('非对象输入整体拒绝', () => {
139
141
  assert.ok(sanitizeConfig(null).errors._)
140
142
  })
141
143
 
144
+ console.log('requireToolConfirmation / 人设预设')
145
+
146
+ check('sanitizeConfig:requireToolConfirmation 布尔校验', () => {
147
+ const { patch, errors } = sanitizeConfig({ requireToolConfirmation: true })
148
+ assert.deepEqual(errors, {})
149
+ assert.equal(patch.requireToolConfirmation, true)
150
+ assert.ok(sanitizeConfig({ requireToolConfirmation: 'yes' }).errors.requireToolConfirmation)
151
+ })
152
+
153
+ check('sanitizePersonaName:trim、长度与保留键拒绝', () => {
154
+ assert.equal(sanitizePersonaName(' 工作模式 '), '工作模式')
155
+ assert.equal(sanitizePersonaName(''), null)
156
+ assert.equal(sanitizePersonaName(' '), null)
157
+ assert.equal(sanitizePersonaName('x'.repeat(31)), null)
158
+ assert.equal(sanitizePersonaName('__proto__'), null)
159
+ assert.equal(sanitizePersonaName(123), null)
160
+ assert.equal(sanitizePersonaName(null), null)
161
+ })
162
+
163
+ check('normalizePersonas:剔除保留键与非对象条目', () => {
164
+ const raw = JSON.parse('{"__proto__":{"a":1},"work":{"style":"roast"},"bad":1}')
165
+ const out = normalizePersonas(raw)
166
+ assert.deepEqual(Object.keys(out).sort(), ['work'])
167
+ assert.deepEqual(normalizePersonas(null), {})
168
+ assert.deepEqual(normalizePersonas([1, 2]), {})
169
+ })
170
+
171
+ check('migrateConfig:personas 归一化透传,缺省时保持缺席;确认模式脏数据回退', () => {
172
+ const config = migrateConfig({ personas: { work: { style: 'roast' } } })
173
+ assert.deepEqual(config.personas, { work: { style: 'roast' } })
174
+ const clean = migrateConfig({ nickname: 'x' })
175
+ assert.equal('personas' in clean, false)
176
+ const dirty = migrateConfig({ requireToolConfirmation: 'yes' })
177
+ assert.equal(dirty.requireToolConfirmation, false)
178
+ })
179
+
142
180
  console.log(`\n全部通过:${passed} 项检查`)