dsh-layered-memory 0.8.7 → 0.8.8
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 +27 -4
- package/README.md +27 -4
- package/dist/client.js +3016 -3293
- package/dist/config.d.ts +50 -4
- package/dist/config.js +8 -0
- package/dist/contract.d.ts +615 -0
- package/dist/contract.js +1 -0
- package/dist/hooks/recall.d.ts +2 -18
- package/dist/index.d.ts +40 -4
- package/dist/index.js +6 -3
- package/dist/llm-usage.d.ts +2 -1
- package/dist/llm.d.ts +30 -4
- package/dist/llm.js +76 -9
- package/dist/pipeline/rebuild.d.ts +2 -21
- package/dist/pipeline/runner.d.ts +3 -3
- package/dist/pipeline/runner.js +38 -10
- package/dist/settings.d.ts +18 -29
- package/dist/settings.js +81 -1
- package/dist/stats.d.ts +2 -26
- package/dist/stats.js +80 -21
- package/dist/store/cost-ledger.d.ts +78 -0
- package/dist/store/cost-ledger.js +173 -0
- package/dist/store/download-queue.d.ts +2 -20
- package/dist/store/embedding-source.d.ts +2 -52
- package/dist/store/runtime-installer.d.ts +2 -13
- package/dist/store/sqlite.d.ts +11 -61
- package/dist/store/sqlite.js +15 -150
- package/dist/token-cost.d.ts +3 -72
- package/package.json +8 -4
package/dist/index.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
59
59
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
60
60
|
includePersona: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
61
61
|
includeSceneNav: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
62
|
-
strategy: import("@deepseek-ai/schemastery").default<"
|
|
62
|
+
strategy: import("@deepseek-ai/schemastery").default<"hybrid" | "keyword" | "embedding", "hybrid" | "keyword" | "embedding">;
|
|
63
63
|
scoreThreshold: import("@deepseek-ai/schemastery").default<number, number>;
|
|
64
64
|
decayHalfLifeDays: import("@deepseek-ai/schemastery").default<number, number>;
|
|
65
65
|
}>, Schemastery.ObjectT<{
|
|
@@ -70,7 +70,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
70
70
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
71
71
|
includePersona: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
72
72
|
includeSceneNav: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
73
|
-
strategy: import("@deepseek-ai/schemastery").default<"
|
|
73
|
+
strategy: import("@deepseek-ai/schemastery").default<"hybrid" | "keyword" | "embedding", "hybrid" | "keyword" | "embedding">;
|
|
74
74
|
scoreThreshold: import("@deepseek-ai/schemastery").default<number, number>;
|
|
75
75
|
decayHalfLifeDays: import("@deepseek-ai/schemastery").default<number, number>;
|
|
76
76
|
}>>;
|
|
@@ -100,6 +100,15 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
100
100
|
llm: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
101
101
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
102
102
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
103
|
+
fallbacks: import("@deepseek-ai/schemastery").default<({
|
|
104
|
+
provider?: string | null | undefined;
|
|
105
|
+
model?: string | null | undefined;
|
|
106
|
+
reasoningEffort?: "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | null | undefined;
|
|
107
|
+
} & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<{
|
|
108
|
+
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
109
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
110
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
111
|
+
}>[]>;
|
|
103
112
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
104
113
|
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
105
114
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
@@ -108,6 +117,15 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
108
117
|
}>, Schemastery.ObjectT<{
|
|
109
118
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
110
119
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
120
|
+
fallbacks: import("@deepseek-ai/schemastery").default<({
|
|
121
|
+
provider?: string | null | undefined;
|
|
122
|
+
model?: string | null | undefined;
|
|
123
|
+
reasoningEffort?: "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | null | undefined;
|
|
124
|
+
} & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<{
|
|
125
|
+
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
126
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
127
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
128
|
+
}>[]>;
|
|
111
129
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
112
130
|
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
113
131
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
@@ -172,7 +190,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
172
190
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
173
191
|
includePersona: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
174
192
|
includeSceneNav: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
175
|
-
strategy: import("@deepseek-ai/schemastery").default<"
|
|
193
|
+
strategy: import("@deepseek-ai/schemastery").default<"hybrid" | "keyword" | "embedding", "hybrid" | "keyword" | "embedding">;
|
|
176
194
|
scoreThreshold: import("@deepseek-ai/schemastery").default<number, number>;
|
|
177
195
|
decayHalfLifeDays: import("@deepseek-ai/schemastery").default<number, number>;
|
|
178
196
|
}>, Schemastery.ObjectT<{
|
|
@@ -183,7 +201,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
183
201
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
184
202
|
includePersona: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
185
203
|
includeSceneNav: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
186
|
-
strategy: import("@deepseek-ai/schemastery").default<"
|
|
204
|
+
strategy: import("@deepseek-ai/schemastery").default<"hybrid" | "keyword" | "embedding", "hybrid" | "keyword" | "embedding">;
|
|
187
205
|
scoreThreshold: import("@deepseek-ai/schemastery").default<number, number>;
|
|
188
206
|
decayHalfLifeDays: import("@deepseek-ai/schemastery").default<number, number>;
|
|
189
207
|
}>>;
|
|
@@ -213,6 +231,15 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
213
231
|
llm: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
214
232
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
215
233
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
234
|
+
fallbacks: import("@deepseek-ai/schemastery").default<({
|
|
235
|
+
provider?: string | null | undefined;
|
|
236
|
+
model?: string | null | undefined;
|
|
237
|
+
reasoningEffort?: "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | null | undefined;
|
|
238
|
+
} & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<{
|
|
239
|
+
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
240
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
241
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
242
|
+
}>[]>;
|
|
216
243
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
217
244
|
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
218
245
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
@@ -221,6 +248,15 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
221
248
|
}>, Schemastery.ObjectT<{
|
|
222
249
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
223
250
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
251
|
+
fallbacks: import("@deepseek-ai/schemastery").default<({
|
|
252
|
+
provider?: string | null | undefined;
|
|
253
|
+
model?: string | null | undefined;
|
|
254
|
+
reasoningEffort?: "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | null | undefined;
|
|
255
|
+
} & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<{
|
|
256
|
+
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
257
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
258
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
259
|
+
}>[]>;
|
|
224
260
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
225
261
|
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
226
262
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
package/dist/index.js
CHANGED
|
@@ -39,7 +39,7 @@ import { SessionModeStore } from './store/session-modes.js';
|
|
|
39
39
|
import { StateStore } from './store/state.js';
|
|
40
40
|
import { registerMemoryTools } from './tools/index.js';
|
|
41
41
|
import { errDetail, withFileLog } from './util/filelog.js';
|
|
42
|
-
import { resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
42
|
+
import { buildRouteChain, resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
43
43
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
44
44
|
import { initTokenCost, resetTokenCost } from './token-cost.js';
|
|
45
45
|
export const name = 'dsh-memory-plugin';
|
|
@@ -170,8 +170,11 @@ export async function apply(ctx, config) {
|
|
|
170
170
|
// 蒸馏模型路由:启动期解析一次并记录(路由错误是最难事后排查的问题之一);
|
|
171
171
|
// 用运行时调参视图解析——用户已用 UI 覆盖蒸馏模型时,日志反映实际路由
|
|
172
172
|
try {
|
|
173
|
-
const
|
|
174
|
-
|
|
173
|
+
const cfgView = effectiveCfg(config, live);
|
|
174
|
+
const route = await resolveModelRoute(ctx, cfgView);
|
|
175
|
+
// 链长度按同一解析口径计(相同条目会被去重),排障时无需 --dump-config 即可确认回退链生效
|
|
176
|
+
const chain = buildRouteChain(route, cfgView.llm.fallbacks, cfgView.llm.reasoningEffort);
|
|
177
|
+
logger.info(`[memory] 蒸馏模型路由: ${route.provider}/${route.model}${chain.length > 1 ? `(+${chain.length - 1} 回退)` : ''}`);
|
|
175
178
|
}
|
|
176
179
|
catch (err) {
|
|
177
180
|
logger.warn(`[memory] 蒸馏模型路由解析失败: ${errDetail(err)}`);
|
package/dist/llm-usage.d.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* 拿不到——按仓库既有口径记输入**字符**(中文 1 字 ≈ 1 token 保守折算,
|
|
11
11
|
* 见 config.ts maxInputChars 注释),报告侧如实标注。
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
import type { DistillLayer } from './contract.js';
|
|
14
|
+
export type { DistillLayer } from './contract.js';
|
|
14
15
|
export interface DistillLayerUsage {
|
|
15
16
|
calls: number;
|
|
16
17
|
failures: number;
|
package/dist/llm.d.ts
CHANGED
|
@@ -24,8 +24,9 @@ export declare const LAYER_MAX_TOKENS_DEDUP = 8000;
|
|
|
24
24
|
export declare const LAYER_MAX_TOKENS_L2 = 32000;
|
|
25
25
|
/** L3 画像(完整 persona 文档)。 */
|
|
26
26
|
export declare const LAYER_MAX_TOKENS_L3 = 16000;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/** 分层输出预算键(已迁入契约 src/contract.ts;import type 供本地使用,re-export 不断裂既有引用)。 */
|
|
28
|
+
import type { DistillBudgetLayer } from './contract.js';
|
|
29
|
+
export type { DistillBudgetLayer } from './contract.js';
|
|
29
30
|
/** 各层内置默认预算(设置页"0 = 跟随默认"的默认值来源)。 */
|
|
30
31
|
export declare const LAYER_DEFAULT_BUDGETS: Record<DistillBudgetLayer, number>;
|
|
31
32
|
/**
|
|
@@ -55,6 +56,29 @@ export declare function resolveModelRoute(ctx: Context, cfg: MemoryConfig): Prom
|
|
|
55
56
|
provider: string;
|
|
56
57
|
model: string;
|
|
57
58
|
}>;
|
|
59
|
+
/** 回退链条目(配置形态;reasoningEffort 为该路由的档位覆盖,'' = 跟随全局)。 */
|
|
60
|
+
export interface FallbackRouteEntry {
|
|
61
|
+
provider: string;
|
|
62
|
+
model: string;
|
|
63
|
+
reasoningEffort?: string;
|
|
64
|
+
}
|
|
65
|
+
/** 链上单条路由(档位候选已解析:条目非空 > 全局静态;发送前仍过能力钳制)。 */
|
|
66
|
+
export interface DistillRoute {
|
|
67
|
+
provider: string;
|
|
68
|
+
model: string;
|
|
69
|
+
effort: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 组装蒸馏路由链(纯决策,smoke 决策表缝):主路由在前、回退条目按配置顺序在后。
|
|
73
|
+
* provider/model 缺失的条目剔除;与主路由或先前条目完全相同(provider+model)的
|
|
74
|
+
* 条目跳过——注定失败的重复尝试不值得占位。每条路由携带生效档位候选:
|
|
75
|
+
* 主路由可带显式档位(运行时统一链注入 primaryEffort),条目档位非空覆盖全局。
|
|
76
|
+
*/
|
|
77
|
+
export declare function buildRouteChain(primary: {
|
|
78
|
+
provider: string;
|
|
79
|
+
model: string;
|
|
80
|
+
effort?: string;
|
|
81
|
+
}, fallbacks: FallbackRouteEntry[] | undefined, globalEffort: string): DistillRoute[];
|
|
58
82
|
export interface ModelEffortInfo {
|
|
59
83
|
/** 模型可设置的思考档位 id(适配器声明;空 = 未声明/不可设置) */
|
|
60
84
|
efforts: string[];
|
|
@@ -76,8 +100,10 @@ export declare function decideSendableEffort(cap: ModelEffortInfo | null, cfgEff
|
|
|
76
100
|
/** 探询 + 决策 + 一次性告警(不支持/未声明时提示降级,不刷屏)。 */
|
|
77
101
|
export declare function planDistillEffort(ctx: Context, provider: string, model: string, cfgEffort: string, logger?: MemoryLogger): Promise<EffortDecision>;
|
|
78
102
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
103
|
+
* 一次完整蒸馏调用(带回退链,ADR-0004):按路由链(主路由 + llm.fallbacks)逐条
|
|
104
|
+
* 尝试,返回首个成功路由的输出。失败(error/aborted finish、网络异常、空输出)
|
|
105
|
+
* 降级下一条;调用方主动取消(signal 已中止)原样上抛不降级;全部失败抛最后一个
|
|
106
|
+
* 错误,由调用方兜底(runner 的按会话指数退避接管重试节奏)。
|
|
81
107
|
*
|
|
82
108
|
* 文本只从 block-end(协议保证携带**组装完成的整块**)取;text-delta 仅在
|
|
83
109
|
* 适配器异常地没有发 block-end 时兜底。两者都累计会把输出翻倍。
|
package/dist/llm.js
CHANGED
|
@@ -54,11 +54,43 @@ export async function resolveModelRoute(ctx, cfg) {
|
|
|
54
54
|
}
|
|
55
55
|
throw new Error('无法解析蒸馏模型路由:请在插件 config 中配置 llm.provider / llm.model,或确保存在默认模型选择');
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* 组装蒸馏路由链(纯决策,smoke 决策表缝):主路由在前、回退条目按配置顺序在后。
|
|
59
|
+
* provider/model 缺失的条目剔除;与主路由或先前条目完全相同(provider+model)的
|
|
60
|
+
* 条目跳过——注定失败的重复尝试不值得占位。每条路由携带生效档位候选:
|
|
61
|
+
* 主路由可带显式档位(运行时统一链注入 primaryEffort),条目档位非空覆盖全局。
|
|
62
|
+
*/
|
|
63
|
+
export function buildRouteChain(primary, fallbacks, globalEffort) {
|
|
64
|
+
const routes = [{ ...primary, effort: primary.effort || globalEffort }];
|
|
65
|
+
const seen = new Set([`${primary.provider}::${primary.model}`]);
|
|
66
|
+
for (const f of fallbacks ?? []) {
|
|
67
|
+
if (!f.provider || !f.model)
|
|
68
|
+
continue;
|
|
69
|
+
const key = `${f.provider}::${f.model}`;
|
|
70
|
+
if (seen.has(key))
|
|
71
|
+
continue;
|
|
72
|
+
seen.add(key);
|
|
73
|
+
routes.push({ provider: f.provider, model: f.model, effort: f.reasoningEffort || globalEffort });
|
|
74
|
+
}
|
|
75
|
+
return routes;
|
|
76
|
+
}
|
|
77
|
+
/** 单路由持续失败的一次性告警去重表(effortWarned 同款;拓扑变化随能力缓存一起失效)。 */
|
|
78
|
+
const routeDeadWarned = new Set();
|
|
79
|
+
function warnRouteDeadOnce(route, logger) {
|
|
80
|
+
if (!logger)
|
|
81
|
+
return;
|
|
82
|
+
const key = `${route.provider}::${route.model}`;
|
|
83
|
+
if (routeDeadWarned.has(key))
|
|
84
|
+
return;
|
|
85
|
+
routeDeadWarned.add(key);
|
|
86
|
+
logger.warn(`[memory] 蒸馏路由 ${route.provider}/${route.model} 失败(每路由仅告警一次;逐次失败原因与降级去向见后续日志)`);
|
|
87
|
+
}
|
|
57
88
|
const effortCache = new Map();
|
|
58
89
|
/** 清空能力缓存(llm/adapters-updated 时调用:供应商增删/改配置后重新探询)。 */
|
|
59
90
|
export function invalidateEffortCache() {
|
|
60
91
|
effortCache.clear();
|
|
61
92
|
effortWarned.clear();
|
|
93
|
+
routeDeadWarned.clear();
|
|
62
94
|
}
|
|
63
95
|
/** 探询某模型的思考档位能力;失败返回 null(调用方保持旧发送行为,不改判)。 */
|
|
64
96
|
export async function resolveModelEfforts(ctx, provider, model) {
|
|
@@ -124,18 +156,46 @@ export async function planDistillEffort(ctx, provider, model, cfgEffort, logger)
|
|
|
124
156
|
return d;
|
|
125
157
|
}
|
|
126
158
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
159
|
+
* 一次完整蒸馏调用(带回退链,ADR-0004):按路由链(主路由 + llm.fallbacks)逐条
|
|
160
|
+
* 尝试,返回首个成功路由的输出。失败(error/aborted finish、网络异常、空输出)
|
|
161
|
+
* 降级下一条;调用方主动取消(signal 已中止)原样上抛不降级;全部失败抛最后一个
|
|
162
|
+
* 错误,由调用方兜底(runner 的按会话指数退避接管重试节奏)。
|
|
129
163
|
*
|
|
130
164
|
* 文本只从 block-end(协议保证携带**组装完成的整块**)取;text-delta 仅在
|
|
131
165
|
* 适配器异常地没有发 block-end 时兜底。两者都累计会把输出翻倍。
|
|
132
166
|
*/
|
|
133
167
|
export async function callLLM(ctx, cfg, opts) {
|
|
134
|
-
const
|
|
168
|
+
const primary = await resolveModelRoute(ctx, cfg);
|
|
169
|
+
const routes = buildRouteChain(
|
|
170
|
+
// 主路由显式档位来自运行时统一链(primaryEffort,'' = 跟随全局静态)
|
|
171
|
+
{ provider: primary.provider, model: primary.model, effort: cfg.llm.primaryEffort || '' }, cfg.llm.fallbacks, cfg.llm.reasoningEffort);
|
|
172
|
+
let lastErr;
|
|
173
|
+
for (let i = 0; i < routes.length; i++) {
|
|
174
|
+
const route = routes[i];
|
|
175
|
+
try {
|
|
176
|
+
return await callRoute(ctx, cfg, opts, route);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
// 调用方主动取消(重建取消/进程关闭)不是路由失败——不降级,原样上抛
|
|
180
|
+
if (opts.signal?.aborted)
|
|
181
|
+
throw err;
|
|
182
|
+
lastErr = err;
|
|
183
|
+
warnRouteDeadOnce(route, opts.logger);
|
|
184
|
+
const next = routes[i + 1];
|
|
185
|
+
if (next) {
|
|
186
|
+
opts.logger?.info(`[memory] 蒸馏路由降级 ${route.provider}/${route.model} → ${next.provider}/${next.model}(${errDetail(err)})`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw lastErr;
|
|
191
|
+
}
|
|
192
|
+
/** 单路由一次尝试(callLLM 循环体;每路由新建超时信号 = 各享全额 timeoutMs)。 */
|
|
193
|
+
async function callRoute(ctx, cfg, opts, route) {
|
|
194
|
+
const { provider, model } = route;
|
|
135
195
|
const signal = opts.signal ?? AbortSignal.timeout(cfg.llm.timeoutMs);
|
|
136
196
|
// 档位按模型能力决策(跨供应商 effort 兼容):不支持的档位不传 + 告警一次,
|
|
137
|
-
// 空配置 =
|
|
138
|
-
const effort = await planDistillEffort(ctx, provider, model,
|
|
197
|
+
// 空配置 = 自动(模型默认 → high);路由的档位候选已在链解析时定好(条目 > 全局)
|
|
198
|
+
const effort = await planDistillEffort(ctx, provider, model, route.effort, opts.logger);
|
|
139
199
|
// 输入预算兜底:任何蒸馏调用的用户 prompt 不超过 maxInputChars
|
|
140
200
|
// (L1 已在数据层分块,这里是 L2/L3 与异常场景的最后一道网)
|
|
141
201
|
const user = opts.user.length > cfg.llm.maxInputChars
|
|
@@ -209,20 +269,27 @@ export async function callLLM(ctx, cfg, opts) {
|
|
|
209
269
|
opts.logger?.warn(`[memory] LLM 调用失败 ${provider}/${model}(${((Date.now() - startedAt) / 1000).toFixed(1)}s): ${errDetail(err)}`);
|
|
210
270
|
throw err;
|
|
211
271
|
}
|
|
212
|
-
if (opts.layer)
|
|
213
|
-
recordDistillCall(opts.layer, user.length, outputTokens, reasoningTokens, false);
|
|
214
|
-
if (opts.layer)
|
|
215
|
-
recordCostCall(provider, model, opts.layer, user.length, outputTokens, reasoningTokens);
|
|
216
272
|
const out = (blockText || deltaText).trim();
|
|
217
273
|
if (out.length === 0) {
|
|
218
274
|
// 空输出是最难排查的失败:流正常结束但一个字没吐。必须记录 finish 原因、
|
|
219
275
|
// token 计数与块分布,才能区分"模型只产出了 reasoning"vs"服务端返回空响应"。
|
|
276
|
+
if (opts.layer)
|
|
277
|
+
recordDistillCall(opts.layer, user.length, outputTokens, reasoningTokens, true);
|
|
278
|
+
if (opts.layer)
|
|
279
|
+
recordCostCall(provider, model, opts.layer, user.length, outputTokens, reasoningTokens);
|
|
220
280
|
opts.logger?.warn(`[memory] LLM 空输出 ${provider}/${model}(${((Date.now() - startedAt) / 1000).toFixed(1)}s,finish=${finishKind || '无 finish 块'}` +
|
|
221
281
|
`,输出 tokens=${outputTokens}${reasoningTokens > 0 ? `/reasoning ${reasoningTokens}` : ''},` +
|
|
222
282
|
`text-delta ${deltaBlocks} 块/${deltaText.length} 字符,reasoning ${reasoningChars} 字符,` +
|
|
223
283
|
`block-end: ${[...blockEndTypes.entries()].map(([t, n]) => `${t}×${n}`).join(', ') || '无'})` +
|
|
224
284
|
(reasoningHead ? `,reasoning 摘录: ${reasoningHead}…` : ''));
|
|
285
|
+
// 空输出按路由失败处理(#31):交给回退链降级或上抛——原先返回空串只是把失败
|
|
286
|
+
// 推迟到下游 JSON/Markdown 解析,诊断信息更差
|
|
287
|
+
throw new Error(`llm empty output: ${provider}/${model} 流正常结束但输出 0 字符`);
|
|
225
288
|
}
|
|
289
|
+
if (opts.layer)
|
|
290
|
+
recordDistillCall(opts.layer, user.length, outputTokens, reasoningTokens, false);
|
|
291
|
+
if (opts.layer)
|
|
292
|
+
recordCostCall(provider, model, opts.layer, user.length, outputTokens, reasoningTokens);
|
|
226
293
|
opts.logger?.info(`[memory] LLM 调用 ${provider}/${model}:输入 ${user.length} 字符 → 输出 ${out.length} 字符(${((Date.now() - startedAt) / 1000).toFixed(1)}s,finish=${finishKind || '无'})`);
|
|
227
294
|
return out;
|
|
228
295
|
}
|
|
@@ -15,27 +15,8 @@ export interface RebuildStores {
|
|
|
15
15
|
persona: Record<MemoryFamily, PersonaStore>;
|
|
16
16
|
state: StateStore;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
export
|
|
20
|
-
running: boolean;
|
|
21
|
-
phase: RebuildPhase;
|
|
22
|
-
/** 已完成的会话块数 / 总块数。 */
|
|
23
|
-
done: number;
|
|
24
|
-
total: number;
|
|
25
|
-
/** L0 体量(idle 时为实时预估,运行中为快照值)。 */
|
|
26
|
-
sessionCount: number;
|
|
27
|
-
messageCount: number;
|
|
28
|
-
/** 预计 LLM 抽取调用次数(下界估算:块数与字符预算取大)。 */
|
|
29
|
-
estCalls: number;
|
|
30
|
-
/** 重建产出的 L1 记录累计条数。 */
|
|
31
|
-
recordsBuilt: number;
|
|
32
|
-
cancelRequested: boolean;
|
|
33
|
-
startedAt: number | null;
|
|
34
|
-
finishedAt: number | null;
|
|
35
|
-
error: string | null;
|
|
36
|
-
/** 归档产物名(提示用户可手工找回)。 */
|
|
37
|
-
archiveNote: string | null;
|
|
38
|
-
}
|
|
18
|
+
import type { RebuildStatus } from '../contract.js';
|
|
19
|
+
export type { RebuildPhase, RebuildStatus } from '../contract.js';
|
|
39
20
|
export interface RebuildChunk {
|
|
40
21
|
sessionId: string;
|
|
41
22
|
messages: ConversationMessage[];
|
|
@@ -36,9 +36,9 @@ export interface PipelineTask {
|
|
|
36
36
|
/** 选取下一个要执行的任务下标:最早的 live 优先,否则队首(rebuild 分块让位)。 */
|
|
37
37
|
export declare function pickNextTaskIndex(tasks: PipelineTask[]): number;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
* (空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm
|
|
41
|
-
* 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
39
|
+
* 运行时调参视图:设置页运行时链(distillChain)与旧单路由/档位键、分层输出预算
|
|
40
|
+
* 可临时覆盖静态 config(空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm
|
|
41
|
+
* 一层,其余键与原 cfg 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
42
42
|
*
|
|
43
43
|
* 蒸馏模型覆盖优先级:部署静态 pin(cfg.llm.provider+model 双字段齐)不可被
|
|
44
44
|
* 运行时覆盖(部署可强制蒸馏走内网路由,防用户选择把对话外送);未 pin 时
|
package/dist/pipeline/runner.js
CHANGED
|
@@ -14,9 +14,9 @@ export function pickNextTaskIndex(tasks) {
|
|
|
14
14
|
return 0;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* (空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm
|
|
19
|
-
* 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
17
|
+
* 运行时调参视图:设置页运行时链(distillChain)与旧单路由/档位键、分层输出预算
|
|
18
|
+
* 可临时覆盖静态 config(空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm
|
|
19
|
+
* 一层,其余键与原 cfg 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
20
20
|
*
|
|
21
21
|
* 蒸馏模型覆盖优先级:部署静态 pin(cfg.llm.provider+model 双字段齐)不可被
|
|
22
22
|
* 运行时覆盖(部署可强制蒸馏走内网路由,防用户选择把对话外送);未 pin 时
|
|
@@ -26,13 +26,31 @@ export function pickNextTaskIndex(tasks) {
|
|
|
26
26
|
export function effectiveCfg(cfg, live) {
|
|
27
27
|
const s = live?.get();
|
|
28
28
|
// 思考档位:设置服务在场时运行时值整体接管——'' = 自动(按模型能力解析),
|
|
29
|
-
// 不再回退静态配置("跟随配置"选项已删);静态值仅无 settings
|
|
29
|
+
// 不再回退静态配置("跟随配置"选项已删);静态值仅无 settings 服务的部署生效。
|
|
30
|
+
// 统一路由链模式下(chain 非空)旧档位键不再参与:链内每条路由自带档位候选
|
|
30
31
|
const eff = s?.reasoningEffort ?? '';
|
|
31
32
|
// 可选链防御:smoke/测试缝构造的最小 cfg 可能没有 llm 字段
|
|
32
33
|
const pinned = Boolean(cfg.llm?.provider && cfg.llm?.model);
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// 运行时统一路由链:**只认显式 distillChain**(非空即权威——主路由档位走
|
|
35
|
+
// primaryEffort、条目档位随链注入 fallbacks、旧键与全局档位接管让位);
|
|
36
|
+
// 未配置链时旧键(distillProvider/distillModel/reasoningEffort)走下方完全
|
|
37
|
+
// 不变的旧路径——旧存量值的语义一个比特都不动(projectDistillChain 只是
|
|
38
|
+
// llm-providers 的 UI 展示视图,不参与生效逻辑)。
|
|
39
|
+
// pinned 时链整体失效(部署锁定路由,链编辑器只读)
|
|
40
|
+
const chain = s?.distillChain?.length ? s.distillChain : [];
|
|
41
|
+
const chainMode = chain.length > 0 && !pinned;
|
|
42
|
+
const chainEffort = chainMode && chain[0].reasoningEffort ? chain[0].reasoningEffort : null;
|
|
43
|
+
// 链非空即整体接管:回退链以运行时链为准(slice(1)),**单行链 = 显式无回退**
|
|
44
|
+
// (空数组覆盖静态 cfg.llm.fallbacks——"UI 所见即所跑",审查修复的语义不对称:
|
|
45
|
+
// 此前单显式行不清静态、单空主路由行清静态,同一意图两种表达)
|
|
46
|
+
const chainFallbacks = chainMode
|
|
47
|
+
? chain.slice(1).map((e) => ({ provider: e.provider, model: e.model, reasoningEffort: e.reasoningEffort || '' }))
|
|
35
48
|
: null;
|
|
49
|
+
const override = chainMode && chain[0].provider && chain[0].model
|
|
50
|
+
? { provider: chain[0].provider, model: chain[0].model }
|
|
51
|
+
: !chain.length && s && !pinned && s.distillProvider && s.distillModel
|
|
52
|
+
? { provider: s.distillProvider, model: s.distillModel }
|
|
53
|
+
: null;
|
|
36
54
|
const b = s?.distillBudgets;
|
|
37
55
|
const budgets = b && (b.extract > 0 || b.dedup > 0 || b.l2 > 0 || b.l3 > 0)
|
|
38
56
|
? {
|
|
@@ -43,18 +61,28 @@ export function effectiveCfg(cfg, live) {
|
|
|
43
61
|
}
|
|
44
62
|
: null;
|
|
45
63
|
const maxInput = s && s.distillMaxInputChars > 0 ? s.distillMaxInputChars : null;
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
64
|
+
// 旧档位键的全局接管(含给静态回退条目盖章):仅非链模式保留(旧存量值兼容)
|
|
65
|
+
const fallbacksTakeover = !chainMode && eff && cfg.llm?.fallbacks?.length
|
|
66
|
+
? cfg.llm.fallbacks.map((f) => ({ ...f, reasoningEffort: eff }))
|
|
67
|
+
: null;
|
|
68
|
+
// 档位注入:链模式下不走全局接管(主路由档位走 primaryEffort、条目档位在链内,
|
|
69
|
+
// 空档位条目回退的是静态全局而非运行时旧键);非链模式保持旧整体接管语义
|
|
70
|
+
const effortInject = live && !chainMode && (eff !== '' || Boolean(cfg.llm?.reasoningEffort))
|
|
71
|
+
? { reasoningEffort: eff }
|
|
72
|
+
: null;
|
|
73
|
+
if (!override && !budgets && !maxInput && !fallbacksTakeover && !chainEffort && !chainFallbacks && !effortInject)
|
|
49
74
|
return cfg;
|
|
50
75
|
return {
|
|
51
76
|
...cfg,
|
|
52
77
|
llm: {
|
|
53
78
|
...cfg.llm,
|
|
54
|
-
...(
|
|
79
|
+
...(effortInject ?? {}),
|
|
55
80
|
...(override ?? {}),
|
|
56
81
|
...(budgets ? { budgets } : {}),
|
|
57
82
|
...(maxInput ? { maxInputChars: maxInput } : {}),
|
|
83
|
+
...(fallbacksTakeover ? { fallbacks: fallbacksTakeover } : {}),
|
|
84
|
+
...(chainEffort ? { primaryEffort: chainEffort } : {}),
|
|
85
|
+
...(chainFallbacks ? { fallbacks: chainFallbacks } : {}),
|
|
58
86
|
},
|
|
59
87
|
};
|
|
60
88
|
}
|
package/dist/settings.d.ts
CHANGED
|
@@ -5,36 +5,25 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Context } from '@deepseek-ai/cordis';
|
|
7
7
|
import Schema from '@deepseek-ai/schemastery';
|
|
8
|
-
import { EFFORT_CHOICES } from './config.js';
|
|
9
|
-
import type { DistillBudgetLayer } from './llm.js';
|
|
10
8
|
import type { MemoryLogger } from './types.js';
|
|
11
|
-
|
|
12
|
-
export type EffortChoice
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/** 蒸馏模型运行时覆盖(模型 id):'' = 跟随静态 config/默认选择。 */
|
|
30
|
-
distillModel: string;
|
|
31
|
-
/** 分层输出预算运行时覆盖(token):extract/dedup/l2/l3 四层,0 = 跟随内置默认;
|
|
32
|
-
* 思考档 high/max 的 ×4 放大在覆盖值之上照常生效。 */
|
|
33
|
-
distillBudgets: DistillBudgets;
|
|
34
|
-
/** 输入预算运行时覆盖(字符,≈token):单次蒸馏调用的输入上限,L1 按此分块、
|
|
35
|
-
* 超限截断;0 = 跟随静态配置 llm.maxInputChars。 */
|
|
36
|
-
distillMaxInputChars: number;
|
|
37
|
-
}
|
|
9
|
+
import type { DistillChainEntry, MemoryLiveSettings } from './contract.js';
|
|
10
|
+
export type { DistillBudgets, DistillChainEntry, EffortChoice, MemoryLiveSettings } from './contract.js';
|
|
11
|
+
/**
|
|
12
|
+
* 运行时统一路由链条目:[0] = 主路由(provider/model 双空 = 跟随默认模型),
|
|
13
|
+
* [1..] = 回退链(按序降级);reasoningEffort 为该路由的档位覆盖('' = 跟随部署全局)。
|
|
14
|
+
*/
|
|
15
|
+
/** 运行时路由链上限(写入门与 UI 同限,防误粘贴巨数组撑爆 settings 存储)。 */
|
|
16
|
+
export declare const DISTILL_CHAIN_MAX = 8;
|
|
17
|
+
/**
|
|
18
|
+
* 运行时统一路由链的**展示投影**(llm-providers 的 chain.current 数据源):
|
|
19
|
+
* distillChain 非空即原样返回;为空时投影旧运行时键(distillProvider/distillModel
|
|
20
|
+
* 成对 → 单行主路由,旧档位 reasoningEffort 作为该主路由的档位——旧语义里它
|
|
21
|
+
* 作用的就是当时唯一的路由)。注意:生效逻辑(effectiveCfg)只认显式
|
|
22
|
+
* distillChain、不走本投影——旧键路径在未配链时按旧语义原样生效。
|
|
23
|
+
*/
|
|
24
|
+
export declare function projectDistillChain(s: Partial<MemoryLiveSettings> | undefined): DistillChainEntry[];
|
|
25
|
+
/** settings-set 写入门校验:返回错误文案(null = 通过)。 */
|
|
26
|
+
export declare function validateDistillChain(chain: unknown): string | null;
|
|
38
27
|
export interface LiveSettingsHandle {
|
|
39
28
|
/** settings 服务是否可用(不可用时 UI 侧隐藏开关面板) */
|
|
40
29
|
supported: boolean;
|
package/dist/settings.js
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery';
|
|
2
2
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
3
3
|
import { EFFORT_CHOICES } from './config.js';
|
|
4
|
+
/**
|
|
5
|
+
* 运行时统一路由链条目:[0] = 主路由(provider/model 双空 = 跟随默认模型),
|
|
6
|
+
* [1..] = 回退链(按序降级);reasoningEffort 为该路由的档位覆盖('' = 跟随部署全局)。
|
|
7
|
+
*/
|
|
8
|
+
/** 运行时路由链上限(写入门与 UI 同限,防误粘贴巨数组撑爆 settings 存储)。 */
|
|
9
|
+
export const DISTILL_CHAIN_MAX = 8;
|
|
10
|
+
/**
|
|
11
|
+
* 运行时统一路由链的**展示投影**(llm-providers 的 chain.current 数据源):
|
|
12
|
+
* distillChain 非空即原样返回;为空时投影旧运行时键(distillProvider/distillModel
|
|
13
|
+
* 成对 → 单行主路由,旧档位 reasoningEffort 作为该主路由的档位——旧语义里它
|
|
14
|
+
* 作用的就是当时唯一的路由)。注意:生效逻辑(effectiveCfg)只认显式
|
|
15
|
+
* distillChain、不走本投影——旧键路径在未配链时按旧语义原样生效。
|
|
16
|
+
*/
|
|
17
|
+
export function projectDistillChain(s) {
|
|
18
|
+
if (s?.distillChain?.length)
|
|
19
|
+
return s.distillChain;
|
|
20
|
+
if (s?.distillProvider && s?.distillModel) {
|
|
21
|
+
return [{ provider: s.distillProvider, model: s.distillModel, reasoningEffort: s.reasoningEffort || '' }];
|
|
22
|
+
}
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
/** settings-set 写入门校验:返回错误文案(null = 通过)。 */
|
|
26
|
+
export function validateDistillChain(chain) {
|
|
27
|
+
if (!Array.isArray(chain))
|
|
28
|
+
return 'distillChain 须为数组';
|
|
29
|
+
if (chain.length > DISTILL_CHAIN_MAX)
|
|
30
|
+
return `路由链最多 ${DISTILL_CHAIN_MAX} 条`;
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
for (let i = 0; i < chain.length; i++) {
|
|
33
|
+
if (!chain[i] || typeof chain[i] !== 'object')
|
|
34
|
+
return `第 ${i + 1} 行须为对象`;
|
|
35
|
+
const e = chain[i];
|
|
36
|
+
const p = typeof e.provider === 'string' ? e.provider : '';
|
|
37
|
+
const m = typeof e.model === 'string' ? e.model : '';
|
|
38
|
+
const eff = typeof e.reasoningEffort === 'string' ? e.reasoningEffort : '';
|
|
39
|
+
if (p.length > 200 || m.length > 200)
|
|
40
|
+
return `第 ${i + 1} 行 provider/model 过长(≤200 字符)`;
|
|
41
|
+
if (!EFFORT_CHOICES.includes(eff))
|
|
42
|
+
return `第 ${i + 1} 行思考档位非法: ${eff || '(空)'}`;
|
|
43
|
+
if (i === 0) {
|
|
44
|
+
if ((p && !m) || (!p && m))
|
|
45
|
+
return '主路由行 provider 与 model 须成对(双空 = 跟随默认模型)';
|
|
46
|
+
}
|
|
47
|
+
else if (!p || !m) {
|
|
48
|
+
return `第 ${i + 1} 行回退路由必须显式选择供应商与模型`;
|
|
49
|
+
}
|
|
50
|
+
if (p && m) {
|
|
51
|
+
const key = `${p}::${m}`;
|
|
52
|
+
if (seen.has(key))
|
|
53
|
+
return `第 ${i + 1} 行与前面的路由重复(${p}/${m})`;
|
|
54
|
+
seen.add(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
4
59
|
const NS = settingsNamespace('dsh-memory');
|
|
5
60
|
const ALWAYS_ON = {
|
|
6
61
|
enabled: true,
|
|
@@ -10,6 +65,7 @@ const ALWAYS_ON = {
|
|
|
10
65
|
reasoningEffort: '',
|
|
11
66
|
distillProvider: '',
|
|
12
67
|
distillModel: '',
|
|
68
|
+
distillChain: [],
|
|
13
69
|
distillBudgets: { extract: 0, dedup: 0, l2: 0, l3: 0 },
|
|
14
70
|
distillMaxInputChars: 0,
|
|
15
71
|
};
|
|
@@ -40,6 +96,11 @@ export function liveSettingsSchema() {
|
|
|
40
96
|
reasoningEffort: Schema.union([...EFFORT_CHOICES]).default(''),
|
|
41
97
|
distillProvider: Schema.string().default(''),
|
|
42
98
|
distillModel: Schema.string().default(''),
|
|
99
|
+
distillChain: Schema.array(Schema.object({
|
|
100
|
+
provider: Schema.string().default(''),
|
|
101
|
+
model: Schema.string().default(''),
|
|
102
|
+
reasoningEffort: Schema.union([...EFFORT_CHOICES]).default(''),
|
|
103
|
+
})).default([]),
|
|
43
104
|
distillBudgets: Schema.object({
|
|
44
105
|
extract: budget(),
|
|
45
106
|
dedup: budget(),
|
|
@@ -153,10 +214,28 @@ export function registerLiveSettings(ctx, logger) {
|
|
|
153
214
|
/** scope.get() 的防御性解析:异常值回退全开(宁可多记不可静默停摆)。 */
|
|
154
215
|
function resolveSettings(value) {
|
|
155
216
|
if (!value || typeof value !== 'object')
|
|
156
|
-
return { ...ALWAYS_ON };
|
|
217
|
+
return { ...ALWAYS_ON, distillChain: [] };
|
|
157
218
|
const v = value;
|
|
158
219
|
const num = (x) => (typeof x === 'number' && Number.isFinite(x) && x >= 0 ? Math.floor(x) : 0);
|
|
159
220
|
const rawBudgets = (v.distillBudgets ?? {});
|
|
221
|
+
// 路由链逐条防御:非对象条目剔除、超长截断、非法档位归空、超限截断到上限
|
|
222
|
+
const rawChain = Array.isArray(v.distillChain) ? v.distillChain : [];
|
|
223
|
+
const chain = [];
|
|
224
|
+
for (const item of rawChain) {
|
|
225
|
+
if (chain.length >= DISTILL_CHAIN_MAX)
|
|
226
|
+
break;
|
|
227
|
+
if (!item || typeof item !== 'object')
|
|
228
|
+
continue;
|
|
229
|
+
const e = item;
|
|
230
|
+
const eff = typeof e.reasoningEffort === 'string' && EFFORT_CHOICES.includes(e.reasoningEffort)
|
|
231
|
+
? e.reasoningEffort
|
|
232
|
+
: '';
|
|
233
|
+
chain.push({
|
|
234
|
+
provider: typeof e.provider === 'string' ? e.provider.slice(0, 200) : '',
|
|
235
|
+
model: typeof e.model === 'string' ? e.model.slice(0, 200) : '',
|
|
236
|
+
reasoningEffort: eff,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
160
239
|
return {
|
|
161
240
|
enabled: v.enabled !== false,
|
|
162
241
|
capture: v.capture !== false,
|
|
@@ -167,6 +246,7 @@ function resolveSettings(value) {
|
|
|
167
246
|
: '',
|
|
168
247
|
distillProvider: typeof v.distillProvider === 'string' ? v.distillProvider : '',
|
|
169
248
|
distillModel: typeof v.distillModel === 'string' ? v.distillModel : '',
|
|
249
|
+
distillChain: chain,
|
|
170
250
|
distillBudgets: {
|
|
171
251
|
extract: num(rawBudgets.extract),
|
|
172
252
|
dedup: num(rawBudgets.dedup),
|