dlaoshi-outfit 2.1.3 → 2.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/CHANGELOG.md +26 -0
- package/README.md +47 -0
- package/client.js +136 -11
- package/index.js +109 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0 — 2026-10-22
|
|
4
|
+
|
|
5
|
+
重磅功能:**称呼与语气**(默认称呼「主人」)。
|
|
6
|
+
|
|
7
|
+
- 宿主通过官方 `ctx.systemPrompt.section()` 注入一段「称呼规则」,`text` 是函数,
|
|
8
|
+
改设置后**下一个模型步骤立刻生效**,不用重启
|
|
9
|
+
- 只影响称呼与句末语气词;提示词里明确禁止改变回答的事实、结论、结构、代码、
|
|
10
|
+
工具调用、专业程度与长度
|
|
11
|
+
- 设置 › 通用新增一行:称呼输入框 + 两个开关(启用称呼 / 语气词)
|
|
12
|
+
- 设置持久化到 `~/.dsh/fatfish-outfit/persona.json`
|
|
13
|
+
- 路由 `/fatfish/persona` 支持 GET/POST,仅回环可用
|
|
14
|
+
- 测试新增:提示词段落注册、默认「主人」、改名即时生效、语气开关、禁用后段落为空、
|
|
15
|
+
非回环 403;测试使用临时 HOME,不再污染真实配置
|
|
16
|
+
|
|
17
|
+
## 2.1.4 — 2026-10-22
|
|
18
|
+
|
|
19
|
+
按 DSH 的「可逆副作用」规范重写挂载流程,支持热重组。
|
|
20
|
+
|
|
21
|
+
- 所有副作用都成对:样式表、`html` 皮肤类、主题 token、槽位占用全部由 `ctx.effect` 托管,
|
|
22
|
+
卸载时逐项回滚,不再依赖页面刷新
|
|
23
|
+
- 二次 `apply()` 幂等:先释放上一批槽位占用再挂载,避免热重组后出现两只桌宠
|
|
24
|
+
- 新增自愈:槽位容器还在、本插件节点消失时(HMR 驱逐),约 5 秒内自动重新挂载
|
|
25
|
+
- 测试新增「重复 apply 不重复占用槽位」断言
|
|
26
|
+
|
|
27
|
+
> 注意:全新安装插件仍需重启一次 Host——DSH 的插件图在启动时组合,这不是插件能绕过的。
|
|
28
|
+
|
|
3
29
|
## 2.1.3 — 2026-10-22
|
|
4
30
|
|
|
5
31
|
修复:网页端加载失败(`__ModuleLoader__` 未注册)。
|
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
| 🫧 点击立牌 | q 弹回弹 + 气泡膜"啵"声 + 单独一个余额气泡(读取官方 `deepseekAccount`) |
|
|
29
29
|
| 🖼️ 聊天背景 | 用你自己的插画做会话区壁纸,主题自适应蒙版保证可读性 |
|
|
30
30
|
| 🧊 液态玻璃 | 输入框磨砂玻璃 + 高光描边(不再有黑气泡,消息为纯文字) |
|
|
31
|
+
| 🗣️ 称呼与语气 | 设置 › 通用 里设置它怎么称呼你(默认「主人」),只改措辞不改回答方式 |
|
|
31
32
|
| ⚙️ 主题开关 | 设置 › 通用 里的滑动开关:一键启用 / 禁用整套外观 |
|
|
32
33
|
|
|
33
34
|
## 安装
|
|
@@ -100,6 +101,22 @@ Copy-Item "C:\Users\你\Documents\dlaoshi-outfit\*" `
|
|
|
100
101
|
- **开**:壁纸 + 液态玻璃 + 桌宠 + 立牌 + 侧栏头像
|
|
101
102
|
- **关**:以上全部隐藏,恢复 DSH 原始界面(开关状态会记住)
|
|
102
103
|
|
|
104
|
+
## 称呼与语气
|
|
105
|
+
|
|
106
|
+
设置 › 通用 › **「D老师」怎么称呼你**:
|
|
107
|
+
|
|
108
|
+
- 输入框里填称呼,默认 **主人**
|
|
109
|
+
- 左侧开关 = 启用 / 关闭称呼
|
|
110
|
+
- 右侧开关 = 是否允许偶尔加句末语气词(呀 / 呢 / 哦 / 啦)
|
|
111
|
+
|
|
112
|
+
实现方式:宿主用官方 `ctx.systemPrompt.section('dlaoshi:address')` 注入一段规则,段落文本是函数,
|
|
113
|
+
改完设置**下一个模型步骤就生效**。提示词里明确写了:
|
|
114
|
+
|
|
115
|
+
> 只改变称呼和语气词:回答的事实、结论、结构、代码、工具调用、专业程度与长度都保持原样。
|
|
116
|
+
|
|
117
|
+
所以它不会改变回答习惯,只是称呼与措辞更亲近。设置保存在
|
|
118
|
+
`~/.dsh/fatfish-outfit/persona.json`,重启后保持。
|
|
119
|
+
|
|
103
120
|
## 兼容性
|
|
104
121
|
|
|
105
122
|
- 只依赖 DSH 自带的 Client 运行时与官方 `deepseekAccount` 能力,**无第三方依赖**
|
|
@@ -123,6 +140,36 @@ Copy-Item "C:\Users\你\Documents\dlaoshi-outfit\*" `
|
|
|
123
140
|
└── examples/ # profile 配置示例
|
|
124
141
|
```
|
|
125
142
|
|
|
143
|
+
## 常见问题
|
|
144
|
+
|
|
145
|
+
### 装完之后没反应 / 第一次启动不生效
|
|
146
|
+
|
|
147
|
+
**这是 DSH 的加载机制,不是插件的问题。** 本插件已按「可逆副作用」实现:卸载即回滚、重复挂载不会叠加、被 HMR 驱逐后会自动重挂,但**全新安装仍需重启一次 Host**(插件图只在启动时组合)。 DSH 的插件图(loader entries)在 **Host 启动时组合**;热更新只对已经加载过的客户端 bundle 生效。全新安装一个插件等于往插件图里加一个新 entry,**必须完整重启一次 DSH** 才会生效。
|
|
148
|
+
|
|
149
|
+
排查顺序:
|
|
150
|
+
|
|
151
|
+
1. 完全退出 DSH(不是关窗口,要从托盘/任务管理器退出)
|
|
152
|
+
2. 重新启动
|
|
153
|
+
3. 设置 → 插件,确认 `dlaoshi-outfit` 存在且已启用
|
|
154
|
+
4. 仍不出现 → 再重启一次(首次安装有时需要两轮:第一轮装包、第二轮组合)
|
|
155
|
+
5. 打开浏览器控制台(F12),看有没有 `__ModuleLoader__` 相关报错
|
|
156
|
+
|
|
157
|
+
### 桌宠突然消失
|
|
158
|
+
|
|
159
|
+
通常是客户端热更新(HMR)把插件卸载后没有重新挂载。按 `Ctrl+R` 刷新页面即可;刷新后仍不在就重启 DSH。
|
|
160
|
+
|
|
161
|
+
### 网页端提示「加载插件失败 / 未注册」
|
|
162
|
+
|
|
163
|
+
确认安装的是 **2.1.3 或更高版本**。`2.1.0` 和 `2.1.1` 的客户端注册 id 与包名不一致,已被标记 deprecated:
|
|
164
|
+
|
|
165
|
+
```powershell
|
|
166
|
+
dsh plugin add dlaoshi-outfit@2.1.3
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 开发时想热更新
|
|
170
|
+
|
|
171
|
+
客户端代码改动只有在 `pnpm run dev:web` 同时运行时才会自动重建并热替换;否则改完 `client.js` 需要重启 DSH。
|
|
172
|
+
|
|
126
173
|
## 开发
|
|
127
174
|
|
|
128
175
|
```powershell
|
package/client.js
CHANGED
|
@@ -78,6 +78,10 @@ window.__ModuleLoader__.load({
|
|
|
78
78
|
listener(outfitEnabled)
|
|
79
79
|
return () => { outfitListeners.delete(listener) }
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
/** Disposer of the currently mounted occupant set, shared across apply()
|
|
83
|
+
* calls so a second mount replaces the first instead of stacking on it. */
|
|
84
|
+
let mountedOccupants = null
|
|
81
85
|
/** Host route that reports the live turn's tokens and think latency. */
|
|
82
86
|
const STATS_PATH = '/fatfish/stats'
|
|
83
87
|
|
|
@@ -247,6 +251,16 @@ html:not(.${SKIN_CLASS}) .${PREFIX}-hero { display: none !important; }
|
|
|
247
251
|
border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.25);
|
|
248
252
|
transition: transform .2s cubic-bezier(.2,.9,.3,1); }
|
|
249
253
|
.${PREFIX}-switch[aria-checked='true'] .${PREFIX}-switch-knob { transform: translateX(20px); }
|
|
254
|
+
/* the addressing row: text field + two switches */
|
|
255
|
+
.${PREFIX}-persona-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
256
|
+
justify-content: flex-end; max-width: 320px; }
|
|
257
|
+
.${PREFIX}-input { flex: 1; min-width: 120px; padding: 6px 10px; border-radius: 8px;
|
|
258
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(150,190,255,.3));
|
|
259
|
+
background: transparent; color: inherit; outline: none;
|
|
260
|
+
font: 500 13px/1.4 "Segoe UI","Microsoft YaHei",system-ui,sans-serif; }
|
|
261
|
+
.${PREFIX}-input:focus { border-color: #2F55E0; }
|
|
262
|
+
.${PREFIX}-persona-hint { flex: 1 0 100%; text-align: right;
|
|
263
|
+
font: 400 11px/16px "Segoe UI","Microsoft YaHei",system-ui,sans-serif; opacity: .7; }
|
|
250
264
|
|
|
251
265
|
/* ============================================================ chat skin == */
|
|
252
266
|
/* The supplied illustration is the chat page's wallpaper. A theme-aware veil
|
|
@@ -1100,7 +1114,65 @@ html.${SKIN_CLASS} body[data-ds-dark-theme] [data-turn-process-answer] {
|
|
|
1100
1114
|
}
|
|
1101
1115
|
|
|
1102
1116
|
/**
|
|
1103
|
-
*
|
|
1117
|
+
* Settings row for the addressing feature: what the model calls the user.
|
|
1118
|
+
*
|
|
1119
|
+
* The Host owns the value (it is written into the prompt as a section), so
|
|
1120
|
+
* this row is a thin client: GET on mount, POST on change.
|
|
1121
|
+
*/
|
|
1122
|
+
function PersonaSettingsRow() {
|
|
1123
|
+
const [persona, setPersona] = useState({ enabled: true, address: '主人', tone: true })
|
|
1124
|
+
useEffect(() => {
|
|
1125
|
+
if (typeof fetch !== 'function') return
|
|
1126
|
+
fetch('/fatfish/persona', { cache: 'no-store' })
|
|
1127
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1128
|
+
.then((data) => { if (data && data.ok && data.persona) setPersona(data.persona) })
|
|
1129
|
+
.catch(() => { /* the host route may be absent on an old build */ })
|
|
1130
|
+
}, [])
|
|
1131
|
+
const save = (next) => {
|
|
1132
|
+
setPersona(next)
|
|
1133
|
+
if (typeof fetch !== 'function') return
|
|
1134
|
+
fetch('/fatfish/persona', {
|
|
1135
|
+
method: 'POST',
|
|
1136
|
+
headers: { 'content-type': 'application/json' },
|
|
1137
|
+
body: JSON.stringify(next),
|
|
1138
|
+
}).catch(() => { /* keep the local value; the next poll retries */ })
|
|
1139
|
+
}
|
|
1140
|
+
return h('div', { class: `${PREFIX}-settings` },
|
|
1141
|
+
h('div', { class: `${PREFIX}-settings-text` },
|
|
1142
|
+
h('div', { class: `${PREFIX}-settings-title` }, '「D老师」怎么称呼你'),
|
|
1143
|
+
h('div', { class: `${PREFIX}-settings-desc` },
|
|
1144
|
+
'默认称呼「主人」;只增加称呼与语气词,不改变回答方式与内容。')),
|
|
1145
|
+
h('div', { class: `${PREFIX}-persona-row` },
|
|
1146
|
+
h('input', {
|
|
1147
|
+
class: `${PREFIX}-input`,
|
|
1148
|
+
type: 'text',
|
|
1149
|
+
value: persona.address,
|
|
1150
|
+
maxLength: 24,
|
|
1151
|
+
placeholder: '主人',
|
|
1152
|
+
'aria-label': '称呼',
|
|
1153
|
+
onChange: (event) => save({ ...persona, address: event.target.value }),
|
|
1154
|
+
}),
|
|
1155
|
+
h('button', {
|
|
1156
|
+
type: 'button',
|
|
1157
|
+
role: 'switch',
|
|
1158
|
+
'aria-checked': persona.enabled ? 'true' : 'false',
|
|
1159
|
+
class: `${PREFIX}-switch`,
|
|
1160
|
+
title: persona.enabled ? '关闭称呼' : '启用称呼',
|
|
1161
|
+
onClick: () => save({ ...persona, enabled: !persona.enabled }),
|
|
1162
|
+
}, h('span', { class: `${PREFIX}-switch-knob` })),
|
|
1163
|
+
h('button', {
|
|
1164
|
+
type: 'button',
|
|
1165
|
+
role: 'switch',
|
|
1166
|
+
'aria-checked': persona.tone ? 'true' : 'false',
|
|
1167
|
+
class: `${PREFIX}-switch`,
|
|
1168
|
+
title: persona.tone ? '关闭语气词' : '启用语气词',
|
|
1169
|
+
onClick: () => save({ ...persona, tone: !persona.tone }),
|
|
1170
|
+
}, h('span', { class: `${PREFIX}-switch-knob` })),
|
|
1171
|
+
h('div', { class: `${PREFIX}-persona-hint` }, '左边开关=称呼,右边开关=语气词')))
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* The Settings row for the whole outfit switch.
|
|
1104
1176
|
*
|
|
1105
1177
|
* Rendered inside DSH's own "General" settings list, so it inherits the
|
|
1106
1178
|
* page's typography and spacing. The switch drives the whole outfit: the
|
|
@@ -1152,18 +1224,34 @@ html.${SKIN_CLASS} body[data-ds-dark-theme] [data-turn-process-answer] {
|
|
|
1152
1224
|
window.__petDebug = { starts: 0, ends: 0, happy: 0, sad: 0, balance: null }
|
|
1153
1225
|
}
|
|
1154
1226
|
|
|
1227
|
+
// Reversible, idempotent mount. Every side effect below is paired with a
|
|
1228
|
+
// disposer owned by this plugin's fiber, so the loader can apply this
|
|
1229
|
+
// module again (hot recompose) without a restart and without leaving a
|
|
1230
|
+
// duplicate stylesheet, class or slot occupant behind.
|
|
1231
|
+
if (typeof document.querySelectorAll === 'function') {
|
|
1232
|
+
for (const stale of document.querySelectorAll('style[data-plugin="dlaoshi-outfit"]')) {
|
|
1233
|
+
try { stale.remove() } catch (err) { /* already detached */ }
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1155
1237
|
// Apply the remembered outfit state before the stylesheet mounts so the
|
|
1156
1238
|
// page never flashes the beautified look on a "switched off" session.
|
|
1157
1239
|
outfitEnabled = readOutfitSetting()
|
|
1158
|
-
|
|
1240
|
+
ctx.effect(() => {
|
|
1241
|
+
applyOutfitClass(outfitEnabled)
|
|
1242
|
+
return () => {
|
|
1243
|
+
try { document.documentElement.classList.remove(SKIN_CLASS) } catch (err) { /* no document */ }
|
|
1244
|
+
}
|
|
1245
|
+
}, 'dlaoshi-pet: skin class')
|
|
1159
1246
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1247
|
+
ctx.effect(() => {
|
|
1248
|
+
const tag = document.createElement('style')
|
|
1249
|
+
tag.dataset.plugin = 'dlaoshi-outfit'
|
|
1250
|
+
tag.textContent = CSS
|
|
1251
|
+
document.head.appendChild(tag)
|
|
1252
|
+
return () => {
|
|
1253
|
+
try { tag.remove() } catch (err) { /* already detached */ }
|
|
1254
|
+
}
|
|
1167
1255
|
}, 'dlaoshi-pet: stylesheet')
|
|
1168
1256
|
|
|
1169
1257
|
ctx.effect(() => ctx.theme.overrideTokens('dlaoshi-outfit', TOKENS),
|
|
@@ -1172,16 +1260,53 @@ html.${SKIN_CLASS} body[data-ds-dark-theme] [data-turn-process-answer] {
|
|
|
1172
1260
|
const occupy = (name, id, order, component) => ctx.slots.inject(name, () =>
|
|
1173
1261
|
ctx.slots.register({ name, id, order }, component))
|
|
1174
1262
|
|
|
1175
|
-
|
|
1263
|
+
// A second mount must replace the first, never stack on top of it: the
|
|
1264
|
+
// loader may re-apply this module while a previous occupant set is still
|
|
1265
|
+
// registered (hot recompose), and two pets would otherwise fight.
|
|
1266
|
+
const mountOccupants = () => {
|
|
1267
|
+
if (typeof mountedOccupants === 'function') {
|
|
1268
|
+
try { mountedOccupants() } catch (err) { /* already gone */ }
|
|
1269
|
+
mountedOccupants = null
|
|
1270
|
+
}
|
|
1176
1271
|
const stop = [
|
|
1177
1272
|
occupy('shell.overlay', 'dlaoshi-companion', 20, Companion),
|
|
1178
1273
|
occupy('sidebar.brand.mark', 'dlaoshi-sidebar', 5, SidebarMark),
|
|
1179
1274
|
occupy('conversation.hero.brand.mark', 'dlaoshi-hero', 5, HeroCharacter),
|
|
1180
|
-
// the
|
|
1275
|
+
// the switches live in Settings › General, like DSH's own rows
|
|
1181
1276
|
occupy('settings.general.item', 'dlaoshi-theme', 40, ThemeSettingsRow),
|
|
1277
|
+
occupy('settings.general.item', 'dlaoshi-persona', 41, PersonaSettingsRow),
|
|
1182
1278
|
]
|
|
1183
1279
|
return () => { for (const fn of stop) if (typeof fn === 'function') fn() }
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
mountedOccupants = mountOccupants()
|
|
1283
|
+
ctx.effect(() => () => {
|
|
1284
|
+
if (typeof mountedOccupants === 'function') mountedOccupants()
|
|
1285
|
+
mountedOccupants = null
|
|
1184
1286
|
}, 'dlaoshi-pet: slot occupants')
|
|
1287
|
+
|
|
1288
|
+
// Self heal for hot recomposition: when the loader evicts this module's
|
|
1289
|
+
// occupants but the overlay slot itself stays mounted, remount instead of
|
|
1290
|
+
// leaving the user with a vanished pet until they refresh.
|
|
1291
|
+
ctx.effect(() => {
|
|
1292
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') return () => {}
|
|
1293
|
+
let misses = 0
|
|
1294
|
+
const timer = setInterval(() => {
|
|
1295
|
+
try {
|
|
1296
|
+
const slot = document.querySelector('[data-slot="shell.overlay"]')
|
|
1297
|
+
if (!slot || typeof slot.querySelector !== 'function') { misses = 0; return }
|
|
1298
|
+
if (slot.querySelector(`.${PREFIX}-pet`)) { misses = 0; return }
|
|
1299
|
+
misses += 1
|
|
1300
|
+
if (misses < 2) return
|
|
1301
|
+
misses = 0
|
|
1302
|
+
mountedOccupants = mountOccupants()
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
misses = 0
|
|
1305
|
+
}
|
|
1306
|
+
}, 2500)
|
|
1307
|
+
if (timer && typeof timer.unref === 'function') timer.unref()
|
|
1308
|
+
return () => clearInterval(timer)
|
|
1309
|
+
}, 'dlaoshi-pet: hot-recompose self heal')
|
|
1185
1310
|
},
|
|
1186
1311
|
}
|
|
1187
1312
|
},
|
package/index.js
CHANGED
|
@@ -22,10 +22,69 @@
|
|
|
22
22
|
/** The web server is the only service required. */
|
|
23
23
|
export const inject = ['webServer']
|
|
24
24
|
|
|
25
|
+
import fs from 'node:fs'
|
|
26
|
+
import os from 'node:os'
|
|
27
|
+
import path from 'node:path'
|
|
28
|
+
|
|
25
29
|
const BASE = '/fatfish'
|
|
26
30
|
const MAX_SESSIONS = 16
|
|
27
31
|
const LOOPBACKS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1'])
|
|
28
32
|
|
|
33
|
+
/* ------------------------------------------------------------ persona ----- */
|
|
34
|
+
|
|
35
|
+
/** Where the "how should it address me" setting lives (survives restarts). */
|
|
36
|
+
const PERSONA_PATH = path.join(os.homedir(), '.dsh', 'fatfish-outfit', 'persona.json')
|
|
37
|
+
const DEFAULT_PERSONA = { enabled: true, address: '主人', tone: true }
|
|
38
|
+
|
|
39
|
+
function loadPersona() {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(fs.readFileSync(PERSONA_PATH, 'utf8'))
|
|
42
|
+
return {
|
|
43
|
+
enabled: parsed.enabled !== false,
|
|
44
|
+
address: typeof parsed.address === 'string' && parsed.address.trim()
|
|
45
|
+
? parsed.address.trim().slice(0, 24)
|
|
46
|
+
: DEFAULT_PERSONA.address,
|
|
47
|
+
tone: parsed.tone !== false,
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return { ...DEFAULT_PERSONA }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function savePersona(persona) {
|
|
55
|
+
try {
|
|
56
|
+
fs.mkdirSync(path.dirname(PERSONA_PATH), { recursive: true })
|
|
57
|
+
fs.writeFileSync(PERSONA_PATH, JSON.stringify(persona, null, 2), 'utf8')
|
|
58
|
+
} catch (err) { /* best effort: the setting still applies for this run */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The prompt section that carries the addressing rule.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately narrow: it changes how the model addresses the user and which
|
|
65
|
+
* light sentence-final particles it may use. It explicitly forbids touching the
|
|
66
|
+
* answer's content, structure, code, tools or length, so answer habits stay the
|
|
67
|
+
* same. Returns '' when the feature is switched off, which makes the section
|
|
68
|
+
* empty and therefore invisible to the model.
|
|
69
|
+
*/
|
|
70
|
+
function personaPrompt(persona) {
|
|
71
|
+
if (!persona.enabled) return ''
|
|
72
|
+
const address = persona.address || DEFAULT_PERSONA.address
|
|
73
|
+
const lines = [
|
|
74
|
+
'## 称呼与语气(只影响措辞,不影响回答内容)',
|
|
75
|
+
`- 在称呼用户时使用「${address}」。`,
|
|
76
|
+
]
|
|
77
|
+
if (persona.tone) {
|
|
78
|
+
lines.push('- 可以在句首或句尾偶尔加一个很轻的语气词(呀 / 呢 / 哦 / 啦),让语气更有陪伴感;不要每句都加。')
|
|
79
|
+
}
|
|
80
|
+
lines.push(
|
|
81
|
+
'- 这条规则只改变称呼和语气词:回答的事实、结论、结构、代码、工具调用、专业程度与长度都保持原样。',
|
|
82
|
+
'- 不要为了使用称呼而重复用户的话,也不要刻意卖萌或降低信息密度。',
|
|
83
|
+
'- 如果用户使用其他语言,请使用该语言里对应的称呼(例如英文用 "Master")。',
|
|
84
|
+
)
|
|
85
|
+
return lines.join('\n')
|
|
86
|
+
}
|
|
87
|
+
|
|
29
88
|
/** One turn's accounting; replaced at every `turn/start`. */
|
|
30
89
|
function blank(sessionId) {
|
|
31
90
|
return {
|
|
@@ -274,6 +333,56 @@ export function apply(ctx) {
|
|
|
274
333
|
return balanceCache.value
|
|
275
334
|
}
|
|
276
335
|
|
|
336
|
+
// ---- addressing: one persona section the model sees on every step ------
|
|
337
|
+
// The section text is a function, so changing the setting takes effect on the
|
|
338
|
+
// very next model step without re-registering anything.
|
|
339
|
+
let persona = loadPersona()
|
|
340
|
+
ctx.inject(['systemPrompt'], (scope) => {
|
|
341
|
+
const registry = scope && scope.systemPrompt
|
|
342
|
+
if (!registry || typeof registry.section !== 'function') return
|
|
343
|
+
registry.section({
|
|
344
|
+
name: 'dlaoshi:address',
|
|
345
|
+
order: 9000,
|
|
346
|
+
text: () => personaPrompt(persona),
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
ctx.effect(() => ctx.webServer.register({
|
|
351
|
+
kind: 'exact',
|
|
352
|
+
path: `${BASE}/persona`,
|
|
353
|
+
handler: (req, res) => {
|
|
354
|
+
if (!isLoopback(req)) {
|
|
355
|
+
respondJson(res, 403, { ok: false, error: 'loopback only' })
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
if (req.method === 'POST') {
|
|
359
|
+
let body = ''
|
|
360
|
+
req.on('data', (chunk) => {
|
|
361
|
+
body += chunk
|
|
362
|
+
if (body.length > 4096) req.destroy()
|
|
363
|
+
})
|
|
364
|
+
req.on('end', () => {
|
|
365
|
+
try {
|
|
366
|
+
const parsed = JSON.parse(body || '{}')
|
|
367
|
+
persona = {
|
|
368
|
+
enabled: parsed.enabled !== false,
|
|
369
|
+
address: typeof parsed.address === 'string' && parsed.address.trim()
|
|
370
|
+
? parsed.address.trim().slice(0, 24)
|
|
371
|
+
: DEFAULT_PERSONA.address,
|
|
372
|
+
tone: parsed.tone !== false,
|
|
373
|
+
}
|
|
374
|
+
savePersona(persona)
|
|
375
|
+
respondJson(res, 200, { ok: true, persona })
|
|
376
|
+
} catch (err) {
|
|
377
|
+
respondJson(res, 400, { ok: false, error: 'invalid json' })
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
respondJson(res, 200, { ok: true, persona })
|
|
383
|
+
},
|
|
384
|
+
}), 'fatfish: persona route')
|
|
385
|
+
|
|
277
386
|
ctx.effect(() => ctx.webServer.register({
|
|
278
387
|
kind: 'exact',
|
|
279
388
|
path: `${BASE}/balance`,
|