@nightpainters/dsh-ling 1.5.3 → 1.6.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/lib/client.js CHANGED
@@ -430,7 +430,7 @@
430
430
 
431
431
  function dark() { return document.documentElement.getAttribute('data-ds-theme') === 'dark' ? '1' : '0'; }
432
432
 
433
- /** 悬停即时小气泡(2026-09-27,用户反馈"太罗嗦 + 窗口叠加"):
433
+ /** 悬停即时小气泡(2026-09-27,尝生反馈"太罗嗦 + 窗口叠加"):
434
434
  * 原生 `title` 由**浏览器**绘制 —— 既不受样式控制,也**不会自动消失**;而悬停 1.5s 会开菜单,
435
435
  * 于是浏览器 tooltip 和菜单叠成两层。故完整提醒改走自绘:悬停瞬间出现、`TIP_MS` 后自动移除,
436
436
  * 且菜单一开就先收掉它 ⇒ 两者永不同屏。与 `toast` 同规格(都是 dsh-ling-menu 容器 + 定时移除),
@@ -1024,7 +1024,7 @@
1024
1024
  return r || {};
1025
1025
  }).catch(function () { toast('切换失败(网关不可达)'); return {}; });
1026
1026
  }
1027
- // ⚠️ 这里**故意不问一句** —— 用户 2026-09-27 裁决(原话):
1027
+ // ⚠️ 这里**故意不问一句** —— 尝生 2026-09-27 裁决(原话):
1028
1028
  // 「创建新会话时只能通过调全局默认来调新会话的模式,所以它的使用比想象的频繁一些」
1029
1029
  // ⇒ 全局默认不是危险开关,而是**开一个不同模式的新会话**的必经入口,属高频正常动作;
1030
1030
  // 事前弹确认 = 给日常添堵,且那个按钮按下本身就是低成本可撤销动作。
@@ -1081,7 +1081,7 @@
1081
1081
  wrap.appendChild(h);
1082
1082
  var kv = el('div', 'kv', '加载中…');
1083
1083
  wrap.appendChild(kv);
1084
- // E3(2026-09-27 用户拍板):「那一行」只读块。**必须另起一个节点** —— 上面 `kv` 整块是纯
1084
+ // E3(2026-09-27 尝生拍板):「那一行」只读块。**必须另起一个节点** —— 上面 `kv` 整块是纯
1085
1085
  // textContent(一段文字),而「看全文」要一个真能展开/收起的子节点(快照原文放 <pre>,默认收起)。
1086
1086
  // 这一块对模型上下文**零占用**:读的全是 `/state` 面板档(`l0Preview=1`)字段,纯读,不进 prompt。
1087
1087
  var e3box = el('div', 'ling-e3');
@@ -1223,7 +1223,7 @@
1223
1223
  }
1224
1224
  // 全局视图的 L2 三行**必然**缺席:后端 lib/host/api.js 在 global 分支写死 snapshot: null,
1225
1225
  // 而且整个返回里不带 idleRefresh 字段 ⇒ 上面两段一个字符都进不来。
1226
- // 不写这一行,"按设计没有"与"坏了"在界面上长得一模一样(用户 2026-09-27 反馈)。
1226
+ // 不写这一行,"按设计没有"与"坏了"在界面上长得一模一样(尝生 2026-09-27 反馈)。
1227
1227
  // 放在 L2 本该出现的位置(待应用更新 → 这里 → 人格),两个视图的行序才对得齐。
1228
1228
  if (global && !r.snapshot && !r.idleRefresh) {
1229
1229
  lines.push('(全局视图无会话快照 —— 快照定稿/空闲刷新是按会话统计的,进入某个会话才显示)' + SNAP_TAG);
@@ -2910,7 +2910,7 @@
2910
2910
 
2911
2911
  // ---------------- 记忆树 · 复盘(D9-b,2026-09-19) ----------------
2912
2912
  // 树形结构 + 复盘驱动的操作:并脉(归类) / 连边 / 调权重 / 矛盾裁定。
2913
- // 「一键复盘」按用户定的设计约束(§10.1)把开场材料一次备齐 —— 复盘的门槛必须接近 0,
2913
+ // 「一键复盘」按尝生定的设计约束(§10.1)把开场材料一次备齐 —— 复盘的门槛必须接近 0,
2914
2914
  // 否则它会像"整理房间"一样被无限推迟。树的任何操作都**不改写记忆内容**。
2915
2915
  var TREE_KIND = { trunk: '主干', branch: '枝', vein: '主脉' };
2916
2916
  var TREE_COLOR = { trunk: 'var(--lg-primary)', branch: 'var(--lg-cyan)', vein: '#c58b2f' };
@@ -2973,7 +2973,7 @@
2973
2973
  };
2974
2974
  }
2975
2975
  row.appendChild(tri);
2976
- // 显示层(用户 2026-09-21 定):前端不写运算层级名;主干用一句话表示"还在生长"。
2976
+ // 显示层(尝生 2026-09-21 定):前端不写运算层级名;主干用一句话表示"还在生长"。
2977
2977
  var isTrunk = n.id === ((st.treeData && st.treeData.trunk) || 'trunk');
2978
2978
  var badge = el('span', null, isTrunk ? '🌱 你们的故事正在生长' : (TREE_KIND[n.kind] || n.kind));
2979
2979
  badge.style.cssText = 'font-size:11px;padding:1px 6px;border-radius:6px;flex:none;'
@@ -2987,7 +2987,7 @@
2987
2987
  + (n.weightScale && Number(n.weightScale) !== 1 ? ' · ×' + n.weightScale : ''));
2988
2988
  meta.style.cssText = 'flex:none;opacity:.72;';
2989
2989
  row.appendChild(meta);
2990
- // ---- 连边小标记(用户 2026-09-25) ----
2990
+ // ---- 连边小标记(尝生 2026-09-25) ----
2991
2991
  // 与侧栏记忆树同款:行内只留一个 ⇄N,不在这里铺开"连到哪里" ——
2992
2992
  // 选中本行后,下方"横向连边"区块才展开它的详情。
2993
2993
  var myLinks = (st.linkIndex && st.linkIndex[n.id]) || [];
@@ -3073,7 +3073,7 @@
3073
3073
 
3074
3074
  // ---------------- 一键生成记忆树(autotree,2026-09-20) ----------------
3075
3075
  // 三阶段:① 机械分段(零模型调用) ② 器灵批量命名 ③ 主人改名字后应用。
3076
- // 用户的设计约束:模型只做"命名"这一件语义活;分段是机械活,不烧 token,也不该由模型做。
3076
+ // 尝生的设计约束:模型只做"命名"这一件语义活;分段是机械活,不烧 token,也不该由模型做。
3077
3077
  function loadAutoPreview() {
3078
3078
  if (st.autoBusy) return;
3079
3079
  st.autoBusy = true;
@@ -3206,7 +3206,7 @@
3206
3206
  inp.oninput = function () { st.autoNames[b.id] = inp.value; };
3207
3207
  row.appendChild(inp);
3208
3208
  // 「看内容」:展开这一簇实际包含哪些标题 —— 只给一个名字(如"零散记录")时
3209
- // 主人无从判断该不该采纳(用户实测反馈)。内容来自 /tree/autobuild/preview 的 sample。
3209
+ // 主人无从判断该不该采纳(尝生实测反馈)。内容来自 /tree/autobuild/preview 的 sample。
3210
3210
  var bPeek = el('button', null, st.autoExpand[b.id] ? '▾ 收起' : '▸ 看内容');
3211
3211
  bPeek.type = 'button';
3212
3212
  bPeek.style.cssText = 'flex:none;font-size:11px;padding:1px 7px;';
@@ -3239,7 +3239,7 @@
3239
3239
  var links = td.links || [];
3240
3240
  var conf = st.conflicts || [];
3241
3241
  var pend = conf.filter(function (c) { return c.status === 'pending'; });
3242
- // ---- 连边索引(用户 2026-09-25):节点 id → 挂在它身上的连边 ----
3242
+ // ---- 连边索引(尝生 2026-09-25):节点 id → 挂在它身上的连边 ----
3243
3243
  // 用途两个:① nodeRow 行内画小标记(⇄N),让"这条枝有横向连边"平时可见;
3244
3244
  // ② 下方"横向连边"区块改为**只显示选中项**那几条 —— 原先是无条件铺开全部,
3245
3245
  // 连边一多这块就越来越臃肿,把真正该看的矛盾区挤下去。
@@ -3252,11 +3252,11 @@
3252
3252
  });
3253
3253
 
3254
3254
  var tools = el('div', 'btnrow');
3255
- // 左对齐(用户 2026-09-25):`.btnrow` 的全局 CSS 是 justify-content:flex-end(对"保存/取消"
3255
+ // 左对齐(尝生 2026-09-25):`.btnrow` 的全局 CSS 是 justify-content:flex-end(对"保存/取消"
3256
3256
  // 这类按钮行合适),但树工具条挂在面板左上,右对齐会让它飘到面板另一头,与下方树列表脱节。
3257
3257
  // 就地覆盖,不动全局 class(它被另外 15 处按钮行复用)。
3258
3258
  tools.style.cssText = 'margin:0 0 4px;flex-wrap:wrap;justify-content:flex-start;';
3259
- // 记忆中心已改为**只读视图**(2026-09-22 用户裁决):扫重复 / 模型判定 / 归并零散枝 /
3259
+ // 记忆中心已改为**只读视图**(2026-09-22 尝生裁决):扫重复 / 模型判定 / 归并零散枝 /
3260
3260
  // 解锁编辑等**复盘操作**全部移到右侧「器灵 · 复盘」栏 —— 这里只留「看」与一个总入口。
3261
3261
  var bRefresh = el('button', null, '刷新');
3262
3262
  var bReview = el('button', null, '🌱 一键复盘');
@@ -3277,7 +3277,7 @@
3277
3277
  treeHead.style.cssText = 'margin:2px 0 4px;font-weight:700;opacity:.9;';
3278
3278
  body.appendChild(treeHead);
3279
3279
  var treeBox = el('div', null);
3280
- // 选中/重绘时保持滚动位置(否则每次重建 DOM 都会跳回顶部 —— 用户 2026-09-22 反馈)
3280
+ // 选中/重绘时保持滚动位置(否则每次重建 DOM 都会跳回顶部 —— 尝生 2026-09-22 反馈)
3281
3281
  treeBox.onscroll = function () { st.treeScroll = treeBox.scrollTop; };
3282
3282
  treeBox.style.cssText = 'border:1px solid var(--lg-border);border-radius:11px;padding:6px 8px;'
3283
3283
  + 'max-height:34vh;overflow:auto;';
@@ -3302,7 +3302,7 @@
3302
3302
  : '🔒 树平时只读 —— 进入复盘模式后可修改。(先点一条枝 / 主脉选中它)'));
3303
3303
  body.appendChild(ops);
3304
3304
 
3305
- // ---- 横向连边(用户 2026-09-25 改):只显示**选中项**的连边 ----
3305
+ // ---- 横向连边(尝生 2026-09-25 改):只显示**选中项**的连边 ----
3306
3306
  // 原先是无条件铺开全部连边 ⇒ 连边一多这块就越来越臃肿,把真正该看的矛盾区挤到下面。
3307
3307
  // 现在:行内只留 ⇄N 小标记(nodeRow 里画),选中哪条才展开它的详情;
3308
3308
  // 未选中时只给一行"共 N 条"的提示,可发现性不丢。
@@ -3384,7 +3384,7 @@
3384
3384
  + (c.winner ? ' · 以 ' + (c.winner === 'a' ? 'A' : 'B') + ' 为准' : ''));
3385
3385
  badge.style.cssText = 'opacity:.75;margin-bottom:4px;';
3386
3386
  card.appendChild(badge);
3387
- // 2026-09-21:每侧标题可点开看**摘要全文** —— 只给标题时人判不出"到底哪矛盾"(用户实测反馈)。
3387
+ // 2026-09-21:每侧标题可点开看**摘要全文** —— 只给标题时人判不出"到底哪矛盾"(尝生实测反馈)。
3388
3388
  function sideLine(tag, title, summary, convId) {
3389
3389
  var wrap = el('div', null);
3390
3390
  var head = el('div', null, tag + ':' + (title || convId || '?'));
@@ -4520,7 +4520,7 @@
4520
4520
  var label = rail
4521
4521
  ? (auditBad ? '⚠' : (mode === 'work' ? '⚙' : '☾'))
4522
4522
  : ((auditBad ? '⚠ ' : '') + (mode === 'work' ? '⚙ 工作' : '☾ 生活'));
4523
- // ⚠️ 原生 `title` 只留**一行**(2026-09-27 用户反馈:原来四行太罗嗦,且浏览器 tooltip 不会自动
4523
+ // ⚠️ 原生 `title` 只留**一行**(2026-09-27 尝生反馈:原来四行太罗嗦,且浏览器 tooltip 不会自动
4524
4524
  // 消失,悬停 1.5s 菜单一开就和它叠成两层)。完整提醒改走自绘气泡 `showTip` ——
4525
4525
  // 悬停即现、3s 自隐、菜单一开就收,见 `tipText`。
4526
4526
  var title = (isGlobal
@@ -4550,7 +4550,7 @@
4550
4550
  if (tipText) showTip(btnRef && btnRef.current, tipText);
4551
4551
  hoverTimer.current = setTimeout(function () {
4552
4552
  hoverTimer.current = null;
4553
- hideTip(); // 菜单与气泡永不同屏(用户反馈的"窗口叠加"就是这两层)
4553
+ hideTip(); // 菜单与气泡永不同屏(尝生反馈的"窗口叠加"就是这两层)
4554
4554
  var node = btnRef && btnRef.current;
4555
4555
  openMenuNear(node ? node.getBoundingClientRect() : null, store);
4556
4556
  }, HOVER_MS);
@@ -4626,7 +4626,7 @@
4626
4626
  return out.join('\n');
4627
4627
  }
4628
4628
 
4629
- /** 复盘解锁:跨面板共享的真值 —— 记忆中心与右侧复盘栏读同一个变量(用户 2026-09-21)。
4629
+ /** 复盘解锁:跨面板共享的真值 —— 记忆中心与右侧复盘栏读同一个变量(尝生 2026-09-21)。
4630
4630
  * 「复盘模式」不是一个 UI 开关,而是"左会话 + 右侧栏"这个状态;树编辑解锁只是它的附属。*/
