dsh-whale-loli 0.9.6 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -65,15 +65,20 @@ window.__ModuleLoader__.load({
65
65
  const describe = (method, parameters) => ({
66
66
  id: `src:${service}#${NAMESPACE}/${method}`,
67
67
  service,
68
+ // `$mount` 的 id 是 `src:<宿主服务>#<命名空间>/<方法>`;
69
+ // 外层 `service` 字段不参与路由,路由只走 namespace + method。
68
70
  namespace: NAMESPACE,
69
71
  method,
70
72
  invocation: { kind: 'direct' },
71
73
  parameters: parameters.map(parameter),
72
74
  result: { mode: 'src-json' },
73
75
  });
76
+ // 宿主读取的是 `contribution.descriptors`,而且每个描述符自己带 namespace。
77
+ // 之前这里写成 `{ package, methods: [...] }`,`$mount` 一进门就抛
78
+ // `contribution.descriptors is not iterable`,命名空间永远挂不上去。
74
79
  return {
75
80
  package: REMOTE_PACKAGE,
76
- methods: [
81
+ descriptors: [
77
82
  describe('status', []),
78
83
  describe('set', ['scope', 'value']),
79
84
  describe('verify', []),
@@ -88,15 +93,60 @@ window.__ModuleLoader__.load({
88
93
  * `invoke()` 本身已经不 reject(除了装配错误),这里再兜一层,
89
94
  * 让面板任何情况下都能渲染出人话而不是白屏。
90
95
  */
96
+ /**
97
+ * `$mount()` 的完成闸门。
98
+ *
99
+ * `apply()` 里挂命名空间是**异步**的,而设置分区组件会**立刻**去调 `status()`。
100
+ * 之前的 `call()` 完全不等它:组件先跑 → `remote['whale-affinity']` 还不存在
101
+ * → 抛「远程方法 whale-affinity.status 未挂载」,并把这句话写进 error state;
102
+ * 之后 `$mount` 就算成功也不会重试,面板就永久显示那句报错(重启多少次都一样)。
103
+ *
104
+ * 所以每个远程调用都先等这道闸门,闸门之后再短轮询,兜住微任务时序。
105
+ */
106
+ let mountGate = null;
107
+
108
+ /** 闸门之后的轮询:25 × 80ms ≈ 2s,足够覆盖一次命名空间装配。 */
109
+ const ATTACH_TRIES = 25;
110
+ const ATTACH_STEP_MS = 80;
111
+
112
+ /**
113
+ * 带外诊断:走同源的 `/whale-report`(宿主侧 `whale-affinity-daemon` 注册的
114
+ * 免鉴权 prefix 路由),**不依赖**那条还没挂上的 remote 通道。
115
+ *
116
+ * 之前的做法是 `remote['whale-affinity'].report(...)` —— 可是 remote 通道本身
117
+ * 就是出问题的那个,等于用坏掉的电话去报告电话坏了,磁盘上永远留不下东西。
118
+ */
119
+ function beacon(stage, message) {
120
+ try {
121
+ const query = new URLSearchParams({
122
+ stage: String(stage),
123
+ message: String(message ?? '').slice(0, 800),
124
+ });
125
+ fetch(`/whale-report?${query}`, { cache: 'no-store' }).catch(() => {});
126
+ } catch {
127
+ // 诊断失败绝不能影响面板
128
+ }
129
+ }
130
+
131
+ function sleep(ms) {
132
+ return new Promise((resolve) => setTimeout(resolve, ms));
133
+ }
134
+
135
+ /** 等闸门 + 轮询,拿到真正可用的远程方法句柄。 */
136
+ async function attach(remote, method) {
137
+ if (mountGate !== null) await mountGate.then(() => {}, () => {});
138
+ for (let attempt = 0; attempt < ATTACH_TRIES; attempt += 1) {
139
+ const fn = remote?.[NAMESPACE]?.[method];
140
+ if (typeof fn === 'function') return fn;
141
+ await sleep(ATTACH_STEP_MS);
142
+ }
143
+ beacon('attach-failed', `${NAMESPACE}.${method}`);
144
+ throw new Error(`远程方法 ${NAMESPACE}.${method} 未挂载`);
145
+ }
146
+
91
147
  function call(remote, method, args) {
92
- return Promise.resolve()
93
- .then(() => {
94
- const fn = remote?.[NAMESPACE]?.[method];
95
- if (typeof fn !== 'function') {
96
- throw new Error(`远程方法 ${NAMESPACE}.${method} 未挂载`);
97
- }
98
- return fn(...args);
99
- })
148
+ return attach(remote, method)
149
+ .then((fn) => fn(...args))
100
150
  .then((result) => {
101
151
  if (result && typeof result === 'object' && 'ok' in result) {
102
152
  if (result.ok) return result.value;
@@ -360,27 +410,28 @@ window.__ModuleLoader__.load({
360
410
  if (typeof remote?.$mount !== 'function') {
361
411
  box.error = '浏览器侧没有 ctx.remote.$mount';
362
412
  console.warn('[whale-affinity] 浏览器侧没有 ctx.remote.$mount,好感度面板会只读');
413
+ beacon('no-mount', 'ctx.remote.$mount 不是函数');
363
414
  return () => {};
364
415
  }
365
416
  let disposed = false;
366
417
  let unmount;
367
- Promise.resolve()
368
- .then(() => remote.$mount(contribution()))
418
+ mountGate = Promise.resolve().then(() => remote.$mount(contribution()));
419
+ mountGate
369
420
  .then((dispose) => {
370
421
  if (disposed) {
371
422
  return dispose?.();
372
423
  }
373
424
  unmount = dispose;
374
425
  box.mounted = true;
426
+ beacon('mounted', 'ok');
375
427
  return undefined;
376
428
  })
377
429
  .catch((cause) => {
378
430
  const message = cause?.message ?? String(cause ?? '');
379
431
  box.error = message;
380
432
  console.warn('[whale-affinity] $mount 失败,好感度面板将无法读写:', cause);
381
- // 浏览器 console 用户看不到 —— 把原因写回宿主日志。
382
- // 注意 `$mount` 失败时命名空间还没挂上,`report` 很可能也调不到;
383
- // 所以这条只能是「尽力而为」,真正保底的信息在面板自己显示的 box.error 上。
433
+ // 走带外通道记档(`report` 在 $mount 失败时根本调不到,见 beacon 注释)。
434
+ beacon('$mount', message);
384
435
  try {
385
436
  const report = remote?.[NAMESPACE]?.report;
386
437
  if (typeof report === 'function') {
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.6"
12
+ "version": "0.9.7"
13
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-whale-loli",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
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
@@ -458,9 +458,68 @@ export function apply(ctx) {
458
458
  trace('ctx.effect 抛错(已吞)', { error: String(error?.message ?? error) });
459
459
  }
460
460
 
461
+ // ── 客户端带外诊断通道 ──────────────────────────────────────────────
462
+ //
463
+ // 浏览器面板出问题时**不能**用 remote 通道回报 —— 那条通道本身就是要诊断的东西。
464
+ // 之前用 `remote['whale-affinity'].report(...)` 打点,结果 $mount 一失败就什么都
465
+ // 写不出来,日志永远是空的。所以这里注册一个免鉴权的 prefix 路由,客户端用同源
466
+ // fetch 打点,落盘到 <DSH_HOME>/whale-affinity-client.log。
467
+ //
468
+ // 依据:`dsh-host-webserver` 的 `register(route)`(lib/index.js:325-332)只是把路由
469
+ // 放进 exact/prefixes 表,**不做任何鉴权**;`dsh-client-modules` 的 `/plugins`
470
+ // 路由同样未鉴权可达(错 URL 返回 404 而不是 401)。
471
+ try {
472
+ ctx.inject?.(['webServer'], (wctx) => {
473
+ wctx.effect(
474
+ () =>
475
+ wctx.webServer.register({
476
+ kind: 'prefix',
477
+ path: '/whale-report',
478
+ handler: handleReport,
479
+ }),
480
+ 'whale-affinity-daemon.report route',
481
+ );
482
+ });
483
+ } catch (error) {
484
+ trace('注册 /whale-report 路由失败(已吞)', { error: String(error?.message ?? error) });
485
+ }
486
+
461
487
  return undefined;
462
488
  }
463
489
 
490
+ /**
491
+ * `/whale-report` 的处理器:把客户端打点写进 `whale-affinity-client.log`。
492
+ *
493
+ * 客户端用 `fetch('/whale-report?stage=...&message=...')` 打过来;`request.url`
494
+ * 可能是完整 URL 也可能是 path,统一用 `new URL(raw, 'http://dsh.invalid')` 兜住。
495
+ *
496
+ * @param request - 宿主 webServer 传进来的请求对象。
497
+ * @returns 一个极小的 200 响应。
498
+ */
499
+ function handleReport(request) {
500
+ try {
501
+ const raw = typeof request?.url === 'string' ? request.url : '/';
502
+ const url = new URL(raw, 'http://dsh.invalid');
503
+ traceClient(url.searchParams.get('stage') ?? '', url.searchParams.get('message') ?? '');
504
+ } catch (error) {
505
+ traceClient('handler-error', String(error?.message ?? error));
506
+ }
507
+ return new Response('ok', {
508
+ status: 200,
509
+ headers: { 'content-type': 'text/plain; charset=utf-8' },
510
+ });
511
+ }
512
+
513
+ /** 把浏览器侧的打点追加到 `<DSH_HOME>/whale-affinity-client.log`。 */
514
+ function traceClient(stage, message) {
515
+ try {
516
+ const line = JSON.stringify({ at: new Date().toISOString(), stage, message });
517
+ appendFileSync(join(dshHome(), 'whale-affinity-client.log'), line + '\n');
518
+ } catch {
519
+ // 诊断失败绝不能影响插件加载
520
+ }
521
+ }
522
+
464
523
  /**
465
524
  * 把启动事实追加到 `<DSH_HOME>/whale-affinity-daemon.log`。
466
525
  *