dlaoshi-outfit 2.1.4 → 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 CHANGED
@@ -1,5 +1,19 @@
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
+
3
17
  ## 2.1.4 — 2026-10-22
4
18
 
5
19
  按 DSH 的「可逆副作用」规范重写挂载流程,支持热重组。
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` 能力,**无第三方依赖**
package/client.js CHANGED
@@ -251,6 +251,16 @@ html:not(.${SKIN_CLASS}) .${PREFIX}-hero { display: none !important; }
251
251
  border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.25);
252
252
  transition: transform .2s cubic-bezier(.2,.9,.3,1); }
253
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; }
254
264
 
255
265
  /* ============================================================ chat skin == */
256
266
  /* The supplied illustration is the chat page's wallpaper. A theme-aware veil
@@ -1104,7 +1114,65 @@ html.${SKIN_CLASS} body[data-ds-dark-theme] [data-turn-process-answer] {
1104
1114
  }
1105
1115
 
1106
1116
  /**
1107
- * The Settings row for the theme switch.
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.
1108
1176
  *
1109
1177
  * Rendered inside DSH's own "General" settings list, so it inherits the
1110
1178
  * page's typography and spacing. The switch drives the whole outfit: the
@@ -1204,8 +1272,9 @@ html.${SKIN_CLASS} body[data-ds-dark-theme] [data-turn-process-answer] {
1204
1272
  occupy('shell.overlay', 'dlaoshi-companion', 20, Companion),
1205
1273
  occupy('sidebar.brand.mark', 'dlaoshi-sidebar', 5, SidebarMark),
1206
1274
  occupy('conversation.hero.brand.mark', 'dlaoshi-hero', 5, HeroCharacter),
1207
- // the switch lives in Settings › General, like DSH's own rows
1275
+ // the switches live in Settings › General, like DSH's own rows
1208
1276
  occupy('settings.general.item', 'dlaoshi-theme', 40, ThemeSettingsRow),
1277
+ occupy('settings.general.item', 'dlaoshi-persona', 41, PersonaSettingsRow),
1209
1278
  ]
1210
1279
  return () => { for (const fn of stop) if (typeof fn === 'function') fn() }
1211
1280
  }
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`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dlaoshi-outfit",
3
- "version": "2.1.4",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "description": "Anime whale-boy maid-outfit desktop pet, token/think-time standee and liquid-glass theme for DeepSeek Harness",
6
6
  "exports": {