4631
4631
  var lingUnlocked = false;
4632
4632
  /** 解锁状态订阅者(2026-09-22 审计 C#3):侧栏是 React 组件,useState 只在**首次渲染**取值,
@@ -4660,7 +4660,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
4660
4660
  /**
4661
4661
  * 复制文本(clipboard 不可用时回退 execCommand)。
4662
4662
  * silent=true 时不再单独 toast「已复制」—— 调用方自己会报一句完整的结果,
4663
- * 否则会出现两个 toast 叠加(2026-09-22 用户实测:『已复制复盘开场白 —— 已复制 …』)。
4663
+ * 否则会出现两个 toast 叠加(2026-09-22 尝生实测:『已复制复盘开场白 —— 已复制 …』)。
4664
4664
  */
4665
4665
  function reviewCopy(text, silent) {
4666
4666
  // 审计 C#13:改成**回报是否成功**(Promise) —— clipboard 是异步的,只有 `.then` 才知道结果;
@@ -4773,7 +4773,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
4773
4773
  if (i >= 0) lingUnlockSubs.splice(i, 1);
4774
4774
  };
4775
4775
  }, []);
4776
- // 选中态统一(2026-09-22 用户):单选与多选共用 s8 的集合,selId 由它派生。
4776
+ // 选中态统一(2026-09-22 尝生):单选与多选共用 s8 的集合,selId 由它派生。
4777
4777
  // 原先 selId(蓝) 与 dragIds(橙) 是两套状态,表现为"一蓝一橙"且拖动只有橙的会动。
