@nightpainters/dsh-ling 1.6.2 → 1.7.1
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 +176 -9
- package/DESIGN.md +89 -6
- package/PHILOSOPHY.md +14 -4
- package/README.md +105 -19
- package/cordis.patch.yml +32 -2
- package/lib/client.js +2544 -61
- package/lib/host/agent-tools.js +286 -0
- package/lib/host/api.js +592 -59
- package/lib/host/archive.js +7 -7
- 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 +3 -3
- package/lib/host/deepsummary.js +80 -4
- package/lib/host/dsweb-summary.js +11 -5
- package/lib/host/genesis.js +5 -3
- package/lib/host/guard.js +53 -0
- package/lib/host/inject.js +18 -5
- package/lib/host/l1.js +17 -11
- 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 +119 -29
- package/lib/host/persona.js +13 -2
- package/lib/host/rawlog.js +67 -4
- package/lib/host/recall-search.js +60 -12
- package/lib/host/retitle.js +5 -5
- package/lib/host/session-line.js +7 -2
- package/lib/host/snapshot.js +9 -5
- package/lib/host/surgery.js +6 -4
- package/lib/host/tools.js +473 -27
- package/lib/host/util.js +1 -1
- package/lib/index.js +120 -81
- package/package.json +10 -5
- package/tools/deep-import-docs.mjs +1210 -0
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
|
//
|
|
@@ -105,8 +105,8 @@ export function archiveBefore(memory, { source, convId, kind = 'forget', reason
|
|
|
105
105
|
|
|
106
106
|
/** 改动日志 —— 这份文件是给**将来的人**看的,所以写全:是什么、为什么、怎么还原。 */
|
|
107
107
|
function readme({ at, s, c, kind, reason, actor, turns }) {
|
|
108
|
-
const who = actor === 'ling' ? '
|
|
109
|
-
// A-01(1.5.1):第三种 kind —— 主脉记忆覆盖(
|
|
108
|
+
const who = actor === 'ling' ? '器灵(工具通道)' : '主人(界面通道)';
|
|
109
|
+
// A-01(1.5.1):第三种 kind —— 主脉记忆覆盖(同一主脉再次采纳提炼结果;用户裁决"备份并覆盖")。
|
|
110
110
|
const what = kind === 'forget' ? '遗忘(软标记 —— **库里的行没有删**)'
|
|
111
111
|
: kind === 'vein-overwrite' ? '主脉记忆**覆盖**(同一主脉再次采纳 —— **上一版正文完整存在这里**)'
|
|
112
112
|
: '回灌(用归档恢复一条记忆 —— **覆盖前的状态已存这里**)';
|
|
@@ -114,14 +114,14 @@ function readme({ at, s, c, kind, reason, actor, turns }) {
|
|
|
114
114
|
// 「怎么还原」按 kind 分三档。用**字符串拼接**而不是嵌套模板,免得外层模板里的反引号打架。
|
|
115
115
|
const restoreBlock = kind === 'forget'
|
|
116
116
|
? '**库里没有删任何行** —— 只是多了一条 `forgotten` 标记,召回路径不再选它。撤销方式二选一:\n\n'
|
|
117
|
-
+ '1.
|
|
117
|
+
+ '1. 让器灵调工具撤销(`memory_forget` 的 `undo`),或\n'
|
|
118
118
|
+ '2. 直接删标记:\n ```sql\n DELETE FROM forgotten WHERE source=\'' + s + '\' AND conv_id=\'' + c + '\';\n ```'
|
|
119
119
|
: kind === 'vein-overwrite'
|
|
120
120
|
? '覆盖只动了 `conv_overview` 里那一行(主脉自己的记忆),原文与会话元数据不受影响。要把上一版换回去:\n\n'
|
|
121
|
-
+ '1.
|
|
121
|
+
+ '1. 让器灵按本目录 `overview.json` 的 `summary` **再采纳一次**旧正文,或\n'
|
|
122
122
|
+ '2. 直接写回:\n ```sql\n UPDATE conv_overview SET summary=\'<overview.json 的 summary>\'\n WHERE source=\'' + s + '\' AND conv_id=\'' + c + '\';\n ```'
|
|
123
123
|
: '回灌只是把归档写回去,而**它覆盖之前的状态存在这里**。要退回去:\n\n'
|
|
124
|
-
+ '1.
|
|
124
|
+
+ '1. 让器灵再回灌一次旧档(`memory_backfill`),或\n'
|
|
125
125
|
+ '2. 按下面「万一库本身没了」的步骤写回。';
|
|
126
126
|
return `# 归档:${title}的原始数据
|
|
127
127
|
|
|
@@ -150,7 +150,7 @@ ${restoreBlock}
|
|
|
150
150
|
|
|
151
151
|
## 为什么保留
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
忘错了要能回头。这是用户定的规矩:**永远不做彻底删除** ——
|
|
154
154
|
所以这个目录是"曾经发生过什么"的证据,不是垃圾。不要随手清。
|
|
155
155
|
|
|
156
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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
2
|
+
import { DEFAULT_LIBRARY_LIMITS } from './library-store.js';
|
|
3
|
+
import { normalizeLibraryConfig } from './library-runtime.js';
|
|
4
|
+
|
|
5
|
+
const libraryFields = Schema.object({
|
|
6
|
+
enabled: Schema.boolean().default(true).description('启用独立图书馆;不迁移记忆、不参与开场注入。'),
|
|
7
|
+
codeRoots: Schema.array(Schema.string()).default([]).description('管理员登记的绝对代码根目录。'),
|
|
8
|
+
officialUrls: Schema.array(Schema.string()).default([]).description('管理员登记的精确 HTTPS 文档 URL。'),
|
|
9
|
+
limits: Schema.object(Object.fromEntries(Object.entries(DEFAULT_LIBRARY_LIMITS)
|
|
10
|
+
.map(([key, value]) => [key, Schema.number().default(value)]))),
|
|
11
|
+
// F10(1.7 修复批次):图书馆配置校验失败的原因。非空 ⇒ **图书馆整块**不可用,记忆/人格/工具照常。
|
|
12
|
+
// 它是"响亮"的载体:理由词给上层,出错的键名给用户。
|
|
13
|
+
configError: Schema.string().default('').description('图书馆配置校验失败的原因(非空 ⇒ 图书馆整块不可用,其余功能照常)'),
|
|
14
|
+
});
|
|
15
|
+
const fields = Schema.object({ library: libraryFields });
|
|
16
|
+
|
|
17
|
+
// Validate raw keys before Schemastery fills defaults or adapts values. Its
|
|
18
|
+
// object resolver otherwise preserves unknown keys, including library hooks.
|
|
19
|
+
// Existing non-library settings keep their established configuration semantics.
|
|
20
|
+
export const Config = Schema.transform(Schema.any().default({}), (raw) => {
|
|
21
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
22
|
+
throw new TypeError('dsh-ling 配置必须是对象');
|
|
23
|
+
}
|
|
24
|
+
// F10(1.7 修复批次):以前 `normalizeLibraryConfig` 抛在这里 ⇒ 异常一路冒到 `apply()` ⇒
|
|
25
|
+
// **记忆 / 人格 / 全部 11 件工具一起不加载**,而且没有任何可指认面;而 `lib/index.js` 对
|
|
26
|
+
// "库打不开"却是有界重试 + 降级 + 排查指引 ⇒ 两种口径自相矛盾。
|
|
27
|
+
// 修法:**不吞、不静默** —— 原因带进 config(含出错的键名,可执行),图书馆进不可用态,
|
|
28
|
+
// 插件其余部分照常加载。**绝不**拿半套配置去开库(那会把"配置写错"变成看不懂的运行期怪象)。
|
|
29
|
+
let library = null;
|
|
30
|
+
let libraryConfigError = '';
|
|
31
|
+
try {
|
|
32
|
+
library = normalizeLibraryConfig(raw.library);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
libraryConfigError = String(error?.message ?? error).slice(0, 300);
|
|
35
|
+
library = normalizeLibraryConfig({}); // 结构仍是合法形状(默认:enabled=true、空白名单)
|
|
36
|
+
}
|
|
37
|
+
return fields({ ...raw, library: { ...library, configError: libraryConfigError } });
|
|
38
|
+
}, true).default({});
|
|
@@ -17,14 +17,14 @@ export const INDEX_SKILL_NAME = 'memory-index';
|
|
|
17
17
|
export const INDEX_SKILL_DESCRIPTION = `我记住的事按 11 类放着(用 recall({q:"关键词"}) 按内容搜 —— 它同时搜**结论**与**会话原文**):
|
|
18
18
|
· 记忆树与记忆库:记忆怎么存、怎么搜、血缘归并、深层库
|
|
19
19
|
· 器灵 dsh-ling:包名、发布仓库、版本与宣发、插件配置
|
|
20
|
-
·
|
|
20
|
+
· 器灵与主人:称呼、纪念日、同伴身份、形象锚与立场
|
|
21
21
|
· 主人其人:职业来路、身体作息、脾气念头、价值观
|
|
22
22
|
· 合作规矩:先审后装、干跑、重启要问、实测复跑、备份
|
|
23
23
|
· 本业与项目:本职的专业领域、论文与研究、自制的装置项目
|
|
24
24
|
· 这台机器与踩坑:VM/Win10、硬件、乱码、沙箱、网络接入
|
|
25
25
|
· 模型与API:选型、推理档位、缓存、上下文、本地小模型(我的手)
|
|
26
26
|
· 插件与界面:装/卸/审计、侧栏、皮肤、权限选择器
|
|
27
|
-
·
|
|
27
|
+
· 器灵的感官和开口:看图/生图、语音、听音乐、主动开口
|
|
28
28
|
· 零散几条:护眼工具、天文观测、投资慢课这类生活杂事`;
|
|
29
29
|
|
|
30
30
|
export const INDEX_SKILL_WHEN_TO_USE = '想知道"我以前说过/定过什么"、或者要在一堆长期结论里找依据时读它;它的每一行是一个**主题簇**,指明了那一类里装着什么样的事。日常回答通常不需要读它 —— 索引行本身已经够你判断该不该用 recall 去搜。';
|
|
@@ -63,7 +63,7 @@ export const INDEX_SKILL_CONTENT = `# 深层库索引 · 我记住的事分 11
|
|
|
63
63
|
|---|---|---|
|
|
64
64
|
| 记忆树与记忆库 | 记忆怎么存、怎么搜,血缘归并,深层库 | 记忆树 · 记忆库 · 血缘 · 召回 |
|
|
65
65
|
| 器灵 dsh-ling | 包名、发布仓库、版本与宣发、插件配置 | 器灵 · dsh-ling · 发布 |
|
|
66
|
-
|
|
|
66
|
+
| 器灵与主人 | 称呼、纪念日、同伴身份、形象锚与立场 | 称呼 · 纪念日 · 同伴 · 形象 |
|
|
67
67
|
| 主人其人 | 职业来路、身体作息、脾气念头、价值观 | 职业 · 睡眠 · 价值观 · 立场 |
|
|
68
68
|
| 合作规矩 | 谁拍板、怎么审计、怎么汇报、红线 | 重启 · 审计 · 备份 · 规矩 |
|
|
69
69
|
| 本业与项目 | 本职的专业领域、论文与研究、自制的装置项目 | 机械 · 论文 · 合金 · 弹跳球 |
|
package/lib/host/deepsummary.js
CHANGED
|
@@ -4,10 +4,15 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { svc, utcIso, dshHome } from './util.js';
|
|
7
|
+
import { wellFormed } from './rawlog.js'; // 1.7.1 F4 闭环:深摘转写直接进请求体
|
|
7
8
|
import { RAW_IMPORT_PREFIX, rawSessionCandidates } from './memory.js';
|
|
8
9
|
import { pickSessionFile } from './backfill.js';
|
|
9
10
|
import { currentModelSelection } from './mode.js';
|
|
10
11
|
import { decodeAll } from './zstd.js';
|
|
12
|
+
// 「后台自动行为」的默认值真源(persona.DEFAULT_SETTINGS.auto)与区间真源(guard.AUTO_NUMBER_RANGES)
|
|
13
|
+
// 都各有唯一一处 —— 本模块只**取来用**,不另抄一份数字(抄一份 = 下次改默认必然漏改一处)。
|
|
14
|
+
import { DEFAULT_SETTINGS } from './persona.js';
|
|
15
|
+
import { AUTO_NUMBER_RANGES } from './guard.js';
|
|
11
16
|
|
|
12
17
|
// 循环外调用(深摘要 / 诞生仪式 / 语气归纳 / 自述起草)一律**跟随用户当前的模型选择**:
|
|
13
18
|
// 平台 API 要求显式给出 {provider, model},故读 agentDefaultModel.currentSelection();
|
|
@@ -23,10 +28,79 @@ export const DEEP_CFG = {
|
|
|
23
28
|
temperature: 0.2,
|
|
24
29
|
inputCap: 26000,
|
|
25
30
|
diskMinBytes: 64 * 1024,
|
|
26
|
-
autoIntervalMin: 30,
|
|
27
|
-
perRunLimit: 2,
|
|
31
|
+
autoIntervalMin: 30, // 旧默认值;1.7 起"自动间隔"的真源是 DEFAULT_SETTINGS.auto.deepIntervalMin(同值,见 resolveAuto)
|
|
32
|
+
perRunLimit: 2, // 仍是 deepCandidates 自己的默认每轮上限;启动块已改走 resolveAuto
|
|
28
33
|
};
|
|
29
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 读出**用户真的写过**的 auto 子树(settings.json 只持久化用户补丁,见 settings-file.js 文件头)。
|
|
37
|
+
*
|
|
38
|
+
* 为什么不能用 `settings.get().auto` 判优先级:`get()` 把 DEFAULT_SETTINGS 深合并了进来
|
|
39
|
+
* ⇒ 那五个键**永远存在**,于是"代码默认值"会冒充"用户设定":插件配置里的旧键
|
|
40
|
+
* (`feedbackAuto` / `deepSummaryAuto` / `deepIntervalMin` / `deepMaxPerRun`)被永久压掉
|
|
41
|
+
* (老用户升级上来行为就变了),面板的 `source` 也会永远显示 settings。
|
|
42
|
+
* 入参两种都收:SettingsFile 实例(读 `.user.auto` —— 真实运行时)或普通设置对象(退回 `.auto`,测试桩用);
|
|
43
|
+
* **「有 .user 但没写 auto」与「没有 .user」必须分开**:前者 = 用户什么都没写 ⇒ `{}`,后者才回退到 `.auto`。
|
|
44
|
+
*/
|
|
45
|
+
export function storedAuto(settingsLike) {
|
|
46
|
+
const s = (settingsLike && typeof settingsLike === 'object') ? settingsLike : {};
|
|
47
|
+
const hasUser = (s.user && typeof s.user === 'object' && !Array.isArray(s.user)) ? s.user : null;
|
|
48
|
+
const cand = hasUser ? hasUser.auto : s.auto;
|
|
49
|
+
return (cand && typeof cand === 'object' && !Array.isArray(cand)) ? cand : {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 「后台自动行为」的**唯一解析处**(2026-10-16 与用户定):settings.auto > 插件配置旧键 > 代码默认。
|
|
54
|
+
*
|
|
55
|
+
* 为什么要收成一支:三级优先级原先散在 `index.js` 的启动块里(且探活延时是裸的 12000、根本没有配置键),
|
|
56
|
+
* 而设置面板必须能如实显示"这个值是谁定的" —— 两处各判一次,迟早会漂成两个答案。
|
|
57
|
+
*
|
|
58
|
+
* 默认值**只从 `DEFAULT_SETTINGS.auto` 取**(不在这里另抄一份数字);区间只从 `AUTO_NUMBER_RANGES` 取。
|
|
59
|
+
* cfg 一侧刻意保留旧代码的 `Number(...)` 口径:插件配置里写 `'60'` 这种字符串,旧实现是生效的,
|
|
60
|
+
* 这里不能因为"更严格"就把它变成没设过(那才是行为变了)。
|
|
61
|
+
*
|
|
62
|
+
* @param {object} settings SettingsFile 实例(读用户补丁)或普通设置对象;见 storedAuto
|
|
63
|
+
* @param {object} [cfg] 插件配置(旧键名:`deepSummaryAuto` / `deepIntervalMin` / `deepMaxPerRun` / `feedbackAuto`)
|
|
64
|
+
* @returns {{deepSummary: boolean, deepIntervalMin: number, deepMaxPerRun: number,
|
|
65
|
+
* bootProbeDelayMs: number, feedbackAuto: boolean,
|
|
66
|
+
* source: Record<string, 'settings'|'config'|'default'>}}
|
|
67
|
+
*/
|
|
68
|
+
export function resolveAuto(settings, cfg = {}) {
|
|
69
|
+
const sAuto = storedAuto(settings);
|
|
70
|
+
const c = (cfg && typeof cfg === 'object' && !Array.isArray(cfg)) ? cfg : {};
|
|
71
|
+
const D = DEFAULT_SETTINGS.auto;
|
|
72
|
+
const source = {};
|
|
73
|
+
/** 布尔:settings 里必须是**真布尔**才算命中(字符串 "false" 不算设过 —— 与写面同一把尺)。 */
|
|
74
|
+
const boolOf = (key, cfgKey, def) => {
|
|
75
|
+
if (typeof sAuto[key] === 'boolean') { source[key] = 'settings'; return sAuto[key]; }
|
|
76
|
+
if (cfgKey && typeof c[cfgKey] === 'boolean') { source[key] = 'config'; return c[cfgKey]; }
|
|
77
|
+
source[key] = 'default'; return def;
|
|
78
|
+
};
|
|
79
|
+
/** 数字:settings 收有限数;cfg 沿用 `?? + Number()` 的旧口径(只有 undefined/null 算"没写");
|
|
80
|
+
* 非有限数(如 'abc')落到默认 —— 旧实现会把它变成 NaN(interval 变 0 = 空转),那不是要保留的行为。 */
|
|
81
|
+
const numOf = (key, cfgKey, def) => {
|
|
82
|
+
const [lo, hi] = AUTO_NUMBER_RANGES[key];
|
|
83
|
+
let raw;
|
|
84
|
+
if (typeof sAuto[key] === 'number' && Number.isFinite(sAuto[key])) { raw = sAuto[key]; source[key] = 'settings'; }
|
|
85
|
+
else {
|
|
86
|
+
const has = cfgKey !== undefined && cfgKey !== null && c[cfgKey] !== undefined && c[cfgKey] !== null;
|
|
87
|
+
const cv = has ? Number(c[cfgKey]) : NaN;
|
|
88
|
+
if (has && Number.isFinite(cv)) { raw = cv; source[key] = 'config'; }
|
|
89
|
+
else { raw = def; source[key] = 'default'; }
|
|
90
|
+
}
|
|
91
|
+
return Math.min(hi, Math.max(lo, raw));
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
// 探活延时**没有**旧配置键(此前硬编码 12000)⇒ 它只可能来自 settings 或默认。
|
|
95
|
+
deepSummary: boolOf('deepSummary', 'deepSummaryAuto', D.deepSummary),
|
|
96
|
+
deepIntervalMin: numOf('deepIntervalMin', 'deepIntervalMin', D.deepIntervalMin),
|
|
97
|
+
deepMaxPerRun: numOf('deepMaxPerRun', 'deepMaxPerRun', D.deepMaxPerRun),
|
|
98
|
+
bootProbeDelayMs: numOf('bootProbeDelayMs', null, D.bootProbeDelayMs),
|
|
99
|
+
feedbackAuto: boolOf('feedbackAuto', 'feedbackAuto', D.feedbackAuto),
|
|
100
|
+
source,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
30
104
|
/** 解析循环外调用使用的模型:跟随当前选择;不可读则用兜底值并留一行 debug。 */
|
|
31
105
|
export function resolveLlmTarget(ctx) {
|
|
32
106
|
const cur = typeof currentModelSelection === 'function' ? currentModelSelection(ctx) : null;
|
|
@@ -156,7 +230,8 @@ export function transcriptFromFile(file, { cap = DEEP_CFG.inputCap } = {}) {
|
|
|
156
230
|
const budget = Math.max(100, cap - mark.length);
|
|
157
231
|
joined = joined.slice(0, Math.floor(budget * 0.6)) + mark + joined.slice(-Math.floor(budget * 0.4));
|
|
158
232
|
}
|
|
159
|
-
|
|
233
|
+
const safe = wellFormed(joined);
|
|
234
|
+
return { text: safe, users, asst, chars: safe.length, file };
|
|
160
235
|
}
|
|
161
236
|
|
|
162
237
|
/** DB 原文转录(role user/assistant,按 seq)。source 给定时按 G4 命名空间取原文
|
|
@@ -188,7 +263,8 @@ export function buildTranscript(memory, sessionId, { cap = DEEP_CFG.inputCap, so
|
|
|
188
263
|
const budget = Math.max(100, cap - mark.length);
|
|
189
264
|
text = text.slice(0, Math.floor(budget * 0.6)) + mark + text.slice(-Math.floor(budget * 0.4));
|
|
190
265
|
}
|
|
191
|
-
|
|
266
|
+
const safe = wellFormed(text);
|
|
267
|
+
return { text: safe, users, parts: parts.length, chars: safe.length };
|
|
192
268
|
}
|
|
193
269
|
|
|
194
270
|
/**
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
// 2) 阈值:轮次数 < minTurns(默认 2)默认不补(纯闲聊),显式 --all 才含;
|
|
10
10
|
// 3) 引擎:默认**本机小助手**(零成本);**绝不静默切换到大模型**(那等于替用户花钱)。
|
|
11
11
|
import { DatabaseSync } from 'node:sqlite';
|
|
12
|
+
// 1.7.1(delta 复审):本文件把会话原文拼进 `JSON.stringify(body)` 直接 POST 给小助手 ——
|
|
13
|
+
// 那也是一条"文本进请求体"的路径,与注入面同罪。原文里若带孤立代理,毒照样发出去。
|
|
14
|
+
import { wellFormed } from './rawlog.js';
|
|
12
15
|
|
|
13
16
|
// S7(2026-09-18 与用户定):默认地址**中性化**为 loopback —— 发布版绝不携带私有内网地址,
|
|
14
17
|
// 主人的真实小助手地址改由 settings.assistant(UI 可配)或环境变量指定,三级覆盖见 resolveAssistant()。
|
|
@@ -137,7 +140,7 @@ export async function probeAssistant(baseUrl = ASSISTANT_DEFAULT, { timeoutMs =
|
|
|
137
140
|
* · **Ollama 原生 `/api/chat` + `think:false` 有效**:实测 thinking 长度 0、正文正常。
|
|
138
141
|
* 因此默认走原生端点(把 base 末尾的 `/v1` 去掉),`noThink:false` 时才用兼容端点。
|
|
139
142
|
*/
|
|
140
|
-
export async function summarizeLocal(baseUrl, model, { system = SUMMARY_SYS, text, maxTokens = MAX_TOKENS_DEFAULT, timeoutMs = 90000, noThink = true } = {}) {
|
|
143
|
+
export async function summarizeLocal(baseUrl, model, { system = SUMMARY_SYS, text, maxTokens = MAX_TOKENS_DEFAULT, timeoutMs = 90000, noThink = true, clean = true } = {}) {
|
|
141
144
|
const t0 = Date.now();
|
|
142
145
|
const base = String(baseUrl).replace(/\/+$/, '');
|
|
143
146
|
const native = base.replace(/\/v1$/, '');
|
|
@@ -146,8 +149,8 @@ export async function summarizeLocal(baseUrl, model, { system = SUMMARY_SYS, tex
|
|
|
146
149
|
const ctrl = new AbortController();
|
|
147
150
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
148
151
|
try {
|
|
149
|
-
const sys = noThink ? system + '\n' + NO_THINK : system;
|
|
150
|
-
const usr = noThink ? text + '\n' + NO_THINK : text;
|
|
152
|
+
const sys = wellFormed(noThink ? system + '\n' + NO_THINK : system);
|
|
153
|
+
const usr = wellFormed(noThink ? text + '\n' + NO_THINK : text);
|
|
151
154
|
const body = noThink
|
|
152
155
|
? {
|
|
153
156
|
model, stream: false, think: false,
|
|
@@ -181,8 +184,11 @@ export async function summarizeLocal(baseUrl, model, { system = SUMMARY_SYS, tex
|
|
|
181
184
|
completionTokens: noThink ? (j?.eval_count ?? null) : (j?.usage?.completion_tokens ?? null),
|
|
182
185
|
promptTokens: noThink ? (j?.prompt_eval_count ?? null) : (j?.usage?.prompt_tokens ?? null),
|
|
183
186
|
};
|
|
184
|
-
|
|
185
|
-
|
|
187
|
+
// `clean:false`(2026-10-12,给图书馆导入通道的模型段用):**原样交出模型回复**。
|
|
188
|
+
// 为什么必须有这一档:`cleanSummary` 会把回复压成一行、截到 300 字 —— 对"一句摘要"正好,
|
|
189
|
+
// 对**要 JSON 的调用方**等于把结构撕碎(它连换行都吃掉)。摘要路径一个字节没变(默认仍是 clean:true)。
|
|
190
|
+
const summary = clean ? cleanSummary(raw) : String(raw || '').trim();
|
|
191
|
+
return { ok: clean ? !!summary : summary !== '', summary, raw, error: (clean ? !!summary : summary !== '') ? null : (diag.reasoningLen > 0 ? 'reasoning-only' : 'empty'), ms: Date.now() - t0, diag };
|
|
186
192
|
} catch (e) {
|
|
187
193
|
return { ok: false, error: String(e?.name === 'AbortError' ? 'timeout' : (e?.message ?? e)).slice(0, 120), ms: Date.now() - t0, path: noThink ? 'native' : 'compat' };
|
|
188
194
|
} finally {
|
package/lib/host/genesis.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
// 措辞必须**陈述式**(我承担/我擅长/我能给出),**禁授权式**(我应当/我必须/我的任务/我的责任是):
|
|
17
17
|
// 授权式写法把她写成一个"被授权者",是语义劫持最好用的钩子(参照 [规矩] 段头那次的教训)。
|
|
18
18
|
|
|
19
|
+
import { wellFormed } from './rawlog.js'; // 1.7.1 F4 闭环:诞生仪式的请求体同样不许带毒
|
|
20
|
+
|
|
19
21
|
/** 授权式措辞清单(1.5.2 I-5,约束 C):起草 prompt 必须逐字列出它们作为**禁止项**。
|
|
20
22
|
* 导出而非内联,是为了让"prompt 里的禁令"和"界面上的提醒(pm 面板里那句『读起来像…建议不选它』)"
|
|
21
23
|
* 以及测试断言共用同一份事实 —— 三处各写一份必然会漂移成说谎的护栏。
|
|
@@ -120,7 +122,7 @@ export function snippetFromRaw(memory, convId, { maxMsgs = 3, perMsg = 200, pron
|
|
|
120
122
|
if (!t) continue;
|
|
121
123
|
parts.push(who + ': ' + (t.length > perMsg ? t.slice(0, perMsg) + '…' : t));
|
|
122
124
|
}
|
|
123
|
-
return parts.join(' | ');
|
|
125
|
+
return wellFormed(parts.join(' | '));
|
|
124
126
|
} catch {
|
|
125
127
|
return '';
|
|
126
128
|
}
|
|
@@ -142,9 +144,9 @@ export function buildGenesisSource(rows, { cap = 18000 } = {}) {
|
|
|
142
144
|
parts.push(line);
|
|
143
145
|
}
|
|
144
146
|
const text = parts.join('\n');
|
|
145
|
-
if (text.length <= cap) return text;
|
|
147
|
+
if (text.length <= cap) return wellFormed(text);
|
|
146
148
|
// 截断:保头丢尾(排序保证最前面的正是生活/情感优先的条目)
|
|
147
|
-
return text.slice(0, cap) + '\n…(历史较长,其余省略)';
|
|
149
|
+
return wellFormed(text.slice(0, cap)) + '\n…(历史较长,其余省略)';
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
/**
|
package/lib/host/guard.js
CHANGED
|
@@ -219,3 +219,56 @@ export function sanitizePersonaPatch(patch) {
|
|
|
219
219
|
}
|
|
220
220
|
return { clean, dropped };
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
// ── ⑤ 后台行为补丁白名单(2026-10-16)──────────────────────────────────────────────
|
|
224
|
+
// 与 ④ 同一纪律、同一原因:`POST /auto` 也把 body 交给 `settings.update`(深合并,**不做键过滤**)
|
|
225
|
+
// ⇒ 不设白名单就等于给了一条新旁路 —— 拿 `/auto` 往 settings.json 里写 `guard.enforce:false` 或
|
|
226
|
+
// 整棵 persona(④ 那次堵的正是这条链)。故:只有下面 5 个键能出这个函数。
|
|
227
|
+
// 额外收窄(比 ④ 更严):**只收扁平对象**。auto 子树没有嵌套语义,接受嵌套/数组纯粹是给原型污染留门
|
|
228
|
+
// (`{"__proto__":{…}}` 经深合并会落到 Object.prototype 上,那是全进程级污染,不是一个设置项)。
|
|
229
|
+
/** auto 子树里允许经 /auto 写入的键(= DEFAULT_SETTINGS.auto 的全部字段)。 */
|
|
230
|
+
export const AUTO_PATCH_KEYS = Object.freeze([
|
|
231
|
+
'deepSummary', 'deepIntervalMin', 'deepMaxPerRun', 'bootProbeDelayMs', 'feedbackAuto',
|
|
232
|
+
]);
|
|
233
|
+
/** 布尔键(其余三个是数字键:类型不符一律丢弃,不猜用户想说什么)。 */
|
|
234
|
+
export const AUTO_BOOL_KEYS = Object.freeze(['deepSummary', 'feedbackAuto']);
|
|
235
|
+
/** 数字键的合法区间 [lo, hi]:**唯一定义处** —— sanitizeAutoPatch(写面)与 resolveAuto(读面)同取这一份,
|
|
236
|
+
* 两处各写一遍迟早会漂成"写得进 5、读出来 10"这种自相矛盾的设置。 */
|
|
237
|
+
export const AUTO_NUMBER_RANGES = Object.freeze({
|
|
238
|
+
deepIntervalMin: Object.freeze([10, 1440]),
|
|
239
|
+
deepMaxPerRun: Object.freeze([1, 20]),
|
|
240
|
+
bootProbeDelayMs: Object.freeze([0, 600000]),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 把一份「后台行为」补丁收窄成"只能落在这 5 个白名单键上"的安全扁平补丁。
|
|
245
|
+
* @param {unknown} patch 待写入的补丁(通常是 body.patch,也接受 body 本身)
|
|
246
|
+
* @returns {{ clean: object, dropped: string[], clamped: string[] }} clean = 可交给 settings.update 的补丁;
|
|
247
|
+
* dropped = 被丢弃的键(白名单外 / 类型不符 / 整个 body 不是普通对象);
|
|
248
|
+
* clamped = 被夹取到区间的键(用于如实回报"你填的值没被原样采纳")
|
|
249
|
+
*/
|
|
250
|
+
export function sanitizeAutoPatch(patch) {
|
|
251
|
+
const clean = {};
|
|
252
|
+
const dropped = [];
|
|
253
|
+
const clamped = [];
|
|
254
|
+
// 数组与 null 都不是补丁(数组的 Object.entries 会给出 "0"/"1" 这类下标键,没有语义)
|
|
255
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return { clean, dropped, clamped };
|
|
256
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
257
|
+
if (!Object.prototype.hasOwnProperty.call(AUTO_NUMBER_RANGES, k)) {
|
|
258
|
+
// 布尔键:只收真正的布尔;字符串 "false" 不猜(猜错方向 = 悄悄关掉一个后台行为)
|
|
259
|
+
if (AUTO_BOOL_KEYS.includes(k)) {
|
|
260
|
+
if (typeof v === 'boolean') clean[k] = v;
|
|
261
|
+
else dropped.push(k);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
dropped.push(k);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) { dropped.push(k); continue; }
|
|
268
|
+
const [lo, hi] = AUTO_NUMBER_RANGES[k];
|
|
269
|
+
const c = Math.min(hi, Math.max(lo, v));
|
|
270
|
+
clean[k] = c;
|
|
271
|
+
if (c !== v) clamped.push(k);
|
|
272
|
+
}
|
|
273
|
+
return { clean, dropped, clamped };
|
|
274
|
+
}
|
package/lib/host/inject.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// - idle sessions with a stale snapshot: rebuild lazily (one-time invalidation);
|
|
7
7
|
// - subagent/child/unknown sessions: return '' (R5).
|
|
8
8
|
import { svc, pick, neutralizeMustache, sessionKind } from './util.js';
|
|
9
|
+
import { wellFormed } from './rawlog.js'; // 1.7.1 F4 闭环:注入面最终出口
|
|
9
10
|
import { buildSnapshotText, firstUserText } from './snapshot.js';
|
|
10
11
|
import { currentMode } from './mode.js';
|
|
11
12
|
import { keywordsFrom } from './inject-common.js';
|
|
@@ -88,7 +89,7 @@ function advanceLogWatermark(memory, sessionId, maxId, shownMaxId, retiredMaxId,
|
|
|
88
89
|
if (retired > 0 && retired > Number(memory.kvGet('branch_log_wm.retired') || 0)) {
|
|
89
90
|
memory.kvSet('branch_log_wm.retired', String(retired));
|
|
90
91
|
}
|
|
91
|
-
// 复盘守门(2026-09-22
|
|
92
|
+
// 复盘守门(2026-09-22 用户:「动记忆树不能是随意的,应该是慎重严肃的」):
|
|
92
93
|
// 另记一个**全局最后同步**水位,回答"自器灵上次开口以来累积了多少笔改动还没交代"
|
|
93
94
|
// (memory.branchLogPending 读它)。与会话水位互补 —— 会话水位管"这个会话看到什么",
|
|
94
95
|
// 本键管"是否还没跟器灵交代过"。
|
|
@@ -96,7 +97,17 @@ function advanceLogWatermark(memory, sessionId, maxId, shownMaxId, retiredMaxId,
|
|
|
96
97
|
} catch { /* 推进失败仅导致下次重复提示,不影响注入 */ }
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
|
|
100
|
+
/** 注册记忆注入段(order 1500)+ 时间锚 context。
|
|
101
|
+
*
|
|
102
|
+
* ── F5(1.7 修复批次,2026-10-15)────────────────────────────────────────────
|
|
103
|
+
* 第 5 参是**逃生开关**:`cordis.patch.yml:16` 早就承诺 `injectEnabled: false` 能关掉注入,
|
|
104
|
+
* 但在这次修之前全仓 `cfg.injectEnabled` **零处读** ⇒ 用户以为关掉了、实际照注(静默失效)。
|
|
105
|
+
* 现在关到底 ⇒ **一段都不注册**(section 与 clock context 都不挂),留痕仍由
|
|
106
|
+
* index.js 的 `inject_enabled_config` 负责。默认(true / 不传)行为**逐字不变**。
|
|
107
|
+
* ⚠️ 这一道只管**记忆段**;人格段走 `settings.persona.enabled`(persona.js:166),两者并列。
|
|
108
|
+
* 回归:tests/injectlog.test.mjs 第 5 节(关到底零注册 + 默认行为正对照 + 接线判据)。 */
|
|
109
|
+
export function registerMemorySection(ctx, gate, memory, settings, { enabled = true } = {}) {
|
|
110
|
+
if (enabled === false) return () => {};
|
|
100
111
|
const sp = svc(ctx, 'systemPrompt');
|
|
101
112
|
if (!sp || typeof sp.section !== 'function') {
|
|
102
113
|
console.debug('[dsh-ling] systemPrompt service unavailable; injection disabled');
|
|
@@ -133,7 +144,9 @@ export function registerMemorySection(ctx, gate, memory, settings) {
|
|
|
133
144
|
// 而 agent-loop 调用处无 try/catch ⇒ **该会话每一步模型调用都失败**。
|
|
134
145
|
// 这正是 1.3.1 拆掉的那颗雷(S12)的同类面:section 侧一直有中和
|
|
135
146
|
// (见 renderMemorySection 的 `neutralizeMustache(anchor)`),搬迁时漏了这一处。
|
|
136
|
-
|
|
147
|
+
// 1.7.1(delta 复审):这是**独立于 snapshot 的第二条出口** —— context 文本也会进模型上下文,
|
|
148
|
+
// 只中和 mustache 不够;时间锚里的称呼/日期同样是往外送的文本,照样过 wellFormed。
|
|
149
|
+
text: () => wellFormed(neutralizeMustache(timeAnchorLive(memory, settings))),
|
|
137
150
|
});
|
|
138
151
|
clockRegistered = true;
|
|
139
152
|
memory.kvSet('inject.clock_ctx_registered', '1');
|
|
@@ -176,9 +189,9 @@ export function renderMemorySection(assembly, gate, memory, settings) {
|
|
|
176
189
|
// 注入面出口 ②(最终侧,2026-09-18):时间锚不经快照,未来新增的 part 也只走这里,
|
|
177
190
|
// 故在最外层再中和一次(幂等)。①+② 合计覆盖全部注入文本。
|
|
178
191
|
const withAnchor = (text) => {
|
|
179
|
-
const body = neutralizeMustache(text || '');
|
|
192
|
+
const body = wellFormed(neutralizeMustache(text || ''));
|
|
180
193
|
if (!anchor) return body;
|
|
181
|
-
return (body ? body + '\n\n' : '') + neutralizeMustache(anchor);
|
|
194
|
+
return wellFormed((body ? body + '\n\n' : '') + neutralizeMustache(anchor));
|
|
182
195
|
};
|
|
183
196
|
const snap = gate.snapshotOf(sessionId);
|
|
184
197
|
// §3 独立开关(1.5.2):只管"树改动播报"。关闭时**不推进任何"已示人"水位** ——
|
package/lib/host/l1.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// heat = α·recency + β·freq + γ·importance (exponential decay, half-life configurable);
|
|
3
3
|
// mode weights by category (knowledge/daily/feeling); optional keyword pre-match.
|
|
4
4
|
import { toEpochMs, isoDate } from './util.js';
|
|
5
|
+
import { wellFormed } from './rawlog.js'; // 1.7.1 F4 闭环:L1 行同样进模型上下文,出口统一净化
|
|
5
6
|
import { userTitleForMode } from './persona.js'; // 1.6-B2:视角框的称呼跟随 userTitle(不硬编码)
|
|
6
7
|
|
|
7
8
|
const DAY_MS = 86_400_000;
|
|
@@ -95,7 +96,7 @@ function kwOverlap(queryKeywords, rowKeywords) {
|
|
|
95
96
|
* (为"查询 4-gram vs 存储 6-gram 的**关键词表**"设计的),而条目路的右操作数是**条目正文**
|
|
96
97
|
* (全库均 52 字)。中文常用 3-gram 遍地都是 ⇒ 几乎任何查询都会命中 3 条以上 ⇒ 打满
|
|
97
98
|
* `KW_BONUS_CAP`(+1.8),把新近(最多 0.3)和 hit(尚为 0)全压死。
|
|
98
|
-
* 实测病灶(
|
|
99
|
+
* 实测病灶(用户 2026-10-04 开新会话看到):开场 8 条**全是同一次生图会话的条目** ——
|
|
99
100
|
* 排序实际上等于"谁的字面碰巧撞上查询词"。
|
|
100
101
|
* 长句只做**连续子串**匹配:那个词真出现在那句话里才算命中。 */
|
|
101
102
|
function kwHitInText(queryKeywords, text) {
|
|
@@ -154,7 +155,7 @@ const HIT_SATURATION = 5; // 采用满 5 次即拿满分(与记忆行 freq
|
|
|
154
155
|
// 新近对**结论层**本就该是弱信号(结论的价值在长期),压到 0.1 后不再主导。
|
|
155
156
|
const RECENCY_W = 0.1;
|
|
156
157
|
const DEEP_HALF_LIFE = 365; // 半衰期一年(旧的概述行是 90 天)
|
|
157
|
-
/** **硬度分**(1.6,2026-10-04
|
|
158
|
+
/** **硬度分**(1.6,2026-10-04 用户的判断:「能不能做成从根源上判断这些条目到底是临时的还是永久的,
|
|
158
159
|
* 或者加"硬度"评分来衡量临时程度/永久程度」)。
|
|
159
160
|
* 它**同时**用于两处:出局(见 `selectL1` 里 `ephemeral` 的 continue)与**排序**。
|
|
160
161
|
* 只做前者是不够的 —— 上一版就只做了前者,于是"长期原则"和"阶段性结论"在排序里**同权**,
|
|
@@ -305,7 +306,7 @@ export function selectL1(store, opts) {
|
|
|
305
306
|
const now = Date.now();
|
|
306
307
|
const summaryBonus = Number.isFinite(Number(o.summaryBonus)) ? Number(o.summaryBonus) : SUMMARY_BONUS_DEFAULT;
|
|
307
308
|
// 2026-09-21:归档会话不参与召回 —— 归档是主人的明确动作("这条别再提了"),
|
|
308
|
-
// 而此前 selectL1 直接吃 listOverviews 全量:归档只影响界面,召回照旧(
|
|
309
|
+
// 而此前 selectL1 直接吃 listOverviews 全量:归档只影响界面,召回照旧(用户实测反馈)。
|
|
309
310
|
// 只对 source='dsh' 生效:archived 是会话级标记,历史网页端/导入条目没有会话行。
|
|
310
311
|
const archived = (() => {
|
|
311
312
|
try {
|
|
@@ -353,7 +354,7 @@ export function selectL1(store, opts) {
|
|
|
353
354
|
// 真库实测 **393/393 条全为空**(schema 注释早写明"记忆树是空间维度、深层库是内容维度,
|
|
354
355
|
// 条目不隶属任何枝"),照那个字段接等于"接了也没接"。走会话归属才有意义:
|
|
355
356
|
// dsh 侧实测主干 73 / 开发日志 25 / 运维排障 18 / 日常 4,非 dsh 侧已挂枝 34、未挂枝 179。
|
|
356
|
-
// 取值:默认 1.0 ⇒ **接线当下零行为变化**(
|
|
357
|
+
// 取值:默认 1.0 ⇒ **接线当下零行为变化**(枝系数没被调过时,一切排序与接线前逐字相同);
|
|
357
358
|
// 复盘期调过谁,谁的记忆才在开场里前后移动 —— 这就是 `README` U-10「装饰性交互要么真生效、
|
|
358
359
|
// 要么删掉」要的那个"真生效"。
|
|
359
360
|
// ⚠️ 它乘在**最后**一项(`raw × 血缘 × 矛盾 × 枝系数`),与 `LINEAGE_*` 同一层:*打对折*,
|
|
@@ -402,7 +403,12 @@ export function selectL1(store, opts) {
|
|
|
402
403
|
// 而 `MAX_PER_CONV` 压的是症状(临时结论往往扎堆在一次会话里)⇒ 两者并存、分工不同。
|
|
403
404
|
if (String(row.durability || 'long') === 'ephemeral') { excluded.ephemeral += 1; continue; }
|
|
404
405
|
const hit = Number(row.hit_count ?? 0);
|
|
405
|
-
|
|
406
|
+
// ⚠️ 2026-10-06:**类目名(kind)也参与关键词命中** —— 修前只匹配 `row.text`,于是查询里的
|
|
407
|
+
// 类目词打不中它们**自己的**条目:实测搜「纪律」时那 18 条纪律一条都加不上分(`kw` 恒为 0),
|
|
408
|
+
// 开场里只靠权重挤进 2 席 —— 同族缺陷对"承诺 / 决定 / 关系"一样成立。
|
|
409
|
+
// kind 是 2 字精确词、且 `kwHitInText` 只做**连续子串**匹配(不共享 n-gram)
|
|
410
|
+
// ⇒ 不会重蹈它上面那段注释里"3-gram 遍地都是、几乎任何查询都打满"的覆辙。
|
|
411
|
+
const kw = kwHitInText(o.keywords || [], row.text + ' ' + String(row.kind || ''));
|
|
406
412
|
const t = toEpochMs(row.at || row.created_at);
|
|
407
413
|
const recency = decayWeight(t === null ? 0 : Math.max(0, (now - t) / DAY_MS), DEEP_HALF_LIFE);
|
|
408
414
|
// 硬度参与排序(不只是出局):`durable` 抬 0.6,与 `long` 拉开约 7 倍 ⇒ 长期原则/承诺/关系
|
|
@@ -439,7 +445,7 @@ export function selectL1(store, opts) {
|
|
|
439
445
|
.filter((x) => !x.pinned)
|
|
440
446
|
.sort((a, b) => b.score - a.score
|
|
441
447
|
|| String(b.row.at || b.row.created_at || '').localeCompare(String(a.row.at || a.row.created_at || '')));
|
|
442
|
-
// **同源限额**(2026-10-04
|
|
448
|
+
// **同源限额**(2026-10-04 用户实测:「会话内的、临时的结论会被灌入」):
|
|
443
449
|
// 同一次会话最多进 `MAX_PER_CONV` 条 —— 否则一次"高产出"的会话能把整个开场占满
|
|
444
450
|
// (实测某次生图会话有 13 条,开场 8 条里它一家全占)。同源被挡下的**进落选者名单**
|
|
445
451
|
// (`reason='conv'`),这样"它为什么没进"仍可查,而不是凭空消失。
|
|
@@ -471,7 +477,7 @@ export function selectL1(store, opts) {
|
|
|
471
477
|
const keepPinned = ranked.filter((x) => x.pinned);
|
|
472
478
|
const restRanked = ranked.filter((x) => !x.pinned);
|
|
473
479
|
const restQuota = Math.max(0, max - keepPinned.length);
|
|
474
|
-
// **事实席位**(
|
|
480
|
+
// **事实席位**(用户 2026-10-04:「8 条也可以考虑改 10 条,其中 2 条作为最硬的事实的席位」)——
|
|
475
481
|
// 动机是他看到的偏斜:承诺/关系优先之后,「我在什么环境里干活」这类**稳定事实**因为类别分为 0
|
|
476
482
|
// 而永远进不来;但它们确实是"接下来要干什么"的前提。⇒ 10 个位置里留 2 个给**最硬的事实**
|
|
477
483
|
// (`durable` 的「事实」类),既不挤占"关于人"的主体(那 8 个),也不让环境事实缺席。
|
|
@@ -564,7 +570,7 @@ export function firstSentence(text, cap = SUMMARY_CHARS_DEFAULT) {
|
|
|
564
570
|
const cut = Math.max(slice.lastIndexOf(','), slice.lastIndexOf(','), slice.lastIndexOf('、'));
|
|
565
571
|
head = (cut >= Math.floor(limit * 0.6) ? slice.slice(0, cut) : slice) + '…';
|
|
566
572
|
}
|
|
567
|
-
return head;
|
|
573
|
+
return wellFormed(head);
|
|
568
574
|
}
|
|
569
575
|
|
|
570
576
|
/**
|
|
@@ -604,7 +610,7 @@ function lineFor(row, { maxChars = 0, summaryChars = SUMMARY_CHARS_DEFAULT, self
|
|
|
604
610
|
// DSH 会话 id 一律以 "session-" 开头,取前 8 位会全都一样 —— 这时取前缀之后的那一段才可区分
|
|
605
611
|
const cid = String(row.conv_id || '');
|
|
606
612
|
const short = cid.startsWith('session-') ? cid.slice(8, 16) : cid.slice(0, 8);
|
|
607
|
-
return `- ${tags} ${date ? `(${date})` : ''}${out ? ' ' + out : ''} (来源: ${srcName}/${short})
|
|
613
|
+
return wellFormed(`- ${tags} ${date ? `(${date})` : ''}${out ? ' ' + out : ''} (来源: ${srcName}/${short})`);
|
|
608
614
|
}
|
|
609
615
|
|
|
610
616
|
/** 1.6 步 5:一条**深层库条目**渲染成一行(取代 `lineFor` 的概述两段式 `标题 —— 摘要首句`)。
|
|
@@ -620,7 +626,7 @@ function deepLine(row, maxChars = 0) {
|
|
|
620
626
|
// 预算裁剪是"装不下就停"(见下面的 for),一条超长条目会把排在它后面的全卡死。
|
|
621
627
|
const cap = Number(maxChars) > 0 ? Math.max(60, Number(maxChars)) : 0;
|
|
622
628
|
if (cap && text.length > cap) text = text.slice(0, cap - 1) + '…';
|
|
623
|
-
return kind ? `- [${kind}] ${text}` : `- ${text}
|
|
629
|
+
return wellFormed(kind ? `- [${kind}] ${text}` : `- ${text}`);
|
|
624
630
|
}
|
|
625
631
|
|
|
626
632
|
// L1 视角框(1.6 · 第一批 B)—— 修「代词混乱」BUG:落点一处、覆盖全部历史条目、零动库。
|
|
@@ -634,7 +640,7 @@ const L1_FRAME_CALLNAME_FALLBACK = '主人';
|
|
|
634
640
|
/**
|
|
635
641
|
* 视角框的称呼 —— **跟随 `persona.userTitle`**,不硬编码。
|
|
636
642
|
*
|
|
637
|
-
* 为什么走配置而不是写死(2026-10-04
|
|
643
|
+
* 为什么走配置而不是写死(2026-10-04 用户的判断:「提早换更合适」):
|
|
638
644
|
* - `dsh-ling` 是要发 npm 的通用包,**把某个用户的名字写死会污染公开件**;
|
|
639
645
|
* - `userTitle` 本来就是**主人自己可编辑**的字段(`persona.js`:「空 = 用"你"」),
|
|
640
646
|
* 走它 = 本机显示他设的称呼、别人的实例显示他们自己的,**本机效果不打折**;
|