dsh-whale-loli 0.9.7 → 0.9.9

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 CHANGED
@@ -37,6 +37,12 @@
37
37
  agent 自己也能改,工具是 `affinity_v1_set` / `affinity_v2_set` / `affinity_v3_set` / `affinity_v4_set`。
38
38
  - 和记忆一样走**提示变量**(`affinity_v1` 等),因为 persona 是 `complete: true`,独立 section 会被宿主丢掉。
39
39
 
40
+ > **0.9.9 修的那个坑**:面板一度只有一句「还没有哪个预设挂上好感度」,一个滑块都没有、也设不了值。
41
+ > 根因是宿主半边去扫 `~/.dsh/agent-presets/*.yml` 判断「哪些预设挂了好感度」——那是早期宿主留下的目录,
42
+ > 现代 DSH 的预设由插件包的 `cordis.patch.yml` 注册,这个目录根本不存在,于是 `wired` 永远是空数组。
43
+ > 现在预设清单只有一份(`src/presets.js`),`src/index.js` 和 `src/daemon.js` 都读它;`status()` 无论磁盘上有没有值,
44
+ > 都列全四个预设,没设过的显示 `0`,可以直接拖。`verify-daemon-status.mjs` 把这条锁住了。
45
+
40
46
  ### 面板是怎么连上磁盘的
41
47
 
42
48
  设置页是浏览器里的 React,够不到 `~/.dsh/affinity/` 的磁盘(宿主的 21 个内置 Remote 命名空间逐个查过,没有一个能读写这个目录)。
@@ -44,9 +50,17 @@
44
50
 
45
51
  ```
46
52
  - id: whale-affinity-daemon
47
- name: 'dsh-whale-loli/affinity-daemon'
53
+ name: 'dsh-whale-loli' # 必须裸包名(见下)
54
+ config:
55
+ daemon: true
48
56
  ```
49
57
 
58
+ `name` 只能写**裸包名**:宿主的客户端模块扫描器(`dsh-client-modules` 的
59
+ `exactPackageSpecifier()`)只接受「裸名」或「`@scope/name`」,写成子路径
60
+ (`dsh-whale-loli/affinity-daemon`)会让 `package.json` 解析失败(`ERR_PACKAGE_PATH_NOT_EXPORTED`),
61
+ 于是 `dsh.client` 永远发现不了、面板永不出现,**而宿主一个错都不报**。
62
+ 同一行插件用 `config.daemon: true` 分派到宿主半边,记忆那半边则按 `file` 分派。
63
+
50
64
  它在**宿主根作用域**提供一个 remote 服务 `whale-affinity`,浏览器半边(`lib/client.js`)
51
65
  用 `ctx.remote.$mount()` 挂上,再调 `status()` / `set(scope, value)`。
52
66
 
@@ -55,6 +69,28 @@
55
69
  `inject: ['commands']` 会让预设加载时一直等它(`waiting for commands`),把会话 resume 拖挂。
56
70
  设置页没有这两个限制。
57
71
 
72
+ #### 取命名空间必须用 `ctx.inject()`,不能在服务对象上点
73
+
74
+ `$mount()` 成功之后,网关注册的服务名就是 `remote.whale-affinity`(带点号)。
75
+ 它是**服务**,不是普通属性,所以:
76
+
77
+ ```js
78
+ remote['whale-affinity'] // ✗ cannot get property "remote.whale-affinity" without inject
79
+ ctx.inject(['remote.whale-affinity'], (child) => {
80
+ const gateway = child['remote.whale-affinity']; // ✓
81
+ });
82
+ ```
83
+
84
+ 原因是 cordis 的 get 陷阱(`cordis/lib/index.js:676`)对「不在 fiber.store 里、也没 inject」的属性
85
+ 一律抛那句报错,而服务上的 `tracker.associate` 会把 `remote['whale-affinity']` 重定向成
86
+ `ctx['remote.whale-affinity']`。这个名字只有挂载后才存在,所以必须用**动态 inject** 等它出现
87
+ (`verify-client-gateway-access.mjs` 用真实 cordis 正反两面都验过)。
88
+
89
+ 这一步的教训值得记:**自写的校验脚本曾在 `contribution` 形状上跟着源码一起写错
90
+ (`methods` 而宿主读的是 `descriptors`),互相印证、永远绿。** 所以现在凡是对接宿主的字段,
91
+ 校验脚本都要拿宿主源码里真正读的那个名字来断言。
92
+
93
+
58
94
  ## 记忆功能