4778
4778
  var refName = React.useRef(null);
4779
4779
  var refRep = React.useRef(null);
@@ -4788,7 +4788,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
4788
4788
  var setSnapshots = s5[1];
4789
4789
  var refSnap = React.useRef(null);
4790
4790
  // R2 会话级展开:哪些枝展开了、该枝的成员缓存(懒加载)
4791
- // C(2026-09-25 用户):此前 openId 是**单个字符串** ⇒ 打开一个就把上一个关掉(独占式)。
4791
+ // C(2026-09-25 尝生):此前 openId 是**单个字符串** ⇒ 打开一个就把上一个关掉(独占式)。
4792
4792
  // 复盘的场景恰恰是"同时对照几条枝",独占等于逼人来回点。改用集合(每节点独立),
4793
4793
  // 可同时展开多个;Shift+C 一键全折叠(见下方 keydown effect)。
4794
4794
  var s6 = React.useState({});
@@ -4815,7 +4815,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
4815
4815
  // 否则落库前的撤回信息里记不到这一项 ⇒ Ctrl+Z 报「已撤回:恢复了 0 项的位置」。
4816
4816
  // 用 ref 而不是 state:拖拽期间不需要重渲染,drop 时读最新值即可。
4817
4817
  var refDragFrom = React.useRef('');
4818
- // 滚动位置自保(2026-09-22 用户:点主脉级会跳回顶部)
4818
+ // 滚动位置自保(2026-09-22 尝生:点主脉级会跳回顶部)
4819
4819
  // 点选节点会触发整块重渲染,内容高度变化时浏览器会把视口弹回顶部;
4820
4820
  // 故实时记录 scrollTop,并在每次渲染后恢复 —— 与记忆中心 st.treeScroll 同思路。
4821
4821
  var boxRef = React.useRef(null);
@@ -4826,7 +4826,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
4826
4826
  });
4827
4827
  // 编辑区只在"恰好选中一个"时生效(多选时显示提示)
4828
4828
  var selId = (dragIds.length === 1) ? dragIds[0] : '';
4829
- // 复盘守门(2026-09-22 用户:「互相复盘不应该被我绕过,动记忆树不能是随意的」):
4829
+ // 复盘守门(2026-09-22 尝生:「互相复盘不应该被我绕过,动记忆树不能是随意的」):
4830
4830
  // 记录"自器灵上次开口以来累积了多少笔结构改动还没交代"。未同步时:
4831
4831
  // ① 面板顶部红字提示 ② **拦住"退出复盘模式"** —— 改完树必须让器灵有机会看到。
4832
4832
  var s10 = React.useState(0);
@@ -4845,7 +4845,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
4845
4845
  var veinSugs = s11[0];
4846
4846
  var setVeinSugs = s11[1];
4847
4847
  // 审计 E10(1.5.1 红蓝对抗):「这条主脉**已经**有主脉记忆了」是**采纳前**就该知道的事。
4848
- // 后端对同主脉的第二次采纳做的是"先把旧版归档、再覆盖"(用户 2026-09-25 裁决),可界面
4848
+ // 后端对同主脉的第二次采纳做的是"先把旧版归档、再覆盖"(尝生 2026-09-25 裁决),可界面
4849
4849
  // 此前对"将要覆盖"零提示 ⇒ 唯一的发现途径是事后去翻「最近改动」。
4850
4850
  // 判据不靠后端加字段:`conv_overview.source='vein'` 的 `conv_id` **就是主脉 id**
4851
4851
  //(memory.js:1821 写入时 `conv_id: veinId`),所以查一次 `?source=vein` 就知道
@@ -4909,7 +4909,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
4909
4909
  }, 8000);
4910
4910
  return function () { clearInterval(t); };
4911
4911
  }, []);
4912
- // Shift+C:一键折叠所有已展开的枝(2026-09-25 用户定,暂定键位)。
4912
+ // Shift+C:一键折叠所有已展开的枝(2026-09-25 尝生定,暂定键位)。
4913
4913
  // ⚠️ 位置纪律:必须落在**本函数所有 early return 之前**(下方有 `if (!data) return …`)。
4914
4914
  // React 要求每次渲染的 hook 数量与顺序恒定;把 useEffect 放在 early return 之后会出现
4915
4915
  // "首次渲染 data 为空 → 提前 return(少一个 hook) → 数据到达后重渲染多一个 hook"
@@ -5206,7 +5206,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
5206
5206
  userSelect: 'none', WebkitUserSelect: 'none',
5207
5207
  // 选中色只有一种(琥珀) —— 单选与 Ctrl 多选视觉一致,避免"一蓝一橙"
5208
5208
  background: on ? 'rgba(197,139,47,.22)' : 'transparent',
5209
- // 拖动悬停目标:青色虚线框,松手前就知道会落到哪(2026-09-22 用户)
5209
+ // 拖动悬停目标:青色虚线框,松手前就知道会落到哪(2026-09-22 尝生)
5210
5210
  outline: (dropTarget === x.n.id) ? '2px dashed #3fb9a6'
5211
5211
  : (on ? '1px solid rgba(197,139,47,.5)' : 'none'),
5212
5212
  outlineOffset: '-2px',
@@ -5221,7 +5221,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
5221
5221
  },
5222
5222
  style: { display: 'inline-block', width: '12px', opacity: hasKids ? 0.75 : 0.25 },
5223
5223
  }, hasKids ? (isOpen ? '▾' : '▸') : '·'),
