@modusensus/dsh-mneme 0.7.13 → 0.7.15
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/README.en.md +2 -2
- package/README.md +7 -5
- package/lib/api.js +338 -7
- package/lib/client.js +1238 -204
- package/lib/index.js +25 -2
- package/lib/mirror.js +93 -59
- package/lib/service.js +2 -0
- package/lib/settings.js +174 -0
- package/lib/store.js +77 -4
- package/lib/summarize.js +8 -8
- package/package.json +12 -1
- package/scripts/check-sync.js +42 -0
- package/src/api.js +338 -7
- package/src/index.js +25 -2
- package/src/mirror.js +93 -59
- package/src/service.js +2 -0
- package/src/settings.js +174 -0
- package/src/store.js +77 -4
- package/src/summarize.js +8 -8
- package/test/api.test.js +485 -2
- package/test/client.test.js +82 -52
- package/test/helpers/peer-worker.mjs +17 -2
- package/test/peer-blockers.test.js +9 -1
- package/test/settings.test.js +136 -0
- package/test/summarize.test.js +28 -0
package/src/settings.js
CHANGED
|
@@ -31,6 +31,155 @@ function parseList(raw) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// --- feature flags(功能开关)白名单 -----------------------------------------
|
|
35
|
+
// 面板可逐项开关的后端能力。设计约束:
|
|
36
|
+
// 1. 键名与类型必须和 config.js schema 同名同型,这份白名单是唯一校验源
|
|
37
|
+
// (api.js 复用它计算 effective),schema 增删能力键时要同步改这里。
|
|
38
|
+
// 2. 持久化(kv "feature_flags")只落白名单键;读到未知键、类型损坏或越界的
|
|
39
|
+
// 值一律丢弃而不是报错——kv 会残留旧版本写入的键,读路径必须向前兼容。
|
|
40
|
+
// 3. 写入是逐键校验的合并写:未知键/类型/范围不符抛 TypeError(消息含键名,
|
|
41
|
+
// 供 API 透传给前端定位),校验不通过不落库,坏值永远进不了 kv。
|
|
42
|
+
const FEATURE_FLAG_BOOLEANS = [
|
|
43
|
+
"autoInject",
|
|
44
|
+
"autoSummarize",
|
|
45
|
+
"hotMemoryEnabled",
|
|
46
|
+
"entityExtractionEnabled",
|
|
47
|
+
"codingRetrospect",
|
|
48
|
+
"autoDream",
|
|
49
|
+
"sleepModeEnabled",
|
|
50
|
+
"hybridInject",
|
|
51
|
+
"selectiveInjectEnabled",
|
|
52
|
+
"searchSemanticDedup",
|
|
53
|
+
"rerankEnabled",
|
|
54
|
+
"adaptiveThresholdEnabled",
|
|
55
|
+
"reflectionUpdateEnabled",
|
|
56
|
+
"reflectionFailureTracking",
|
|
57
|
+
"bm25SearchEnabled",
|
|
58
|
+
"conflictFreezeEnabled",
|
|
59
|
+
"trustEpistemicWeighting",
|
|
60
|
+
// 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
|
|
61
|
+
// 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
|
|
62
|
+
// 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
|
|
63
|
+
"memoryQualityFilter.enabled",
|
|
64
|
+
"llmAudit.enabled"
|
|
65
|
+
];
|
|
66
|
+
// 整数开关的闭区间,与 config.js 里 z.natural().min().max() 对齐。
|
|
67
|
+
const FEATURE_FLAG_INT_RANGES = {
|
|
68
|
+
distillRateLimitIntervalMs: [0, 60000],
|
|
69
|
+
distillRateLimitRetries: [0, 10],
|
|
70
|
+
distillRateLimitBaseDelayMs: [100, 60000],
|
|
71
|
+
distillMaxChars: [1000, 200000],
|
|
72
|
+
codingBoostFactor: [1, 5]
|
|
73
|
+
};
|
|
74
|
+
// 自由字符串开关(与 config.js 的 z.string() 同名同型):trim 后 ≤200 字符,
|
|
75
|
+
// 空串合法(= 跟随主对话模型/默认路径,面板显示 placeholder)。
|
|
76
|
+
const FEATURE_FLAG_STRINGS = [
|
|
77
|
+
"dreamProvider",
|
|
78
|
+
"dreamModel",
|
|
79
|
+
"localEmbedModel",
|
|
80
|
+
"ollamaModel"
|
|
81
|
+
];
|
|
82
|
+
// URL 字符串开关:trim 后必须为空或合法 http/https URL(new URL() 校验协议,
|
|
83
|
+
// 拒绝其余协议——这是 SSRF 防线的一部分)。
|
|
84
|
+
const FEATURE_FLAG_URLS = ["ollamaBaseUrl"];
|
|
85
|
+
// 枚举开关(与 config.js 的 z.union(z.const(...)) 对齐):仅允许列出的值。
|
|
86
|
+
const FEATURE_FLAG_ENUMS = {
|
|
87
|
+
embedProvider: ["openai", "local", "ollama"]
|
|
88
|
+
};
|
|
89
|
+
const FEATURE_FLAG_STRING_MAX = 200;
|
|
90
|
+
|
|
91
|
+
// 供 api.js 复用同一份白名单(effective 只在白名单键上计算)。
|
|
92
|
+
export const FEATURE_FLAG_SPEC = {
|
|
93
|
+
booleans: FEATURE_FLAG_BOOLEANS,
|
|
94
|
+
ints: FEATURE_FLAG_INT_RANGES,
|
|
95
|
+
strings: FEATURE_FLAG_STRINGS,
|
|
96
|
+
urls: FEATURE_FLAG_URLS,
|
|
97
|
+
enums: FEATURE_FLAG_ENUMS
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** ollamaBaseUrl 的协议白名单:只接受 http/https(SSRF 防线的一部分)。 */
|
|
101
|
+
function isHttpUrl(value) {
|
|
102
|
+
try {
|
|
103
|
+
const protocol = new URL(value).protocol;
|
|
104
|
+
return protocol === "http:" || protocol === "https:";
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 校验单个开关值;不合法抛 TypeError(消息含键名)。 */
|
|
111
|
+
function validateFlag(key, value) {
|
|
112
|
+
if (FEATURE_FLAG_BOOLEANS.includes(key)) {
|
|
113
|
+
if (typeof value !== "boolean") {
|
|
114
|
+
throw new TypeError(`feature flag "${key}" must be a boolean`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
const range = FEATURE_FLAG_INT_RANGES[key];
|
|
119
|
+
if (range) {
|
|
120
|
+
const [min, max] = range;
|
|
121
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
122
|
+
throw new TypeError(`feature flag "${key}" must be an integer in [${min}, ${max}]`);
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
if (FEATURE_FLAG_STRINGS.includes(key)) {
|
|
127
|
+
if (typeof value !== "string") {
|
|
128
|
+
throw new TypeError(`feature flag "${key}" must be a string`);
|
|
129
|
+
}
|
|
130
|
+
const trimmed = value.trim();
|
|
131
|
+
if (trimmed.length > FEATURE_FLAG_STRING_MAX) {
|
|
132
|
+
throw new TypeError(`feature flag "${key}" must be at most ${FEATURE_FLAG_STRING_MAX} characters`);
|
|
133
|
+
}
|
|
134
|
+
return trimmed; // 空串合法 = 跟随默认
|
|
135
|
+
}
|
|
136
|
+
if (FEATURE_FLAG_URLS.includes(key)) {
|
|
137
|
+
if (typeof value !== "string") {
|
|
138
|
+
throw new TypeError(`feature flag "${key}" must be a string`);
|
|
139
|
+
}
|
|
140
|
+
const trimmed = value.trim();
|
|
141
|
+
if (trimmed && !isHttpUrl(trimmed)) {
|
|
142
|
+
throw new TypeError(`feature flag "${key}" must be empty or a valid http(s) URL`);
|
|
143
|
+
}
|
|
144
|
+
return trimmed; // 空串合法 = 跟随默认
|
|
145
|
+
}
|
|
146
|
+
const allowed = FEATURE_FLAG_ENUMS[key];
|
|
147
|
+
if (allowed) {
|
|
148
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
149
|
+
throw new TypeError(`feature flag "${key}" must be one of: ${allowed.join(", ")}`);
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
throw new TypeError(`unknown feature flag "${key}"`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 清洗已存的 feature_flags 对象:只保留白名单键,类型/范围损坏的键丢弃。 */
|
|
157
|
+
function sanitizeFlags(raw) {
|
|
158
|
+
const out = {};
|
|
159
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return out;
|
|
160
|
+
for (const key of FEATURE_FLAG_BOOLEANS) {
|
|
161
|
+
if (typeof raw[key] === "boolean") out[key] = raw[key];
|
|
162
|
+
}
|
|
163
|
+
for (const [key, [min, max]] of Object.entries(FEATURE_FLAG_INT_RANGES)) {
|
|
164
|
+
if (Number.isInteger(raw[key]) && raw[key] >= min && raw[key] <= max) out[key] = raw[key];
|
|
165
|
+
}
|
|
166
|
+
for (const key of FEATURE_FLAG_STRINGS) {
|
|
167
|
+
if (typeof raw[key] === "string" && raw[key].trim().length <= FEATURE_FLAG_STRING_MAX) {
|
|
168
|
+
out[key] = raw[key].trim();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const key of FEATURE_FLAG_URLS) {
|
|
172
|
+
if (typeof raw[key] === "string") {
|
|
173
|
+
const trimmed = raw[key].trim();
|
|
174
|
+
if (!trimmed || isHttpUrl(trimmed)) out[key] = trimmed;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const [key, allowed] of Object.entries(FEATURE_FLAG_ENUMS)) {
|
|
178
|
+
if (allowed.includes(raw[key])) out[key] = raw[key];
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
34
183
|
export function createSettings(db) {
|
|
35
184
|
db.exec(SCHEMA);
|
|
36
185
|
|
|
@@ -177,6 +326,31 @@ export function createSettings(db) {
|
|
|
177
326
|
},
|
|
178
327
|
setPanelMode(mode) {
|
|
179
328
|
setSetting("panel_mode", mode === "light" ? "light" : "standard");
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Feature flags(kv "feature_flags"):面板对后端能力的显式覆盖。读取只
|
|
333
|
+
* 返回白名单内的合法键(默认 {}),写入是逐键校验后的合并持久化。
|
|
334
|
+
*/
|
|
335
|
+
getFeatureFlags() {
|
|
336
|
+
const raw = getSetting("feature_flags");
|
|
337
|
+
if (!raw) return {};
|
|
338
|
+
try {
|
|
339
|
+
return sanitizeFlags(JSON.parse(raw));
|
|
340
|
+
} catch {
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
setFeatureFlags(patch) {
|
|
345
|
+
if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
|
|
346
|
+
throw new TypeError("feature flags patch must be a plain object");
|
|
347
|
+
}
|
|
348
|
+
const merged = this.getFeatureFlags();
|
|
349
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
350
|
+
merged[key] = validateFlag(key, value);
|
|
351
|
+
}
|
|
352
|
+
setSetting("feature_flags", JSON.stringify(merged));
|
|
353
|
+
return merged;
|
|
180
354
|
}
|
|
181
355
|
};
|
|
182
356
|
}
|
package/src/store.js
CHANGED
|
@@ -303,6 +303,25 @@ function escapeLike(q) {
|
|
|
303
303
|
return q.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// 日期过滤参数归一化(list/count 共用,保证分页 total 与行同过滤):接受 ISO
|
|
307
|
+
// 日期("2026-09-01")或完整时间戳,返回闭区间的 UTC ISO 边界。date-only 的
|
|
308
|
+
// updatedFrom 按当天 00:00:00.000Z 起、updatedTo 按当天 23:59:59.999Z 收;非
|
|
309
|
+
// 法值一律返回 undefined → 不进 WHERE(忽略而非报错:面板传坏参数时宁可放宽
|
|
310
|
+
// 过滤也不要白屏)。updated_at 列是 toISOString 产生的 UTC "Z" 字符串,字典
|
|
311
|
+
// 序与时间序一致,SQL 里可直接比较。
|
|
312
|
+
function updatedAtBounds(updatedFrom, updatedTo) {
|
|
313
|
+
const norm = (raw, endOfDay) => {
|
|
314
|
+
if (typeof raw !== "string" || !raw.trim()) return undefined;
|
|
315
|
+
const s = raw.trim();
|
|
316
|
+
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(s);
|
|
317
|
+
const ms = Date.parse(dateOnly ? `${s}T00:00:00.000Z` : s);
|
|
318
|
+
if (Number.isNaN(ms)) return undefined;
|
|
319
|
+
if (dateOnly && endOfDay) return `${s}T23:59:59.999Z`;
|
|
320
|
+
return new Date(ms).toISOString();
|
|
321
|
+
};
|
|
322
|
+
return { from: norm(updatedFrom, false), to: norm(updatedTo, true) };
|
|
323
|
+
}
|
|
324
|
+
|
|
306
325
|
function parseTags(raw) {
|
|
307
326
|
try {
|
|
308
327
|
const arr = JSON.parse(raw);
|
|
@@ -618,7 +637,7 @@ export function createStore(path) {
|
|
|
618
637
|
return ts;
|
|
619
638
|
}
|
|
620
639
|
|
|
621
|
-
function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false } = {}) {
|
|
640
|
+
function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false, onlyArchived = false, updatedFrom = null, updatedTo = null } = {}) {
|
|
622
641
|
const clauses = [];
|
|
623
642
|
const params = [];
|
|
624
643
|
if (type !== undefined) {
|
|
@@ -634,10 +653,24 @@ export function createStore(path) {
|
|
|
634
653
|
clauses.push("source = ?");
|
|
635
654
|
params.push(source);
|
|
636
655
|
}
|
|
656
|
+
// updated_at 闭区间:与 list() 共用 updatedAtBounds 归一化,非法值被忽略
|
|
657
|
+
// (不进 WHERE),total 才能和行保持同过滤。
|
|
658
|
+
const bounds = updatedAtBounds(updatedFrom, updatedTo);
|
|
659
|
+
if (bounds.from) {
|
|
660
|
+
clauses.push("updated_at >= ?");
|
|
661
|
+
params.push(bounds.from);
|
|
662
|
+
}
|
|
663
|
+
if (bounds.to) {
|
|
664
|
+
clauses.push("updated_at <= ?");
|
|
665
|
+
params.push(bounds.to);
|
|
666
|
+
}
|
|
637
667
|
if (!includeForgotten) {
|
|
638
668
|
clauses.push("forgotten = 0");
|
|
639
669
|
}
|
|
640
|
-
|
|
670
|
+
// 与 list() 同过滤:total 才能和归档列表的行保持一致。
|
|
671
|
+
if (onlyArchived) {
|
|
672
|
+
clauses.push("archived = 1");
|
|
673
|
+
} else if (!includeArchived) {
|
|
641
674
|
clauses.push("archived = 0");
|
|
642
675
|
}
|
|
643
676
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
@@ -891,7 +924,7 @@ export function createStore(path) {
|
|
|
891
924
|
return rows.map(toRow);
|
|
892
925
|
}
|
|
893
926
|
|
|
894
|
-
function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, minImportance = null, source = null } = {}) {
|
|
927
|
+
function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, onlyArchived = false, minImportance = null, source = null, updatedFrom = null, updatedTo = null } = {}) {
|
|
895
928
|
const clauses = [];
|
|
896
929
|
const params = [];
|
|
897
930
|
if (type) {
|
|
@@ -908,10 +941,25 @@ export function createStore(path) {
|
|
|
908
941
|
clauses.push("source = ?");
|
|
909
942
|
params.push(source);
|
|
910
943
|
}
|
|
944
|
+
// Optional updated_at closed range (date-only "to" is normalized to the
|
|
945
|
+
// end of that day). Same helper as count() so total matches the rows.
|
|
946
|
+
const bounds = updatedAtBounds(updatedFrom, updatedTo);
|
|
947
|
+
if (bounds.from) {
|
|
948
|
+
clauses.push("updated_at >= ?");
|
|
949
|
+
params.push(bounds.from);
|
|
950
|
+
}
|
|
951
|
+
if (bounds.to) {
|
|
952
|
+
clauses.push("updated_at <= ?");
|
|
953
|
+
params.push(bounds.to);
|
|
954
|
+
}
|
|
911
955
|
if (!includeForgotten) {
|
|
912
956
|
clauses.push("forgotten = 0");
|
|
913
957
|
}
|
|
914
|
-
|
|
958
|
+
// onlyArchived:只看归档(状态页的归档列表用);与 includeArchived(含
|
|
959
|
+
// 归档混看)互斥,同时给时归档视图优先。
|
|
960
|
+
if (onlyArchived) {
|
|
961
|
+
clauses.push("archived = 1");
|
|
962
|
+
} else if (!includeArchived) {
|
|
915
963
|
clauses.push("archived = 0");
|
|
916
964
|
}
|
|
917
965
|
const { limit: lim, offset: off } = sanitizePage(limit, offset, 50);
|
|
@@ -1598,6 +1646,30 @@ export function createStore(path) {
|
|
|
1598
1646
|
).all(memoryId).map(toAttr);
|
|
1599
1647
|
}
|
|
1600
1648
|
|
|
1649
|
+
/**
|
|
1650
|
+
* 一条记忆关联到的实体(记忆详情侧栏用):entity_attrs.memory_id 反查实体,
|
|
1651
|
+
* 一条 JOIN 完成。同一记忆对同一实体的多次提及(多条 attr 行)按 name 去重,
|
|
1652
|
+
* 每个实体只出现一次,按提及次数降序。只取 name/type——attr 详情走
|
|
1653
|
+
* entity-attrs 端点。无关联(或记忆不存在)返回空数组。
|
|
1654
|
+
*/
|
|
1655
|
+
function entitiesForMemory(memoryId) {
|
|
1656
|
+
const rows = db.prepare(
|
|
1657
|
+
`SELECT e.id, e.name, e.type, e.mention_count, e.last_seen
|
|
1658
|
+
FROM entity_attrs ea JOIN entities e ON e.id = ea.entity_id
|
|
1659
|
+
WHERE ea.memory_id = ?
|
|
1660
|
+
GROUP BY e.id
|
|
1661
|
+
ORDER BY e.mention_count DESC, e.last_seen DESC, e.name ASC`
|
|
1662
|
+
).all(memoryId ?? "");
|
|
1663
|
+
const seen = new Set();
|
|
1664
|
+
const out = [];
|
|
1665
|
+
for (const row of rows) {
|
|
1666
|
+
if (seen.has(row.name)) continue;
|
|
1667
|
+
seen.add(row.name);
|
|
1668
|
+
out.push({ name: row.name, type: row.type ?? null });
|
|
1669
|
+
}
|
|
1670
|
+
return out;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1601
1673
|
/**
|
|
1602
1674
|
* Memories carrying a currently-valid attr matching key=value (deduped).
|
|
1603
1675
|
* When value is empty/undefined, the attr_value filter is dropped and every
|
|
@@ -1941,6 +2013,7 @@ export function createStore(path) {
|
|
|
1941
2013
|
getCurrentAttrs,
|
|
1942
2014
|
getAttrHistory,
|
|
1943
2015
|
getAttrsByMemory,
|
|
2016
|
+
entitiesForMemory,
|
|
1944
2017
|
findMemoriesByAttr,
|
|
1945
2018
|
saveRelation,
|
|
1946
2019
|
migrateAttrsToMemory,
|
package/src/summarize.js
CHANGED
|
@@ -8,7 +8,7 @@ const SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内
|
|
|
8
8
|
// 编码记忆蒸馏 prompt(codingRetrospect 开启时启用):在通用记忆之外,额外提取
|
|
9
9
|
// 三类编码专属记忆,专治重复踩坑 / 遗忘被否决方案 / 丢失工程约束。字段仍沿用
|
|
10
10
|
// title/content 单列结构(store 无结构化字段),信息浓缩进 content。
|
|
11
|
-
const CODING_SUMMARY_PROMPT =
|
|
11
|
+
const CODING_SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内容(含用户输入、助手回答、工具调用与结果),提炼值得跨会话记住的原子记忆。
|
|
12
12
|
原子记忆原则:每条记忆只装一个独立事实/偏好/决策,短小、自带完整上下文(把数字、报错信息、命令、路径、结论等原始细节保留在 content 里,不要抽象概括);宁可拆成多条也绝不合并丢细节。信息量一般提 2-4 条,信息密集的对话可提 4-8 条。
|
|
13
13
|
只输出 JSON 数组,每项形如 {"type":"preference|project|decision|history|rejected_solution|pitfall|constraint","title":"简短标题","content":"保留原始细节的一句话","importance":1-5}。
|
|
14
14
|
若对话涉及编码/调试,可额外提取编码类记忆:
|
|
@@ -91,18 +91,22 @@ function toProtocolChunk(chunk) {
|
|
|
91
91
|
// check below.
|
|
92
92
|
//
|
|
93
93
|
// codingRetrospect: the distill context is the FULL turn transcript —
|
|
94
|
-
// user prompts plus assistant
|
|
94
|
+
// user prompts plus assistant public replies, tool calls + results and code
|
|
95
95
|
// dispatch output — so the summarizer can see tool errors and extract pitfall
|
|
96
96
|
// root causes, not just what the user typed. The same filtering stays: only
|
|
97
97
|
// source.kind === "user" prompts enter (plugin/machine content is excluded).
|
|
98
98
|
// The result is a single text transcript passed to the LLM as one user message
|
|
99
99
|
// (SUMMARY_PROMPT already says "根据下面的会话内容").
|
|
100
|
+
//
|
|
101
|
+
// Privacy: assistant `reasoning` (private thought) blocks are deliberately NOT
|
|
102
|
+
// collected — distilled memories must never sink private reasoning chains.
|
|
103
|
+
// Only public text blocks (type "text") reach the summarizer.
|
|
100
104
|
function collectMessages(session, maxChars = 8000) {
|
|
101
105
|
// DSH 0.1.2-rc.1 起 Session 改用 snapshotEvents(),兼容旧版 .events
|
|
102
106
|
const events = session.snapshotEvents?.() ?? session.events ?? [];
|
|
103
107
|
const lines = [];
|
|
104
108
|
// 兼容严格形状 [{type:"text",text}] 与宽松形状 ["字符串", ...](lib-smoke 用例
|
|
105
|
-
//
|
|
109
|
+
// 直接传字符串数组)。只取公开文本块;reasoning 私有推理块不进蒸馏上下文。
|
|
106
110
|
const textOf = (content) => {
|
|
107
111
|
if (typeof content === "string") return content;
|
|
108
112
|
if (!Array.isArray(content)) return "";
|
|
@@ -127,11 +131,7 @@ function collectMessages(session, maxChars = 8000) {
|
|
|
127
131
|
const blocks = Array.isArray(msg?.content) ? msg.content : [];
|
|
128
132
|
const text = textOf(blocks);
|
|
129
133
|
if (text.trim()) lines.push(`助手:${text}`);
|
|
130
|
-
|
|
131
|
-
.map((b) => (typeof b === "string" ? "" : (b && b.type === "reasoning" && typeof b.text === "string" ? b.text : "")))
|
|
132
|
-
.filter((s) => s)
|
|
133
|
-
.join("\n");
|
|
134
|
-
if (thinking.trim()) lines.push(`助手思考:${thinking}`);
|
|
134
|
+
// 私有推理块(reasoning)刻意不采集:蒸馏记忆不得沉淀模型私有思考链。
|
|
135
135
|
break;
|
|
136
136
|
}
|
|
137
137
|
case "tool/call": {
|