@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/CHANGELOG.md +75 -6
- package/DESIGN.md +8 -4
- package/README.md +181 -7
- package/lib/client.js +35 -35
- package/lib/host/api.js +29 -22
- package/lib/host/archive.js +15 -9
- package/lib/host/autotree.js +2 -2
- package/lib/host/clock-skill.js +1 -1
- package/lib/host/deep-index-skill.js +111 -0
- package/lib/host/inject.js +1 -1
- package/lib/host/l1.js +350 -23
- package/lib/host/lifecycle.js +65 -10
- package/lib/host/memory.js +323 -37
- package/lib/host/persona.js +2 -2
- package/lib/host/rawlog.js +317 -0
- package/lib/host/recall-search.js +210 -0
- package/lib/host/retitle.js +5 -5
- package/lib/host/snapshot.js +65 -13
- package/lib/host/summarizer.js +10 -4
- package/lib/host/surgery.js +2 -2
- package/lib/host/tools.js +327 -14
- package/lib/host/tree-ops.js +12 -6
- package/lib/host/util.js +1 -1
- package/lib/index.js +41 -2
- package/package.json +6 -2
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
|
-
// ⚠️ 这里**故意不问一句** ——
|
|
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
|
|
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
|
-
// 不写这一行,"按设计没有"与"坏了"在界面上长得一模一样(
|
|
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
|
-
//
|
|
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
|
-
// 显示层(
|
|
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
|
-
// ---- 连边小标记(
|
|
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
|
-
//
|
|
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
|
-
// 主人无从判断该不该采纳(
|
|
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
|
-
// ---- 连边索引(
|
|
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
|
-
// 左对齐(
|
|
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 都会跳回顶部 ——
|
|
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
|
-
// ---- 横向连边(
|
|
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
|
|
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
|
-
/** 复盘解锁:跨面板共享的真值 —— 记忆中心与右侧复盘栏读同一个变量(
|
|
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
|
|
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
|
|
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
|
-
// 后端对同主脉的第二次采纳做的是"先把旧版归档、再覆盖"(
|
|
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
|
-
//
|
|
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
|
|
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
|
-
// 覆盖面 = 全部
|
|
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
|
-
// · 覆盖面 =
|
|
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 ??
|
|
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
|
-
// 零散合簇(
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
3120
|
+
// 手术门(第 14/12 条写面)⚠️ **前移到 importBundle 之前** —— 这是本批唯一一处"改前门在后面"的修正:
|
|
3114
3121
|
// 改前:`memory.importBundle()`(下一行)先跑,门在它**之后** ⇒ 一个带着恶意 bundle 的请求
|
|
3115
3122
|
// 即使人格回写被拒,**记忆也已经整体被换掉了**(DESIGN §3 该行备注"门拦不住记忆被换")。
|
|
3116
3123
|
// 现在:已定型时,未过门(非本机 / 无票 / 原句不符)的请求**一个字节都不落库**。
|
package/lib/host/archive.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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(器灵侧正规通道)
|
package/lib/host/autotree.js
CHANGED
|
@@ -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 /
|
|
9
|
+
// 模型擅长的是"给一段对话起主题名",不是"在 1593 条里做聚类"(设计稿 §8 / 尝生 2026-09-20)。
|
|
10
10
|
//
|
|
11
11
|
// 不变量:本模块**只读概述索引**、只产出候选,**从不改写任何记忆内容**。
|
|
12
12
|
|
package/lib/host/clock-skill.js
CHANGED