5224
- // A(2026-09-25 用户):主脉与枝此前在列表里长得一模一样,只能靠"点开会显示 [主脉]"
5224
+ // A(2026-09-25 尝生):主脉与枝此前在列表里长得一模一样,只能靠"点开会显示 [主脉]"
5225
5225
  // 分辨 —— 而「零散记录」(枝) 与「零散会话」(主脉) 这类名字只差一个字,必然选错。
5226
5226
  // 现在行首给标记:🌱 主干 / ◆ 主脉 / · 枝。
5227
5227
  (x.n.kind === 'trunk' ? '🌱 ' : (x.n.kind === 'vein' ? '◆ ' : '· ')) + (x.n.name || x.n.id)
@@ -5290,7 +5290,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
5290
5290
  paddingLeft: ((x.d + 1) * 12 + 12) + 'px', fontSize: '11px', opacity: 0.85,
5291
5291
  cursor: 'grab', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
5292
5292
  userSelect: 'none', WebkitUserSelect: 'none', borderRadius: '5px',
5293
- // 会话级也要有选中可视化(2026-09-22 用户:会话能拖但看不出选没选中)
5293
+ // 会话级也要有选中可视化(2026-09-22 尝生:会话能拖但看不出选没选中)
5294
5294
  background: con ? 'rgba(197,139,47,.22)' : 'transparent',
5295
5295
  outline: (dropTarget === cid) ? '2px dashed #3fb9a6'
5296
5296
  : (con ? '1px solid rgba(197,139,47,.5)' : 'none'),
@@ -5465,7 +5465,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
5465
5465
  .catch(function (e) { toast('✗ 提炼失败:' + ((e && e.message) || '未知')); });
5466
5466
  };
5467
5467
  var resolveSug = function (id, action, text) {
5468
- // 用户 2026-09-25 裁决(乙):**提炼不锁、采纳要解锁**。
5468
+ // 尝生 2026-09-25 裁决(乙):**提炼不锁、采纳要解锁**。
5469
5469
  // 「提炼」只往候选表写一行草稿,不碰记忆 ⇒ 不锁(锁它会抬启动成本);
5470
5470
  // 「采纳」会写 conv_overview(source='vein') + assignConv 改树归属 ⇒ 与改名/并脉同级,故要走同一道门。
5471
5471
  // 「驳回」不写任何东西 ⇒ 不锁。
@@ -5505,7 +5505,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
5505
5505
  h('button', { key: 'b', type: 'button', onClick: function () { doDistill(selX.id); } }, '✦ 提炼主脉'),
5506
5506
  ]));
5507
5507
  } else if (selX && selX.kind === 'branch') {
5508
- // B(2026-09-25 用户):枝上也要有提炼入口 —— 但提炼的素材是"主脉及其子枝",
5508
+ // B(2026-09-25 尝生):枝上也要有提炼入口 —— 但提炼的素材是"主脉及其子枝",
5509
5509
  // 对枝本身提炼没有意义。所以这里先沿 parentId 往上找它所属的主脉,
5510
5510
  // 按钮直接对**那条主脉**做提炼,并在文案里说清对象,省掉"自己去列表里找主脉"这一跳。
5511
5511
  var vAnc = null;
@@ -5531,7 +5531,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
5531
5531
  '✦ 提炼 —— 这条枝还没挂在主脉下;先并脉(挂到某条主脉),再对它提炼。')]));
5532
5532
  }
5533
5533
  // ---- R7 主脉提炼审核队列(2026-09-23) ----
5534
- // 2026-09-25 用户:「提炼结果放在『主脉提炼』那一条底下」—— 审核队列紧跟提炼入口,
5534
+ // 2026-09-25 尝生:「提炼结果放在『主脉提炼』那一条底下」—— 审核队列紧跟提炼入口,
5535
5535
  // 点完提炼往下看就是候选,不必滚到页面别处去找。
5536
5536
  // 设计稿 §5:提炼结果先入队、带派生来源;采纳后才写进主脉记忆,且可"改后采纳"(文本框里改完再点采纳)。
5537
5537
  if (veinSugs.length) {
@@ -5726,7 +5726,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
5726
5726
  });
5727
5727
  }
5728
5728
  // ---- 最近改动 ----
5729
- // 2026-09-25 用户:「『最近改动』块挪到『树的保存/恢复』底下」—— 它记的是"谁在什么时候
5729
+ // 2026-09-25 尝生:「『最近改动』块挪到『树的保存/恢复』底下」—— 它记的是"谁在什么时候
5730
5730
  // 动了树",与上面那排快照是同一件事的两面(一份是存档,一份是流水),放一起才顺。