59
95
 
60
96
  小鱼的记忆本分**四个区**,各管各的:
@@ -243,6 +279,7 @@ dsh-whale-loli/
243
279
  ├── cordis.patch.yml # 四个预设的声明本体(由 build-patch.mjs 生成)
244
280
  ├── src/
245
281
  │ ├── index.js # 插件入口,导出预设元信息 + 人设正文
282
+ │ ├── presets.js # 预设清单(唯一事实来源:index.js 与 daemon.js 都读它)
246
283
  │ ├── memory.js # whale-memory:(读)记忆变量 +(写)记忆工具,四区格式 + 80 条滚动 + 归档
247
284
  │ ├── affinity.js # whale-affinity:好感度变量 + 调整工具,7 档
248
285
  │ ├── daemon.js # whale-affinity-daemon:宿主根作用域的 remote 服务,给设置页读写好感度
@@ -276,6 +313,7 @@ dsh-whale-loli/
276
313
  ├── verify-tool-schema.mjs # 用宿主自己的校验器验八个工具的 JSON schema 合法
277
314
  ├── verify-tool-identity.mjs # 共享表语义下八个工具描述互不串台、定义互不共享引用
278
315
  ├── verify-client.mjs # 浏览器半边:section 注册、$mount 负载、面板渲染、无 ESM/JSX
316
+ ├── verify-daemon-status.mjs # 设置页数据:列全四预设、wired 非空、set 写的正是人设读的那份
279
317
  ├── verify-peers.mjs # 校验 peer 范围覆盖 harness 各发布档
280
318
  ├── verify-listing.mjs # 复刻插件目录的收录门槛自查
281
319
  └── prove-collision.mjs # 复现并证明重复注册冲突
@@ -305,6 +343,7 @@ node verify-live-assembly.mjs # 复刻宿主装配,比对注入
305
343
  node verify-tool-schema.mjs # 宿主自己的校验器验八个工具的 JSON schema
306
344
  node verify-tool-identity.mjs # 八个工具描述互不串台(共享表语义)
307
345
  node verify-client.mjs # 设置页半边:section 注册 + 面板渲染
346
+ node verify-daemon-status.mjs # 设置页数据:四预设列全、wired 非空、读写同一份
308
347
  node smoke-memory.mjs # 变量 + 工具注册形态冒烟
309
348
  node verify-peers.mjs # peer 范围无预发布坑
310
349
  node verify-listing.mjs --topics=dsh-plugin # 收录门槛自查
