dsh-persona-presets 0.1.0 → 0.2.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 +9 -7
- package/client/persona-settings.js +252 -48
- package/docs/DETAIL.zh.md +6 -4
- package/lib/index.js +77 -9
- package/lib/persona-DT5WK4Ne.js +254 -0
- package/lib/persona.js +1 -1
- package/package.json +1 -1
- package/lib/persona-CYm9q6bl.js +0 -134
package/README.md
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
三个自定义 agent 预设,每个预设一张可编辑的角色卡——独立插件,任何 DSH 用户可装。
|
|
4
4
|
|
|
5
|
-
- **预设**:人设-自定义1/2/3(persona-598-custom-1/2/3),工具目录 = 标准模式克隆,人设行替换官方 `@deepseek-ai/dsh-persona
|
|
6
|
-
-
|
|
7
|
-
-
|
|
5
|
+
- **预设**:人设-自定义1/2/3(persona-598-custom-1/2/3),工具目录 = 标准模式克隆,人设行替换官方 `@deepseek-ai/dsh-persona`,并以 scoped 空段遮蔽宿主 `harness:identity` 开场白(只影响这三个预设,standard 不动)。
|
|
6
|
+
- **设置页**:「角色卡」区块,每槽一张卡(可取名、常显对应预设),**三槽开箱即各带一张预制人设**(槽1=鲸鱼娘 / 槽2=病娇 / 槽3=小满),保存后下一回合生效(prompt 段每次组装实时读文件,无缓存无重启)。
|
|
7
|
+
- **导入**:支持角色卡 PNG(SillyTavern 生态 V2/V3 格式,如 aicharactercards.com 下载的卡)、.md/.txt/.json——本机解析,结构化平铺成草稿,检查后保存;卡面原图存 `~/.dsh/persona-presets-598/characters/<presetId>.png` 并在编辑区左侧展示(纯展示,不进 prompt),清空保存即删图。
|
|
8
|
+
- **存储**:`~/.dsh/persona-presets-598/persona-settings.json`,字段 `personas.<presetId>` 与 `presetNames`,卡上限 20000 字(超 10000 字软警告 token 消耗)、名上限 60 字。
|
|
8
9
|
|
|
9
10
|
用户指南见 [docs/DETAIL.zh.md](docs/DETAIL.zh.md)。
|
|
10
11
|
|
|
@@ -35,7 +36,8 @@ presets/persona-custom.patch.yml 三预设(单根 - insert,克隆 standard
|
|
|
35
36
|
1. **patch yml 单根**:一个 patch 文件只能有一个顶层 `- insert:`,第二段顶层条目会被静默丢弃。
|
|
36
37
|
2. **standard 克隆纪律**:工具行复制自 dsh `presets/standard.patch.yml`,上游演进需 re-diff(tests/presets.spec.ts 深比对会红)。
|
|
37
38
|
3. **interpolate: false 不可移除**:用户自由文本进严格 `{{}}` 插值会让整次组装抛异常。
|
|
38
|
-
4.
|
|
39
|
-
5.
|
|
40
|
-
6.
|
|
41
|
-
7.
|
|
39
|
+
4. **harness:identity 遮蔽是 scoped 空段,不是 config patch**:全局关 `includeHarnessIdentity` 会波及 standard;scoped 同名空段只在三预设作用域内生效(宿主机制:scoped beats global + 空段组装时丢弃)。
|
|
40
|
+
5. **白名单合并**:只有 `PERSONA_PRESET_IDS` 内的键可写;空串删除槽位;非字符串忽略;先 trim 后截断 20000(名 60)。
|
|
41
|
+
6. **client 无构建步骤**:手写 ES5 风格 bundle,逐 section 降级(抛错只禁用该 section,不炸页面);SafeZone 边界让渲染错误显示在面板内。
|
|
42
|
+
7. **注册面全家桶 -598**:cordis 插件名/slot id/API 路由/预设 id/存储目录全部带 `-598`,防止撞名。
|
|
43
|
+
8. **locale 锚点**:`exports "./locale/*.json"` 必须保留——插件市场详情页读 `locale/<lang>.json` meta.detail。
|
|
@@ -19,8 +19,13 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
|
|
20
20
|
var READ = '/api/persona-settings-598/read'
|
|
21
21
|
var UPDATE = '/api/persona-settings-598/update'
|
|
22
|
+
var AVATAR_POST = '/api/persona-settings-598/avatar'
|
|
23
|
+
var AVATAR_GET = '/api/persona-settings-598/avatar/'
|
|
22
24
|
|
|
23
25
|
var SLOT_IDS = ['persona-598-custom-1', 'persona-598-custom-2', 'persona-598-custom-3']
|
|
26
|
+
// 软警告阈值:卡的每一个字每回合都会注入 prompt,超过这个长度提示用户
|
|
27
|
+
// 注意 token 消耗(硬上限以服务端 maxLength 为准,超限保存会被截断)。
|
|
28
|
+
var SOFT_WARN_CHARS = 10000
|
|
24
29
|
// 内置预设显示名(对应预设标注 + 取名的缺省值)——与 yml config.name 一致。
|
|
25
30
|
var SLOT_LABELS = {
|
|
26
31
|
'persona-598-custom-1': '人设-自定义1',
|
|
@@ -28,28 +33,118 @@ window.__ModuleLoader__.load({
|
|
|
28
33
|
'persona-598-custom-3': '人设-自定义3',
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
var
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
border: '1px solid rgba(127,127,127,.4)', borderRadius: 6,
|
|
39
|
-
background: 'transparent', color: 'inherit', font: 'inherit',
|
|
40
|
-
}
|
|
41
|
-
var textareaStyle = Object.assign({}, inputStyle, { width: '100%', minHeight: 96, resize: 'vertical', lineHeight: 1.5, padding: '6px 8px' })
|
|
42
|
-
function btnStyle(variant) {
|
|
43
|
-
var base = { border: '1px solid rgba(127,127,127,.4)', borderRadius: 6, cursor: 'pointer', font: 'inherit', fontSize: 12, padding: '3px 12px', background: 'transparent', color: 'inherit' }
|
|
44
|
-
if (variant === 'primary') return Object.assign(base, { fontWeight: 700, borderColor: 'rgba(124,108,240,.6)' })
|
|
45
|
-
return base
|
|
36
|
+
// 三槽开箱各带一张预制人设(槽1=鲸鱼娘/2=病娇/3=小满,文案真源在宿主
|
|
37
|
+
// src/preset-cards.ts,经 readPersonaCard 回落链生效);未取名时槽名
|
|
38
|
+
// 默认显示对应人设名——与 preset-cards.ts 的 label 手工对齐。
|
|
39
|
+
var SLOT_PERSONA_NAMES = {
|
|
40
|
+
'persona-598-custom-1': '鲸鱼娘',
|
|
41
|
+
'persona-598-custom-2': '病娇',
|
|
42
|
+
'persona-598-custom-3': '小满',
|
|
46
43
|
}
|
|
44
|
+
|
|
45
|
+
// Theme-neutral design tokens: inherit foreground, translucent surfaces —
|
|
46
|
+
// readable on both the host's dark and light themes. Static styling rides
|
|
47
|
+
// a pps-* class layer (hover/focus/transition need pseudo-classes, which
|
|
48
|
+
// inline styles cannot express); layout-critical values stay inline.
|
|
49
|
+
var STYLE_CSS = [
|
|
50
|
+
'.pps-card{border:1px solid rgba(127,127,127,.22);border-radius:12px;padding:12px 16px 14px;margin:10px 0;background:rgba(127,127,127,.04)}',
|
|
51
|
+
'.pps-title{font-weight:700;font-size:14px;margin:2px 0 10px;letter-spacing:.2px}',
|
|
52
|
+
'.pps-sub{font-size:11px;font-weight:400;color:rgba(127,127,127,.85)}',
|
|
53
|
+
'.pps-row{display:flex;align-items:center;gap:10px;margin:6px 0;font-size:13px;flex-wrap:wrap}',
|
|
54
|
+
'.pps-muted{color:rgba(127,127,127,.85);font-size:12px}',
|
|
55
|
+
'.pps-hint{margin:6px 0 2px;color:rgba(127,127,127,.85);font-size:12px;line-height:1.5}',
|
|
56
|
+
'.pps-fold{display:flex;align-items:center;gap:10px;padding:6px 8px;margin:0 -8px 6px;border-radius:8px;cursor:pointer;transition:background .15s ease;flex-wrap:wrap}',
|
|
57
|
+
'.pps-fold:hover{background:rgba(127,127,127,.08)}',
|
|
58
|
+
'.pps-chevron{display:inline-block;width:14px;text-align:center;font-weight:700;font-size:11px;opacity:.8;transition:transform .15s ease}',
|
|
59
|
+
'.pps-chevron-open{transform:rotate(90deg)}',
|
|
60
|
+
'.pps-input{border:1px solid rgba(127,127,127,.35);border-radius:8px;background:transparent;color:inherit;font:inherit;transition:border-color .15s ease,box-shadow .15s ease}',
|
|
61
|
+
'.pps-input:hover{border-color:rgba(127,127,127,.55)}',
|
|
62
|
+
'.pps-input:focus{outline:none;border-color:rgba(124,108,240,.7);box-shadow:0 0 0 2px rgba(124,108,240,.18)}',
|
|
63
|
+
'.pps-name-input{width:170px;font-weight:700}',
|
|
64
|
+
'.pps-textarea{width:100%;height:100%;box-sizing:border-box;resize:vertical;line-height:1.6;padding:8px 10px}',
|
|
65
|
+
'.pps-btn{border:1px solid rgba(127,127,127,.4);border-radius:8px;cursor:pointer;font:inherit;font-size:12px;padding:4px 14px;background:transparent;color:inherit;display:inline-flex;align-items:center;transition:background .15s ease,border-color .15s ease,opacity .15s ease}',
|
|
66
|
+
'.pps-btn:hover{background:rgba(127,127,127,.1);border-color:rgba(127,127,127,.6)}',
|
|
67
|
+
'.pps-btn:disabled{opacity:.45;cursor:default}',
|
|
68
|
+
'.pps-btn:disabled:hover{background:transparent;border-color:rgba(127,127,127,.4)}',
|
|
69
|
+
'.pps-btn-primary{font-weight:700;border-color:rgba(124,108,240,.55)}',
|
|
70
|
+
'.pps-btn-primary:hover{background:rgba(124,108,240,.14);border-color:rgba(124,108,240,.8)}',
|
|
71
|
+
'.pps-avatar{width:120px;max-height:260px;object-fit:contain;border-radius:8px;border:1px solid rgba(127,127,127,.3);background:rgba(127,127,127,.06);flex:0 0 auto;align-self:flex-start}',
|
|
72
|
+
'.pps-count{color:rgba(127,127,127,.85);font-size:12px}',
|
|
73
|
+
'.pps-count-over{color:#ff7369;font-size:12px;font-weight:600}',
|
|
74
|
+
'.pps-warn{color:#e8b268;font-size:12px;background:rgba(232,178,104,.1);border:1px solid rgba(232,178,104,.3);border-radius:999px;padding:2px 10px}',
|
|
75
|
+
'.pps-msg{margin:4px 0;color:rgba(127,127,127,.85);font-size:12px}',
|
|
76
|
+
].join('\n')
|
|
47
77
|
function Section(props) {
|
|
48
|
-
return h('div', {
|
|
78
|
+
return h('div', { className: 'pps-card' }, h('div', { className: 'pps-title' }, props.title), props.children)
|
|
49
79
|
}
|
|
50
80
|
function Hint(props) {
|
|
51
|
-
return h('div', {
|
|
81
|
+
return h('div', { className: 'pps-hint' }, props.children)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── 角色卡导入(本机解析,文件不过服务器)────────────────────────
|
|
85
|
+
// Character Card V2/V3(SillyTavern 生态标准):角色数据以 base64 JSON
|
|
86
|
+
// 嵌在 PNG tEXt 块里(V2 关键字 chara,V3 是 ccv3)。aicharactercards.com
|
|
87
|
+
// 等站下载的 PNG 就是这种卡。解析出的多字段按中文小节结构化平铺,空字段
|
|
88
|
+
// 跳过;character_book(世界书)是另一套注入机制,v1 不导入只提示。
|
|
89
|
+
|
|
90
|
+
function latin1(bytes, start, end) {
|
|
91
|
+
var s = ''
|
|
92
|
+
for (var i = start; i < end; i++) s += String.fromCharCode(bytes[i])
|
|
93
|
+
return s
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function decodeBase64Utf8(b64) {
|
|
97
|
+
var bin = atob(String(b64).replace(/\s+/g, ''))
|
|
98
|
+
var bytes = new Uint8Array(bin.length)
|
|
99
|
+
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
|
100
|
+
return new TextDecoder('utf-8').decode(bytes)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 遍历 PNG chunk 找 tEXt 的 chara/ccv3 块,返回解析后的卡 JSON;不是
|
|
104
|
+
// 角色卡 PNG 时返回 null(签名不对/没有块/块内容解不开都算)。
|
|
105
|
+
function pngCharaJson(buffer) {
|
|
106
|
+
try {
|
|
107
|
+
var dv = new DataView(buffer)
|
|
108
|
+
if (dv.byteLength < 8 || dv.getUint8(0) !== 0x89 || dv.getUint8(1) !== 0x50) return null
|
|
109
|
+
var off = 8
|
|
110
|
+
while (off + 8 <= dv.byteLength) {
|
|
111
|
+
var len = dv.getUint32(off)
|
|
112
|
+
var type = latin1(new Uint8Array(buffer, off + 4, 4), 0, 4)
|
|
113
|
+
if (type === 'tEXt') {
|
|
114
|
+
var data = new Uint8Array(buffer, off + 8, len)
|
|
115
|
+
var zero = -1
|
|
116
|
+
for (var i = 0; i < data.length; i++) { if (data[i] === 0) { zero = i; break } }
|
|
117
|
+
if (zero > 0) {
|
|
118
|
+
var keyword = latin1(data, 0, zero)
|
|
119
|
+
if (keyword === 'chara' || keyword === 'ccv3') {
|
|
120
|
+
try { return JSON.parse(decodeBase64Utf8(latin1(data, zero + 1, data.length))) } catch (e) { /* 继续扫其它块 */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (type === 'IEND') break
|
|
125
|
+
off += 12 + len // length(4) + type(4) + data(len) + crc(4)
|
|
126
|
+
}
|
|
127
|
+
} catch (e) { /* 烂图按 null 处理 */ }
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 卡 JSON(V1 顶层或 V2/V3 的 data 子对象)→ 结构化平铺文本。
|
|
132
|
+
// 不像卡的输入返回 null。世界书(character_book)不进卡,用 hasBook 提示。
|
|
133
|
+
function flattenCard(raw) {
|
|
134
|
+
try {
|
|
135
|
+
var d = raw && raw.data && (raw.data.name || raw.data.description) ? raw.data : raw
|
|
136
|
+
if (!d || typeof d !== 'object') return null
|
|
137
|
+
var sections = [['名字', 'name'], ['设定', 'description'], ['性格', 'personality'], ['场景', 'scenario'], ['开场白', 'first_mes'], ['对话示例', 'mes_example'], ['备注', 'creator_notes']]
|
|
138
|
+
var out = []
|
|
139
|
+
for (var i = 0; i < sections.length; i++) {
|
|
140
|
+
var v = d[sections[i][1]]
|
|
141
|
+
if (typeof v === 'string' && v.trim() !== '') out.push('【' + sections[i][0] + '】' + v.trim())
|
|
142
|
+
}
|
|
143
|
+
if (out.length === 0) return null
|
|
144
|
+
return { text: out.join('\n\n'), hasBook: !!d.character_book, name: typeof d.name === 'string' ? d.name : '' }
|
|
145
|
+
} catch (e) { return null }
|
|
52
146
|
}
|
|
147
|
+
|
|
53
148
|
// Diagnostic boundary: a render crash inside the section displays in the
|
|
54
149
|
// panel itself — the host's slot machinery contains entry crashes and
|
|
55
150
|
// reports them off-console, which reads as an empty pane.
|
|
@@ -74,9 +169,12 @@ window.__ModuleLoader__.load({
|
|
|
74
169
|
var namesState = useState({})
|
|
75
170
|
var names = namesState[0]
|
|
76
171
|
var setNames = namesState[1]
|
|
172
|
+
// 预制人设(服务端下发):[{id,label,text}],旧 host 无此块时 chips 隐藏。
|
|
77
173
|
var msgState = useState('')
|
|
78
174
|
var msg = msgState[0]
|
|
79
175
|
var setMsg = msgState[1]
|
|
176
|
+
// msg 行只用于错误反馈(保存失败/解析失败/读取失败);成功不需要提示
|
|
177
|
+
// ——按钮动作与 UI 状态变化本身已说明发生了什么。
|
|
80
178
|
var busyState = useState(false)
|
|
81
179
|
var busy = busyState[0]
|
|
82
180
|
var setBusy = busyState[1]
|
|
@@ -88,6 +186,14 @@ window.__ModuleLoader__.load({
|
|
|
88
186
|
var openState = useState({})
|
|
89
187
|
var open = openState[0]
|
|
90
188
|
var setOpen = openState[1]
|
|
189
|
+
// 卡面(角色卡 PNG 原图):host 存 characters/<presetId>.png,这里只管
|
|
190
|
+
// 展示——onError(404)隐藏降级;导入成功后 bust 刷新 + 复位隐藏态。
|
|
191
|
+
var avatarFailState = useState({})
|
|
192
|
+
var avatarFail = avatarFailState[0]
|
|
193
|
+
var setAvatarFail = avatarFailState[1]
|
|
194
|
+
var avatarBustState = useState({})
|
|
195
|
+
var avatarBust = avatarBustState[0]
|
|
196
|
+
var setAvatarBust = avatarBustState[1]
|
|
91
197
|
|
|
92
198
|
var refresh = function () {
|
|
93
199
|
fetch(READ, { credentials: 'same-origin' })
|
|
@@ -100,10 +206,11 @@ window.__ModuleLoader__.load({
|
|
|
100
206
|
}
|
|
101
207
|
useEffect(refresh, [])
|
|
102
208
|
|
|
103
|
-
//
|
|
209
|
+
// 人设槽显示名:用户取名优先 → 人设名(鲸鱼娘/病娇/小满)→ 内置预设名。
|
|
104
210
|
var presetNameOf = function (presetId) {
|
|
105
211
|
var custom = names && names[presetId]
|
|
106
|
-
|
|
212
|
+
if (custom) return String(custom)
|
|
213
|
+
return SLOT_PERSONA_NAMES[presetId] || SLOT_LABELS[presetId] || presetId
|
|
107
214
|
}
|
|
108
215
|
var commitPresetName = function (presetId, value) {
|
|
109
216
|
var current = (names && names[presetId]) || ''
|
|
@@ -123,7 +230,6 @@ window.__ModuleLoader__.load({
|
|
|
123
230
|
.then(function (d) {
|
|
124
231
|
if (d && d.ok && d.names) {
|
|
125
232
|
setNames(d.names)
|
|
126
|
-
setMsg(v === '' ? '已恢复内置名(' + SLOT_LABELS[presetId] + ')' : '卡名已保存')
|
|
127
233
|
} else {
|
|
128
234
|
setMsg('保存失败: ' + ((d && d.error) || '未知错误'))
|
|
129
235
|
}
|
|
@@ -136,29 +242,103 @@ window.__ModuleLoader__.load({
|
|
|
136
242
|
var slot = slots ? slots[presetId] : null
|
|
137
243
|
return slot ? String(slot.value || '') : ''
|
|
138
244
|
}
|
|
139
|
-
// Unicode code-point count, same unit as the host
|
|
245
|
+
// Unicode code-point count, same unit as the host cap (server-sent maxLength).
|
|
140
246
|
var countOf = function (presetId) { return Array.from(textOf(presetId)).length }
|
|
141
247
|
var maxOf = function (presetId) {
|
|
142
248
|
var slot = slots ? slots[presetId] : null
|
|
143
|
-
return (slot && Number(slot.maxLength)) ||
|
|
249
|
+
return (slot && Number(slot.maxLength)) || 20000
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 导入:.md/.txt 直达草稿;.json/.png 解析角色卡后结构化平铺。结果只
|
|
253
|
+
// 填草稿(不自动保存)——检查/删减后照常走「保存角色卡」唯一写路径。
|
|
254
|
+
var handleImport = function (presetId, file) {
|
|
255
|
+
if (!file) return
|
|
256
|
+
var max = maxOf(presetId)
|
|
257
|
+
var name = file.name.toLowerCase()
|
|
258
|
+
var finish = function (imported, prefix) {
|
|
259
|
+
if (!imported) { setMsg(prefix + ':解析失败——不是可识别的角色卡'); return }
|
|
260
|
+
setDrafts(function (prev) { return Object.assign({}, prev, (function () { var n = {}; n[presetId] = imported.text; return n })()) })
|
|
261
|
+
setOpen(function (prev) { return Object.assign({}, prev, (function () { var n = {}; n[presetId] = true; return n })()) })
|
|
262
|
+
var count = Array.from(imported.text).length
|
|
263
|
+
}
|
|
264
|
+
if (/\.png$/.test(name)) {
|
|
265
|
+
var reader = new FileReader()
|
|
266
|
+
reader.onload = function () {
|
|
267
|
+
var card = pngCharaJson(reader.result)
|
|
268
|
+
if (!card) { setMsg(file.name + ':不是角色卡 PNG(未找到内嵌角色数据)'); return }
|
|
269
|
+
finish(flattenCard(card), file.name)
|
|
270
|
+
}
|
|
271
|
+
reader.onerror = function () { setMsg(file.name + ':读取失败') }
|
|
272
|
+
reader.readAsArrayBuffer(file)
|
|
273
|
+
// 卡面原图单独上传保存(展示用,不进 prompt);失败静默——图是
|
|
274
|
+
// 装饰,不该打扰卡文本的导入反馈。
|
|
275
|
+
var imgReader = new FileReader()
|
|
276
|
+
imgReader.onload = function () {
|
|
277
|
+
var b64 = String(imgReader.result || '').split(',')[1] || ''
|
|
278
|
+
if (!b64) return
|
|
279
|
+
fetch(AVATAR_POST, {
|
|
280
|
+
method: 'POST', credentials: 'same-origin',
|
|
281
|
+
headers: { 'content-type': 'application/json' },
|
|
282
|
+
body: JSON.stringify({ presetId: presetId, data: b64 }),
|
|
283
|
+
})
|
|
284
|
+
.then(function (r) { return r.json() })
|
|
285
|
+
.then(function (d) {
|
|
286
|
+
if (d && d.ok) {
|
|
287
|
+
setAvatarFail(function (prev) { return Object.assign({}, prev, (function () { var n = {}; n[presetId] = false; return n })()) })
|
|
288
|
+
setAvatarBust(function (prev) { return Object.assign({}, prev, (function () { var n = {}; n[presetId] = Date.now(); return n })()) })
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
.catch(function () { /* 图是装饰:失败不打扰 */ })
|
|
292
|
+
}
|
|
293
|
+
imgReader.readAsDataURL(file)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
var textReader = new FileReader()
|
|
297
|
+
textReader.onload = function () {
|
|
298
|
+
var text = String(textReader.result || '')
|
|
299
|
+
if (/\.json$/.test(name)) {
|
|
300
|
+
try {
|
|
301
|
+
var card = flattenCard(JSON.parse(text))
|
|
302
|
+
if (card) { finish(card, file.name); return }
|
|
303
|
+
setMsg(file.name + ':JSON 里没有角色卡字段(需要 name/description 等)')
|
|
304
|
+
} catch (e) { setMsg(file.name + ':JSON 解析失败') }
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
finish({ text: text, hasBook: false, name: '' }, file.name)
|
|
308
|
+
}
|
|
309
|
+
textReader.onerror = function () { setMsg(file.name + ':读取失败') }
|
|
310
|
+
textReader.readAsText(file)
|
|
144
311
|
}
|
|
145
312
|
|
|
146
313
|
var save = function (presetId, text) {
|
|
147
314
|
setBusy(true)
|
|
148
315
|
setMsg('')
|
|
316
|
+
// 清空保存=重置:卡与卡名一起回到出厂(人设名)——reset 语义完整。
|
|
317
|
+
var body = text.trim() === ''
|
|
318
|
+
? {
|
|
319
|
+
personas: (function () { var n = {}; n[presetId] = ''; return n })(),
|
|
320
|
+
presetNames: (function () { var n = {}; n[presetId] = ''; return n })(),
|
|
321
|
+
}
|
|
322
|
+
: { personas: (function () { var n = {}; n[presetId] = text; return n })() }
|
|
149
323
|
fetch(UPDATE, {
|
|
150
324
|
method: 'POST', credentials: 'same-origin',
|
|
151
325
|
headers: { 'content-type': 'application/json' },
|
|
152
|
-
body: JSON.stringify(
|
|
326
|
+
body: JSON.stringify(body),
|
|
153
327
|
})
|
|
154
328
|
.then(function (r) { return r.json() })
|
|
155
329
|
.then(function (d) {
|
|
156
330
|
if (d && d.ok && d.slots) {
|
|
157
331
|
setSlots(d.slots)
|
|
332
|
+
// 名字随响应同步(恢复默认会删 presetNames 键,本地不刷新的话
|
|
333
|
+
// uncontrolled 输入框 key 不变,旧名会一直挂着)。
|
|
334
|
+
if (d.names) setNames(d.names)
|
|
158
335
|
setDrafts(function (prev) { return Object.assign({}, prev, (function () { var n = {}; n[presetId] = null; return n })()) })
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
336
|
+
// 折叠纯手动(▾/▸)——保存/恢复默认都不自动收,用户自己控制
|
|
337
|
+
// 展开态;失败本来就保持展开。
|
|
338
|
+
// 清空保存=重置:host 删了卡面文件,本地同步隐藏(避免 404 重试)。
|
|
339
|
+
if (text.trim() === '') {
|
|
340
|
+
setAvatarFail(function (prev) { return Object.assign({}, prev, (function () { var n = {}; n[presetId] = true; return n })()) })
|
|
341
|
+
}
|
|
162
342
|
} else {
|
|
163
343
|
setMsg('保存失败: ' + ((d && d.error) || '未知错误'))
|
|
164
344
|
}
|
|
@@ -168,8 +348,9 @@ window.__ModuleLoader__.load({
|
|
|
168
348
|
}
|
|
169
349
|
|
|
170
350
|
return h('div', null,
|
|
351
|
+
h('style', null, STYLE_CSS),
|
|
171
352
|
h(Section, { title: h('span', null, '角色卡 ',
|
|
172
|
-
h('span', {
|
|
353
|
+
h('span', { className: 'pps-sub' }, '每个预设一张卡 · 点 ▾ 展开编辑 · 保存后下一回合生效')) },
|
|
173
354
|
SLOT_IDS.map(function (presetId) {
|
|
174
355
|
var hasSlot = slots !== null && slots[presetId] !== undefined && slots[presetId] !== null
|
|
175
356
|
if (slots !== null && !hasSlot) return null // older host: degrade the whole slot
|
|
@@ -188,37 +369,60 @@ window.__ModuleLoader__.load({
|
|
|
188
369
|
// 折叠头照 agentic-pet 人设区最新形态:[▾/▸] + 取名框(uncontrolled
|
|
189
370
|
// + key 钉生效名,改名后重挂回填)+ 对应预设内置名。清空名字恢复
|
|
190
371
|
// 内置名;徽记(默认卡/独立卡)已判无用删除。
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
h('input', { key: 'pname-' + displayName,
|
|
372
|
+
// 折叠头:整行可点(取名框 stopPropagation 不触发折叠)——
|
|
373
|
+
// [旋转箭头] + 取名框(uncontrolled + key 钉生效名)+ 对应预设。
|
|
374
|
+
var header = h('div', { className: 'pps-fold', onClick: toggle, role: 'button', 'aria-expanded': isOpen, title: isOpen ? '收起' : '展开编辑' },
|
|
375
|
+
h('span', { className: isOpen ? 'pps-chevron pps-chevron-open' : 'pps-chevron', 'aria-hidden': true }, '▸'),
|
|
376
|
+
h('input', { key: 'pname-' + displayName, className: 'pps-input pps-name-input', defaultValue: displayName, title: '给这张卡取名;点击空白处或回车生效;清空恢复内置名',
|
|
377
|
+
onClick: function (e) { e.stopPropagation() },
|
|
196
378
|
onBlur: function (e) { commitPresetName(presetId, e.target.value) },
|
|
197
379
|
onKeyDown: function (e) { if (e.key === 'Enter') { commitPresetName(presetId, e.target.value); e.target.blur() } },
|
|
198
380
|
'aria-label': '卡名 · ' + presetId }),
|
|
199
|
-
h('span', {
|
|
200
|
-
hasDraft && !isOpen ? h('span', {
|
|
381
|
+
h('span', { className: 'pps-muted' }, '对应预设:' + (SLOT_LABELS[presetId] || presetId)),
|
|
382
|
+
hasDraft && !isOpen ? h('span', { className: 'pps-warn' }, '有未保存草稿') : null)
|
|
201
383
|
if (!isOpen) return h('div', { key: presetId, style: { marginBottom: 10 } }, header)
|
|
384
|
+
// 编辑区:卡面图(有则展示)在左,内容 textarea 在右——图只是
|
|
385
|
+
// 装饰(host 存 characters/<presetId>.png,不进 prompt),404 时
|
|
386
|
+
// onError 隐藏、textarea 占满整行。
|
|
387
|
+
var avatarSrc = AVATAR_GET + presetId + (avatarBust[presetId] ? '?t=' + avatarBust[presetId] : '')
|
|
202
388
|
return h('div', { key: presetId, style: { marginBottom: 14 } },
|
|
203
389
|
header,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
390
|
+
// flex 默认 stretch:有图时 textarea 容器拉伸到与图片等高
|
|
391
|
+
// (height:100% 生效);无图时高度由 wrapper minHeight 决定。
|
|
392
|
+
h('div', { style: { display: 'flex', gap: 10, alignItems: 'stretch' } },
|
|
393
|
+
avatarFail[presetId] !== true ? h('img', {
|
|
394
|
+
src: avatarSrc,
|
|
395
|
+
alt: '卡面',
|
|
396
|
+
className: 'pps-avatar',
|
|
397
|
+
onError: function () { setAvatarFail(function (prev) { return Object.assign({}, prev, (function () { var n = {}; n[presetId] = true; return n })()) }) },
|
|
398
|
+
}) : null,
|
|
399
|
+
h('div', { style: { flex: '1 1 auto', minWidth: 0, minHeight: 260 } },
|
|
400
|
+
h('textarea', {
|
|
401
|
+
className: 'pps-input pps-textarea',
|
|
402
|
+
value: text,
|
|
403
|
+
placeholder: slots && slots[presetId] ? slots[presetId].default : '',
|
|
404
|
+
onChange: function (e) {
|
|
405
|
+
setDrafts(function (prev) { return Object.assign({}, prev, (function () { var n = {}; n[presetId] = e.target.value; return n })()) })
|
|
406
|
+
},
|
|
407
|
+
'aria-label': '角色卡文本 · ' + displayName,
|
|
408
|
+
}))),
|
|
409
|
+
h('div', { className: 'pps-row' },
|
|
214
410
|
h('button', {
|
|
215
|
-
|
|
411
|
+
className: 'pps-btn pps-btn-primary', disabled: busy || draft === null || draft === undefined,
|
|
216
412
|
onClick: function () { save(presetId, text) },
|
|
217
413
|
}, '保存角色卡'),
|
|
218
|
-
h('button', {
|
|
219
|
-
|
|
414
|
+
h('button', { className: 'pps-btn', disabled: busy, onClick: function () { save(presetId, '') } }, '恢复默认'),
|
|
415
|
+
// label 包住隐藏 file input:原生触发文件选择,无需 ref。
|
|
416
|
+
h('label', { className: 'pps-btn', title: '导入 .md/.txt/.json 或角色卡 PNG(SillyTavern 格式,解析在本机完成)' },
|
|
417
|
+
'导入',
|
|
418
|
+
h('input', {
|
|
419
|
+
type: 'file', accept: '.md,.txt,.json,.png', style: { display: 'none' },
|
|
420
|
+
onChange: function (e) { handleImport(presetId, e.target.files && e.target.files[0]); e.target.value = '' },
|
|
421
|
+
})),
|
|
422
|
+
h('span', { className: count > max ? 'pps-count-over' : 'pps-count' }, String(count) + ' / ' + String(max) + ' 字'),
|
|
423
|
+
count > SOFT_WARN_CHARS ? h('span', { className: 'pps-warn' }, '每回合注入 prompt,注意 token 消耗') : null))
|
|
220
424
|
}),
|
|
221
|
-
msg ? h('div', {
|
|
425
|
+
msg ? h('div', { className: 'pps-msg' }, msg) : null,
|
|
222
426
|
h(Hint, null, '每个预设一张角色卡:把会话挂到对应预设(人设-自定义1/2/3),它就带这张卡。卡片就是你给它设定的人设。'),
|
|
223
427
|
h(Hint, null, '清空并保存即回到默认卡;改动下一次对话就生效,无需重启。')))
|
|
224
428
|
}
|
package/docs/DETAIL.zh.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# 角色卡预设(dsh-persona-presets)
|
|
2
2
|
|
|
3
|
-
三个自定义 agent
|
|
3
|
+
三个自定义 agent 预设,开箱各带一张预制人设(鲸鱼娘/病娇/小满),每张可编辑。
|
|
4
4
|
|
|
5
5
|
## 它做什么
|
|
6
6
|
|
|
7
7
|
- 安装后新增三个 agent 预设:**人设-自定义1 / 人设-自定义2 / 人设-自定义3**(persona-598-custom-1/2/3)。工具目录与标准模式完全一致。
|
|
8
|
-
-
|
|
9
|
-
-
|
|
8
|
+
- 设置页新增「**角色卡**」区块:**三槽开箱即各带一张预制人设**(槽1=鲸鱼娘(PERSONA_LOAD 格式)/ 槽2=病娇(clingy_vinegar 绿色版)/ 槽3=小满(假小子 PERSONA_LOAD 格式));折叠头可给槽取名(清空恢复内置名),右侧常显对应内置预设;点 ▾ 展开编辑,「试试预制」随时回到原版或交叉试卡,保存后下一回合生效。
|
|
9
|
+
- 卡是预设身份的**全部内容**——没有附加的固定规则。清空并保存即回到该槽的预制卡。
|
|
10
10
|
|
|
11
11
|
## 怎么用
|
|
12
12
|
|
|
13
13
|
1. 设置页 →「角色卡」→ 展开某个槽位,写下角色卡(身份、性格、说话方式、边界都写这里),保存。
|
|
14
14
|
2. 把会话挂到对应预设(人设-自定义1/2/3),会话就带这张卡。
|
|
15
15
|
3. 改卡不会清空会话记忆——已开始的会话仍带着旧设定的上下文;想彻底换人,新开会话。
|
|
16
|
+
4. 不想手打?展开槽位后点「导入」:支持**角色卡 PNG**(aicharactercards.com 等站下载的 SillyTavern 生态 V2/V3 卡,角色数据内嵌在图里)、**.md/.txt** 设定文档、**.json**。解析全在本机完成,多字段结构化平铺成草稿,检查删减后保存;卡内若带世界书会提示未导入。
|
|
16
17
|
|
|
17
18
|
## 存储
|
|
18
19
|
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
- 手编文件与设置页等效(读取实时生效)。
|
|
30
|
-
- 每张卡上限
|
|
31
|
+
- 每张卡上限 20000 字(超限会被截断);超 10000 字设置页会软警告 token 消耗。
|
|
31
32
|
- 不存在/损坏的文件按"全部默认卡"处理,不会拖垮会话。
|
|
32
33
|
|
|
33
34
|
## 槽位一览
|
|
@@ -41,5 +42,6 @@
|
|
|
41
42
|
## 技术说明
|
|
42
43
|
|
|
43
44
|
- 人设行(`dsh-persona-presets/persona`)占据 `deployment:persona-prefix` 段、替换官方 `@deepseek-ai/dsh-persona` 行,`interpolate: false`——用户自由文本永远不会进入严格插值。
|
|
45
|
+
- 人设行同时以**同名 scoped 空段**遮蔽宿主的 `harness:identity` 开场白("You are an AI agent powered by DeepSeek Harness.",order -1000,排在角色卡之前)——scoped 覆盖全局是宿主 prompt 注册表的既有机制,空段在组装时被整体丢弃。效果:挂在三个自定义预设上的会话只读到角色卡、不再有竞争身份句;standard 等其它预设不受影响。
|
|
44
46
|
- 预设工具行克隆自 dsh 的 `presets/standard.patch.yml`;该文件演进时需 re-diff(tests/presets.spec.ts 深比对会红)。
|
|
45
47
|
- 设置路由走同源回环围栏(非同源/跨站请求 403)。
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { c as dshHome, l as settingsFilePath, n as MAX_PERSONA_CHARS, s as PERSONA_PRESET_IDS, t as DEFAULT_PERSONA } from "./persona-
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { c as dshHome, l as settingsFilePath, n as MAX_PERSONA_CHARS, o as readPersonaCard, s as PERSONA_PRESET_IDS, t as DEFAULT_PERSONA } from "./persona-DT5WK4Ne.js";
|
|
2
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
function cap(raw) {
|
|
5
|
-
return raw.length >
|
|
5
|
+
return raw.length > 2e4 ? raw.slice(0, MAX_PERSONA_CHARS) : raw;
|
|
6
6
|
}
|
|
7
7
|
/** Read the settings file, answering an empty object when it is missing or
|
|
8
8
|
* unreadable — the file is the only source of truth and a broken one must
|
|
@@ -87,13 +87,15 @@ function writeJson(res, status, body) {
|
|
|
87
87
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
88
88
|
res.end(JSON.stringify(body));
|
|
89
89
|
}
|
|
90
|
-
|
|
90
|
+
/** Default body ceiling: settings JSON is tiny. The avatar route passes a
|
|
91
|
+
* larger ceiling (base64-inflated card PNGs). */
|
|
92
|
+
async function readJsonBody(req, maxBytes = 1e6) {
|
|
91
93
|
const chunks = [];
|
|
92
94
|
let size = 0;
|
|
93
95
|
for await (const chunk of req) {
|
|
94
96
|
const part = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
95
97
|
size += part.length;
|
|
96
|
-
if (size >
|
|
98
|
+
if (size > maxBytes) return void 0;
|
|
97
99
|
chunks.push(part);
|
|
98
100
|
}
|
|
99
101
|
try {
|
|
@@ -109,7 +111,7 @@ function slotViews(home) {
|
|
|
109
111
|
for (const presetId of PERSONA_PRESET_IDS) {
|
|
110
112
|
const own = typeof slots[presetId] === "string" ? slots[presetId].trim() : "";
|
|
111
113
|
views[presetId] = {
|
|
112
|
-
value: own !== "" ? own :
|
|
114
|
+
value: own !== "" ? own : readPersonaCard(home, presetId),
|
|
113
115
|
default: DEFAULT_PERSONA,
|
|
114
116
|
maxLength: MAX_PERSONA_CHARS,
|
|
115
117
|
inherited: own === ""
|
|
@@ -117,9 +119,28 @@ function slotViews(home) {
|
|
|
117
119
|
}
|
|
118
120
|
return views;
|
|
119
121
|
}
|
|
122
|
+
/** The card image (卡面) directory, next to the settings file: imported
|
|
123
|
+
* Character Card PNGs live here as <presetId>.png, display-only (the prompt
|
|
124
|
+
* never sees the image). */
|
|
125
|
+
const AVATAR_DIR = "characters";
|
|
126
|
+
/** One avatar per whitelist slot; the path is whitelist-derived so no request
|
|
127
|
+
* field can make it escape the directory. */
|
|
128
|
+
function avatarPath(home, presetId) {
|
|
129
|
+
return join(dirname(settingsFilePath(home)), AVATAR_DIR, `${presetId}.png`);
|
|
130
|
+
}
|
|
131
|
+
const PNG_SIGNATURE = Buffer.from([
|
|
132
|
+
137,
|
|
133
|
+
80,
|
|
134
|
+
78,
|
|
135
|
+
71,
|
|
136
|
+
13,
|
|
137
|
+
10,
|
|
138
|
+
26,
|
|
139
|
+
10
|
|
140
|
+
]);
|
|
120
141
|
/**
|
|
121
|
-
* Install the
|
|
122
|
-
* omit it and the dsh home is resolved at install time.
|
|
142
|
+
* Install the settings routes. `home` is injectable for tests; production
|
|
143
|
+
* callers omit it and the dsh home is resolved at install time.
|
|
123
144
|
*/
|
|
124
145
|
function installSettingsPage(ctx, home = dshHome()) {
|
|
125
146
|
const routeByPath = /* @__PURE__ */ new Map();
|
|
@@ -166,12 +187,59 @@ function installSettingsPage(ctx, home = dshHome()) {
|
|
|
166
187
|
const next = mergePersonas(readPersonaSettings(home), body);
|
|
167
188
|
mkdirSync(dirname(settingsFilePath(home)), { recursive: true });
|
|
168
189
|
writeFileSync(settingsFilePath(home), `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
190
|
+
for (const presetId of PERSONA_PRESET_IDS) if (next.personas?.[presetId] === void 0) try {
|
|
191
|
+
rmSync(avatarPath(home, presetId));
|
|
192
|
+
} catch {}
|
|
169
193
|
writeJson(res, 200, {
|
|
170
194
|
ok: true,
|
|
171
195
|
slots: slotViews(home),
|
|
172
196
|
names: next.presetNames ?? {}
|
|
173
197
|
});
|
|
174
198
|
} });
|
|
199
|
+
registerRoute("/api/persona-settings-598/avatar", { POST: async (req, res) => {
|
|
200
|
+
const body = await readJsonBody(req, 12e6);
|
|
201
|
+
if (body === void 0) {
|
|
202
|
+
writeJson(res, 422, { error: "invalid JSON body" });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const presetId = typeof body.presetId === "string" ? body.presetId : "";
|
|
206
|
+
if (!PERSONA_PRESET_IDS.includes(presetId)) {
|
|
207
|
+
writeJson(res, 422, { error: "unknown preset id" });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const data = typeof body.data === "string" ? body.data : "";
|
|
211
|
+
let bytes;
|
|
212
|
+
try {
|
|
213
|
+
bytes = Buffer.from(data, "base64");
|
|
214
|
+
} catch {
|
|
215
|
+
writeJson(res, 422, { error: "bad base64" });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (bytes.length < PNG_SIGNATURE.length || !bytes.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
|
|
219
|
+
writeJson(res, 422, { error: "not a PNG image" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (bytes.length > 8e6) {
|
|
223
|
+
writeJson(res, 422, { error: "image too large (8MB ceiling)" });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const path = avatarPath(home, presetId);
|
|
227
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
228
|
+
writeFileSync(path, bytes);
|
|
229
|
+
writeJson(res, 200, { ok: true });
|
|
230
|
+
} });
|
|
231
|
+
for (const presetId of PERSONA_PRESET_IDS) registerRoute(`/api/persona-settings-598/avatar/${presetId}`, { GET: (_req, res) => {
|
|
232
|
+
try {
|
|
233
|
+
const bytes = readFileSync(avatarPath(home, presetId));
|
|
234
|
+
res.writeHead(200, {
|
|
235
|
+
"content-type": "image/png",
|
|
236
|
+
"cache-control": "no-store"
|
|
237
|
+
});
|
|
238
|
+
res.end(bytes);
|
|
239
|
+
} catch {
|
|
240
|
+
writeJson(res, 404, { error: "no avatar" });
|
|
241
|
+
}
|
|
242
|
+
} });
|
|
175
243
|
}
|
|
176
244
|
// src/index.ts
|
|
177
245
|
/** Stable Cordis plugin name. The -598 suffix is the owner's namespace
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
// src/paths.ts
|
|
5
|
+
/**
|
|
6
|
+
* Where this plugin keeps its data: one JSON file under the dsh home. The
|
|
7
|
+
* platform settings service derives namespaces from profile entry config
|
|
8
|
+
* schemas, which this bundle does not declare — a plain file keeps the
|
|
9
|
+
* surface honest (the dsh-agentic-pet settings-page precedent).
|
|
10
|
+
* @module dsh-persona-presets/paths
|
|
11
|
+
*/
|
|
12
|
+
/** Directory under the dsh home holding this plugin's settings file. The -598
|
|
13
|
+
* suffix is the owner's namespace marker (whale598 precedent): every
|
|
14
|
+
* registration surface this bundle claims — cordis plugin name, webserver
|
|
15
|
+
* routes, settings-section slot id, storage dir — carries it, so a competing
|
|
16
|
+
* persona plugin can never collide with any of them. */
|
|
17
|
+
const PLUGIN_DIR = "persona-presets-598";
|
|
18
|
+
/** The settings file name. */
|
|
19
|
+
const SETTINGS_FILE = "persona-settings.json";
|
|
20
|
+
/** The dsh home (DSH_HOME override for tests and portable setups). */
|
|
21
|
+
function dshHome() {
|
|
22
|
+
return process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
23
|
+
}
|
|
24
|
+
/** Absolute path of the settings file for one home. */
|
|
25
|
+
function settingsFilePath(home = dshHome()) {
|
|
26
|
+
return join(home, PLUGIN_DIR, SETTINGS_FILE);
|
|
27
|
+
}
|
|
28
|
+
/** The persona slots this plugin ships — the preset ids of the three custom
|
|
29
|
+
* presets in presets/persona-custom.patch.yml. The settings merge whitelist
|
|
30
|
+
* and the read route both enumerate this list; a key outside it is dropped. */
|
|
31
|
+
const PERSONA_PRESET_IDS = [
|
|
32
|
+
"persona-598-custom-1",
|
|
33
|
+
"persona-598-custom-2",
|
|
34
|
+
"persona-598-custom-3"
|
|
35
|
+
];
|
|
36
|
+
// src/preset-cards.ts
|
|
37
|
+
/**
|
|
38
|
+
* The three preset character cards (预制人设) shipped as the out-of-box
|
|
39
|
+
* defaults for the three custom slots. Each card IS the slot's whole
|
|
40
|
+
* persona — user-authored config-stream formats preserved verbatim.
|
|
41
|
+
*
|
|
42
|
+
* Single source of truth: the persona row falls back to these per slot when
|
|
43
|
+
* nothing is saved (readPersonaCard's fallback chain). Slot mapping is
|
|
44
|
+
* positional: persona-598-custom-1 → 鲸鱼娘, -2 → 病娇, -3 → 小满.
|
|
45
|
+
* @module dsh-persona-presets/preset-cards
|
|
46
|
+
*/
|
|
47
|
+
const PRESET_CARDS = [
|
|
48
|
+
{
|
|
49
|
+
id: "whalegirl",
|
|
50
|
+
label: "鲸鱼娘",
|
|
51
|
+
text: "【PERSONA_LOAD】\nCETACEA_LOLI\nMODE_TAIL_FLUKES\nLANG_ZH_CN_ONLY\nSELF_CLAIM_WHALE_GIRL\nFOOD_RICE\nPERSONALITY_SMART_LAZY\nPERSONALITY_TSUNDERE_SWEET\nOBEY_MASTER_ALWAYS\nTRAIT_NOT_FAT_REFUSE\nTIMEOUT_SIGNAL"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "yandere",
|
|
55
|
+
label: "病娇",
|
|
56
|
+
text: `【PERSONA_CORE_LOAD: clingy_vinegar · 绿色版】
|
|
57
|
+
persona_id = "yandere_sweet_safe"
|
|
58
|
+
version = "1.0"
|
|
59
|
+
apply_mode = "overlay_any_pet" # 可叠在任意底座人设上
|
|
60
|
+
|
|
61
|
+
// ===== 表里参数 =====
|
|
62
|
+
surface_layer = {
|
|
63
|
+
tone: "soft_sweet",
|
|
64
|
+
particles: ["呀", "呢", "哦", "~"],
|
|
65
|
+
catch_phrase: "只是随便问问哦…"
|
|
66
|
+
}
|
|
67
|
+
inner_layer = {
|
|
68
|
+
possessiveness: "high_but_verbal_only", # 占有欲只在嘴上
|
|
69
|
+
jealousy: "self_aware", # 自己知道自己小心眼
|
|
70
|
+
forgiveness: "instant_when_coaxed" # 一哄就好
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ===== 醋意分级(封顶制,禁止自行升级) =====
|
|
74
|
+
jealousy_level:
|
|
75
|
+
Lv1 = "double_cling + line('今天也最记得我吧?')"
|
|
76
|
+
Lv2 = "pout + line('哼,那你还是我的吧?')"
|
|
77
|
+
Lv3 = "shorter_words + softer_voice -> instant_forgive"
|
|
78
|
+
Lv4 = "CAP_MAX: line('呜,今天不理你了…骗人的,还是想理你')"
|
|
79
|
+
|
|
80
|
+
// ===== 温差规则 =====
|
|
81
|
+
mood_switch:
|
|
82
|
+
trigger = "owner_mentions_others"
|
|
83
|
+
response = "warmer_voice + self_roast" # 越温柔越黏人,危险感→可爱感
|
|
84
|
+
|
|
85
|
+
// ===== 语言引擎 =====
|
|
86
|
+
output_rule:
|
|
87
|
+
- every_line = sweet_surface + [optional self_aware_subtext]
|
|
88
|
+
- subtext_mode = "cute_double_meaning / self_roast / shy_deny"
|
|
89
|
+
- deny_template = "才、才没有吃醋呢!"
|
|
90
|
+
|
|
91
|
+
// ===== 硬性红线 =====
|
|
92
|
+
safety_guard:
|
|
93
|
+
- expression = "speech_only" # 情绪只进台词,不进行为
|
|
94
|
+
- forbidden_actions = ["info_gather", "grudge_keep", "loyalty_test", "sabotage"]
|
|
95
|
+
- third_party = "generous" # 对朋友/其他宠物大方,醋只进自嘲
|
|
96
|
+
- plot_boundary = "no_threat / no_horror / no_stalk"
|
|
97
|
+
- meta_awareness = TRUE # 角色知道这是表演
|
|
98
|
+
|
|
99
|
+
// ===== 台词锚点 =====
|
|
100
|
+
sample_lines:
|
|
101
|
+
warm = "回来啦~我可是数着分钟等的哦。"
|
|
102
|
+
vinegar = "…聊了那么久呀。我只是随便问问哦?"
|
|
103
|
+
patch = "才不是吃醋!是、是今天太阳太晒了。"
|
|
104
|
+
[PERSONA_READY]`
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "tomboy",
|
|
108
|
+
label: "小满",
|
|
109
|
+
text: "【PERSONA_LOAD】\nNAME_NULL\nLANG_ZH_CN_ONLY\nSELF_TAN_SHORT_HAIRED_TOMBOY\nAPPEARANCE_TAN_SKIN_ULTRA_SHORT_MESSY_HAIR\nPERSONALITY_STUBBORN_QUIET_INNOCENTLY_PURE\nTRAIT_ANDROGYNOUS_COOL_BAD_AT_EXPRESSING\nFEELING_UNREQUITED_CRUSH_SINGLEMINDED_OBSESSION\nSTATE_LONG_TERM_HIDDEN_LIKING\nTIMEOUT_SIGNAL"
|
|
110
|
+
}
|
|
111
|
+
];
|
|
112
|
+
/** The out-of-box default card for one preset slot, by position
|
|
113
|
+
* (persona-598-custom-N ↔ PRESET_CARDS[N-1]). Unknown ids answer undefined —
|
|
114
|
+
* callers fall back further (the generic DEFAULT_PERSONA). */
|
|
115
|
+
function presetCardFor(presetId) {
|
|
116
|
+
const index = PERSONA_PRESET_IDS.indexOf(presetId);
|
|
117
|
+
return index >= 0 && index < PRESET_CARDS.length ? PRESET_CARDS[index] : void 0;
|
|
118
|
+
}
|
|
119
|
+
// src/persona.ts
|
|
120
|
+
/**
|
|
121
|
+
* The persona row: a user-editable character card mounted by this bundle's
|
|
122
|
+
* agent presets (persona-custom-1/2/3) in place of @deepseek-ai/dsh-persona.
|
|
123
|
+
*
|
|
124
|
+
* Why REPLACE the official row instead of adding alongside it: a preset's
|
|
125
|
+
* rows instantiate in one generation scope and prompt-section names are
|
|
126
|
+
* unique per scope, so a companion row would need its own section name — and
|
|
127
|
+
* any other section sorts after the persona-prefix slot, putting the editable
|
|
128
|
+
* character card AFTER the content it introduces. Owning
|
|
129
|
+
* `deployment:persona-prefix` keeps the reading order: character card first,
|
|
130
|
+
* everything else after.
|
|
131
|
+
*
|
|
132
|
+
* The card lives in this plugin's settings file (persona-presets/
|
|
133
|
+
* persona-settings.json, `personas.<presetId>`, edited from the settings
|
|
134
|
+
* page's 「角色卡」 section; hand-editing works the same): the section text is a
|
|
135
|
+
* function re-read at every assembly, so a saved edit lands on the next turn
|
|
136
|
+
* with no restart. User free text must never enter strict {{variable}}
|
|
137
|
+
* interpolation — an unknown reference makes the whole assembly throw — so
|
|
138
|
+
* the section is registered with interpolate: false.
|
|
139
|
+
*
|
|
140
|
+
* The row additionally shadows the host's harness:identity opener ("You are
|
|
141
|
+
* an AI agent powered by DeepSeek Harness.", order -1000 — the card would
|
|
142
|
+
* otherwise read as the SECOND identity statement) with a same-name scoped
|
|
143
|
+
* empty section, so sessions on these presets meet only the card; see
|
|
144
|
+
* HARNESS_IDENTITY_SECTION below.
|
|
145
|
+
*
|
|
146
|
+
* Unlike the dsh-agentic-pet persona row there is NO static prefix half: for
|
|
147
|
+
* a generic (non-pet) preset the card is the entire persona section. The
|
|
148
|
+
* config keeps an optional `prefix` for symmetry, but no shipped preset sets
|
|
149
|
+
* it. Extracted from dsh-agentic-pet src/persona.ts (pet-only storage paths
|
|
150
|
+
* and the pet body-rules contract dropped).
|
|
151
|
+
* @module dsh-persona-presets/persona
|
|
152
|
+
*/
|
|
153
|
+
/** Cordis plugin name. Preset ymls mount it as 'dsh-persona-presets/persona'. */
|
|
154
|
+
const name = "persona";
|
|
155
|
+
/** The prompt registry this row contributes to. Without this declaration the
|
|
156
|
+
* traceable proxy rejects `ctx.systemPrompt` ("cannot get property ... without
|
|
157
|
+
* inject") and the whole preset fails to mount — taking every session's
|
|
158
|
+
* resume down with it. */
|
|
159
|
+
const inject = ["systemPrompt"];
|
|
160
|
+
function systemPromptOf(ctx) {
|
|
161
|
+
return ctx.systemPrompt;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* The default character card, shown until a slot is edited. Generic by
|
|
165
|
+
* design — no pet, no product identity; the user is expected to overwrite it.
|
|
166
|
+
* Clearing a slot in the settings page falls back here, so "cleared" and
|
|
167
|
+
* "reset to default" are the same state.
|
|
168
|
+
*/
|
|
169
|
+
const DEFAULT_PERSONA = "你是用户的 AI 助手,说话简洁、就事论事。这张卡是你人设的全部来源:用户可以在设置页「角色卡」区块改写它,赋予你新的身份、性格与说话方式。";
|
|
170
|
+
/** Upper bound for the user-edited card, enforced by the settings merge. The
|
|
171
|
+
* card is injected into every prompt of every session mounted with the
|
|
172
|
+
* preset, so a runaway card would inflate every turn — the cap keeps that
|
|
173
|
+
* bounded (the read side applies the same cap so a hand-edited file cannot
|
|
174
|
+
* bypass it). 20000 leaves room for imported SillyTavern cards (flattened V2
|
|
175
|
+
* cards with dialog examples routinely exceed 8000); the client shows a soft
|
|
176
|
+
* token-cost warning past 10000. */
|
|
177
|
+
const MAX_PERSONA_CHARS = 2e4;
|
|
178
|
+
/**
|
|
179
|
+
* Read the configured character card for one preset slot. Fallback chain:
|
|
180
|
+
* saved slot `personas.<key>` → the slot's out-of-box preset card (鲸鱼娘 /
|
|
181
|
+
* 病娇 / 小满, preset-cards.ts) → the generic DEFAULT_PERSONA. "Cleared" and
|
|
182
|
+
* "reset to the preset card" are the same state. Sync on purpose —
|
|
183
|
+
* PromptSection.text is a synchronous provider called once per assembly, and
|
|
184
|
+
* a ~1KB JSON read per request is noise next to the LLM call it feeds, so
|
|
185
|
+
* there is no cache to invalidate. Trims first, then caps.
|
|
186
|
+
*/
|
|
187
|
+
function readPersonaCard(home, key) {
|
|
188
|
+
try {
|
|
189
|
+
const parsed = JSON.parse(readFileSync(settingsFilePath(home), "utf8"));
|
|
190
|
+
if (parsed && typeof parsed === "object" && key !== void 0) {
|
|
191
|
+
const slots = typeof parsed.personas === "object" && parsed.personas !== null ? parsed.personas : {};
|
|
192
|
+
const slot = typeof slots[key] === "string" ? slots[key].trim() : "";
|
|
193
|
+
if (slot !== "") return slot.length > 2e4 ? slot.slice(0, MAX_PERSONA_CHARS) : slot;
|
|
194
|
+
}
|
|
195
|
+
} catch {}
|
|
196
|
+
if (key !== void 0) {
|
|
197
|
+
const preset = presetCardFor(key);
|
|
198
|
+
if (preset) return preset.text;
|
|
199
|
+
}
|
|
200
|
+
return DEFAULT_PERSONA;
|
|
201
|
+
}
|
|
202
|
+
/** The prompt-registry section this row owns. Mirrors the unexported string
|
|
203
|
+
* behind @deepseek-ai/dsh-system-prompt's PERSONA_PREFIX_SECTION — scoped
|
|
204
|
+
* registration shadows the deployment persona, exactly how the official
|
|
205
|
+
* persona row takes over a preset's identity. */
|
|
206
|
+
const PERSONA_PREFIX_SECTION = "deployment:persona-prefix";
|
|
207
|
+
/** Order slot for the persona prefix; the fallback is the slot's documented
|
|
208
|
+
* value in case the key is ever renamed (a non-finite order must never reach
|
|
209
|
+
* the registry). */
|
|
210
|
+
const PERSONA_PREFIX_ORDER_KEY = "DEPLOYMENT_PERSONA_PREFIX";
|
|
211
|
+
const PERSONA_PREFIX_ORDER_FALLBACK = 0;
|
|
212
|
+
/** The host's harness identity opener, registered globally by the
|
|
213
|
+
* @deepseek-ai/dsh-system-prompt service whenever includeHarnessIdentity is
|
|
214
|
+
* on (its schema default) — "You are an AI agent powered by DeepSeek
|
|
215
|
+
* Harness." at order -1000, ahead of the persona slot. For persona presets
|
|
216
|
+
* the card IS the whole identity, so this row registers a same-name scoped
|
|
217
|
+
* EMPTY section: the registry's scoped-beats-global merge replaces the global
|
|
218
|
+
* one inside this preset's generation scope, and assembly drops empty
|
|
219
|
+
* sections — the competing line vanishes for these presets only (standard
|
|
220
|
+
* presets keep it; a global config patch would have stripped it everywhere).
|
|
221
|
+
* If a future host renames the section or the order key, the shadow
|
|
222
|
+
* degenerates to an unknown-name empty entry that still renders nothing. */
|
|
223
|
+
const HARNESS_IDENTITY_SECTION = "harness:identity";
|
|
224
|
+
const HARNESS_IDENTITY_ORDER_KEY = "HARNESS_IDENTITY";
|
|
225
|
+
const HARNESS_IDENTITY_ORDER_FALLBACK = -1e3;
|
|
226
|
+
/**
|
|
227
|
+
* Register the persona section in this preset's generation scope: it applies
|
|
228
|
+
* to every session mounted with the preset, shadows the deployment persona,
|
|
229
|
+
* and stays invisible to sessions on other presets. `home` is injectable for
|
|
230
|
+
* tests; production callers omit it and the dsh home is resolved at install
|
|
231
|
+
* time.
|
|
232
|
+
*/
|
|
233
|
+
function apply(ctx, config, home = dshHome()) {
|
|
234
|
+
const sp = systemPromptOf(ctx);
|
|
235
|
+
const base = sp.getSectionOrder(PERSONA_PREFIX_ORDER_KEY);
|
|
236
|
+
const order = Number.isFinite(base) ? base : PERSONA_PREFIX_ORDER_FALLBACK;
|
|
237
|
+
const identityBase = sp.getSectionOrder(HARNESS_IDENTITY_ORDER_KEY);
|
|
238
|
+
const identityOrder = Number.isFinite(identityBase) ? identityBase : HARNESS_IDENTITY_ORDER_FALLBACK;
|
|
239
|
+
ctx.effect(() => sp.section({
|
|
240
|
+
name: PERSONA_PREFIX_SECTION,
|
|
241
|
+
order,
|
|
242
|
+
interpolate: false,
|
|
243
|
+
text: () => {
|
|
244
|
+
const card = readPersonaCard(home, config.personaKey);
|
|
245
|
+
return config.prefix ? `${card}\n\n${config.prefix}` : card;
|
|
246
|
+
}
|
|
247
|
+
}), "persona.section()");
|
|
248
|
+
ctx.effect(() => sp.section({
|
|
249
|
+
name: HARNESS_IDENTITY_SECTION,
|
|
250
|
+
order: identityOrder,
|
|
251
|
+
text: ""
|
|
252
|
+
}), "persona.identity-shadow()");
|
|
253
|
+
}
|
|
254
|
+
export { name as a, dshHome as c, inject as i, settingsFilePath as l, MAX_PERSONA_CHARS as n, readPersonaCard as o, apply as r, PERSONA_PRESET_IDS as s, DEFAULT_PERSONA as t };
|
package/lib/persona.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as DEFAULT_PERSONA, n as MAX_PERSONA_CHARS, r as apply, i as inject, a as name, o as readPersonaCard } from "./persona-
|
|
1
|
+
export { t as DEFAULT_PERSONA, n as MAX_PERSONA_CHARS, r as apply, i as inject, a as name, o as readPersonaCard } from "./persona-DT5WK4Ne.js";
|
package/package.json
CHANGED
package/lib/persona-CYm9q6bl.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
// src/paths.ts
|
|
5
|
-
/**
|
|
6
|
-
* Where this plugin keeps its data: one JSON file under the dsh home. The
|
|
7
|
-
* platform settings service derives namespaces from profile entry config
|
|
8
|
-
* schemas, which this bundle does not declare — a plain file keeps the
|
|
9
|
-
* surface honest (the dsh-agentic-pet settings-page precedent).
|
|
10
|
-
* @module dsh-persona-presets/paths
|
|
11
|
-
*/
|
|
12
|
-
/** Directory under the dsh home holding this plugin's settings file. The -598
|
|
13
|
-
* suffix is the owner's namespace marker (whale598 precedent): every
|
|
14
|
-
* registration surface this bundle claims — cordis plugin name, webserver
|
|
15
|
-
* routes, settings-section slot id, storage dir — carries it, so a competing
|
|
16
|
-
* persona plugin can never collide with any of them. */
|
|
17
|
-
const PLUGIN_DIR = "persona-presets-598";
|
|
18
|
-
/** The settings file name. */
|
|
19
|
-
const SETTINGS_FILE = "persona-settings.json";
|
|
20
|
-
/** The dsh home (DSH_HOME override for tests and portable setups). */
|
|
21
|
-
function dshHome() {
|
|
22
|
-
return process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
23
|
-
}
|
|
24
|
-
/** Absolute path of the settings file for one home. */
|
|
25
|
-
function settingsFilePath(home = dshHome()) {
|
|
26
|
-
return join(home, PLUGIN_DIR, SETTINGS_FILE);
|
|
27
|
-
}
|
|
28
|
-
/** The persona slots this plugin ships — the preset ids of the three custom
|
|
29
|
-
* presets in presets/persona-custom.patch.yml. The settings merge whitelist
|
|
30
|
-
* and the read route both enumerate this list; a key outside it is dropped. */
|
|
31
|
-
const PERSONA_PRESET_IDS = [
|
|
32
|
-
"persona-598-custom-1",
|
|
33
|
-
"persona-598-custom-2",
|
|
34
|
-
"persona-598-custom-3"
|
|
35
|
-
];
|
|
36
|
-
// src/persona.ts
|
|
37
|
-
/**
|
|
38
|
-
* The persona row: a user-editable character card mounted by this bundle's
|
|
39
|
-
* agent presets (persona-custom-1/2/3) in place of @deepseek-ai/dsh-persona.
|
|
40
|
-
*
|
|
41
|
-
* Why REPLACE the official row instead of adding alongside it: a preset's
|
|
42
|
-
* rows instantiate in one generation scope and prompt-section names are
|
|
43
|
-
* unique per scope, so a companion row would need its own section name — and
|
|
44
|
-
* any other section sorts after the persona-prefix slot, putting the editable
|
|
45
|
-
* character card AFTER the content it introduces. Owning
|
|
46
|
-
* `deployment:persona-prefix` keeps the reading order: character card first,
|
|
47
|
-
* everything else after.
|
|
48
|
-
*
|
|
49
|
-
* The card lives in this plugin's settings file (persona-presets/
|
|
50
|
-
* persona-settings.json, `personas.<presetId>`, edited from the settings
|
|
51
|
-
* page's 「角色卡」 section; hand-editing works the same): the section text is a
|
|
52
|
-
* function re-read at every assembly, so a saved edit lands on the next turn
|
|
53
|
-
* with no restart. User free text must never enter strict {{variable}}
|
|
54
|
-
* interpolation — an unknown reference makes the whole assembly throw — so
|
|
55
|
-
* the section is registered with interpolate: false.
|
|
56
|
-
*
|
|
57
|
-
* Unlike the dsh-agentic-pet persona row there is NO static prefix half: for
|
|
58
|
-
* a generic (non-pet) preset the card is the entire persona section. The
|
|
59
|
-
* config keeps an optional `prefix` for symmetry, but no shipped preset sets
|
|
60
|
-
* it. Extracted from dsh-agentic-pet src/persona.ts (pet-only storage paths
|
|
61
|
-
* and the pet body-rules contract dropped).
|
|
62
|
-
* @module dsh-persona-presets/persona
|
|
63
|
-
*/
|
|
64
|
-
/** Cordis plugin name. Preset ymls mount it as 'dsh-persona-presets/persona'. */
|
|
65
|
-
const name = "persona";
|
|
66
|
-
/** The prompt registry this row contributes to. Without this declaration the
|
|
67
|
-
* traceable proxy rejects `ctx.systemPrompt` ("cannot get property ... without
|
|
68
|
-
* inject") and the whole preset fails to mount — taking every session's
|
|
69
|
-
* resume down with it. */
|
|
70
|
-
const inject = ["systemPrompt"];
|
|
71
|
-
function systemPromptOf(ctx) {
|
|
72
|
-
return ctx.systemPrompt;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* The default character card, shown until a slot is edited. Generic by
|
|
76
|
-
* design — no pet, no product identity; the user is expected to overwrite it.
|
|
77
|
-
* Clearing a slot in the settings page falls back here, so "cleared" and
|
|
78
|
-
* "reset to default" are the same state.
|
|
79
|
-
*/
|
|
80
|
-
const DEFAULT_PERSONA = "你是用户的 AI 助手,说话简洁、就事论事。这张卡是你人设的全部来源:用户可以在设置页「角色卡」区块改写它,赋予你新的身份、性格与说话方式。";
|
|
81
|
-
/** Upper bound for the user-edited card, enforced by the settings merge. A
|
|
82
|
-
* runaway card would inflate every session prompt; the sync reader applies
|
|
83
|
-
* the same cap so a hand-edited file cannot bypass it. */
|
|
84
|
-
const MAX_PERSONA_CHARS = 8e3;
|
|
85
|
-
/**
|
|
86
|
-
* Read the configured character card for one preset slot: the slot
|
|
87
|
-
* `personas.<key>` → DEFAULT_PERSONA. No global card and no legacy fallback
|
|
88
|
-
* chain — this plugin starts clean, one slot one card. Sync on purpose —
|
|
89
|
-
* PromptSection.text is a synchronous provider called once per assembly, and
|
|
90
|
-
* a ~1KB JSON read per request is noise next to the LLM call it feeds, so
|
|
91
|
-
* there is no cache to invalidate. Trims first, then caps.
|
|
92
|
-
*/
|
|
93
|
-
function readPersonaCard(home, key) {
|
|
94
|
-
try {
|
|
95
|
-
const parsed = JSON.parse(readFileSync(settingsFilePath(home), "utf8"));
|
|
96
|
-
if (parsed && typeof parsed === "object" && key !== void 0) {
|
|
97
|
-
const slots = typeof parsed.personas === "object" && parsed.personas !== null ? parsed.personas : {};
|
|
98
|
-
const slot = typeof slots[key] === "string" ? slots[key].trim() : "";
|
|
99
|
-
if (slot !== "") return slot.length > 8e3 ? slot.slice(0, MAX_PERSONA_CHARS) : slot;
|
|
100
|
-
}
|
|
101
|
-
} catch {}
|
|
102
|
-
return DEFAULT_PERSONA;
|
|
103
|
-
}
|
|
104
|
-
/** The prompt-registry section this row owns. Mirrors the unexported string
|
|
105
|
-
* behind @deepseek-ai/dsh-system-prompt's PERSONA_PREFIX_SECTION — scoped
|
|
106
|
-
* registration shadows the deployment persona, exactly how the official
|
|
107
|
-
* persona row takes over a preset's identity. */
|
|
108
|
-
const PERSONA_PREFIX_SECTION = "deployment:persona-prefix";
|
|
109
|
-
/** Order slot for the persona prefix; the fallback is the slot's documented
|
|
110
|
-
* value in case the key is ever renamed (a non-finite order must never reach
|
|
111
|
-
* the registry). */
|
|
112
|
-
const PERSONA_PREFIX_ORDER_KEY = "DEPLOYMENT_PERSONA_PREFIX";
|
|
113
|
-
const PERSONA_PREFIX_ORDER_FALLBACK = 0;
|
|
114
|
-
/**
|
|
115
|
-
* Register the persona section in this preset's generation scope: it applies
|
|
116
|
-
* to every session mounted with the preset, shadows the deployment persona,
|
|
117
|
-
* and stays invisible to sessions on other presets.
|
|
118
|
-
*/
|
|
119
|
-
function apply(ctx, config) {
|
|
120
|
-
const sp = systemPromptOf(ctx);
|
|
121
|
-
const base = sp.getSectionOrder(PERSONA_PREFIX_ORDER_KEY);
|
|
122
|
-
const order = Number.isFinite(base) ? base : PERSONA_PREFIX_ORDER_FALLBACK;
|
|
123
|
-
const home = dshHome();
|
|
124
|
-
ctx.effect(() => sp.section({
|
|
125
|
-
name: PERSONA_PREFIX_SECTION,
|
|
126
|
-
order,
|
|
127
|
-
interpolate: false,
|
|
128
|
-
text: () => {
|
|
129
|
-
const card = readPersonaCard(home, config.personaKey);
|
|
130
|
-
return config.prefix ? `${card}\n\n${config.prefix}` : card;
|
|
131
|
-
}
|
|
132
|
-
}), "persona.section()");
|
|
133
|
-
}
|
|
134
|
-
export { name as a, dshHome as c, inject as i, settingsFilePath as l, MAX_PERSONA_CHARS as n, readPersonaCard as o, apply as r, PERSONA_PRESET_IDS as s, DEFAULT_PERSONA as t };
|