5731
5731
  if (data.log.length) {
5732
5732
  kids.push(h('div', { key: 'lh', style: { fontWeight: 700, margin: '9px 0 2px' } }, '最近改动'));
@@ -5767,7 +5767,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
5767
5767
  style: unlocked ? { borderColor: '#c58b2f', color: '#c58b2f' } : {},
5768
5768
  onClick: function () {
5769
5769
  var next = !unlocked;
5770
- // 复盘守门(2026-09-22 用户:「互相复盘不应该被我绕过」):退出前必须先把
5770
+ // 复盘守门(2026-09-22 尝生:「互相复盘不应该被我绕过」):退出前必须先把
5771
5771
  // 未同步的改动交代给器灵 —— 不能"改完几十处直接走人",那等于假复盘。
5772
5772
  // 2026-09-23 修:显示值可能滞后(器灵开口才推进水位,而"发言"不触发本面板重载),
5773
5773
  // 所以退出前**实时查一次**服务端 —— 避免"明明已经同步了却拦着不让退"。
@@ -5793,7 +5793,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
5793
5793
  }
5794
5794
  setUnlocked(next);
5795
5795
  setLingUnlocked(next, true);
5796
- // 进入复盘模式 = 解锁 + 备好开场白(2026-09-22 用户:行为与总控板「一键进入复盘」对齐)
5796
+ // 进入复盘模式 = 解锁 + 备好开场白(2026-09-22 尝生:行为与总控板「一键进入复盘」对齐)
5797
5797
  // 审计 C#17:原来这里把 reviewCopy 当同步函数 —— 它返回 Promise,所以 try/catch
5798
5798
  // 根本抓不到复制失败,而且**不等结果**就先报「已复制」。改成等结果再说文案。
5799
5799
  if (next) {
@@ -6100,7 +6100,7 @@ var lingCenterReload = null; // 侧栏改完树后通知记忆中心重载(两
6100
6100
  // ⚠️ 只等 sidebarRightTabs —— 不要把 sidebarRight 也写进来:
6101
6101
  // 它是 per-session 绑定的控制器,在该注入作用域里可能永不满足,
6102
6102
  // 回调就永远不执行 ⇒ 面板类型注册不上 ⇒ openTab 抛
6103
- // 'sidebarRight: no tab type is registered as "ling-review"'(用户实测)。
6103
+ // 'sidebarRight: no tab type is registered as "ling-review"'(尝生实测)。
6104
6104
  // 打开面板时用 lingCtx.get('sidebarRight') 动态取即可(那时必然已挂载)。
6105
6105
  // 🔴 根因(2026-09-21 定位):服务必须从**回调参数**上取(scoped),
6106
6106
  // 不能用闭包里的 ctx —— cordis 的属性访问在未 inject 时抛
package/lib/host/api.js CHANGED
@@ -461,7 +461,7 @@ export function registerApi(ctx, deps, config) {
461
461
  '/deep/run-import', '/deep/run', '/deep/one',
462
462
  '/dsweb/summary/run',
463
463
  '/feedback/apply', '/feedback/dismiss', '/suggestions/apply', '/suggestions/dismiss',
464
- // ── 1.5.3(2026-09-29 夜,用户批;依据作者的审计台账《1.5.3 端点写面 · 未进 POST_ONLY》,该台账不随包公开)──
464
+ // ── 1.5.3(2026-09-29 夜,尝生批;依据作者的审计台账《1.5.3 端点写面 · 未进 POST_ONLY》,该台账不随包公开)──
465
465
  // ⚠️ 版本号由主会话校正:本批**尚未发布**(`package.json` 仍是 `1.5.3-dev`)⇒ 归 **1.5.3**;
466
466
  // 原先这里写的 `1.5.4` 是修复代理想当然的版本号,仓库里从无 1.5.4 的计划。
467
467
  // 三条都是**形态上就不该由 GET 执行**的端点,补进同一处声明(仍守"一处维护、一眼看全"):
@@ -478,7 +478,7 @@ export function registerApi(ctx, deps, config) {
478
478
  ]);
479
479
  // ── 手术门下沉(2026-09-30 深夜;规格 §1.3「门下沉为一处」)──────────────────────────
480
480
  // 判据本体**只在** lib/host/surgery.js 的 requireSurgery() 里;这里只是**唯一**的调用点。
481
- // 覆盖面 = 全部 14 条能改人格/规矩/习惯的写面(逐条清单见 surgery.js 头部注释):挂在 guard(handler)
481
+ // 覆盖面 = 全部 12 条能改人格/规矩/习惯的写面(逐条清单见 surgery.js 头部注释):挂在 guard(handler)
482
482
  // 上就"一眼看全、零回归面",与 POST_ONLY / 413 出口同一个取舍 —— 而不是在 49 个 register 调用点各写一遍。
483
483
  // 安全的位置:本调用在 POST_ONLY(405)与 guardVerdict(403)**之后** ⇒ 被挡下的请求绝不会走到 handler。
484
484
  // ⚠️ 承诺句必须**惰性**取:多数写端点的载荷由 `await readBody(req)` 消费,而请求流**只能读一次** ——
@@ -530,7 +530,7 @@ export function registerApi(ctx, deps, config) {
530
530
  // 但调用点必须落在"载荷已经解析出来"的地方(见下),因为请求流只能读一次。
531
531
  // 每个写端点的形状固定为:`const body = JSON.parse(…); if (!gateSurgery(req, res, sendJson, body, { settings, logFile: surgeryLog, endpoint: sub })) return;`
532
532
  // · 这一句就是全部接线:判据(本机 / 每启动票据 / 承诺句)与拒绝回应都在 surgery.js;
533
- // · 覆盖面 = 14 条写面,逐条清单与"改前是否过门"见 surgery.js 头部注释。
533
+ // · 覆盖面 = 12 条写面,逐条清单与"改前是否过门"见 surgery.js 头部注释。
534
534
  try {
535
535
  await handler(req, res);
536
536
  } catch (e) {
@@ -897,7 +897,7 @@ export function registerApi(ctx, deps, config) {
897
897
  sessionId, // D9-a 血缘加权:按该会话所属枝给记忆定档
898
898
  categoryWeights: base.memory?.categoryWeights,
899
899
  maxItems: base.memory?.l1MaxItems,
900
- budgetChars: Math.max(200, Math.round((base.memory?.l1BudgetTokens ?? 1200) * 1.4)),
900
+ budgetChars: Math.max(200, Math.round((base.memory?.l1BudgetTokens ?? 720) * 1.4)),
901
901
  });
902
902
  return {
903
903
  ...light,
@@ -1130,7 +1130,7 @@ export function registerApi(ctx, deps, config) {
1130
1130
  sendJson(res, 200, { ok: true, total: list.length, log: list });
1131
1131
  }));
1132
1132
 
1133
- // POST /branch-log/pending —— 复盘守门(2026-09-22 用户:「互相复盘不应该被我绕过」)。
1133
+ // POST /branch-log/pending —— 复盘守门(2026-09-22 尝生:「互相复盘不应该被我绕过」)。
1134
1134
  // 返回"自器灵上次开口以来累积了多少笔结构改动还没交代";侧栏据此提示,
1135
1135
  // 并在未同步时**拦住"退出复盘模式"** —— 改完树必须让器灵有机会看到。
1136
1136
  // ⚠️ 进 `POST_ONLY` 的原因:它不是纯读 —— `memory.branchLogPending()` 在"水位键缺失 +
@@ -1159,12 +1159,12 @@ export function registerApi(ctx, deps, config) {
1159
1159
  // GET /conflicts?status=pending|confirmed|dismissed —— 待复盘矛盾(带两侧标题,复盘界面直接可读)
1160
1160
  disposers.push(register('/conflicts', async (req, res) => {
1161
1161
  const q = new URL(req.url, 'http://dsh.internal').searchParams;
1162
- // 2026-09-21:补两侧**摘要全文** —— 只给标题时人根本判不出"到底哪矛盾"(用户实测反馈)。
1162
+ // 2026-09-21:补两侧**摘要全文** —— 只给标题时人根本判不出"到底哪矛盾"(尝生实测反馈)。
1163
1163
  const ovOf = (src, cid) => {
1164
1164
  try { return memory.overviewById(String(src), String(cid)) || null; } catch { return null; }
1165
1165
  };
1166
1166
  // 2026-09-21:涉及归档会话的矛盾不进列表 —— 归档意味着"这条别再提了",
1167
- // 但已登记的旧记录不会自动消失,所以在这里按当前归档状态实时筛(用户实测反馈:
1167
+ // 但已登记的旧记录不会自动消失,所以在这里按当前归档状态实时筛(尝生实测反馈:
1168
1168
  // 扫描侧上一轮已排除归档,可已存在的记录仍会一直显示)。
1169
1169
  const archSet = memory.archivedConvIdSet();
1170
1170
  const isArch = (side) => side && side.source === 'dsh' && archSet.has(String(side.convId));
@@ -1199,7 +1199,7 @@ export function registerApi(ctx, deps, config) {
1199
1199
  sendJson(res, r.ok ? 200 : 400, r);
1200
1200
  }));
1201
1201
 
1202
- // ---- 一键生成记忆树(D9-b,2026-09-21 与用户定案) ----
1202
+ // ---- 一键生成记忆树(D9-b,2026-09-21 与尝生定案) ----
1203
1203
  // 两阶段:① 机械分段(零成本、只读) ② 小助手命名 ③ 主人审核后落库。
1204
1204
  // 不变量:不改写任何记忆内容 —— 只建枝/主脉 + 挂会话归属。
1205
1205
  const autoSeg = (gapDays, maxSize) => bucketize(memory.overviewIndex(), { gapDays, maxSize });
@@ -1303,7 +1303,7 @@ export function registerApi(ctx, deps, config) {
1303
1303
  const created = [];
1304
1304
  let assigned = 0;
1305
1305
  let skipped = 0;
1306
- // 零散合簇(用户 2026-09-21 裁决):名字含「零散」的簇统一挂到同一个主脉「零散会话」下。
1306
+ // 零散合簇(尝生 2026-09-21 裁决):名字含「零散」的簇统一挂到同一个主脉「零散会话」下。
1307
1307
  // 理由:历史网页端的一次性问答主题天然分散、不属于任何工作线;合并成一簇后可在
1308
1308
  // 复盘时展开细化(设计稿 §10.3)。主脉按名复用,不重复建。
1309
1309
  const ZERO_NAME = '零散会话';
@@ -1490,7 +1490,7 @@ export function registerApi(ctx, deps, config) {
1490
1490
  const text = '主脉名称:' + String(v.name || veinId) + '\n\n素材(共 ' + mat.length + ' 条):\n'
1491
1491
  + mat.map((m, i) => (i + 1) + '. ' + (m.title || m.convId) + ' —— ' + m.summary).join('\n');
1492
1492
  const t0 = Date.now();
1493
- // 全局大模型(2026-09-25 用户:「你在给自己做整理,别让小助手代劳」)。
1493
+ // 全局大模型(2026-09-25 尝生:「你在给自己做整理,别让小助手代劳」)。
1494
1494
  // 原先走本机小助手(summarizeWithRetry + /no_think),但"这几条枝讲的是不是一件事"
1495
1495
  // 是**判断**不是搬砖 —— 该用器灵自己的脑;小助手留给压缩/分类这类机械活。
1496
1496
  //
@@ -1799,7 +1799,7 @@ export function registerApi(ctx, deps, config) {
1799
1799
  const body = JSON.parse((await readBody(req)) || '{}');
1800
1800
  const limit = Math.max(10, Math.min(200, Number(body.limit) || 60));
1801
1801
  const threshold = Math.max(0.3, Math.min(0.95, Number(body.threshold) || 0.55));
1802
- // 2026-09-21:排除**已归档**会话 —— 封存的对话不该再被拉出来互相判矛盾(用户实测反馈)。
1802
+ // 2026-09-21:排除**已归档**会话 —— 封存的对话不该再被拉出来互相判矛盾(尝生实测反馈)。
1803
1803
  const archived = memory.archivedConvIdSet();
1804
1804
  const rows = memory.queryOverviews({ limit }).items
1805
1805
  .filter((r) => r.source === 'dsh' || r.source === 'import')
@@ -2081,7 +2081,7 @@ export function registerApi(ctx, deps, config) {
2081
2081
  // 上锁(sealed:true)需带新承诺句 → 覆盖哈希;撤锁清哈希;每次成功变更后存「结果档案」。
2082
2082
  disposers.push(register('/persona', async (req, res) => {
2083
2083
  const body = JSON.parse((await readBody(req)) || '{}');
2084
- // ── 手术门(第 1/14 条写面)────────────────────────────────────────────────
2084
+ // ── 手术门(第 1/12 条写面)────────────────────────────────────────────────
2085
2085
  // 载荷必须**在这里**交上去:请求流只能读一次(见 guard 里的接线说明)。
2086
2086
  // 只交 `unlock` 一项:门只读它(§1.3 判据 3)。把整份 patch 交上去 = 让留痕的"字段指纹"
2087
2087
  // 记下一串与判据无关的内容(越少记越好;要留痕的是"这次手术",不是"他改成了什么")。
@@ -2133,7 +2133,7 @@ export function registerApi(ctx, deps, config) {
2133
2133
  disposers.push(register('/persona/check', async (req, res) => {
2134
2134
  const body = JSON.parse((await readBody(req)) || '{}');
2135
2135
  const cur = settings.get();
2136
- // 手术门(第 2/14 条写面):**这一条本身不写** —— 它是"验句预言机"(在线猜句的入口)。
2136
+ // 手术门(第 2/12 条写面):**这一条本身不写** —— 它是"验句预言机"(在线猜句的入口)。
2137
2137
  // 门口径与别处**完全一致**:已定型 ⇒ 必须本机 + 带票 + 原句,否则连"这句对不对"都不告诉它
2138
2138
  // (改前它对任意调用者有求必应 ⇒ 猜句成功率只受网络延迟限制)。
2139
2139
  if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/check' })) return;
@@ -2165,7 +2165,7 @@ export function registerApi(ctx, deps, config) {
2165
2165
  // 无论是否定型,只允许更新 persona.aiTitle(定位自述)单字段;其余字段仍需手术/承诺句。
2166
2166
  disposers.push(register('/persona/self-summary', async (req, res) => {
2167
2167
  const body = JSON.parse((await readBody(req)) || '{}');
2168
- // 手术门(第 3/14 条写面):身份自述 aiTitle 属身份保护区。只交 unlock(见 /persona 处说明)。
2168
+ // 手术门(第 3/12 条写面):身份自述 aiTitle 属身份保护区。只交 unlock(见 /persona 处说明)。
2169
2169
  if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/self-summary' })) return;
2170
2170
  const text = String(body.text ?? '').trim();
2171
2171
  if (!text) return sendJson(res, 200, { ok: false, reason: 'empty', message: '内容为空' });
@@ -2189,7 +2189,7 @@ export function registerApi(ctx, deps, config) {
2189
2189
  // (习惯不能直达;这里的用户点击就是确认动作),重量前先归档。
2190
2190
  disposers.push(register('/persona/hint-adopt', async (req, res) => {
2191
2191
  const body = JSON.parse((await readBody(req)) || '{}');
2192
- // 手术门(第 4/14 条写面):一次点击即 propose+confirm 落一条习惯。只交 unlock。
2192
+ // 手术门(第 4/12 条写面):一次点击即 propose+confirm 落一条习惯。只交 unlock。
2193
2193
  if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/hint-adopt' })) return;
2194
2194
  const text = String(body.text ?? '').trim();
2195
2195
  if (text.length < 4 || text.length > 400) {
@@ -2229,7 +2229,7 @@ export function registerApi(ctx, deps, config) {
2229
2229
  // "去掉 quote"**不再等于**"来自面板":归因只由 ruleWriteAttribution() 一处判(见文件头)。
2230
2230
  disposers.push(register('/persona/rule', async (req, res) => {
2231
2231
  const body = JSON.parse((await readBody(req)) || '{}');
2232
- // 手术门(第 5+6/14 条写面:add 与 remove 同一端点,一次判过):
2232
+ // 手术门(第 5+6/12 条写面:add 与 remove 同一端点,一次判过):
2233
2233
  // 规矩是他的指令(quote 是"经同意"的凭据),但**删规矩能整批删光** ⇒ 一律过门。
2234
2234
  if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/rule' })) return;
2235
2235
  const action = String(body.action || 'add');
@@ -2271,7 +2271,7 @@ export function registerApi(ctx, deps, config) {
2271
2271
  // 习惯不直达:propose(任一方可提)/ confirm(对方点头)/ reject / remove。
2272
2272
  disposers.push(register('/persona/habit', async (req, res) => {
2273
2273
  const body = JSON.parse((await readBody(req)) || '{}');
2274
- // 手术门(第 7+8/14 条写面:propose/confirm/reject 与 remove 同一端点,一次判过)。
2274
+ // 手术门(第 7+8/12 条写面:propose/confirm/reject 与 remove 同一端点,一次判过)。
2275
2275
  // 改前只有 remove 过门 —— "加比删容易"正是侦察里指出的不对称,现在两条都过门。
2276
2276
  if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/habit' })) return;
2277
2277
  const action = String(body.action || 'propose');
@@ -2507,7 +2507,7 @@ export function registerApi(ctx, deps, config) {
2507
2507
  // 自性类成长通道:白名单字段(语气枚举 + 风格追加),锁定下亦可采纳;不可逆操作一律不做。
2508
2508
  disposers.push(register('/persona/grow', async (req, res) => {
2509
2509
  const body = JSON.parse((await readBody(req)) || '{}');
2510
- // 手术门(第 9/14 条写面):改语气 / 追加风格注 —— 旧注释自陈"锁定下亦可采纳",那正是缺口。
2510
+ // 手术门(第 9/12 条写面):改语气 / 追加风格注 —— 旧注释自陈"锁定下亦可采纳",那正是缺口。
2511
2511
  if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/grow' })) return;
2512
2512
  const patch = {};
2513
2513
  const toneIn = body.tone && typeof body.tone === 'object' ? body.tone : {};
@@ -2543,7 +2543,7 @@ export function registerApi(ctx, deps, config) {
2543
2543
  // POST /persona/rollback { ts, unlock? } 回滚到某历史存档(同样受定型门约束)
2544
2544
  disposers.push(register('/persona/rollback', async (req, res) => {
2545
2545
  const body = JSON.parse((await readBody(req)) || '{}');
2546
- // 手术门(第 10/14 条写面):整体回写 {persona,styles}(含 sealed/sealPhrase)——
2546
+ // 手术门(第 10/12 条写面):整体回写 {persona,styles}(含 sealed/sealPhrase)——
2547
2547
  // 回滚到定型前快照 = **撤锁**,故判据必须在"看存档是否存在"之前:格式不对的请求也不该探到存档。
2548
2548
  if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/rollback' })) return;
2549
2549
  const ts = Number(body.ts);
@@ -2918,6 +2918,13 @@ export function registerApi(ctx, deps, config) {
2918
2918
  // (生活/情感优先加权抽样)。每行三级取用:深摘 → 非占位概述 → 原文片段/线索。
2919
2919
  disposers.push(register('/persona/genesis', async (req, res) => {
2920
2920
  const body = JSON.parse((await readBody(req)) || '{}');
2921
+ // ⚠️ **手术门补在这一条上**(2026-10-04 发布关④ 抓到):它**写人格**(诞生仪式生成的自我介绍
2922
+ // 会被主人采纳进 persona),却一直没有门 —— 而 README 与 `gateSurgery` 的注释都写着
2923
+ // "**全部 12 条写面**已由手术门统一判过",实测全仓只有 11 处 `gateSurgery` 调用点。
2924
+ // 补门后它与其余写面同级:本机 + 每启动票据 + 原句匹配。
2925
+ // ⚠️ 载荷只交 `unlock` 一项(与 `/persona` 同口径):门只读它,把整份 body 交上去
2926
+ // 会让留痕的"字段指纹"记下与判据无关的内容。
2927
+ if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/persona/genesis' })) return;
2921
2928
  const scope = body.scope === 'all' ? 'all' : 'import';
2922
2929
  const pn = pronounOf(settings.get());
2923
2930
  const comp = composeGenesisRows(memory, { scope, pronoun: pn });
@@ -2967,7 +2974,7 @@ export function registerApi(ctx, deps, config) {
2967
2974
  // POST /feedback/apply { id }
2968
2975
  disposers.push(register('/feedback/apply', async (req, res) => {
2969
2976
  const body = JSON.parse((await readBody(req)) || '{}');
2970
- // 手术门(第 11/14 条写面):采纳建议 → 落习惯 / 改自称。改前只有身份两 kind 有门。
2977
+ // 手术门(第 11/12 条写面):采纳建议 → 落习惯 / 改自称。改前只有身份两 kind 有门。
2971
2978
  if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/feedback/apply' })) return;
2972
2979
  const r = await applySuggestionAsRule(memory, settings, body.id);
2973
2980
  if (!r.ok) return sendJson(res, 200, { ok: false, reason: r.reason });
@@ -3009,7 +3016,7 @@ export function registerApi(ctx, deps, config) {
3009
3016
  // POST /suggestions/apply { id }
3010
3017
  disposers.push(register('/suggestions/apply', async (req, res) => {
3011
3018
  const body = JSON.parse((await readBody(req)) || '{}');
3012
- // 手术门(第 13/14 条写面):采纳建议。改前**只有** kind∈{aiName,userTitle} 才验句,
3019
+ // 手术门(第 13/12 条写面):采纳建议。改前**只有** kind∈{aiName,userTitle} 才验句,
3013
3020
  // 惯例类(hardrule 等)可自由追加 ⇒ 现在整条端点统一过门(下面那段身份分支因此成了一道冗余的深防线,
3014
3021
  // 保留不动:判据更严的一侧永远不会把请求放过去)。
3015
3022
  if (!gateSurgery(req, res, sendJson, { unlock: body.unlock }, { settings, logFile: surgeryLog, endpoint: '/suggestions/apply' })) return;
@@ -3110,7 +3117,7 @@ export function registerApi(ctx, deps, config) {
3110
3117
  const q = new URL(req.url, 'http://dsh.internal').searchParams;
3111
3118
  const text = await readBody(req);
3112
3119
  const parsed = JSON.parse(text || '{}');
3113
- // 手术门(第 14/14 条写面)⚠️ **前移到 importBundle 之前** —— 这是本批唯一一处"改前门在后面"的修正:
3120
+ // 手术门(第 14/12 条写面)⚠️ **前移到 importBundle 之前** —— 这是本批唯一一处"改前门在后面"的修正:
3114
3121
  // 改前:`memory.importBundle()`(下一行)先跑,门在它**之后** ⇒ 一个带着恶意 bundle 的请求
3115
3122
  // 即使人格回写被拒,**记忆也已经整体被换掉了**(DESIGN §3 该行备注"门拦不住记忆被换")。
3116
3123
  // 现在:已定型时,未过门(非本机 / 无票 / 原句不符)的请求**一个字节都不落库**。
@@ -1,6 +1,6 @@
1
1
  // dsh-ling host — 改动前的归档落盘(Part G · G2,2026-09-25)
2
2
  //
3
- // **原则(用户定):永远不做彻底删除。**
3
+ // **原则(尝生定):永远不做彻底删除。**
4
4
  // 任何"清除/覆盖"类的操作,动手之前先把原数据**完整写到磁盘上的历史目录**,并附一份
5
5
  // 人可读的改动日志 —— 谁、什么时候、为什么、以及怎么还原。
6
6
  //
@@ -77,9 +77,15 @@ export function archiveBefore(memory, { source, convId, kind = 'forget', reason
77
77
  // 标记永久留下、id 永不召回、界面还显示"忘过了",而回执写着"原文与概述已归档"。
78
78
  // 这里把判据**显式化**:`empty` 看的是**归档内容**(原文轮数 + 正文有无),
79
79
  // **不是**目标是否存在 —— 目的就是让"还打不打得动标记"这件事有据可依。
80
+ // ⚠️ 2026-10-04 修(B-05 回归网 + 红队 P1 + 蓝队复核共同定位),判据从三条腿收成**两条**:
81
+ // ① 旧判据 `!data.sessionMeta` 太宽 —— 一条**空壳**元数据(`raw_seq=0`)被当成"有内容",
82
+ // 于是**真空归档被放行**,而 B-05 的实害正是"标记永久留下、id 永不召回、界面显示忘过了";
83
+ // ② 一度改成 `raw_seq > 0`,但那是**水位**("历史上捕获过多少事件"),**不是"现在库里有多少内容"**
84
+ // —— 合成用例 `raw_seq=5 + 0 轮 + 无概述` 会因此放行并落标记(潜在 fail-open,真库 0 例)。
85
+ // ⇒ 只留**内容本身**两条腿:原文轮数、概述正文。这样"只有概述正文"的条目能忘得掉
86
+ // (红队实测真库 3 条:概述 145/148/254 字、0 轮原文),而"什么都没保住"的一律挡住。
80
87
  const empty = (data.turns || []).length === 0
81
- && !String(data.overview?.summary || '').trim()
82
- && !data.sessionMeta;
88
+ && !String(data.overview?.summary || '').trim();
83
89
  try {
84
90
  mkdirSync(dir, { recursive: true });
85
91
  // 原文逐行 JSONL —— 能 grep、能逐行还原,比一个大 JSON 数组更耐损
@@ -99,8 +105,8 @@ export function archiveBefore(memory, { source, convId, kind = 'forget', reason
99
105
 
100
106
  /** 改动日志 —— 这份文件是给**将来的人**看的,所以写全:是什么、为什么、怎么还原。 */
101
107
  function readme({ at, s, c, kind, reason, actor, turns }) {
102
- const who = actor === 'ling' ? '器灵(工具通道)' : '主人(界面通道)';
103
- // A-01(1.5.1):第三种 kind —— 主脉记忆覆盖(同一主脉再次采纳提炼结果;用户裁决"备份并覆盖")。
108
+ const who = actor === 'ling' ? '鱼姬(工具通道)' : '主人(界面通道)';
109
+ // A-01(1.5.1):第三种 kind —— 主脉记忆覆盖(同一主脉再次采纳提炼结果;尝生裁决"备份并覆盖")。
104
110
  const what = kind === 'forget' ? '遗忘(软标记 —— **库里的行没有删**)'
105
111
  : kind === 'vein-overwrite' ? '主脉记忆**覆盖**(同一主脉再次采纳 —— **上一版正文完整存在这里**)'
106
112
  : '回灌(用归档恢复一条记忆 —— **覆盖前的状态已存这里**)';
@@ -108,14 +114,14 @@ function readme({ at, s, c, kind, reason, actor, turns }) {
108
114
  // 「怎么还原」按 kind 分三档。用**字符串拼接**而不是嵌套模板,免得外层模板里的反引号打架。
109
115
  const restoreBlock = kind === 'forget'
110
116
  ? '**库里没有删任何行** —— 只是多了一条 `forgotten` 标记,召回路径不再选它。撤销方式二选一:\n\n'
111
- + '1. 让器灵调工具撤销(`memory_forget` 的 `undo`),或\n'
117
+ + '1. 让鱼姬调工具撤销(`memory_forget` 的 `undo`),或\n'
112
118
  + '2. 直接删标记:\n ```sql\n DELETE FROM forgotten WHERE source=\'' + s + '\' AND conv_id=\'' + c + '\';\n ```'
113
119
  : kind === 'vein-overwrite'
114
120
  ? '覆盖只动了 `conv_overview` 里那一行(主脉自己的记忆),原文与会话元数据不受影响。要把上一版换回去:\n\n'
115
- + '1. 让器灵按本目录 `overview.json` 的 `summary` **再采纳一次**旧正文,或\n'
121
+ + '1. 让鱼姬按本目录 `overview.json` 的 `summary` **再采纳一次**旧正文,或\n'
116
122
  + '2. 直接写回:\n ```sql\n UPDATE conv_overview SET summary=\'<overview.json 的 summary>\'\n WHERE source=\'' + s + '\' AND conv_id=\'' + c + '\';\n ```'
117
123
  : '回灌只是把归档写回去,而**它覆盖之前的状态存在这里**。要退回去:\n\n'
118
- + '1. 让器灵再回灌一次旧档(`memory_backfill`),或\n'
124
+ + '1. 让鱼姬再回灌一次旧档(`memory_backfill`),或\n'
119
125
  + '2. 按下面「万一库本身没了」的步骤写回。';
120
126
  return `# 归档:${title}的原始数据
121
127
 
@@ -144,7 +150,7 @@ ${restoreBlock}
144
150
 
145
151
  ## 为什么保留
146
152
 
147
- 忘错了要能回头。这是用户定的规矩:**永远不做彻底删除** ——
153
+ 忘错了要能回头。这是尝生定的规矩:**永远不做彻底删除** ——
148
154
  所以这个目录是"曾经发生过什么"的证据,不是垃圾。不要随手清。
149
155
 
150
156
  > 生成于 dsh-ling · Part G(器灵侧正规通道)
@@ -1,4 +1,4 @@
1
- // dsh-ling host — 一键生成记忆树(D9-b,2026-09-20 与用户定案)。
1
+ // dsh-ling host — 一键生成记忆树(D9-b,2026-09-20 与尝生定案)。
2
2
  //
3
3
  // 主干 1593 条太粗,需要先把树"初步划一遍"。分两阶段:
4
4
  // ① 机械分段 —— 按时序相邻性把全库概述切成若干簇(零成本、确定、可复现);
@@ -6,7 +6,7 @@
6
6
  // ③ 主人审核后落库(apply 在 api.js 侧) —— 建枝/主脉 + 挂会话。
7
7
  //
8
8
  // 为什么不把 1593 条直接丢给模型:装不进上下文,而且"按时序切段"本就是机械活。
9
- // 模型擅长的是"给一段对话起主题名",不是"在 1593 条里做聚类"(设计稿 §8 / 用户 2026-09-20)。
9
+ // 模型擅长的是"给一段对话起主题名",不是"在 1593 条里做聚类"(设计稿 §8 / 尝生 2026-09-20)。
10
10
  //
11
11
  // 不变量:本模块**只读概述索引**、只产出候选,**从不改写任何记忆内容**。
12
12
 
@@ -1,4 +1,4 @@
1
- // dsh-ling host — clock skill:精确时间按需查询(2026-09-23 用户定)。
1
+ // dsh-ling host — clock skill:精确时间按需查询(2026-09-23 尝生定)。
2
2
  //
3
3
  // 为什么做成 skill 而不常驻注入:
4
4
  // 时间锚搬迁之后(见内部时间锚设计笔记),system 侧只留 [今天](日期+星期,一天变一次),