dsh-layered-memory 0.8.2 → 0.8.3
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 +7 -4
- package/README.md +12 -3
- package/dist/client.js +368 -104
- package/dist/config.d.ts +4 -4
- package/dist/config.js +4 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -2
- package/dist/llm.d.ts +20 -0
- package/dist/llm.js +82 -6
- package/dist/pipeline/runner.js +6 -2
- package/dist/settings.d.ts +2 -1
- package/dist/settings.js +2 -2
- package/dist/stats.js +46 -12
- package/dist/store/download-queue.d.ts +3 -0
- package/dist/store/download-queue.js +22 -2
- package/dist/store/embedding-source.d.ts +1 -1
- package/dist/store/embedding-source.js +3 -3
- package/dist/store/io.d.ts +10 -2
- package/dist/store/io.js +28 -6
- package/dist/store/l0.js +7 -2
- package/dist/store/l1.js +17 -7
- package/dist/store/local-embedding.d.ts +3 -1
- package/dist/store/local-embedding.js +5 -2
- package/dist/store/runtime-installer.d.ts +8 -1
- package/dist/store/runtime-installer.js +34 -5
- package/dist/store/scenes.d.ts +2 -1
- package/dist/store/scenes.js +10 -1
- package/dist/store/sqlite.js +3 -3
- package/dist/tools/index.js +12 -2
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -198,7 +198,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
198
198
|
provider: Schema<string, string>;
|
|
199
199
|
model: Schema<string, string>;
|
|
200
200
|
maxTokens: Schema<number, number>;
|
|
201
|
-
reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
201
|
+
reasoningEffort: Schema<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
202
202
|
temperature: Schema<number, number>;
|
|
203
203
|
maxInputChars: Schema<number, number>;
|
|
204
204
|
timeoutMs: Schema<number, number>;
|
|
@@ -206,7 +206,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
206
206
|
provider: Schema<string, string>;
|
|
207
207
|
model: Schema<string, string>;
|
|
208
208
|
maxTokens: Schema<number, number>;
|
|
209
|
-
reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
209
|
+
reasoningEffort: Schema<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
210
210
|
temperature: Schema<number, number>;
|
|
211
211
|
maxInputChars: Schema<number, number>;
|
|
212
212
|
timeoutMs: Schema<number, number>;
|
|
@@ -303,7 +303,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
303
303
|
provider: Schema<string, string>;
|
|
304
304
|
model: Schema<string, string>;
|
|
305
305
|
maxTokens: Schema<number, number>;
|
|
306
|
-
reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
306
|
+
reasoningEffort: Schema<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
307
307
|
temperature: Schema<number, number>;
|
|
308
308
|
maxInputChars: Schema<number, number>;
|
|
309
309
|
timeoutMs: Schema<number, number>;
|
|
@@ -311,7 +311,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
311
311
|
provider: Schema<string, string>;
|
|
312
312
|
model: Schema<string, string>;
|
|
313
313
|
maxTokens: Schema<number, number>;
|
|
314
|
-
reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
314
|
+
reasoningEffort: Schema<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
315
315
|
temperature: Schema<number, number>;
|
|
316
316
|
maxInputChars: Schema<number, number>;
|
|
317
317
|
timeoutMs: Schema<number, number>;
|
package/dist/config.js
CHANGED
|
@@ -61,9 +61,10 @@ export const memorySchema = Schema.object({
|
|
|
61
61
|
// 推理模型(如 v4-flash)的 reasoning 计入输出预算:预算不足会被思考吃光导致正文 0 字符。
|
|
62
62
|
// 0.8.0 起各蒸馏层显式传分层预算(见 llm.ts LAYER_MAX_TOKENS_*),本值为未分层调用的兜底总闸
|
|
63
63
|
maxTokens: Schema.number().min(1024).max(1_000_000).default(65_536),
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
64
|
+
// 蒸馏思考档位:'' = 自动(按模型能力解析:模型默认档 → high,见 llm.ts decideSendableEffort);
|
|
65
|
+
// 显式值仅在该模型声明支持时发送(跨供应商 effort 词汇表不同:deepseek 认 'off',
|
|
66
|
+
// openai 系是 'none',未声明档位的模型不传)。旧默认 'off' 在非 deepseek 模型上必炸(400/本地拒绝)
|
|
67
|
+
reasoningEffort: Schema.union(['', 'off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']).default(''),
|
|
67
68
|
temperature: Schema.number().min(0).max(2).default(0.3),
|
|
68
69
|
// 模型上下文 1M token,日常压到 ~700k 使用(中文按 1 字≈1 token 保守折算)
|
|
69
70
|
maxInputChars: Schema.number().min(1000).max(1_000_000).default(700_000),
|
package/dist/index.d.ts
CHANGED
|
@@ -99,7 +99,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
99
99
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
100
100
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
101
101
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
102
|
-
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
102
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
103
103
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
104
104
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
105
105
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
@@ -107,7 +107,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
107
107
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
108
108
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
109
109
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
110
|
-
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
110
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
111
111
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
112
112
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
113
113
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
@@ -204,7 +204,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
204
204
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
205
205
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
206
206
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
207
|
-
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
207
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
208
208
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
209
209
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
210
210
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
@@ -212,7 +212,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
212
212
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
213
213
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
214
214
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
215
|
-
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
215
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
216
216
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
217
217
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
218
218
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import { SessionModeStore } from './store/session-modes.js';
|
|
|
38
38
|
import { StateStore } from './store/state.js';
|
|
39
39
|
import { registerMemoryTools } from './tools/index.js';
|
|
40
40
|
import { errDetail, withFileLog } from './util/filelog.js';
|
|
41
|
-
import { resolveModelRoute } from './llm.js';
|
|
41
|
+
import { resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
42
42
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
43
43
|
export const name = 'dsh-memory-plugin';
|
|
44
44
|
/** 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。 */
|
|
@@ -60,6 +60,8 @@ export async function apply(ctx, config) {
|
|
|
60
60
|
// dsh 宿主无持久化日志,镜像 info+ 到数据目录 memory.log 供蒸馏问题诊断
|
|
61
61
|
const fileLogger = withFileLog(dataDir, logger);
|
|
62
62
|
logger = fileLogger;
|
|
63
|
+
// 供应商拓扑变化(增删/改配置)→ 思考档位能力缓存失效,下次调用重新探询
|
|
64
|
+
ctx.on('llm/adapters-updated', () => invalidateEffortCache());
|
|
63
65
|
// 存储初始化失败只降级(禁用捕获/蒸馏),绝不拖垮宿主——
|
|
64
66
|
// 记忆是增强能力,数据目录不可写时 dsh 本体必须照常启动。
|
|
65
67
|
let storageOk = true;
|
|
@@ -92,7 +94,7 @@ export async function apply(ctx, config) {
|
|
|
92
94
|
logger,
|
|
93
95
|
proxy: config.embedding.proxy,
|
|
94
96
|
});
|
|
95
|
-
const makeLocalService = makeLocalServiceFactory(installer, downloader, logger);
|
|
97
|
+
const makeLocalService = makeLocalServiceFactory(installer, downloader, logger, config.embedding.maxInputChars);
|
|
96
98
|
let initial = { svc: new NoopEmbeddingService(), dims: 0 };
|
|
97
99
|
/** 管理器引用:启动重嵌链/backfill 闭包在运行期解引用(声明早于创建避免 TDZ)。 */
|
|
98
100
|
let embedManagerRef;
|
package/dist/llm.d.ts
CHANGED
|
@@ -46,6 +46,26 @@ export declare function resolveModelRoute(ctx: Context, cfg: MemoryConfig): Prom
|
|
|
46
46
|
provider: string;
|
|
47
47
|
model: string;
|
|
48
48
|
}>;
|
|
49
|
+
export interface ModelEffortInfo {
|
|
50
|
+
/** 模型可设置的思考档位 id(适配器声明;空 = 未声明/不可设置) */
|
|
51
|
+
efforts: string[];
|
|
52
|
+
/** 适配器配置的默认档位(省略 effort 时的请求值) */
|
|
53
|
+
defaultEffort?: string;
|
|
54
|
+
}
|
|
55
|
+
/** 清空能力缓存(llm/adapters-updated 时调用:供应商增删/改配置后重新探询)。 */
|
|
56
|
+
export declare function invalidateEffortCache(): void;
|
|
57
|
+
/** 探询某模型的思考档位能力;失败返回 null(调用方保持旧发送行为,不改判)。 */
|
|
58
|
+
export declare function resolveModelEfforts(ctx: Context, provider: string, model: string): Promise<ModelEffortInfo | null>;
|
|
59
|
+
export type EffortDecisionReason = 'supported' | 'auto-default' | 'auto-high' | 'alias-none' | 'unsupported' | 'no-efforts' | 'no-capability';
|
|
60
|
+
export interface EffortDecision {
|
|
61
|
+
/** 实际发送的档位;'' = 不发送(跟随模型默认) */
|
|
62
|
+
effort: string;
|
|
63
|
+
reason: EffortDecisionReason;
|
|
64
|
+
}
|
|
65
|
+
/** 纯决策:配置档位 + 模型能力 → 实际发送值(callLLM 与 settings-get 共用)。 */
|
|
66
|
+
export declare function decideSendableEffort(cap: ModelEffortInfo | null, cfgEffort: string): EffortDecision;
|
|
67
|
+
/** 探询 + 决策 + 一次性告警(不支持/未声明时提示降级,不刷屏)。 */
|
|
68
|
+
export declare function planDistillEffort(ctx: Context, provider: string, model: string, cfgEffort: string, logger?: MemoryLogger): Promise<EffortDecision>;
|
|
49
69
|
/**
|
|
50
70
|
* 一次完整蒸馏调用:流式收集文本,返回最终字符串。
|
|
51
71
|
* 失败(error/aborted finish)抛错,由调用方兜底。
|
package/dist/llm.js
CHANGED
|
@@ -31,7 +31,7 @@ export function resolveLayerTokens(cfg, layer) {
|
|
|
31
31
|
* 预算致正文 0 字符)——effort 为 high/max 时分层预算 ×4。
|
|
32
32
|
*/
|
|
33
33
|
export function layerMaxTokens(base, reasoningEffort) {
|
|
34
|
-
return reasoningEffort === 'high' || reasoningEffort === 'max' ? base * 4 : base;
|
|
34
|
+
return reasoningEffort === 'high' || reasoningEffort === 'xhigh' || reasoningEffort === 'max' ? base * 4 : base;
|
|
35
35
|
}
|
|
36
36
|
/** 解析蒸馏用的 provider/model:配置优先,其次当前默认选择。 */
|
|
37
37
|
export async function resolveModelRoute(ctx, cfg) {
|
|
@@ -46,6 +46,75 @@ export async function resolveModelRoute(ctx, cfg) {
|
|
|
46
46
|
}
|
|
47
47
|
throw new Error('无法解析蒸馏模型路由:请在插件 config 中配置 llm.provider / llm.model,或确保存在默认模型选择');
|
|
48
48
|
}
|
|
49
|
+
const effortCache = new Map();
|
|
50
|
+
/** 清空能力缓存(llm/adapters-updated 时调用:供应商增删/改配置后重新探询)。 */
|
|
51
|
+
export function invalidateEffortCache() {
|
|
52
|
+
effortCache.clear();
|
|
53
|
+
effortWarned.clear();
|
|
54
|
+
}
|
|
55
|
+
/** 探询某模型的思考档位能力;失败返回 null(调用方保持旧发送行为,不改判)。 */
|
|
56
|
+
export async function resolveModelEfforts(ctx, provider, model) {
|
|
57
|
+
const key = `${provider}::${model}`;
|
|
58
|
+
const hit = effortCache.get(key);
|
|
59
|
+
if (hit)
|
|
60
|
+
return hit;
|
|
61
|
+
try {
|
|
62
|
+
if (typeof ctx.llm?.resolveModelInfo !== 'function')
|
|
63
|
+
return null;
|
|
64
|
+
const info = await ctx.llm.resolveModelInfo(provider, model);
|
|
65
|
+
const efforts = (info.reasoning?.efforts ?? [])
|
|
66
|
+
.map((e) => String(e.id))
|
|
67
|
+
.filter((id) => id.length > 0);
|
|
68
|
+
const cap = {
|
|
69
|
+
efforts,
|
|
70
|
+
...(info.reasoning?.defaultEffort ? { defaultEffort: String(info.reasoning.defaultEffort) } : {}),
|
|
71
|
+
};
|
|
72
|
+
effortCache.set(key, cap);
|
|
73
|
+
return cap;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null; // 不缓存失败:路由尚未注册等瞬时态,下次调用重试
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** 纯决策:配置档位 + 模型能力 → 实际发送值(callLLM 与 settings-get 共用)。 */
|
|
80
|
+
export function decideSendableEffort(cap, cfgEffort) {
|
|
81
|
+
if (!cap)
|
|
82
|
+
return { effort: cfgEffort, reason: 'no-capability' };
|
|
83
|
+
if (cfgEffort) {
|
|
84
|
+
if (cap.efforts.includes(cfgEffort))
|
|
85
|
+
return { effort: cfgEffort, reason: 'supported' };
|
|
86
|
+
if (cfgEffort === 'off' && cap.efforts.includes('none'))
|
|
87
|
+
return { effort: 'none', reason: 'alias-none' };
|
|
88
|
+
if (cap.efforts.length === 0)
|
|
89
|
+
return { effort: '', reason: 'no-efforts' };
|
|
90
|
+
return { effort: '', reason: 'unsupported' };
|
|
91
|
+
}
|
|
92
|
+
// 空配置 = 自动:模型默认档 → 无默认取 high(用户规则:未声明/无默认一律 high)→ 仍无则不传
|
|
93
|
+
if (cap.defaultEffort && cap.efforts.includes(cap.defaultEffort)) {
|
|
94
|
+
return { effort: cap.defaultEffort, reason: 'auto-default' };
|
|
95
|
+
}
|
|
96
|
+
if (cap.efforts.includes('high'))
|
|
97
|
+
return { effort: 'high', reason: 'auto-high' };
|
|
98
|
+
return { effort: '', reason: 'no-efforts' };
|
|
99
|
+
}
|
|
100
|
+
const effortWarned = new Set();
|
|
101
|
+
/** 探询 + 决策 + 一次性告警(不支持/未声明时提示降级,不刷屏)。 */
|
|
102
|
+
export async function planDistillEffort(ctx, provider, model, cfgEffort, logger) {
|
|
103
|
+
const cap = await resolveModelEfforts(ctx, provider, model);
|
|
104
|
+
const d = decideSendableEffort(cap, cfgEffort);
|
|
105
|
+
if ((d.reason === 'unsupported' || d.reason === 'no-efforts') && logger) {
|
|
106
|
+
const key = `${provider}::${model}::${cfgEffort}::${d.reason}`;
|
|
107
|
+
if (!effortWarned.has(key)) {
|
|
108
|
+
effortWarned.add(key);
|
|
109
|
+
logger.warn(`[memory] 蒸馏思考档位 ${cfgEffort || '(auto)'} 不被 ${provider}/${model} 支持` +
|
|
110
|
+
(d.reason === 'no-efforts'
|
|
111
|
+
? '(模型未声明思考档位)'
|
|
112
|
+
: `(支持: ${cap?.efforts.join('/')})`) +
|
|
113
|
+
',本次调用不传档位(跟随模型默认)');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return d;
|
|
117
|
+
}
|
|
49
118
|
/**
|
|
50
119
|
* 一次完整蒸馏调用:流式收集文本,返回最终字符串。
|
|
51
120
|
* 失败(error/aborted finish)抛错,由调用方兜底。
|
|
@@ -56,22 +125,29 @@ export async function resolveModelRoute(ctx, cfg) {
|
|
|
56
125
|
export async function callLLM(ctx, cfg, opts) {
|
|
57
126
|
const { provider, model } = await resolveModelRoute(ctx, cfg);
|
|
58
127
|
const signal = opts.signal ?? AbortSignal.timeout(cfg.llm.timeoutMs);
|
|
128
|
+
// 档位按模型能力决策(跨供应商 effort 兼容):不支持的档位不传 + 告警一次,
|
|
129
|
+
// 空配置 = 自动(模型默认档 → high);详见 decideSendableEffort
|
|
130
|
+
const effort = await planDistillEffort(ctx, provider, model, cfg.llm.reasoningEffort, opts.logger);
|
|
59
131
|
// 输入预算兜底:任何蒸馏调用的用户 prompt 不超过 maxInputChars
|
|
60
132
|
// (L1 已在数据层分块,这里是 L2/L3 与异常场景的最后一道网)
|
|
61
133
|
const user = opts.user.length > cfg.llm.maxInputChars
|
|
62
134
|
? `${opts.user.slice(0, cfg.llm.maxInputChars)}\n\n[输入超出 ${cfg.llm.maxInputChars} 字符预算,已截断]`
|
|
63
135
|
: opts.user;
|
|
136
|
+
// 输出预算 ×4 防线跟随【实际发送】的档位:阶段侧按原始配置放大(high/max),
|
|
137
|
+
// 这里补自动档('' → 模型默认/high)解析出高档时欠放大的缺口
|
|
138
|
+
const baseMaxTokens = opts.maxTokens ?? cfg.llm.maxTokens;
|
|
139
|
+
const maxTokens = ['high', 'xhigh', 'max'].includes(effort.effort) && !['high', 'max'].includes(cfg.llm.reasoningEffort)
|
|
140
|
+
? layerMaxTokens(baseMaxTokens, 'high')
|
|
141
|
+
: baseMaxTokens;
|
|
64
142
|
const stream = ctx.llm.stream({
|
|
65
143
|
provider,
|
|
66
144
|
model,
|
|
67
145
|
system: opts.system,
|
|
68
146
|
messages: [createUserMessage({ content: [{ type: 'text', text: user }], source: { kind: 'user' } })],
|
|
69
147
|
temperature: opts.temperature ?? cfg.llm.temperature,
|
|
70
|
-
maxTokens
|
|
71
|
-
//
|
|
72
|
-
...(
|
|
73
|
-
? { reasoningEffort: ReasoningEffortId(cfg.llm.reasoningEffort) }
|
|
74
|
-
: {}),
|
|
148
|
+
maxTokens,
|
|
149
|
+
// 档位只在能力决策给出非空值时传;空串不传(跟随模型默认)
|
|
150
|
+
...(effort.effort ? { reasoningEffort: ReasoningEffortId(effort.effort) } : {}),
|
|
75
151
|
signal,
|
|
76
152
|
});
|
|
77
153
|
const startedAt = Date.now();
|
package/dist/pipeline/runner.js
CHANGED
|
@@ -25,6 +25,8 @@ export function pickNextTaskIndex(tasks) {
|
|
|
25
25
|
*/
|
|
26
26
|
export function effectiveCfg(cfg, live) {
|
|
27
27
|
const s = live?.get();
|
|
28
|
+
// 思考档位:设置服务在场时运行时值整体接管——'' = 自动(按模型能力解析),
|
|
29
|
+
// 不再回退静态配置("跟随配置"选项已删);静态值仅无 settings 服务的部署生效
|
|
28
30
|
const eff = s?.reasoningEffort ?? '';
|
|
29
31
|
// 可选链防御:smoke/测试缝构造的最小 cfg 可能没有 llm 字段
|
|
30
32
|
const pinned = Boolean(cfg.llm?.provider && cfg.llm?.model);
|
|
@@ -41,13 +43,15 @@ export function effectiveCfg(cfg, live) {
|
|
|
41
43
|
}
|
|
42
44
|
: null;
|
|
43
45
|
const maxInput = s && s.distillMaxInputChars > 0 ? s.distillMaxInputChars : null;
|
|
44
|
-
|
|
46
|
+
// 无任何注入且(无 live,或运行时 '' 且静态本就 '')→ 原引用返回,保持引用稳定性
|
|
47
|
+
const effNoop = eff === '' && (!live || !cfg.llm?.reasoningEffort);
|
|
48
|
+
if (!override && !budgets && !maxInput && effNoop)
|
|
45
49
|
return cfg;
|
|
46
50
|
return {
|
|
47
51
|
...cfg,
|
|
48
52
|
llm: {
|
|
49
53
|
...cfg.llm,
|
|
50
|
-
...(
|
|
54
|
+
...(live ? { reasoningEffort: eff } : {}),
|
|
51
55
|
...(override ?? {}),
|
|
52
56
|
...(budgets ? { budgets } : {}),
|
|
53
57
|
...(maxInput ? { maxInputChars: maxInput } : {}),
|
package/dist/settings.d.ts
CHANGED
|
@@ -8,7 +8,8 @@ import Schema from '@deepseek-ai/schemastery';
|
|
|
8
8
|
import type { DistillBudgetLayer } from './llm.js';
|
|
9
9
|
import type { MemoryLogger } from './types.js';
|
|
10
10
|
/** 蒸馏思考档位可选项:'' = 跟随静态 config(部署默认)。 */
|
|
11
|
-
|
|
11
|
+
/** 蒸馏思考档位:'' = 自动(模型默认档 → high);其余为各适配器通用档位词汇表。 */
|
|
12
|
+
export type EffortChoice = '' | 'off' | 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
12
13
|
/** 分层输出预算(与 llm.ts 的 DistillBudgetLayer 同键;0 = 跟随内置默认)。 */
|
|
13
14
|
export type DistillBudgets = Record<DistillBudgetLayer, number>;
|
|
14
15
|
export interface MemoryLiveSettings {
|
package/dist/settings.js
CHANGED
|
@@ -36,7 +36,7 @@ export function liveSettingsSchema() {
|
|
|
36
36
|
capture: Schema.boolean().default(true),
|
|
37
37
|
distill: Schema.boolean().default(true),
|
|
38
38
|
recall: Schema.boolean().default(true),
|
|
39
|
-
reasoningEffort: Schema.union(['', 'off', 'high', 'max']).default(''),
|
|
39
|
+
reasoningEffort: Schema.union(['', 'off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']).default(''),
|
|
40
40
|
distillProvider: Schema.string().default(''),
|
|
41
41
|
distillModel: Schema.string().default(''),
|
|
42
42
|
distillBudgets: Schema.object({
|
|
@@ -154,7 +154,7 @@ function resolveSettings(value) {
|
|
|
154
154
|
if (!value || typeof value !== 'object')
|
|
155
155
|
return { ...ALWAYS_ON };
|
|
156
156
|
const v = value;
|
|
157
|
-
const efforts = ['', 'off', 'high', 'max'];
|
|
157
|
+
const efforts = ['', 'off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
158
158
|
const num = (x) => (typeof x === 'number' && Number.isFinite(x) && x >= 0 ? Math.floor(x) : 0);
|
|
159
159
|
const rawBudgets = (v.distillBudgets ?? {});
|
|
160
160
|
return {
|
package/dist/stats.js
CHANGED
|
@@ -11,7 +11,7 @@ import { closeSync, openSync, readSync, statSync } from 'node:fs';
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { resolveDataDir } from './config.js';
|
|
13
13
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
14
|
-
import { LAYER_DEFAULT_BUDGETS, resolveModelRoute } from './llm.js';
|
|
14
|
+
import { decideSendableEffort, LAYER_DEFAULT_BUDGETS, resolveModelEfforts, resolveModelRoute } from './llm.js';
|
|
15
15
|
import { errDetail } from './util/filelog.js';
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
17
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
@@ -132,6 +132,15 @@ async function buildStats(cfg, stores, status) {
|
|
|
132
132
|
thresholds: { l2MinNewMemories: cfg.l2.minNewMemories, l3Interval: cfg.l3.interval },
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
|
+
/** RPC 字符串入参上限校验:防 loopback 面畸形超长载荷
|
|
136
|
+
* (超长 sessionId 持久化进 session-modes.json / 超长 query 触发 jieba 全量分词 CPU 峰值)。 */
|
|
137
|
+
function expectSessionId(v) {
|
|
138
|
+
if (typeof v !== 'string' || !v)
|
|
139
|
+
throw new Error('sessionId 缺失');
|
|
140
|
+
if (v.length > 512)
|
|
141
|
+
throw new Error('sessionId 过长(≤512 字符)');
|
|
142
|
+
return v;
|
|
143
|
+
}
|
|
135
144
|
async function handleEndpoint(endpoint, payload, deps) {
|
|
136
145
|
const { cfg, stores, status, live, modes, dataDir, rebuild, embedManager } = deps;
|
|
137
146
|
switch (endpoint) {
|
|
@@ -141,27 +150,44 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
141
150
|
if (!modes)
|
|
142
151
|
throw new Error('档位存储未初始化');
|
|
143
152
|
const p = (payload ?? {});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return { sessionId: p.sessionId, mode: modes.get(p.sessionId), defaultMode: modes.default };
|
|
153
|
+
const sessionId = expectSessionId(p.sessionId);
|
|
154
|
+
return { sessionId, mode: modes.get(sessionId), defaultMode: modes.default };
|
|
147
155
|
}
|
|
148
156
|
case 'dsh-memory/session-mode-set': {
|
|
149
157
|
if (!modes)
|
|
150
158
|
throw new Error('档位存储未初始化');
|
|
151
159
|
const p = (payload ?? {});
|
|
152
|
-
|
|
153
|
-
throw new Error('sessionId 缺失');
|
|
160
|
+
const sessionId = expectSessionId(p.sessionId);
|
|
154
161
|
const allowed = ['auto', 'chat', 'work', 'off'];
|
|
155
162
|
if (typeof p.mode !== 'string' || !allowed.includes(p.mode)) {
|
|
156
163
|
throw new Error(`非法档位: ${String(p.mode)}(允许 ${allowed.join('/')})`);
|
|
157
164
|
}
|
|
158
|
-
modes.set(
|
|
159
|
-
deps.logger.info(`[memory] 会话档位设置 session=${
|
|
160
|
-
return { sessionId
|
|
165
|
+
modes.set(sessionId, p.mode);
|
|
166
|
+
deps.logger.info(`[memory] 会话档位设置 session=${sessionId} mode=${p.mode}`);
|
|
167
|
+
return { sessionId, mode: p.mode };
|
|
161
168
|
}
|
|
162
169
|
case 'dsh-memory/settings-get': {
|
|
163
170
|
const s = live?.get();
|
|
164
171
|
const budgets = s?.distillBudgets ?? { extract: 0, dedup: 0, l2: 0, l3: 0 };
|
|
172
|
+
// 蒸馏思考档位:current 是运行时值('' = 自动);effective 是能力探询后实际发送值
|
|
173
|
+
// ('' = 不传,跟随模型默认);options 是当前生效模型声明的档位表(空声明 → 只显示
|
|
174
|
+
// high,用户规则:无声明默认 high),供蒸馏思考选择器渲染;fallback 是静态部署值
|
|
175
|
+
let effortEffective = s?.reasoningEffort || cfg.llm.reasoningEffort;
|
|
176
|
+
let effortOptions = ['high'];
|
|
177
|
+
let effortRoute = null;
|
|
178
|
+
try {
|
|
179
|
+
const ecfg = effectiveCfg(cfg, live);
|
|
180
|
+
effortRoute = await resolveModelRoute(deps.ctx, ecfg);
|
|
181
|
+
const cap = await resolveModelEfforts(deps.ctx, effortRoute.provider, effortRoute.model);
|
|
182
|
+
if (cap) {
|
|
183
|
+
effortEffective = decideSendableEffort(cap, ecfg.llm.reasoningEffort).effort;
|
|
184
|
+
if (cap.efforts.length > 0)
|
|
185
|
+
effortOptions = cap.efforts;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* 路由解析/探询失败保持占位(effective 用运行时||静态值) */
|
|
190
|
+
}
|
|
165
191
|
return {
|
|
166
192
|
supported: live?.supported ?? false,
|
|
167
193
|
settings: s ?? {
|
|
@@ -171,11 +197,12 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
171
197
|
},
|
|
172
198
|
// 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
|
|
173
199
|
ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
|
|
174
|
-
// 蒸馏思考档位:current 是运行时覆盖('' = 跟随配置),effective 是实际生效值
|
|
175
200
|
effort: {
|
|
176
201
|
current: s?.reasoningEffort ?? '',
|
|
177
|
-
effective:
|
|
202
|
+
effective: effortEffective,
|
|
178
203
|
fallback: cfg.llm.reasoningEffort,
|
|
204
|
+
options: effortOptions,
|
|
205
|
+
...(effortRoute ? { route: effortRoute } : {}),
|
|
179
206
|
},
|
|
180
207
|
// 分层输出预算:current 是运行时覆盖(0 = 跟随默认),defaults 是内置默认(UI 占位/提示用)
|
|
181
208
|
budgets: {
|
|
@@ -251,8 +278,10 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
251
278
|
}
|
|
252
279
|
case 'dsh-memory/list-records': {
|
|
253
280
|
const p = (payload ?? {});
|
|
281
|
+
if (p.query !== undefined && p.query.length > 4096)
|
|
282
|
+
throw new Error('query 过长(≤4096 字符)');
|
|
254
283
|
const limit = Math.min(Math.max(Number(p.limit) || 50, 1), 200);
|
|
255
|
-
const offset = Math.max(Number(p.offset) || 0, 0);
|
|
284
|
+
const offset = Math.min(Math.max(Number(p.offset) || 0, 0), 1_000_000);
|
|
256
285
|
// 关键词路径:复用检索唯一缝(与召回同源),取回后做场景过滤 + 手工分页。
|
|
257
286
|
// 检索侧单次上限 200:分页窗口触达上限时显式标记 truncated(结果可能不完整),
|
|
258
287
|
// 不再静默返回空结果让用户误以为"没有更多"等于"不存在更多"。
|
|
@@ -371,6 +400,8 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
371
400
|
const p = (payload ?? {});
|
|
372
401
|
if (typeof p.provider !== 'string' || !p.provider)
|
|
373
402
|
throw new Error('provider 缺失');
|
|
403
|
+
if (p.provider.length > 200)
|
|
404
|
+
throw new Error('provider 过长(≤200 字符)');
|
|
374
405
|
// 两个内置适配器(deepseek/pi-ai)的 listModels 都读本地快照不触网;
|
|
375
406
|
// 仍加超时兜底,防第三方适配器实现为远端查询拖死 RPC 轮询
|
|
376
407
|
const models = await Promise.race([
|
|
@@ -395,6 +426,9 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
395
426
|
if (p.source !== 'remote' && p.source !== 'local' && p.source !== 'off') {
|
|
396
427
|
throw new Error('source 必须是 remote | local | off');
|
|
397
428
|
}
|
|
429
|
+
if (typeof p.activeModel === 'string' && p.activeModel.length > 200) {
|
|
430
|
+
throw new Error('activeModel 过长(≤200 字符)');
|
|
431
|
+
}
|
|
398
432
|
const r = embedManager.requestSource({ source: p.source, activeModel: p.activeModel ?? null });
|
|
399
433
|
if (!r.accepted)
|
|
400
434
|
throw new Error(r.error ?? '切换请求被拒绝');
|
|
@@ -101,4 +101,7 @@ export declare class ModelDownloadQueue {
|
|
|
101
101
|
* 镜像直连往往间歇不可达(真实事故:直连超时与污染字节交替出现)。
|
|
102
102
|
*/
|
|
103
103
|
export declare function resolveProxyUrl(setting: string | undefined, host: string): string;
|
|
104
|
+
/** 代理 URL 日志脱敏:剥掉 userinfo(内网代理常带 user:pass 凭据),只留 scheme//host;
|
|
105
|
+
* 解析失败的串原样也可能是凭据形态,返回占位符。 */
|
|
106
|
+
export declare function maskProxyUrl(proxy: string): string;
|
|
104
107
|
export {};
|
|
@@ -40,8 +40,15 @@ export class ModelDownloadQueue {
|
|
|
40
40
|
}
|
|
41
41
|
const proxy = resolveProxyUrl(opts.proxy, host);
|
|
42
42
|
if (proxy) {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
try {
|
|
44
|
+
this.agent = new ProxyAgent(proxy);
|
|
45
|
+
opts.logger?.info(`[memory] 模型下载走代理 ${maskProxyUrl(proxy)}(镜像直连在国内网络间歇不可达)`);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
// 与畸形 mirror 同款容错(见上):畸形代理(无 scheme 等常见笔误)只降级直连,
|
|
49
|
+
// 绝不炸构造器——本构造发生在 apply 装配链上,抛错会拖垮宿主启动
|
|
50
|
+
opts.logger?.warn(`[memory] 代理配置无效,已忽略并直连(${maskProxyUrl(proxy)}): ${err instanceof Error ? err.message : String(err)}`);
|
|
51
|
+
}
|
|
45
52
|
}
|
|
46
53
|
this.defaultFetch = ((u, init) => {
|
|
47
54
|
const dispatch = this.agent;
|
|
@@ -382,6 +389,8 @@ export function resolveProxyUrl(setting, host) {
|
|
|
382
389
|
if (value)
|
|
383
390
|
return value;
|
|
384
391
|
const noProxy = process.env.NO_PROXY ?? process.env.no_proxy ?? '';
|
|
392
|
+
if (noProxy.trim() === '*')
|
|
393
|
+
return '';
|
|
385
394
|
if (noProxy) {
|
|
386
395
|
for (const raw of noProxy.split(',')) {
|
|
387
396
|
const entry = raw.trim().replace(/^\./, '').toLowerCase();
|
|
@@ -400,6 +409,17 @@ export function resolveProxyUrl(setting, host) {
|
|
|
400
409
|
}
|
|
401
410
|
return '';
|
|
402
411
|
}
|
|
412
|
+
/** 代理 URL 日志脱敏:剥掉 userinfo(内网代理常带 user:pass 凭据),只留 scheme//host;
|
|
413
|
+
* 解析失败的串原样也可能是凭据形态,返回占位符。 */
|
|
414
|
+
export function maskProxyUrl(proxy) {
|
|
415
|
+
try {
|
|
416
|
+
const u = new URL(proxy);
|
|
417
|
+
return `${u.protocol}//${u.host}`;
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return '<invalid-url>';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
403
423
|
async function sha256File(p) {
|
|
404
424
|
const { createReadStream } = await import('node:fs');
|
|
405
425
|
const hash = createHash('sha256');
|
|
@@ -36,7 +36,7 @@ export interface InitialEmbedding {
|
|
|
36
36
|
export declare function remoteCeiling(cfg: MemoryConfig): boolean;
|
|
37
37
|
export declare function resolveInitialEmbedding(cfg: MemoryConfig, sourceStore: EmbeddingSourceStore, downloader: ModelDownloadQueue, makeLocal: (modelId: string) => LocalEmbeddingService | null, logger?: MemoryLogger): Promise<InitialEmbedding>;
|
|
38
38
|
/** 本地服务构造工厂(index.ts 的初始解析与 Manager 共用一份实现,防漂移)。 */
|
|
39
|
-
export declare function makeLocalServiceFactory(installer: RuntimeInstaller, downloader: ModelDownloadQueue, logger?: MemoryLogger): (modelId: string) => LocalEmbeddingService | null;
|
|
39
|
+
export declare function makeLocalServiceFactory(installer: RuntimeInstaller, downloader: ModelDownloadQueue, logger?: MemoryLogger, maxInputChars?: number): (modelId: string) => LocalEmbeddingService | null;
|
|
40
40
|
export type ApplyPhase = 'idle' | 'installing-runtime' | 'warming' | 'switching' | 'reindexing' | 'done' | 'error';
|
|
41
41
|
export interface ReindexProgressState {
|
|
42
42
|
running: boolean;
|
|
@@ -102,12 +102,12 @@ export async function resolveInitialEmbedding(cfg, sourceStore, downloader, make
|
|
|
102
102
|
return { svc, dims: cfg.embedding.dimensions, providerInfo: svc.getProviderInfo() };
|
|
103
103
|
}
|
|
104
104
|
/** 本地服务构造工厂(index.ts 的初始解析与 Manager 共用一份实现,防漂移)。 */
|
|
105
|
-
export function makeLocalServiceFactory(installer, downloader, logger) {
|
|
105
|
+
export function makeLocalServiceFactory(installer, downloader, logger, maxInputChars) {
|
|
106
106
|
return (modelId) => {
|
|
107
107
|
const entry = catalogById(modelId);
|
|
108
108
|
if (!entry)
|
|
109
109
|
return null;
|
|
110
|
-
return new LocalEmbeddingService(entry, downloader.modelsDir(entry.id), () => Promise.resolve(installer.resolveModule()), logger);
|
|
110
|
+
return new LocalEmbeddingService(entry, downloader.modelsDir(entry.id), () => Promise.resolve(installer.resolveModule()), logger, maxInputChars);
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
export class EmbeddingManager {
|
|
@@ -167,7 +167,7 @@ export class EmbeddingManager {
|
|
|
167
167
|
}
|
|
168
168
|
/** 构造绑定真实运行时 loader 的本地服务(deps.makeLocal 可注入,测试替换)。 */
|
|
169
169
|
makeLocalService(modelId) {
|
|
170
|
-
const factory = this.deps.makeLocal ?? makeLocalServiceFactory(this.installer, this.downloader, this.deps.logger);
|
|
170
|
+
const factory = this.deps.makeLocal ?? makeLocalServiceFactory(this.installer, this.downloader, this.deps.logger, this.deps.cfg.embedding.maxInputChars);
|
|
171
171
|
return factory(modelId);
|
|
172
172
|
}
|
|
173
173
|
/** 活切换请求:验证通过即接受,后台执行应用链(进度轮询可见)。 */
|
package/dist/store/io.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
export declare function ensureDir(dir: string): Promise<void>;
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* 原子写文本文件。tmp 写满后先 fsync 数据块再 rename——否则断电时文件系统可能
|
|
4
|
+
* 先持久化 rename 元数据、后持久化数据块(ext4 delayed allocation / NTFS 均可能),
|
|
5
|
+
* 目标文件变成空文件或半截。tmp 名带随机段防同毫秒碰撞;失败路径清理孤儿 tmp。
|
|
6
|
+
*/
|
|
3
7
|
export declare function atomicWriteText(file: string, content: string): Promise<void>;
|
|
4
8
|
/** 原子写 JSON。 */
|
|
5
9
|
export declare function atomicWriteJson(file: string, value: unknown): Promise<void>;
|
|
6
10
|
export declare function readJsonIfExists<T>(file: string): Promise<T | undefined>;
|
|
7
11
|
export declare function readTextIfExists(file: string): Promise<string | undefined>;
|
|
8
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* 追加 JSONL 行(存在则追加,否则创建)。走 OS 写回不加 fsync:追加是热路径
|
|
14
|
+
* (每轮对话一次),逐条 fsync 的延迟代价大于崩溃窗口丢尾部几行的损失——
|
|
15
|
+
* 事实源的完整性边界见 README「日志与故障排查」。
|
|
16
|
+
*/
|
|
9
17
|
export declare function appendJsonl(file: string, lines: unknown[]): Promise<void>;
|
|
10
18
|
/** 读取 JSONL 全部行。 */
|
|
11
19
|
export declare function readJsonl<T>(file: string): Promise<T[]>;
|
package/dist/store/io.js
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 持久化工具:原子写(tmp + rename)、JSON 状态。
|
|
2
|
+
* 持久化工具:原子写(tmp + fsync + rename)、JSON 状态。
|
|
3
3
|
*/
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { promises as fs } from 'node:fs';
|
|
5
6
|
import * as path from 'node:path';
|
|
6
7
|
export async function ensureDir(dir) {
|
|
7
8
|
await fs.mkdir(dir, { recursive: true });
|
|
8
9
|
}
|
|
9
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* 原子写文本文件。tmp 写满后先 fsync 数据块再 rename——否则断电时文件系统可能
|
|
12
|
+
* 先持久化 rename 元数据、后持久化数据块(ext4 delayed allocation / NTFS 均可能),
|
|
13
|
+
* 目标文件变成空文件或半截。tmp 名带随机段防同毫秒碰撞;失败路径清理孤儿 tmp。
|
|
14
|
+
*/
|
|
10
15
|
export async function atomicWriteText(file, content) {
|
|
11
16
|
await ensureDir(path.dirname(file));
|
|
12
|
-
const tmp = `${file}.${process.pid}.${
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const tmp = `${file}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
18
|
+
try {
|
|
19
|
+
await fs.writeFile(tmp, content, 'utf-8');
|
|
20
|
+
const fh = await fs.open(tmp, 'r+');
|
|
21
|
+
try {
|
|
22
|
+
await fh.sync();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await fh.close();
|
|
26
|
+
}
|
|
27
|
+
await fs.rename(tmp, file);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
await fs.unlink(tmp).catch(() => { });
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
15
33
|
}
|
|
16
34
|
/** 原子写 JSON。 */
|
|
17
35
|
export async function atomicWriteJson(file, value) {
|
|
@@ -34,7 +52,11 @@ export async function readTextIfExists(file) {
|
|
|
34
52
|
return undefined;
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* 追加 JSONL 行(存在则追加,否则创建)。走 OS 写回不加 fsync:追加是热路径
|
|
57
|
+
* (每轮对话一次),逐条 fsync 的延迟代价大于崩溃窗口丢尾部几行的损失——
|
|
58
|
+
* 事实源的完整性边界见 README「日志与故障排查」。
|
|
59
|
+
*/
|
|
38
60
|
export async function appendJsonl(file, lines) {
|
|
39
61
|
if (lines.length === 0)
|
|
40
62
|
return;
|