@nightpainters/dsh-ling 1.6.1 → 1.7.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 +207 -41
- package/DESIGN.md +121 -29
- package/PHILOSOPHY.md +184 -0
- package/README.md +112 -21
- package/cordis.patch.yml +32 -2
- package/lib/client.js +2520 -55
- package/lib/host/agent-tools.js +286 -0
- package/lib/host/api.js +612 -61
- package/lib/host/archive.js +3 -3
- package/lib/host/autotree.js +2 -2
- package/lib/host/clock-skill.js +1 -1
- package/lib/host/config.js +38 -0
- package/lib/host/deep-index-skill.js +9 -0
- package/lib/host/deepsummary.js +75 -2
- package/lib/host/dsweb-summary.js +6 -3
- package/lib/host/guard.js +53 -0
- package/lib/host/inject.js +12 -2
- package/lib/host/l1.js +59 -10
- package/lib/host/library-checker.js +552 -0
- package/lib/host/library-import-engine.js +211 -0
- package/lib/host/library-import.js +911 -0
- package/lib/host/library-runtime.js +199 -0
- package/lib/host/library-sqlite.js +344 -0
- package/lib/host/library-store.js +1714 -0
- package/lib/host/memory.js +89 -30
- package/lib/host/persona.js +13 -3
- package/lib/host/rawlog.js +10 -5
- package/lib/host/recall-search.js +192 -17
- package/lib/host/snapshot.js +2 -1
- package/lib/host/surgery.js +7 -2
- package/lib/host/tools.js +506 -31
- package/lib/host/util.js +1 -1
- package/lib/index.js +120 -81
- package/package.json +11 -5
- package/tools/deep-import-docs.mjs +1210 -0
|
@@ -24,14 +24,49 @@
|
|
|
24
24
|
import { isMemoryEligibleHeader, sessionKind } from './util.js';
|
|
25
25
|
import { localStamp, oneLine } from './rawlog.js';
|
|
26
26
|
|
|
27
|
-
/** 一页几个会话(默认)。
|
|
28
|
-
|
|
27
|
+
/** 一页几个会话(默认):**5 条**(2026-10-05 主人拍板 B2,原为 10)。
|
|
28
|
+
* 起因:实测"搜什么都中今天" —— 全文索引里**当前会话自己**也在,任何词都能命中几十秒前刚打的那句话,
|
|
29
|
+
* 而原文层默认 10 行会把上面的**条目层**(结论,一句话说完)挤到看不见。砍到 5 条让条目重新占视觉重心;
|
|
30
|
+
* 要更多**显式传 `limit`**(上限 50 不变,想翻就翻)。 */
|
|
31
|
+
export const SEARCH_HITS_DEFAULT = 5;
|
|
29
32
|
/** 一页几个会话(上限):给模型的是"下一步去哪",不是把库倒出来。 */
|
|
30
33
|
export const SEARCH_HITS_MAX = 50;
|
|
31
34
|
/** 单条摘要的显示字数 —— **报短摘要而不是 id**(2026-10-04 生态复核:同生态对手的 README 逐字
|
|
32
35
|
* 「查 id 是什么的那一步最容易省略,省略了就等于没报」)。这一条是本次复核直接抄来的。 */
|
|
33
36
|
export const SEARCH_SNIPPET_CHARS = 160;
|
|
34
37
|
|
|
38
|
+
/** 宿主检索的**超时上限**(2026-10-05,主人定 20 秒)。
|
|
39
|
+
*
|
|
40
|
+
* 为什么必须有它:宿主的懒对账(`_reconcile` → `_observeStable`)是**串行化**的。一旦撞上卡住的对账,
|
|
41
|
+
* `searchSessions` 就**一直不返回** —— 实测有一次挂了 **约 30 分钟**,而当时**没有任何中断入口**:
|
|
42
|
+
* GUI 没有,工具层也没有。按本项目理念(「逃生开关永远可用」),**"不可中断"本身就该按最严重一级对待**,
|
|
43
|
+
* 与它挂了多久无关。
|
|
44
|
+
* ⇒ 我们改不了宿主(拿不到取消口),但可以**不给它无限期**:超时后放弃原文层,并**照样把条目层交出去**
|
|
45
|
+
* (条目是本地 SQLite,毫秒级,完全不受宿主影响)。
|
|
46
|
+
* ⚠️ 别调小到 5 秒以下:一次"慢但对"的检索比"快速失败"有用得多;
|
|
47
|
+
* 也别指望它治并发 —— 并发撞对账是宿主侧的事,这里只保证**任何一次都不会挂死**。 */
|
|
48
|
+
export const SEARCH_TIMEOUT_MS = 20000;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 给 Promise 套超时(纯函数,可单测)。
|
|
52
|
+
* ⚠️ 超时**不取消**底层操作(宿主没给取消口),只是我们不再等它;它之后若返回,结果被丢弃 ——
|
|
53
|
+
* 检索是只读的,丢弃不影响任何状态。
|
|
54
|
+
* @returns `{ok:true, value}` | `{ok:false, reason:'timeout', ms, label}`
|
|
55
|
+
*/
|
|
56
|
+
export function withTimeout(promise, ms, label = '检索') {
|
|
57
|
+
const limit = Math.max(1, Number(ms) || SEARCH_TIMEOUT_MS);
|
|
58
|
+
let timer = null;
|
|
59
|
+
const timeout = new Promise((resolve) => {
|
|
60
|
+
timer = setTimeout(() => resolve({ ok: false, reason: 'timeout', ms: limit, label }), limit);
|
|
61
|
+
// 别让这个定时器拖住进程退出(工具层常在短命进程里被调用)
|
|
62
|
+
if (typeof timer?.unref === 'function') timer.unref();
|
|
63
|
+
});
|
|
64
|
+
return Promise.race([
|
|
65
|
+
Promise.resolve(promise).then((value) => ({ ok: true, value })),
|
|
66
|
+
timeout,
|
|
67
|
+
]).finally(() => { if (timer) clearTimeout(timer); });
|
|
68
|
+
}
|
|
69
|
+
|
|
35
70
|
/** 条数夹取(纯函数,便于单测)。 */
|
|
36
71
|
export function clampSearchLimit(n) {
|
|
37
72
|
const v = Math.floor(Number(n));
|
|
@@ -62,13 +97,101 @@ export function searchDeepItems(memory, q, limit = SEARCH_DEEP_MAX) {
|
|
|
62
97
|
const words = String(q || '').trim().split(/\s+/).map((w) => w.replace(/[%_\\]/g, '')).filter(Boolean);
|
|
63
98
|
if (!words.length) return [];
|
|
64
99
|
try {
|
|
65
|
-
|
|
100
|
+
// ⚠️ 2026-10-06:**类目名(kind)也在检索面里** —— 修前只扫 `text`,于是查询里的类目词打不中
|
|
101
|
+
// 它们自己的条目:实测搜「纪律」时那 18 条只回来 2 条(恰好正文含"纪律"二字的那些),
|
|
102
|
+
// 而**同族缺陷对"承诺 / 决定 / 关系 / 事实 / 偏好"一样成立**。
|
|
103
|
+
// 多词仍是 **AND**(每词都得在 text 或 kind 里出现其一),与宿主检索面同解,不另立口径。
|
|
104
|
+
const where = words.map(() => "(text LIKE ? ESCAPE '\\' OR kind LIKE ? ESCAPE '\\')").join(' AND ');
|
|
105
|
+
const args = [];
|
|
106
|
+
for (const w of words) { args.push(`%${w}%`, `%${w}%`); }
|
|
107
|
+
args.push(Math.max(1, Math.min(50, Number(limit) || SEARCH_DEEP_MAX)));
|
|
66
108
|
return db.prepare(
|
|
67
109
|
`SELECT id, kind, text, conv_id, src, seq_from, seq_to, pinned, hit_count FROM deep_item
|
|
68
110
|
WHERE superseded_by='' AND ${where}
|
|
69
111
|
ORDER BY pinned DESC, hit_count DESC, created_at DESC LIMIT ?`,
|
|
70
|
-
).all(...
|
|
71
|
-
} catch {
|
|
112
|
+
).all(...args);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
// ── F3(1.7 修复批次,2026-10-15)─────────────────────────────────────────────
|
|
115
|
+
// 修前是 `catch { return []; }` —— 把**库读失败**(损坏 / 被占用 / SQL 错)归并成**零命中**,
|
|
116
|
+
// 调用方读到的就是"库里没有",而真实情况是"我们自己的库读不出来"。违反本项目口径
|
|
117
|
+
// 「**库不可用 ≠ 零命中**」(同文件对宿主那条路一直严格分开:`search-unavailable` / 超时文案)。
|
|
118
|
+
// 现在**只吞"表 / 列不存在"** —— 那是"这张表还没建"的正常态;其余一律往上冒,由工具层
|
|
119
|
+
// 转成 `local-unavailable`(见 tools.js 的 runSearch)。
|
|
120
|
+
if (/no such table|no such column/i.test(String(e?.message ?? e))) return [];
|
|
121
|
+
throw e;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 检索档本地原文层一次最多带出几轮(默认同 `SEARCH_HITS_DEFAULT`)。 */
|
|
126
|
+
export const SEARCH_LOCAL_TURNS_MAX = 50;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 在**我们自己的库**里按内容搜原文轮(`dsh_turns_raw`)—— 检索档的**默认**原文来源。
|
|
130
|
+
*
|
|
131
|
+
* 为什么默认走这里(2026-10-05 夜 · 主人拍板方案 A):
|
|
132
|
+
* 宿主 `sessionQuery` 的每次搜索都会做一遍**全量对账**(`_observeStable` 里 list 两遍、
|
|
133
|
+
* 最多重试 2 轮),实测**每次都超过 20 秒**(重启后第一次也一样),而这边的
|
|
134
|
+
* 同一个查询只要 **2~45 毫秒**。快的设成默认、慢的降级成显式选项,是这一档的取舍。
|
|
135
|
+
* ⚠️ 代价**必须如实报出去**:库里这份原文是**残的** —— capture 侧只收"真人消息 +
|
|
136
|
+
* 有正文的回复",所以我自己的长回复**中段**搜不到。回执里要写明这一层是"库内原文层"。
|
|
137
|
+
*
|
|
138
|
+
* 与 `searchDeepItems` 同解的三条(不另立口径):多词 = **AND**;词里的 `% _ \` 先剥掉;
|
|
139
|
+
* 表不存在时**静默返回空**(检索面不该因为某张表缺席而整体失败)。
|
|
140
|
+
* ⚠️ F3(1.7 修复批次)把这条**收窄**了:静默只对"**表 / 列不存在**"这一类;
|
|
141
|
+
* 库损坏 / 被占用 / SQL 错**往上冒** —— 库不可用 ≠ 零命中,两者的下一步动作完全不同。
|
|
142
|
+
*
|
|
143
|
+
* @param {object} memory MemoryStore
|
|
144
|
+
* @param {string} q 查询词(空格分隔 = 同时满足)
|
|
145
|
+
* @param {number} limit 最多几轮
|
|
146
|
+
* @param {object} [opts] `{ selfId, callName, includeSelf }` —— `selfId` 是当前会话 id;
|
|
147
|
+
* **默认把它整条排除**(见下方注释),要看自己刚说的传 `includeSelf:true`(那时它排到最后并标 `isSelf`)
|
|
148
|
+
* @returns `{{rows:object[], selfExcluded:number}}` —— `selfExcluded` 是**因为"是本会话"而被排除的条数**,
|
|
149
|
+
* 调用方**必须把它报出去**:不说就会被读成"库里只有这些"(那是说假话)
|
|
150
|
+
*/
|
|
151
|
+
export function searchLocalTurns(memory, q, limit = SEARCH_HITS_DEFAULT, { selfId = '', callName = '主人', includeSelf = false } = {}) {
|
|
152
|
+
const db = memory?.db;
|
|
153
|
+
if (!db?.prepare) return { rows: [], selfExcluded: 0 };
|
|
154
|
+
const words = String(q || '').trim().split(/\s+/).map((w) => w.replace(/[%_\\]/g, '')).filter(Boolean);
|
|
155
|
+
if (!words.length) return { rows: [], selfExcluded: 0 };
|
|
156
|
+
const cap = Math.max(1, Math.min(SEARCH_LOCAL_TURNS_MAX, Number(limit) || SEARCH_HITS_DEFAULT));
|
|
157
|
+
let rows;
|
|
158
|
+
try {
|
|
159
|
+
const where = words.map(() => "text LIKE ? ESCAPE '\\'").join(' AND ');
|
|
160
|
+
rows = db.prepare(
|
|
161
|
+
`SELECT session_id, seq, role, ts, text FROM dsh_turns_raw
|
|
162
|
+
WHERE ${where} ORDER BY ts DESC LIMIT ?`,
|
|
163
|
+
).all(...words.map((w) => `%${w}%`), cap);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// F3(1.7 修复批次):同 `searchDeepItems` —— **只吞"表 / 列不存在"**(表缺席是正常态),
|
|
166
|
+
// 库损坏 / 被占用 / SQL 错必须往上冒(往上冒才会被渲染成"库读不出来",而不是"没找到")。
|
|
167
|
+
if (/no such table|no such column/i.test(String(e?.message ?? e))) return { rows: [], selfExcluded: 0 };
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
const self = String(selfId || '');
|
|
171
|
+
const mapped = rows.map((r) => {
|
|
172
|
+
const id = String(r.session_id ?? '');
|
|
173
|
+
const role = String(r.role ?? '');
|
|
174
|
+
const isUser = role.startsWith('user');
|
|
175
|
+
// 命中位置附近截一段(而不是从头截):词可能出现在长回复的中间
|
|
176
|
+
const text = String(r.text ?? '');
|
|
177
|
+
const at = words.map((w) => text.indexOf(w)).filter((i) => i >= 0).sort((a, b) => a - b)[0] ?? 0;
|
|
178
|
+
const from = Math.max(0, at - 40);
|
|
179
|
+
return {
|
|
180
|
+
conv: id, seq: Number(r.seq) || 0, role,
|
|
181
|
+
time: r.ts ? localStamp(r.ts) : '',
|
|
182
|
+
who: isUser ? callName : '鱼姬',
|
|
183
|
+
snippet: oneLine(text.slice(from, from + SEARCH_SNIPPET_CHARS), SEARCH_SNIPPET_CHARS),
|
|
184
|
+
isSelf: Boolean(self) && id === self,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
// ⚠️ **本会话默认排除**(2026-10-05 夜 · 主人拍板):它就在库里、而它的话又最新 ⇒ 任何词都能命中它,
|
|
188
|
+
// 把要回溯的历史**整页挤掉** —— 真机实测「检索」10 条里 9 条是本会话;光把它排到末尾只治了"占头部",
|
|
189
|
+
// 没治"占位"。而"我刚说过什么"**根本不需要搜**:它就在我的上下文里;
|
|
190
|
+
// 搜索这个动作的全部意义是回溯**我这里没有的**东西。要看自己刚说的传 `includeSelf:true`。
|
|
191
|
+
// ⇒ 排除的条数**如实报出去**(`selfExcluded`),不说就会被读成"库里只有这些"。
|
|
192
|
+
const kept = includeSelf ? mapped : mapped.filter((r) => !r.isSelf);
|
|
193
|
+
const selfExcluded = includeSelf ? 0 : mapped.length - kept.length;
|
|
194
|
+
return { rows: kept.sort((a, b) => Number(a.isSelf) - Number(b.isSelf)), selfExcluded };
|
|
72
195
|
}
|
|
73
196
|
|
|
74
197
|
/**
|
|
@@ -123,8 +246,9 @@ export function visibleIdSet(records) {
|
|
|
123
246
|
* @param all 越界开关(D6:显式才生效)
|
|
124
247
|
* @returns `{ rows, skipped }` —— `skipped` 是**被范围挡掉**的条数,必须报出来(不静默丢)
|
|
125
248
|
*/
|
|
126
|
-
export function toSearchRows(hits, { visible = null, all = false, callName = '主人' } = {}) {
|
|
249
|
+
export function toSearchRows(hits, { visible = null, all = false, callName = '主人', selfId = '' } = {}) {
|
|
127
250
|
const rows = [];
|
|
251
|
+
const self = String(selfId || '');
|
|
128
252
|
let skipped = 0;
|
|
129
253
|
for (const h of hits || []) {
|
|
130
254
|
const header = h?.header ?? {};
|
|
@@ -140,8 +264,14 @@ export function toSearchRows(hits, { visible = null, all = false, callName = '
|
|
|
140
264
|
time: m.time ? localStamp(m.time) : '',
|
|
141
265
|
who: isUser ? callName : '鱼姬',
|
|
142
266
|
snippet: oneLine(m.snippet || '', SEARCH_SNIPPET_CHARS),
|
|
267
|
+
isSelf: Boolean(self) && String(id) === self,
|
|
143
268
|
});
|
|
144
269
|
}
|
|
270
|
+
// ⚠️ 本会话排到最后(2026-10-05 主人拍板 C):**当前会话自己也在索引里**,于是"搜什么都中今天" ——
|
|
271
|
+
// 任何词都能命中几十秒前刚打的那句话,而它排在头一条,把要回溯的历史全挤下去。
|
|
272
|
+
// **不删**它(有时确实要找自己刚才那句),只挪到末尾并标注「← 本会话」。
|
|
273
|
+
// `sort` 在 V8 上是**稳定**的 ⇒ 其余命中保持宿主的原序(相关性+新近),不打乱。
|
|
274
|
+
if (self) rows.sort((a, b) => Number(a.isSelf) - Number(b.isSelf));
|
|
145
275
|
// ⚠️ `degraded`(蓝队 2026-10-04 抓到 P3):宿主**没有** `listSessions` 时,`visible` 恒为 null
|
|
146
276
|
// ⇒ 范围限制**整条失效**(一条不挡),而回执却仍写着"已在主人自己的会话内" —— 那是**说假话**。
|
|
147
277
|
// 取舍(主代理 2026-10-04 定):**保持 fail-open 但如实标注** —— 完全禁用会让"按内容找会话"
|
|
@@ -162,15 +292,37 @@ export function renderSearch(v) {
|
|
|
162
292
|
+ ' 这不是"库里没有",是**通道没接上** —— 按内容找会话这条路暂时走不通,'
|
|
163
293
|
+ '可改用**不带参数**的 recall() 按时间翻清单。',
|
|
164
294
|
'search-failed': `检索报错,这次没搜成:${msg}。换个更短的词再试(中文 2~4 字命中最好)。`,
|
|
295
|
+
// F3(1.7 修复批次):**新开**一个理由词,不复用 `search-unavailable` ——
|
|
296
|
+
// 那句写的是"**宿主**检索面不可用",而这一条是"**我们自己的库**读不出来",
|
|
297
|
+
// 套用就是把假话换了个地方说(两条路该采取的动作也不同:前者换通道,后者查库/看占用)。
|
|
298
|
+
'local-unavailable':
|
|
299
|
+
`本机库读不出来,这次没搜成。原因:${msg}`
|
|
300
|
+
+ ' **这不是"库里没有",是我们自己的库读不出来** —— 条目层与原文层都在这个库里,'
|
|
301
|
+
+ '这次两半都没拿到(所以别据此断定"没发生过")。'
|
|
302
|
+
+ '可先改用**不带参数**的 recall() 按时间翻清单;若反复如此,'
|
|
303
|
+
+ '多半是库文件损坏或被别的进程占着(库路径见启动日志的 dataDir)。',
|
|
165
304
|
}[v?.reason];
|
|
166
305
|
return why || `未搜到内容(${v?.reason || '未知'})。`;
|
|
167
306
|
}
|
|
168
307
|
const scope = v.all
|
|
169
308
|
? '**未限定范围**(all=true —— 含子代理等非主人会话)'
|
|
170
|
-
: (v.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
309
|
+
: (v.localScope
|
|
310
|
+
// ⚠️ 本地路**没有可见集过滤**(2026-10-06 外部文档核对抓到):它直查 `dsh_turns_raw` ——
|
|
311
|
+
// 那是**全量**(实测 11,296 行 / 1,647 会话,其中 `source='dsweb'` 的 1,523 个网页端会话、
|
|
312
|
+
// 未标来源的 104 个)⇒ **不许印"已在主人自己的会话内"**,那是说假话。
|
|
313
|
+
? '⚠️ **本机库全量**(这条路不过 `listSessions` 可见集:含网页端导入的会话与未标来源的会话)'
|
|
314
|
+
: (v.degraded
|
|
315
|
+
? '⚠️ **没能限定范围** —— 宿主这次没给 `listSessions`,所以下面这份结果**可能含子代理或其它会话**,'
|
|
316
|
+
+ '不是"只搜主人自己的会话"(范围限制这条通道暂时失效,不是"库里只有这些")'
|
|
317
|
+
: '已在「**主人自己的会话**」内(默认范围)'));
|
|
318
|
+
// ⚠️ **原文来源必须看得出来是哪一个**(2026-10-05 夜 · 主人拍板方案 A):
|
|
319
|
+
// 两条路覆盖面**不一样**,不说清就会被读成"库里只有这些"。
|
|
320
|
+
// · 本地(默认):库内原文层 —— 只含"真人消息 + 有正文的回复"(capture 侧就这么收的),
|
|
321
|
+
// 我自己的长回复**中段搜不到**;代价换来的是 **2~45 毫秒**(实测)。
|
|
322
|
+
// · 宿主(显式 hostSearch):覆盖全部事件,但每次**全量对账**、实测**每次 >20 秒**。
|
|
323
|
+
const layerNote = v.local
|
|
324
|
+
? '(原文来源:**本机库内原文层** —— 只含真人消息与有正文的回复,毫秒级;要全量检索请传 hostSearch=true)'
|
|
325
|
+
: '';
|
|
174
326
|
const hitRows = Array.isArray(v.rows) ? v.rows : [];
|
|
175
327
|
const deepRows = Array.isArray(v.deep) ? v.deep : [];
|
|
176
328
|
// 条目排在前:它是**结论层**(一句话说完),会话在后(要下钻才知道内容)。
|
|
@@ -186,22 +338,45 @@ export function renderSearch(v) {
|
|
|
186
338
|
}).join('\n') + '\n\n'
|
|
187
339
|
: '';
|
|
188
340
|
if (!hitRows.length && !deepRows.length) {
|
|
189
|
-
|
|
341
|
+
// 超时是**另一种结论**,不能说成"没找到" —— 前者该"重发或换窄词",后者该"换词",
|
|
342
|
+
// 而"库里没有"是第三种(最不该被误读成它)。
|
|
343
|
+
if (v.timedOut) {
|
|
344
|
+
return `查询「${v.q}」这次**没跑完**:宿主检索超过 ${Math.round((Number(v.timeoutMs) || SEARCH_TIMEOUT_MS) / 1000)} 秒没返回`
|
|
345
|
+
+ '(多半是它在补索引对账 —— 宿主侧的串行操作,插件打断不了它)。'
|
|
346
|
+
+ `\n条目层是本地库、毫秒级,它也没有命中 ⇒ 这次**两层都空**。`
|
|
347
|
+
+ '\n换个更窄的词(中文 2~4 字)重发一次,或稍后再试。'
|
|
348
|
+
+ `\n(范围:${scope})`;
|
|
349
|
+
}
|
|
350
|
+
return `按内容没找到(查询「${v.q}」· ${scope})${layerNote}。`
|
|
190
351
|
+ (v.skipped ? `\n(有 ${v.skipped} 条被范围挡掉 —— 要看它们得显式传 all=true。)\n` : '\n')
|
|
191
352
|
+ '换词试试:中文 **2~4 字**短词命中最好(`记忆树` / `血缘` / `注入面`);'
|
|
192
353
|
+ '**多个词用空格分隔 = 同时满足**(AND);**没有子串匹配**(`call` 搜不到 `recall`)。';
|
|
193
354
|
}
|
|
194
355
|
const rows = hitRows.map((r) => {
|
|
195
356
|
const at = [r.seq !== null && r.seq !== undefined ? `#${r.seq}` : '', r.time].filter(Boolean).join(' ');
|
|
196
|
-
|
|
357
|
+
// 「← 本会话」:它被排到了末尾(见 toSearchRows),标注是让模型知道**这条是刚发生的、不是历史** ——
|
|
358
|
+
// 否则它会把"我刚说的话"当成"主人当年说过的话"来引用。
|
|
359
|
+
const selfTag = r.isSelf ? ' **← 本会话(刚发生的)**' : '';
|
|
360
|
+
return `· ${r.conv}${selfTag} · ${at} ${r.who}\n ${r.snippet}`;
|
|
197
361
|
});
|
|
198
362
|
const head = deepRows.length && rows.length
|
|
199
|
-
? `按内容命中 **${deepRows.length} 条深层库条目** + ${rows.length}
|
|
363
|
+
? `按内容命中 **${deepRows.length} 条深层库条目** + ${rows.length} 轮原文(查询「${v.q}」· ${scope})${layerNote}:`
|
|
200
364
|
: deepRows.length
|
|
201
|
-
? `按内容命中 **${deepRows.length} 条深层库条目**(查询「${v.q}」· ${scope}):`
|
|
202
|
-
: `按内容找到 ${rows.length}
|
|
203
|
-
const convBlock = rows.length ?
|
|
204
|
-
|
|
365
|
+
? `按内容命中 **${deepRows.length} 条深层库条目**(查询「${v.q}」· ${scope})${layerNote}:`
|
|
366
|
+
: `按内容找到 ${rows.length} 轮原文(查询「${v.q}」· ${scope})${layerNote}:`;
|
|
367
|
+
const convBlock = rows.length ? `${v.local ? '【库内原文层】' : '【会话原文层】'}\n${rows.join('\n')}` : '';
|
|
368
|
+
// ⚠️ 超时**不算失败**:条目层已经拿到了(本地库),必须照常交出去。这一行是"如实说明少了一半",
|
|
369
|
+
// 而不是"报错" —— 两者的下一步动作不同(前者重发/换窄词,后者换通道)。
|
|
370
|
+
const timeoutNote = v.timedOut
|
|
371
|
+
? `\n\n⚠️ **原文层超时**(等满 ${Math.round((Number(v.timeoutMs) || SEARCH_TIMEOUT_MS) / 1000)} 秒没返回,多半是宿主在补索引对账):`
|
|
372
|
+
+ (deepRows.length ? '**条目层是本地库,不受影响,就在上面** —— 先把它们用起来。' : '这次连条目也没命中。')
|
|
373
|
+
+ '要原文就**重发同一条**(多数第二次会快,对账已经补过了),或换更窄的词。'
|
|
374
|
+
: '';
|
|
375
|
+
// ⚠️ 排除本会话要**说出来**:不说就会被读成"库里只有这些"(那是说假话)。
|
|
376
|
+
const selfNote = Number(v.selfExcluded) > 0
|
|
377
|
+
? `\n(另有 ${v.selfExcluded} 轮命中属于**当前会话自己**,已按默认排除 —— 要看自己刚说的传 \`includeSelf=true\`。)`
|
|
378
|
+
: '';
|
|
379
|
+
return `${head}\n${deepBlock}${convBlock}${timeoutNote}${selfNote}`
|
|
205
380
|
+ (v.skipped ? `\n(${v.skipped} 条被范围挡掉 —— 要看它们得显式传 all=true。)` : '')
|
|
206
381
|
+ (v.hasMore ? '\n(这一页满了,**还有更多** —— 把 limit 调大,或换个更窄的词。)' : '')
|
|
207
382
|
+ (deepRows.length
|
package/lib/host/snapshot.js
CHANGED
|
@@ -93,7 +93,8 @@ export function buildSnapshotText(ctx, memory, settings, sessionId, { keywords =
|
|
|
93
93
|
// 下一轮照样进候选;与 §3 开关"只管渲染、不管交代记账"同一取向)。
|
|
94
94
|
const logLines = Array.isArray(d?.lines) ? d.lines : [];
|
|
95
95
|
const logDetailLines = logLines.filter((x) => !BRANCH_LOG_FOLD_LINE.test(String(x)));
|
|
96
|
-
// §3 独立开关:只管**树改动播报**(
|
|
96
|
+
// §3 独立开关:只管**树改动播报**(与 `settings.persona.enabled` **并列**的一道闸 ——
|
|
97
|
+
// 外层 `cfg.injectEnabled` 关掉的是**整个记忆段**,见 inject.js:registerMemorySection)。
|
|
97
98
|
// 关掉时:不成文、也不推进"已示人/已处置"水位(否则关着关着就把一段内容标成已读 ⇒
|
|
98
99
|
// 再打开时那段内容永远看不到)。`.last` 照走 —— 它是复盘退出判据,不是"已示人"判据:
|
|
99
100
|
// **开关只管"渲染",不管"交代记账"**(停推进会卡死复盘模式的退出,回归见 tree 套 9-5)。
|
package/lib/host/surgery.js
CHANGED
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
// "没有 UA"正是那条路的指纹(详见下面 isLocalSurgeryRequest 的注释)。**不许在本文件里另写一份 IP 判断。**
|
|
13
13
|
// 1.2 每启动票据 = $DSH_HOME/cache/dsh-ling/surgery.ticket(32 字节随机),只对**本机直连**(无代理头且带 UA)的请求下发,
|
|
14
14
|
// 每启动轮换(旧票即废);客户端首次手术前取一次并缓存,随后带 `x-dsh-ling-surgery: <ticket>`。
|
|
15
|
-
// 1.3 门下沉为一处 requireSurgery(headers, body, opts),覆盖全部
|
|
15
|
+
// 1.3 门下沉为一处 requireSurgery(headers, body, opts),覆盖全部 **14** 条人格/规矩/习惯写面。
|
|
16
|
+
// ⚠️ 条数在 2026-10-05 夜由 12 订正为 14(B-5):下方的覆盖面清单**逐条列了 14 行**,那才是有权威的那份。
|
|
17
|
+
// 当时本行与 `api.js` 的门下沉注释还写着"12" ⇒ **两处都在说谎**。2026-10-16 复核:`api.js` 那侧
|
|
18
|
+
// 已全部对齐到 14(`api.js:541/603/2154/…/3231` 逐处都是 14)⇒ 本句现在只是在解释历史,
|
|
19
|
+
// **不再是当下的口径指控**(别再照它去找 api.js 的"12")。
|
|
20
|
+
// 口径不变:**条数一律以清单行数为准**,改清单时必须同批改这里与 api.js 那一处。
|
|
16
21
|
// 1.4 事件留痕 = $DSH_HOME/logs/surgery-YYYY-MM.jsonl,append-only + 整行哈希链(改一行即断链)。
|
|
17
22
|
// 1.5 逃生开关:DSH_LING_GUARD=off 或 settings.guard.enforce === false ⇒ 整体放行(绝不把主人锁在门外)。
|
|
18
23
|
//
|
|
@@ -31,7 +36,7 @@ import { join } from 'node:path';
|
|
|
31
36
|
import { dshHome, defaultDataDir } from './util.js';
|
|
32
37
|
import { headerOf, realPeerAddress } from './guard.js';
|
|
33
38
|
|
|
34
|
-
// ── 覆盖面清单:
|
|
39
|
+
// ── 覆盖面清单:14 条能改人格/规矩/习惯的写面(行号 = 2026-09-30 深夜实测,改 api.js 后须复核)──
|
|
35
40
|
// # 端点 register 行 "原"过门情况(改前) 改后
|
|
36
41
|
// 1 /persona 1809 仅 wasSealed && !wantLock(wantLock 可绕过) 过门
|
|
37
42
|
// 2 /persona/check 1855 无门(只读验钥 —— 但是一把**验句预言机**) 过门
|