dsh-layered-memory 0.8.2 → 0.8.4

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/dist/config.d.ts CHANGED
@@ -5,6 +5,13 @@
5
5
  */
6
6
  import Schema from '@deepseek-ai/schemastery';
7
7
  import type { ExtractMode } from './types.js';
8
+ /**
9
+ * 蒸馏思考档位全词汇表(唯一事实源):'' = 自动(模型默认档 → high),
10
+ * 其余为各适配器通用档位词汇表(deepseek 认 'off',OpenAI 系是 'none')。
11
+ * schema(config/settings)、运行时解析(settings.resolveSettings)与 RPC
12
+ * 写入门(stats.settings-set)共用,勿在别处再抄字面量表。
13
+ */
14
+ export declare const EFFORT_CHOICES: readonly ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"];
8
15
  export interface MemoryConfig {
9
16
  /** 数据目录;留空则用 $DSH_HOME/memory。 */
10
17
  dataDir: string;
@@ -198,7 +205,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
198
205
  provider: Schema<string, string>;
199
206
  model: Schema<string, string>;
200
207
  maxTokens: Schema<number, number>;
201
- reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
208
+ reasoningEffort: Schema<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
202
209
  temperature: Schema<number, number>;
203
210
  maxInputChars: Schema<number, number>;
204
211
  timeoutMs: Schema<number, number>;
@@ -206,7 +213,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
206
213
  provider: Schema<string, string>;
207
214
  model: Schema<string, string>;
208
215
  maxTokens: Schema<number, number>;
209
- reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
216
+ reasoningEffort: Schema<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
210
217
  temperature: Schema<number, number>;
211
218
  maxInputChars: Schema<number, number>;
212
219
  timeoutMs: Schema<number, number>;
@@ -303,7 +310,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
303
310
  provider: Schema<string, string>;
304
311
  model: Schema<string, string>;
305
312
  maxTokens: Schema<number, number>;
306
- reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
313
+ reasoningEffort: Schema<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
307
314
  temperature: Schema<number, number>;
308
315
  maxInputChars: Schema<number, number>;
309
316
  timeoutMs: Schema<number, number>;
@@ -311,7 +318,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
311
318
  provider: Schema<string, string>;
312
319
  model: Schema<string, string>;
313
320
  maxTokens: Schema<number, number>;
314
- reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
321
+ reasoningEffort: Schema<"" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max", "" | "off" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max">;
315
322
  temperature: Schema<number, number>;
316
323
  maxInputChars: Schema<number, number>;
317
324
  timeoutMs: Schema<number, number>;
package/dist/config.js CHANGED
@@ -5,6 +5,13 @@
5
5
  */
6
6
  import Schema from '@deepseek-ai/schemastery';
7
7
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
8
+ /**
9
+ * 蒸馏思考档位全词汇表(唯一事实源):'' = 自动(模型默认档 → high),
10
+ * 其余为各适配器通用档位词汇表(deepseek 认 'off',OpenAI 系是 'none')。
11
+ * schema(config/settings)、运行时解析(settings.resolveSettings)与 RPC
12
+ * 写入门(stats.settings-set)共用,勿在别处再抄字面量表。
13
+ */
14
+ export const EFFORT_CHOICES = ['', 'off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
8
15
  export const memorySchema = Schema.object({
9
16
  dataDir: Schema.string().default(''),
10
17
  family: Schema.union(['auto', 'chat', 'work']).default('auto'),
@@ -61,9 +68,10 @@ export const memorySchema = Schema.object({
61
68
  // 推理模型(如 v4-flash)的 reasoning 计入输出预算:预算不足会被思考吃光导致正文 0 字符。
62
69
  // 0.8.0 起各蒸馏层显式传分层预算(见 llm.ts LAYER_MAX_TOKENS_*),本值为未分层调用的兜底总闸
63
70
  maxTokens: Schema.number().min(1024).max(1_000_000).default(65_536),
64
- // 蒸馏是结构化抽取任务,默认关思考(off):v4-flash 默认 high 档的思考可把任意 maxTokens
65
- // 预算全部吃光导致正文 0 字符;非推理模型不认识 effort 时会报 UNSUPPORTED_REASONING_EFFORT,设空串跳过
66
- reasoningEffort: Schema.union(['', 'off', 'high', 'max']).default('off'),
71
+ // 蒸馏思考档位:'' = 自动(按模型能力解析:模型默认档 → high,见 llm.ts decideSendableEffort);
72
+ // 显式值仅在该模型声明支持时发送(跨供应商 effort 词汇表不同:deepseek 'off',
73
+ // openai 系是 'none',未声明档位的模型不传)。旧默认 'off' 在非 deepseek 模型上必炸(400/本地拒绝)
74
+ reasoningEffort: Schema.union([...EFFORT_CHOICES]).default(''),
67
75
  temperature: Schema.number().min(0).max(2).default(0.3),
68
76
  // 模型上下文 1M token,日常压到 ~700k 使用(中文按 1 字≈1 token 保守折算)
69
77
  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
@@ -28,7 +28,7 @@ export declare const LAYER_DEFAULT_BUDGETS: Record<DistillBudgetLayer, number>;
28
28
  /**
29
29
  * 解析某蒸馏层的生效输出预算:运行时覆盖(cfg.llm.budgets,由 effectiveCfg 从
30
30
  * 设置页 distillBudgets 注入,0/缺省 = 跟随)→ 内置默认 → 思考档放大
31
- * (high/max ×4,reasoning 计入输出预算的历史事故防线)。
31
+ * (high/xhigh/max ×4,reasoning 计入输出预算的历史事故防线)。
32
32
  */
33
33
  export declare function resolveLayerTokens(cfg: {
34
34
  llm: {
@@ -36,9 +36,15 @@ export declare function resolveLayerTokens(cfg: {
36
36
  budgets?: Partial<Record<DistillBudgetLayer, number>>;
37
37
  };
38
38
  }, layer: DistillBudgetLayer): number;
39
+ /**
40
+ * 高思考档集合(输出预算 ×4 的档位):阶段侧 layerMaxTokens 与 callLLM 的
41
+ * 自动档防线共用同一张表——此前两处字面量表分叉(防线漏 xhigh),显式 xhigh
42
+ * 配置被双重放大 ×16。勿再在别处抄写该列表。
43
+ */
44
+ export declare const HIGH_EFFORT_TIERS: readonly ["high", "xhigh", "max"];
39
45
  /**
40
46
  * 思考档预算放大:reasoning 计入输出预算(v4-flash 事故:high 思考可吃光全部
41
- * 预算致正文 0 字符)——effort 为 high/max 时分层预算 ×4。
47
+ * 预算致正文 0 字符)——effort 为 high/xhigh/max 时分层预算 ×4。
42
48
  */
43
49
  export declare function layerMaxTokens(base: number, reasoningEffort: string): number;
44
50
  /** 解析蒸馏用的 provider/model:配置优先,其次当前默认选择。 */
@@ -46,6 +52,26 @@ export declare function resolveModelRoute(ctx: Context, cfg: MemoryConfig): Prom
46
52
  provider: string;
47
53
  model: string;
48
54
  }>;
55
+ export interface ModelEffortInfo {
56
+ /** 模型可设置的思考档位 id(适配器声明;空 = 未声明/不可设置) */
57
+ efforts: string[];
58
+ /** 适配器配置的默认档位(省略 effort 时的请求值) */
59
+ defaultEffort?: string;
60
+ }
61
+ /** 清空能力缓存(llm/adapters-updated 时调用:供应商增删/改配置后重新探询)。 */
62
+ export declare function invalidateEffortCache(): void;
63
+ /** 探询某模型的思考档位能力;失败返回 null(调用方保持旧发送行为,不改判)。 */
64
+ export declare function resolveModelEfforts(ctx: Context, provider: string, model: string): Promise<ModelEffortInfo | null>;
65
+ export type EffortDecisionReason = 'supported' | 'auto-default' | 'auto-high' | 'alias-none' | 'unsupported' | 'no-efforts' | 'no-capability';
66
+ export interface EffortDecision {
67
+ /** 实际发送的档位;'' = 不发送(跟随模型默认) */
68
+ effort: string;
69
+ reason: EffortDecisionReason;
70
+ }
71
+ /** 纯决策:配置档位 + 模型能力 → 实际发送值(callLLM 与 settings-get 共用)。 */
72
+ export declare function decideSendableEffort(cap: ModelEffortInfo | null, cfgEffort: string): EffortDecision;
73
+ /** 探询 + 决策 + 一次性告警(不支持/未声明时提示降级,不刷屏)。 */
74
+ export declare function planDistillEffort(ctx: Context, provider: string, model: string, cfgEffort: string, logger?: MemoryLogger): Promise<EffortDecision>;
49
75
  /**
50
76
  * 一次完整蒸馏调用:流式收集文本,返回最终字符串。
51
77
  * 失败(error/aborted finish)抛错,由调用方兜底。
package/dist/llm.js CHANGED
@@ -20,18 +20,24 @@ export const LAYER_DEFAULT_BUDGETS = {
20
20
  /**
21
21
  * 解析某蒸馏层的生效输出预算:运行时覆盖(cfg.llm.budgets,由 effectiveCfg 从
22
22
  * 设置页 distillBudgets 注入,0/缺省 = 跟随)→ 内置默认 → 思考档放大
23
- * (high/max ×4,reasoning 计入输出预算的历史事故防线)。
23
+ * (high/xhigh/max ×4,reasoning 计入输出预算的历史事故防线)。
24
24
  */
25
25
  export function resolveLayerTokens(cfg, layer) {
26
26
  const override = cfg.llm.budgets?.[layer];
27
27
  return layerMaxTokens(override && override > 0 ? override : LAYER_DEFAULT_BUDGETS[layer], cfg.llm.reasoningEffort);
28
28
  }
29
+ /**
30
+ * 高思考档集合(输出预算 ×4 的档位):阶段侧 layerMaxTokens 与 callLLM 的
31
+ * 自动档防线共用同一张表——此前两处字面量表分叉(防线漏 xhigh),显式 xhigh
32
+ * 配置被双重放大 ×16。勿再在别处抄写该列表。
33
+ */
34
+ export const HIGH_EFFORT_TIERS = ['high', 'xhigh', 'max'];
29
35
  /**
30
36
  * 思考档预算放大:reasoning 计入输出预算(v4-flash 事故:high 思考可吃光全部
31
- * 预算致正文 0 字符)——effort 为 high/max 时分层预算 ×4。
37
+ * 预算致正文 0 字符)——effort 为 high/xhigh/max 时分层预算 ×4。
32
38
  */
33
39
  export function layerMaxTokens(base, reasoningEffort) {
34
- return reasoningEffort === 'high' || reasoningEffort === 'max' ? base * 4 : base;
40
+ return HIGH_EFFORT_TIERS.includes(reasoningEffort) ? base * 4 : base;
35
41
  }
36
42
  /** 解析蒸馏用的 provider/model:配置优先,其次当前默认选择。 */
37
43
  export async function resolveModelRoute(ctx, cfg) {
@@ -46,6 +52,75 @@ export async function resolveModelRoute(ctx, cfg) {
46
52
  }
47
53
  throw new Error('无法解析蒸馏模型路由:请在插件 config 中配置 llm.provider / llm.model,或确保存在默认模型选择');
48
54
  }
55
+ const effortCache = new Map();
56
+ /** 清空能力缓存(llm/adapters-updated 时调用:供应商增删/改配置后重新探询)。 */
57
+ export function invalidateEffortCache() {
58
+ effortCache.clear();
59
+ effortWarned.clear();
60
+ }
61
+ /** 探询某模型的思考档位能力;失败返回 null(调用方保持旧发送行为,不改判)。 */
62
+ export async function resolveModelEfforts(ctx, provider, model) {
63
+ const key = `${provider}::${model}`;
64
+ const hit = effortCache.get(key);
65
+ if (hit)
66
+ return hit;
67
+ try {
68
+ if (typeof ctx.llm?.resolveModelInfo !== 'function')
69
+ return null;
70
+ const info = await ctx.llm.resolveModelInfo(provider, model);
71
+ const efforts = (info.reasoning?.efforts ?? [])
72
+ .map((e) => String(e.id))
73
+ .filter((id) => id.length > 0);
74
+ const cap = {
75
+ efforts,
76
+ ...(info.reasoning?.defaultEffort ? { defaultEffort: String(info.reasoning.defaultEffort) } : {}),
77
+ };
78
+ effortCache.set(key, cap);
79
+ return cap;
80
+ }
81
+ catch {
82
+ return null; // 不缓存失败:路由尚未注册等瞬时态,下次调用重试
83
+ }
84
+ }
85
+ /** 纯决策:配置档位 + 模型能力 → 实际发送值(callLLM 与 settings-get 共用)。 */
86
+ export function decideSendableEffort(cap, cfgEffort) {
87
+ if (!cap)
88
+ return { effort: cfgEffort, reason: 'no-capability' };
89
+ if (cfgEffort) {
90
+ if (cap.efforts.includes(cfgEffort))
91
+ return { effort: cfgEffort, reason: 'supported' };
92
+ if (cfgEffort === 'off' && cap.efforts.includes('none'))
93
+ return { effort: 'none', reason: 'alias-none' };
94
+ if (cap.efforts.length === 0)
95
+ return { effort: '', reason: 'no-efforts' };
96
+ return { effort: '', reason: 'unsupported' };
97
+ }
98
+ // 空配置 = 自动:模型默认档 → 无默认取 high(用户规则:未声明/无默认一律 high)→ 仍无则不传
99
+ if (cap.defaultEffort && cap.efforts.includes(cap.defaultEffort)) {
100
+ return { effort: cap.defaultEffort, reason: 'auto-default' };
101
+ }
102
+ if (cap.efforts.includes('high'))
103
+ return { effort: 'high', reason: 'auto-high' };
104
+ return { effort: '', reason: 'no-efforts' };
105
+ }
106
+ const effortWarned = new Set();
107
+ /** 探询 + 决策 + 一次性告警(不支持/未声明时提示降级,不刷屏)。 */
108
+ export async function planDistillEffort(ctx, provider, model, cfgEffort, logger) {
109
+ const cap = await resolveModelEfforts(ctx, provider, model);
110
+ const d = decideSendableEffort(cap, cfgEffort);
111
+ if ((d.reason === 'unsupported' || d.reason === 'no-efforts') && logger) {
112
+ const key = `${provider}::${model}::${cfgEffort}::${d.reason}`;
113
+ if (!effortWarned.has(key)) {
114
+ effortWarned.add(key);
115
+ logger.warn(`[memory] 蒸馏思考档位 ${cfgEffort || '(auto)'} 不被 ${provider}/${model} 支持` +
116
+ (d.reason === 'no-efforts'
117
+ ? '(模型未声明思考档位)'
118
+ : `(支持: ${cap?.efforts.join('/')})`) +
119
+ ',本次调用不传档位(跟随模型默认)');
120
+ }
121
+ }
122
+ return d;
123
+ }
49
124
  /**
50
125
  * 一次完整蒸馏调用:流式收集文本,返回最终字符串。
51
126
  * 失败(error/aborted finish)抛错,由调用方兜底。
@@ -56,22 +131,31 @@ export async function resolveModelRoute(ctx, cfg) {
56
131
  export async function callLLM(ctx, cfg, opts) {
57
132
  const { provider, model } = await resolveModelRoute(ctx, cfg);
58
133
  const signal = opts.signal ?? AbortSignal.timeout(cfg.llm.timeoutMs);
134
+ // 档位按模型能力决策(跨供应商 effort 兼容):不支持的档位不传 + 告警一次,
135
+ // 空配置 = 自动(模型默认档 → high);详见 decideSendableEffort
136
+ const effort = await planDistillEffort(ctx, provider, model, cfg.llm.reasoningEffort, opts.logger);
59
137
  // 输入预算兜底:任何蒸馏调用的用户 prompt 不超过 maxInputChars
60
138
  // (L1 已在数据层分块,这里是 L2/L3 与异常场景的最后一道网)
61
139
  const user = opts.user.length > cfg.llm.maxInputChars
62
140
  ? `${opts.user.slice(0, cfg.llm.maxInputChars)}\n\n[输入超出 ${cfg.llm.maxInputChars} 字符预算,已截断]`
63
141
  : opts.user;
142
+ // 输出预算 ×4 防线跟随【实际发送】的档位:阶段侧已按原始配置的高档位
143
+ // (HIGH_EFFORT_TIERS)放大过,这里只补自动档('' → 模型默认/高档)解析出
144
+ // 高档时的欠放大缺口——两侧共用一张表,配置本身就是高档时不再放大(防 ×16 双乘)
145
+ const baseMaxTokens = opts.maxTokens ?? cfg.llm.maxTokens;
146
+ const highTiers = HIGH_EFFORT_TIERS;
147
+ const maxTokens = highTiers.includes(effort.effort) && !highTiers.includes(cfg.llm.reasoningEffort)
148
+ ? layerMaxTokens(baseMaxTokens, 'high')
149
+ : baseMaxTokens;
64
150
  const stream = ctx.llm.stream({
65
151
  provider,
66
152
  model,
67
153
  system: opts.system,
68
154
  messages: [createUserMessage({ content: [{ type: 'text', text: user }], source: { kind: 'user' } })],
69
155
  temperature: opts.temperature ?? cfg.llm.temperature,
70
- maxTokens: opts.maxTokens ?? cfg.llm.maxTokens,
71
- // 默认 off:蒸馏是结构化抽取,high 思考可吃光全部输出预算致正文 0 字符;空串不传(非推理模型)
72
- ...(cfg.llm.reasoningEffort
73
- ? { reasoningEffort: ReasoningEffortId(cfg.llm.reasoningEffort) }
74
- : {}),
156
+ maxTokens,
157
+ // 档位只在能力决策给出非空值时传;空串不传(跟随模型默认)
158
+ ...(effort.effort ? { reasoningEffort: ReasoningEffortId(effort.effort) } : {}),
75
159
  signal,
76
160
  });
77
161
  const startedAt = Date.now();
@@ -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
- if (!eff && !override && !budgets && !maxInput)
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
- ...(eff ? { reasoningEffort: eff } : {}),
54
+ ...(live ? { reasoningEffort: eff } : {}),
51
55
  ...(override ?? {}),
52
56
  ...(budgets ? { budgets } : {}),
53
57
  ...(maxInput ? { maxInputChars: maxInput } : {}),
@@ -5,10 +5,11 @@
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';
8
9
  import type { DistillBudgetLayer } from './llm.js';
9
10
  import type { MemoryLogger } from './types.js';
10
- /** 蒸馏思考档位可选项:'' = 跟随静态 config(部署默认)。 */
11
- export type EffortChoice = '' | 'off' | 'high' | 'max';
11
+ /** 蒸馏思考档位:'' = 自动(模型默认档 → high);词汇表单源于 config.ts 的 EFFORT_CHOICES。 */
12
+ export type EffortChoice = (typeof EFFORT_CHOICES)[number];
12
13
  /** 分层输出预算(与 llm.ts 的 DistillBudgetLayer 同键;0 = 跟随内置默认)。 */
13
14
  export type DistillBudgets = Record<DistillBudgetLayer, number>;
14
15
  export interface MemoryLiveSettings {
package/dist/settings.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import Schema from '@deepseek-ai/schemastery';
2
2
  import { settingsNamespace } from '@deepseek-ai/dsh-settings';
3
+ import { EFFORT_CHOICES } from './config.js';
3
4
  const NS = settingsNamespace('dsh-memory');
4
5
  const ALWAYS_ON = {
5
6
  enabled: true,
@@ -36,7 +37,7 @@ export function liveSettingsSchema() {
36
37
  capture: Schema.boolean().default(true),
37
38
  distill: Schema.boolean().default(true),
38
39
  recall: Schema.boolean().default(true),
39
- reasoningEffort: Schema.union(['', 'off', 'high', 'max']).default(''),
40
+ reasoningEffort: Schema.union([...EFFORT_CHOICES]).default(''),
40
41
  distillProvider: Schema.string().default(''),
41
42
  distillModel: Schema.string().default(''),
42
43
  distillBudgets: Schema.object({
@@ -154,7 +155,6 @@ function resolveSettings(value) {
154
155
  if (!value || typeof value !== 'object')
155
156
  return { ...ALWAYS_ON };
156
157
  const v = value;
157
- const efforts = ['', 'off', 'high', '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 {
@@ -162,7 +162,7 @@ function resolveSettings(value) {
162
162
  capture: v.capture !== false,
163
163
  distill: v.distill !== false,
164
164
  recall: v.recall !== false,
165
- reasoningEffort: typeof v.reasoningEffort === 'string' && efforts.includes(v.reasoningEffort)
165
+ reasoningEffort: typeof v.reasoningEffort === 'string' && EFFORT_CHOICES.includes(v.reasoningEffort)
166
166
  ? v.reasoningEffort
167
167
  : '',
168
168
  distillProvider: typeof v.distillProvider === 'string' ? v.distillProvider : '',
package/dist/stats.js CHANGED
@@ -9,9 +9,9 @@
9
9
  import { createRequire } from 'node:module';
10
10
  import { closeSync, openSync, readSync, statSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
- import { resolveDataDir } from './config.js';
12
+ import { EFFORT_CHOICES, 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
- if (typeof p.sessionId !== 'string' || !p.sessionId)
145
- throw new Error('sessionId 缺失');
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
- if (typeof p.sessionId !== 'string' || !p.sessionId)
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(p.sessionId, p.mode);
159
- deps.logger.info(`[memory] 会话档位设置 session=${p.sessionId} mode=${p.mode}`);
160
- return { sessionId: p.sessionId, mode: p.mode };
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: s?.reasoningEffort || cfg.llm.reasoningEffort,
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: {
@@ -207,8 +234,10 @@ async function handleEndpoint(endpoint, payload, deps) {
207
234
  }
208
235
  if (patch.reasoningEffort !== undefined) {
209
236
  const v = String(patch.reasoningEffort);
210
- if (!['', 'off', 'high', 'max'].includes(v)) {
211
- throw new Error(`非法思考档位: ${v}(允许 ''/off/high/max)`);
237
+ // 白名单与 schema/settings 同源(config.ts EFFORT_CHOICES)——此前此处漏扩词表,
238
+ // 设置页新词汇(none/minimal/low/medium/xhigh)被拒并回滚
239
+ if (!EFFORT_CHOICES.includes(v)) {
240
+ throw new Error(`非法思考档位: ${v}(允许 '' 或 ${EFFORT_CHOICES.filter((x) => x !== '').join('/')})`);
212
241
  }
213
242
  clean.reasoningEffort = v;
214
243
  }
@@ -251,8 +280,10 @@ async function handleEndpoint(endpoint, payload, deps) {
251
280
  }
252
281
  case 'dsh-memory/list-records': {
253
282
  const p = (payload ?? {});
283
+ if (p.query !== undefined && p.query.length > 4096)
284
+ throw new Error('query 过长(≤4096 字符)');
254
285
  const limit = Math.min(Math.max(Number(p.limit) || 50, 1), 200);
255
- const offset = Math.max(Number(p.offset) || 0, 0);
286
+ const offset = Math.min(Math.max(Number(p.offset) || 0, 0), 1_000_000);
256
287
  // 关键词路径:复用检索唯一缝(与召回同源),取回后做场景过滤 + 手工分页。
257
288
  // 检索侧单次上限 200:分页窗口触达上限时显式标记 truncated(结果可能不完整),
258
289
  // 不再静默返回空结果让用户误以为"没有更多"等于"不存在更多"。
@@ -371,6 +402,8 @@ async function handleEndpoint(endpoint, payload, deps) {
371
402
  const p = (payload ?? {});
372
403
  if (typeof p.provider !== 'string' || !p.provider)
373
404
  throw new Error('provider 缺失');
405
+ if (p.provider.length > 200)
406
+ throw new Error('provider 过长(≤200 字符)');
374
407
  // 两个内置适配器(deepseek/pi-ai)的 listModels 都读本地快照不触网;
375
408
  // 仍加超时兜底,防第三方适配器实现为远端查询拖死 RPC 轮询
376
409
  const models = await Promise.race([
@@ -395,6 +428,9 @@ async function handleEndpoint(endpoint, payload, deps) {
395
428
  if (p.source !== 'remote' && p.source !== 'local' && p.source !== 'off') {
396
429
  throw new Error('source 必须是 remote | local | off');
397
430
  }
431
+ if (typeof p.activeModel === 'string' && p.activeModel.length > 200) {
432
+ throw new Error('activeModel 过长(≤200 字符)');
433
+ }
398
434
  const r = embedManager.requestSource({ source: p.source, activeModel: p.activeModel ?? null });
399
435
  if (!r.accepted)
400
436
  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
- this.agent = new ProxyAgent(proxy);
44
- opts.logger?.info(`[memory] 模型下载走代理 ${proxy}(镜像直连在国内网络间歇不可达)`);
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;