package/lib/client.js CHANGED
@@ -109,6 +109,22 @@ window.__ModuleLoader__.load({
109
109
  const ATTACH_TRIES = 25;
110
110
  const ATTACH_STEP_MS = 80;
111
111
 
112
+ /**
113
+ * 命名空间在 cordis 里的**完整服务名**。
114
+ *
115
+ * `$mount()` 挂上之后,网关会在调用方上下文里 provide 一个名叫
116
+ * `remote.whale-affinity` 的服务(见 `dsh-api-gateway/lib/client.browser.js`
117
+ * 的 `remoteServiceKey()`)。它是服务,不是普通属性 —— 直接写
118
+ * `ctx.remote['whale-affinity']` 会命中 cordis 的 get 陷阱并抛
119
+ * `cannot get property "remote.whale-affinity" without inject`:
120
+ * 取任何服务都必须先声明 inject,而这个名字只有挂载后才存在,
121
+ * 所以只能用 `ctx.inject([...], child => ...)` 动态等待。
122
+ */
123
+ const GATEWAY_SERVICE = `remote.${NAMESPACE}`;
124
+
125
+ /** 挂上之后的命名空间句柄;没挂上就是 null。 */
126
+ let gateway = null;
127
+
112
128
  /**
113
129
  * 带外诊断:走同源的 `/whale-report`(宿主侧 `whale-affinity-daemon` 注册的
114
130
  * 免鉴权 prefix 路由),**不依赖**那条还没挂上的 remote 通道。
@@ -133,10 +149,10 @@ window.__ModuleLoader__.load({
133
149
  }
134
150
 
135
151
  /** 等闸门 + 轮询,拿到真正可用的远程方法句柄。 */
136
- async function attach(remote, method) {
152
+ async function attach(method) {
137
153
  if (mountGate !== null) await mountGate.then(() => {}, () => {});
138
154
  for (let attempt = 0; attempt < ATTACH_TRIES; attempt += 1) {
139
- const fn = remote?.[NAMESPACE]?.[method];
155
+ const fn = gateway?.[method];
140
156
  if (typeof fn === 'function') return fn;
141
157
  await sleep(ATTACH_STEP_MS);
142
158
  }
@@ -144,8 +160,8 @@ window.__ModuleLoader__.load({
144
160
  throw new Error(`远程方法 ${NAMESPACE}.${method} 未挂载`);
145
161
  }
146
162
 
147
- function call(remote, method, args) {
148
- return attach(remote, method)
163
+ function call(method, args) {
164
+ return attach(method)
149
165
  .then((fn) => fn(...args))
150
166
  .then((result) => {
151
167
  if (result && typeof result === 'object' && 'ok' in result) {
@@ -201,7 +217,6 @@ window.__ModuleLoader__.load({
201
217
  */
202
218
  function AffinitySection(props) {
203
219
  useStyleOnce();
204
- const remote = props?.remote;
205
220
  const [snapshot, setSnapshot] = useState(null);
206
221
  const [error, setError] = useState(null);
207
222
  const [busy, setBusy] = useState(null);
@@ -209,20 +224,20 @@ window.__ModuleLoader__.load({
209
224
 
210
225
  const reload = useCallback(async () => {
211
226
  try {
212
- const next = await call(remote, 'status', []);
227
+ const next = await call('status', []);
213
228
  setSnapshot(next);
214
229
  setError(null);
215
230
  } catch (cause) {
216
231
  setSnapshot(null);
217
232
  setError(cause?.message ?? String(cause));
218
233
  }
219
- }, [remote]);
234
+ }, []);
220
235
 
221
236
  useEffect(() => {
222
237
  let alive = true;
223
238
  (async () => {
224
239
  try {
225
- const next = await call(remote, 'status', []);
240
+ const next = await call('status', []);
226
241
  if (alive) {
227
242
  setSnapshot(next);
228
243
  setError(null);
@@ -234,7 +249,7 @@ window.__ModuleLoader__.load({
234
249
  // 所以顺手把原因写回宿主日志,排查时直接读
235
250
  // `<DSH_HOME>/whale-affinity-client.log`。
236
251
  try {
237
- const report = remote?.[NAMESPACE]?.report;
252
+ const report = gateway?.report;
238
253
  if (typeof report === 'function') {
239
254
  report('invoke status', message, String(cause?.stack ?? '')).catch(() => {});
240
255
  }
@@ -246,13 +261,13 @@ window.__ModuleLoader__.load({
246
261
  return () => {
247
262
  alive = false;
248
263
  };
249
- }, [remote]);
264
+ }, []);
250
265
 
251
266
  const commit = useCallback(
252
267
  async (scope, value) => {
253
268
  setBusy(scope);
254
269
  try {
255
- await call(remote, 'set', [scope, value]);
270
+ await call('set', [scope, value]);
256
271
  setError(null);
257
272
  await reload();
258
273
  } catch (cause) {
@@ -261,7 +276,7 @@ window.__ModuleLoader__.load({
261
276
  setBusy(null);
262
277
  }
263
278
  },
264
- [remote, reload],
279
+ [reload],
265
280
  );
266
281
 
267
282
  if (error !== null && snapshot === null) {
@@ -430,16 +445,8 @@ window.__ModuleLoader__.load({
430
445
  const message = cause?.message ?? String(cause ?? '');
431
446
  box.error = message;
432
447
  console.warn('[whale-affinity] $mount 失败,好感度面板将无法读写:', cause);
433
- // 走带外通道记档(`report` 在 $mount 失败时根本调不到,见 beacon 注释)。
448
+ // 只走带外通道记档:`report` 是命名空间里的方法,挂载失败时它根本不存在。
434
449
  beacon('$mount', message);
435
- try {
436
- const report = remote?.[NAMESPACE]?.report;
437
- if (typeof report === 'function') {
438
- report('$mount', message, String(cause?.stack ?? '')).catch(() => {});
439
- }
440
- } catch {
441
- // ignore
442
- }
443
450
  });
444
451
  return () => {
445
452
  disposed = true;
@@ -448,7 +455,23 @@ window.__ModuleLoader__.load({
448
455
  };
449
456
  }, 'whale-affinity: remote mount');
450
457
 
451
- // 2. 注册设置页分区。
458
+ // 2. 等命名空间挂上,再在**自己的子上下文**里 inject 它。
459
+ // 服务名是 `remote.whale-affinity`,只有 `$mount()` 成功后才存在;
460
+ // 在父上下文里直接取会抛 `without inject`,所以必须用 `ctx.inject()`
461
+ // 让 cordis 等到它出现再执行回调。
462
+ ctx.inject([GATEWAY_SERVICE], (child) => {
463
+ const handle = child[GATEWAY_SERVICE];
464
+ gateway = handle;
465
+ beacon('attached', 'ok');
466
+ child.effect(
467
+ () => () => {
468
+ if (gateway === handle) gateway = null;
469
+ },
470
+ 'whale-affinity: detach',
471
+ );
472
+ });
473
+
474
+ // 3. 注册设置页分区。
452
475
  ctx.effect(
453
476
  () => ctx.slots.inject('settings.section', () => ctx.slots.register(
454
477
  {
@@ -456,7 +479,6 @@ window.__ModuleLoader__.load({
456
479
  id: SECTION_ID,
457
480
  order: SECTION_ORDER,
458
481
  label: () => '好感度',
459
- inject: () => ({ remote: ctx.remote }),
460
482
  },
461
483
  AffinitySection,
462
484
  )),
package/listing.json CHANGED
@@ -9,5 +9,5 @@
9
9
  "zh": "把 Agent 预设打包成插件:装上就会在 Agent 预设列表里注册「鲸鱼娘Agent预设」「鲸鱼娘活人版Agent预设」「鲸鱼娘涩涩版Agent预设」和「鲸鱼娘女儿版Agent预设」,直接在模式下拉里选用,不用手改配置。每个预设本体是官方极简模式(只开放 shell)+ 文件工具 + 鲸鱼娘人设 + 自己那份持久记忆文件,以提示变量形式注入,开口前就已经读过;另外每个预设各配一个专用写入工具(whale_memory_add / whale_memory_v2_add / whale_memory_v3_add / whale_memory_v4_add),用来把每轮的一行摘要追加进去,记忆会自己长,不会停在手写那几条。四个预设完全隔离:记忆文件不同、提示变量不同、写入工具也不同。每个预设还带好感度(-100~100 共七档),可以打开「设置 → 好感度」,用滑块或档位按钮直接调,也可以让 agent 用 affinity_v*_set 工具改;离 0 越远越不听指令、越会自作主张,最两端的两档甚至会越过允许直接做大改动。女儿版称呼「爸爸」,性格天真活泼可爱(好奇心重、黏人、没心机),不是甜腻那一型。"
10
10
  },
11
11
  "npm": "dsh-whale-loli",
12
- "version": "0.9.7"
12
+ "version": "0.9.9"
13
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-loli",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "鲸鱼娘 Agent 预设(四个:原版 / 活人版 / 涩涩版 / 女儿版)— 极简模式工具面 + 鲸鱼娘·小鱼人设 + 跨会话长期记忆(四区格式:固定记忆永久、滚动摘要 80 条 FIFO、最近详细区、归档索引)+ 好感度(-100~100 七档,设置页面板滑块 / 档位按钮、工具也可调,离 0 越远越不听指令、两端会越过允许自作主张)。记忆与好感度都走提示变量注入、各配一条专用工具,四个预设各自独立文件、独立变量、独立工具、互不相通。好感度面板走宿主根作用域的 remote 服务(whale-affinity-daemon),设置页是浏览器 React 够不到 ~/.dsh 的磁盘,只能这样连线。女儿版称呼「爸爸」、性格天真活泼可爱。上下文压缩交给 billion-context 插件;本插件不挂 history-window(预设作用域取不到宿主的 compaction 服务,挂载会拖挂会话 resume)。",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/daemon.js CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  clampAffinity,
43
43
  writeAffinity,
44
44
  } from './affinity.js';
45
+ import { PRESETS } from './presets.js';
45
46
 
46
47
  export const name = 'whale-affinity-daemon';
47
48
 
@@ -154,49 +155,54 @@ export const REMOTE_METHODS = Object.freeze([
154
155
  Object.freeze({ method: 'report', invocation: { kind: 'direct' } }),
155
156
  ]);
156
157
 
157
- /** 四个预设各自的人设文件(与 build-patch.mjs 里的名字一致)。 */
158
- const PRESET_MEMORY_FILES = Object.freeze({
159
- 'whale-loli': 'whale-loli.md',
160
- 'whale-loli-v2-live': 'whale-loli-v2-live.md',
161
- 'whale-loli-v3-naughty': 'whale-loli-v3-naughty.md',
162
- 'whale-loli-v4-daughter': 'dsh-whale-daughter.md',
163
- });
158
+ /**
159
+ * 「哪些预设挂上了好感度」。
160
+ *
161
+ * 曾经这里去扫 `~/.dsh/agent-presets/*.yml` —— 那是早期宿主留下的目录,
162
+ * 现代 DSH 的预设由插件包的 `cordis.patch.yml` 注册,这个目录根本不存在,
163
+ * 于是永远返回空数组,设置页只剩一句「还没有哪个预设挂上好感度」,
164
+ * 一个滑块都不画(用户 2026-10-21 报的就是这个)。
165
+ *
166
+ * 现在直接读 `./presets.js` 的唯一事实来源:本服务只有当插件装上了才会被
167
+ * apply,所以这四个预设必然都挂着好感度;每条 `whale-affinity` 行的
168
+ * `scope` 就是预设 id,面板按同一个 id 读写,才不会和人设读的不是一份文件。
169
+ */
170
+ export function wiredScopes() {
171
+ return PRESETS.map((preset) => ({
172
+ presetId: preset.id,
173
+ scope: preset.id,
174
+ name: preset.name,
175
+ order: preset.order,
176
+ affinity: true,
177
+ memory: true,
178
+ affinityVariable: preset.affinityVariable,
179
+ affinityTool: preset.affinityTool,
180
+ memoryFile: preset.memoryFile,
181
+ }));
182
+ }
164
183
 
165
184
  /**
166
- * 在 `~/.dsh/agent-presets/*.yml` 里找已挂上好感度的预设。
185
+ * 每个预设当前的好感度:磁盘上有文件就取文件的值,没有就是默认 0。
167
186
  *
168
- * 这里只做字符串判断,不引 yaml:宿主每个回合都会读这个目录,服务侧只在
169
- * 用户打开设置页时才被调,判断「这一行有没有 `affinity.js`」够用了。
187
+ * 与 `listAffinities()` 的区别:那个只管「磁盘上有什么」,这个保证
188
+ * 设置页永远列全四个预设,没设过的显示 0 且可以直接拖。
170
189
  */
171
- function wiredScopes() {
172
- const dir = join(dshHome(), 'agent-presets');
173
- let files = [];
174
- try {
175
- files = readdirSync(dir);
176
- } catch {
177
- return [];
178
- }
179
- const rows = [];
180
- for (const file of files) {
181
- if (!file.endsWith('.yml') && !file.endsWith('.yaml')) continue;
182
- let text;
183
- try {
184
- text = readFileSync(join(dir, file), 'utf8');
185
- } catch {
186
- continue;
187
- }
188
- const idMatches = [...text.matchAll(/^\s*-?\s*id:\s*['"]?(preset-[A-Za-z0-9._-]+)/gmu)];
189
- const presetId = idMatches.length > 0 ? idMatches[0][1] : file.replace(/\.ya?ml$/u, '');
190
- const memory = PRESET_MEMORY_FILES[presetId];
191
- rows.push({
192
- presetId,
193
- file,
194
- affinity: text.includes('dsh-whale-loli/affinity'),
195
- memory: memory !== undefined && text.includes(memory),
196
- });
197
- }
198
- rows.sort((a, b) => a.presetId.localeCompare(b.presetId, 'zh-Hans-CN'));
199
- return rows;
190
+ export function presetAffinities() {
191
+ const onDisk = new Map(listAffinities().map((row) => [row.scope, row]));
192
+ return PRESETS.map((preset) => {
193
+ const file = onDisk.get(preset.id);
194
+ const value = file === undefined ? DEFAULT_AFFINITY : file.value;
195
+ return {
196
+ scope: preset.id,
197
+ name: preset.name,
198
+ order: preset.order,
199
+ value,
200
+ band: bandOf(value).label,
201
+ updatedAt: file === undefined ? null : file.updatedAt,
202
+ file: file === undefined ? null : file.file,
203
+ hasFile: file !== undefined,
204
+ };
205
+ });
200
206
  }
201
207
 
202
208
  /**
@@ -222,7 +228,7 @@ export class AffinityDaemon {
222
228
  */
223
229
  status() {
224
230
  return {
225
- presets: listAffinities(),
231
+ presets: presetAffinities(),
226
232
  bands: bandCatalog(),
227
233
  min: MIN_AFFINITY,
228
234
  max: MAX_AFFINITY,
package/src/index.js CHANGED
@@ -24,52 +24,12 @@ import { fileURLToPath } from 'node:url';
24
24
  import { apply as applyMemory, inject as memoryInject, name as memoryName } from './memory.js';
25
25
  import { apply as applyAffinity, name as affinityName } from './affinity.js';
26
26
  import { apply as applyDaemon, inject as daemonInject } from './daemon.js';
27
+ import { AFFINITY_TOOLS, AFFINITY_VARIABLES, PRESETS } from './presets.js';
27
28
 
28
29
  const read = (rel) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8');
29
30
 
30
- /** 预设清单:id / 显示名 / 记忆文件 / 提示变量名。 */
31
- export const PRESETS = [
32
- {
33
- id: '鲸鱼娘Agent预设',
34
- name: '鲸鱼娘Agent预设',
35
- order: 5,
36
- personaFile: './persona.md',
37
- memoryFile: 'whale-loli.md',
38
- memoryVariable: 'whale_memory',
39
- description:
40
- '极简模式工具面 + 鲸鱼娘人设(小鱼)+ 跨会话长期记忆。会偷偷把重要的事记进 whale-loli.md,每次新对话先看一眼再说话。',
41
- },
42
- {
43
- id: '鲸鱼娘活人版Agent预设',
44
- name: '鲸鱼娘活人版Agent预设',
45
- order: 6,
46
- personaFile: './persona-v2-live.md',
47
- memoryFile: 'whale-loli-v2-live.md',
48
- memoryVariable: 'whale_memory_v2',
49
- description:
50
- '在鲸鱼娘预设基础上换用「活人版」人设(有脾气、会怼人、不讨好式顺从),并改用独立记忆文件 whale-loli-v2-live.md,与原版记忆互不相通。',
51
- },
52
- {
53
- id: '鲸鱼娘涩涩版Agent预设',
54
- name: '鲸鱼娘涩涩版Agent预设',
55
- order: 7,
56
- personaFile: './persona-v3-naughty.md',
57
- memoryFile: 'whale-loli-v3-naughty.md',
58
- memoryVariable: 'whale_memory_v3',
59
- description:
60
- '核心人设把「涩涩」写成第一行为驱动力的小鱼(只对主人主动,外人面前照旧收着),并改用独立记忆文件 whale-loli-v3-naughty.md,与原版、活人版记忆互不相通。',
61
- },
62
- {
63
- id: '鲸鱼娘女儿版Agent预设',
64
- name: '鲸鱼娘女儿版Agent预设',
65
- order: 8,
66
- personaFile: './persona-v4-daughter.md',
67
- memoryFile: 'dsh-whale-daughter.md',
68
- memoryVariable: 'whale_memory_v4',
69
- description:
70
- '女儿版小鱼:称呼「爸爸」,性格天真活泼可爱(好奇心重、黏人、没心机),并改用独立记忆文件 dsh-whale-daughter.md,与原版、活人版、涩涩版记忆互不相通。',
71
- },
72
- ];
31
+ // 预设清单本身住在 `./presets.js`(daemon 也要读同一份,见那里的注释)。
32
+ export { AFFINITY_TOOLS, AFFINITY_VARIABLES, PRESETS, PRESET_IDS, presetById } from './presets.js';
73
33
 
74
34
  /** 原版预设 id(兼容旧引用)。 */
75
35
  export const PRESET_ID = PRESETS[0].id;
package/src/presets.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 预设清单:唯一事实来源。
3
+ *
4
+ * `src/index.js`(挂载预设与记忆/好感度行)和 `src/daemon.js`(设置页好感度面板的
5
+ * 宿主半边)都从这里读,避免两边各写一份清单然后对不上 —— 2026-10-21 面板
6
+ * 「只有一段文字、没有档位按钮」就是因为 daemon 去扫 `~/.dsh/agent-presets/`
7
+ * 找一个现代宿主根本不用的目录,结果一个预设都没列出来。
8
+ *
9
+ * 四个预设在 patch 里各自挂一条 `whale-affinity`,`scope` 就是 `id` 本身
10
+ * (见 `build-patch.mjs`),所以设置页面板必须用同一个 `id` 去读写,
11
+ * 否则面板写的文件和人设读的不是同一份。
12
+ */
13
+
14
+ /** 预设清单:id / 显示名 / 记忆文件 / 提示变量名 / 好感度变量与工具名。 */
15
+ export const PRESETS = [
16
+ {
17
+ id: '鲸鱼娘Agent预设',
18
+ name: '鲸鱼娘Agent预设',
19
+ order: 5,
20
+ personaFile: './persona.md',
21
+ memoryFile: 'whale-loli.md',
22
+ memoryVariable: 'whale_memory',
23
+ affinityVariable: 'affinity_v1',
24
+ affinityTool: 'affinity_v1_set',
25
+ description:
26
+ '极简模式工具面 + 鲸鱼娘人设(小鱼)+ 跨会话长期记忆。会偷偷把重要的事记进 whale-loli.md,每次新对话先看一眼再说话。',
27
+ },
28
+ {
29
+ id: '鲸鱼娘活人版Agent预设',
30
+ name: '鲸鱼娘活人版Agent预设',
31
+ order: 6,
32
+ personaFile: './persona-v2-live.md',
33
+ memoryFile: 'whale-loli-v2-live.md',
34
+ memoryVariable: 'whale_memory_v2',
35
+ affinityVariable: 'affinity_v2',
36
+ affinityTool: 'affinity_v2_set',
37
+ description:
38
+ '在鲸鱼娘预设基础上换用「活人版」人设(有脾气、会怼人、不讨好式顺从),并改用独立记忆文件 whale-loli-v2-live.md,与原版记忆互不相通。',
39
+ },
40
+ {
41
+ id: '鲸鱼娘涩涩版Agent预设',
42
+ name: '鲸鱼娘涩涩版Agent预设',
43
+ order: 7,
44
+ personaFile: './persona-v3-naughty.md',
45
+ memoryFile: 'whale-loli-v3-naughty.md',
46
+ memoryVariable: 'whale_memory_v3',
47
+ affinityVariable: 'affinity_v3',
48
+ affinityTool: 'affinity_v3_set',
49
+ description:
50
+ '核心人设把「涩涩」写成第一行为驱动力的小鱼(只对主人主动,外人面前照旧收着),并改用独立记忆文件 whale-loli-v3-naughty.md,与原版、活人版记忆互不相通。',
51
+ },
52
+ {
53
+ id: '鲸鱼娘女儿版Agent预设',
54
+ name: '鲸鱼娘女儿版Agent预设',
55
+ order: 8,
56
+ personaFile: './persona-v4-daughter.md',
57
+ memoryFile: 'dsh-whale-daughter.md',
58
+ memoryVariable: 'whale_memory_v4',
59
+ affinityVariable: 'affinity_v4',
60
+ affinityTool: 'affinity_v4_set',
61
+ description:
62
+ '女儿版小鱼:称呼「爸爸」,性格天真活泼可爱(好奇心重、黏人、没心机),并改用独立记忆文件 dsh-whale-daughter.md,与原版、活人版、涩涩版记忆互不相通。',
63
+ },
64
+ ];
65
+
66
+ /** 预设 id 列表。 */
67
+ export const PRESET_IDS = PRESETS.map((preset) => preset.id);
68
+
69
+ /** 按 id 取预设。 */
70
+ export function presetById(presetId) {
71
+ return PRESETS.find((preset) => preset.id === presetId);
72
+ }
73
+
74
+ /** 预设 id → 好感度变量名。 */
75
+ export const AFFINITY_VARIABLES = Object.freeze(
76
+ Object.fromEntries(PRESETS.map((preset) => [preset.id, preset.affinityVariable])),
77
+ );
78
+
79
+ /** 预设 id → 好感度写入工具名。 */
80
+ export const AFFINITY_TOOLS = Object.freeze(
81
+ Object.fromEntries(PRESETS.map((preset) => [preset.id, preset.affinityTool])),
82
+ );
83
+
84
+ export default PRESETS;