dsh-layered-memory 0.8.0 → 0.8.2
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 +54 -11
- package/README.md +51 -9
- package/assets/readme/bench-dialog.svg +87 -0
- package/assets/readme/bench-workflow.svg +79 -0
- package/assets/readme/flow.svg +74 -74
- package/assets/readme/storage.svg +56 -56
- package/dist/client.js +369 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10 -3
- package/dist/llm.d.ts +15 -0
- package/dist/llm.js +16 -0
- package/dist/pipeline/l1.js +3 -3
- package/dist/pipeline/l2.js +2 -2
- package/dist/pipeline/l3.js +2 -2
- package/dist/pipeline/rebuild.js +2 -2
- package/dist/pipeline/runner.d.ts +9 -3
- package/dist/pipeline/runner.js +37 -4
- package/dist/settings.d.ts +14 -0
- package/dist/settings.js +41 -2
- package/dist/stats.js +113 -1
- package/dist/store/download-queue.d.ts +35 -2
- package/dist/store/download-queue.js +102 -5
- package/dist/store/model-catalog.js +1 -1
- package/package.json +14 -12
|
@@ -36,10 +36,16 @@ export interface PipelineTask {
|
|
|
36
36
|
/** 选取下一个要执行的任务下标:最早的 live 优先,否则队首(rebuild 分块让位)。 */
|
|
37
37
|
export declare function pickNextTaskIndex(tasks: PipelineTask[]): number;
|
|
38
38
|
/**
|
|
39
|
-
* 运行时调参视图:UI
|
|
40
|
-
*
|
|
39
|
+
* 运行时调参视图:UI 选择器可临时覆盖蒸馏思考档位、蒸馏模型路由与分层输出预算
|
|
40
|
+
* (空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm 一层,其余键与原 cfg
|
|
41
|
+
* 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
42
|
+
*
|
|
43
|
+
* 蒸馏模型覆盖优先级:部署静态 pin(cfg.llm.provider+model 双字段齐)不可被
|
|
44
|
+
* 运行时覆盖(部署可强制蒸馏走内网路由,防用户选择把对话外送);未 pin 时
|
|
45
|
+
* 运行时选择(distillProvider+distillModel 成对)生效;再退 agentDefaultModel。
|
|
46
|
+
* 输出预算无部署上限语义:非零运行时值直接注入 cfg.llm.budgets(0/缺省不注入)。
|
|
41
47
|
*/
|
|
42
|
-
export declare function effectiveCfg(cfg: MemoryConfig, live
|
|
48
|
+
export declare function effectiveCfg(cfg: MemoryConfig, live?: LiveSettingsHandle): MemoryConfig;
|
|
43
49
|
export declare class MemoryRunner {
|
|
44
50
|
private readonly ctx;
|
|
45
51
|
private readonly cfg;
|
package/dist/pipeline/runner.js
CHANGED
|
@@ -14,12 +14,45 @@ export function pickNextTaskIndex(tasks) {
|
|
|
14
14
|
return 0;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* 运行时调参视图:UI
|
|
18
|
-
*
|
|
17
|
+
* 运行时调参视图:UI 选择器可临时覆盖蒸馏思考档位、蒸馏模型路由与分层输出预算
|
|
18
|
+
* (空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm 一层,其余键与原 cfg
|
|
19
|
+
* 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
20
|
+
*
|
|
21
|
+
* 蒸馏模型覆盖优先级:部署静态 pin(cfg.llm.provider+model 双字段齐)不可被
|
|
22
|
+
* 运行时覆盖(部署可强制蒸馏走内网路由,防用户选择把对话外送);未 pin 时
|
|
23
|
+
* 运行时选择(distillProvider+distillModel 成对)生效;再退 agentDefaultModel。
|
|
24
|
+
* 输出预算无部署上限语义:非零运行时值直接注入 cfg.llm.budgets(0/缺省不注入)。
|
|
19
25
|
*/
|
|
20
26
|
export function effectiveCfg(cfg, live) {
|
|
21
|
-
const
|
|
22
|
-
|
|
27
|
+
const s = live?.get();
|
|
28
|
+
const eff = s?.reasoningEffort ?? '';
|
|
29
|
+
// 可选链防御:smoke/测试缝构造的最小 cfg 可能没有 llm 字段
|
|
30
|
+
const pinned = Boolean(cfg.llm?.provider && cfg.llm?.model);
|
|
31
|
+
const override = s && !pinned && s.distillProvider && s.distillModel
|
|
32
|
+
? { provider: s.distillProvider, model: s.distillModel }
|
|
33
|
+
: null;
|
|
34
|
+
const b = s?.distillBudgets;
|
|
35
|
+
const budgets = b && (b.extract > 0 || b.dedup > 0 || b.l2 > 0 || b.l3 > 0)
|
|
36
|
+
? {
|
|
37
|
+
...(b.extract > 0 ? { extract: b.extract } : {}),
|
|
38
|
+
...(b.dedup > 0 ? { dedup: b.dedup } : {}),
|
|
39
|
+
...(b.l2 > 0 ? { l2: b.l2 } : {}),
|
|
40
|
+
...(b.l3 > 0 ? { l3: b.l3 } : {}),
|
|
41
|
+
}
|
|
42
|
+
: null;
|
|
43
|
+
const maxInput = s && s.distillMaxInputChars > 0 ? s.distillMaxInputChars : null;
|
|
44
|
+
if (!eff && !override && !budgets && !maxInput)
|
|
45
|
+
return cfg;
|
|
46
|
+
return {
|
|
47
|
+
...cfg,
|
|
48
|
+
llm: {
|
|
49
|
+
...cfg.llm,
|
|
50
|
+
...(eff ? { reasoningEffort: eff } : {}),
|
|
51
|
+
...(override ?? {}),
|
|
52
|
+
...(budgets ? { budgets } : {}),
|
|
53
|
+
...(maxInput ? { maxInputChars: maxInput } : {}),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
23
56
|
}
|
|
24
57
|
/** 单桶堆积上限(防无限堆积;重建分块不受限——历史会话需全量入桶蒸馏)。 */
|
|
25
58
|
const PENDING_BUCKET_CAP = 200;
|
package/dist/settings.d.ts
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Context } from '@deepseek-ai/cordis';
|
|
7
7
|
import Schema from '@deepseek-ai/schemastery';
|
|
8
|
+
import type { DistillBudgetLayer } from './llm.js';
|
|
8
9
|
import type { MemoryLogger } from './types.js';
|
|
9
10
|
/** 蒸馏思考档位可选项:'' = 跟随静态 config(部署默认)。 */
|
|
10
11
|
export type EffortChoice = '' | 'off' | 'high' | 'max';
|
|
12
|
+
/** 分层输出预算(与 llm.ts 的 DistillBudgetLayer 同键;0 = 跟随内置默认)。 */
|
|
13
|
+
export type DistillBudgets = Record<DistillBudgetLayer, number>;
|
|
11
14
|
export interface MemoryLiveSettings {
|
|
12
15
|
/** 总开关:关 = 捕获/蒸馏/召回注入全停(数据保留) */
|
|
13
16
|
enabled: boolean;
|
|
@@ -19,6 +22,17 @@ export interface MemoryLiveSettings {
|
|
|
19
22
|
recall: boolean;
|
|
20
23
|
/** 蒸馏思考档位运行时覆盖:'' = 跟随静态 config(llm.reasoningEffort) */
|
|
21
24
|
reasoningEffort: EffortChoice;
|
|
25
|
+
/** 蒸馏模型运行时覆盖(供应商 id,用户已配置的路由):'' = 跟随静态 config/默认选择。
|
|
26
|
+
* 与 distillModel 成对生效(单字段不算);部署静态 pin(provider+model 双字段)优先。 */
|
|
27
|
+
distillProvider: string;
|
|
28
|
+
/** 蒸馏模型运行时覆盖(模型 id):'' = 跟随静态 config/默认选择。 */
|
|
29
|
+
distillModel: string;
|
|
30
|
+
/** 分层输出预算运行时覆盖(token):extract/dedup/l2/l3 四层,0 = 跟随内置默认;
|
|
31
|
+
* 思考档 high/max 的 ×4 放大在覆盖值之上照常生效。 */
|
|
32
|
+
distillBudgets: DistillBudgets;
|
|
33
|
+
/** 输入预算运行时覆盖(字符,≈token):单次蒸馏调用的输入上限,L1 按此分块、
|
|
34
|
+
* 超限截断;0 = 跟随静态配置 llm.maxInputChars。 */
|
|
35
|
+
distillMaxInputChars: number;
|
|
22
36
|
}
|
|
23
37
|
export interface LiveSettingsHandle {
|
|
24
38
|
/** settings 服务是否可用(不可用时 UI 侧隐藏开关面板) */
|
package/dist/settings.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery';
|
|
2
2
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
3
3
|
const NS = settingsNamespace('dsh-memory');
|
|
4
|
-
const ALWAYS_ON = {
|
|
4
|
+
const ALWAYS_ON = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
capture: true,
|
|
7
|
+
distill: true,
|
|
8
|
+
recall: true,
|
|
9
|
+
reasoningEffort: '',
|
|
10
|
+
distillProvider: '',
|
|
11
|
+
distillModel: '',
|
|
12
|
+
distillBudgets: { extract: 0, dedup: 0, l2: 0, l3: 0 },
|
|
13
|
+
distillMaxInputChars: 0,
|
|
14
|
+
};
|
|
5
15
|
/**
|
|
6
16
|
* 进程内 scope 复用(fiber 重启重挂)。
|
|
7
17
|
* dsh-settings 的 register 把注册挂在其**服务自身 ctx** 的 effect 上
|
|
@@ -20,12 +30,22 @@ let cachedScope;
|
|
|
20
30
|
let cachedUnwatch;
|
|
21
31
|
let cachedSvc;
|
|
22
32
|
export function liveSettingsSchema() {
|
|
33
|
+
const budget = () => Schema.number().min(0).max(1_000_000).default(0);
|
|
23
34
|
return Schema.object({
|
|
24
35
|
enabled: Schema.boolean().default(true),
|
|
25
36
|
capture: Schema.boolean().default(true),
|
|
26
37
|
distill: Schema.boolean().default(true),
|
|
27
38
|
recall: Schema.boolean().default(true),
|
|
28
39
|
reasoningEffort: Schema.union(['', 'off', 'high', 'max']).default(''),
|
|
40
|
+
distillProvider: Schema.string().default(''),
|
|
41
|
+
distillModel: Schema.string().default(''),
|
|
42
|
+
distillBudgets: Schema.object({
|
|
43
|
+
extract: budget(),
|
|
44
|
+
dedup: budget(),
|
|
45
|
+
l2: budget(),
|
|
46
|
+
l3: budget(),
|
|
47
|
+
}).default({ extract: 0, dedup: 0, l2: 0, l3: 0 }),
|
|
48
|
+
distillMaxInputChars: Schema.number().min(0).max(1_000_000).default(0),
|
|
29
49
|
});
|
|
30
50
|
}
|
|
31
51
|
export function registerLiveSettings(ctx, logger) {
|
|
@@ -42,8 +62,16 @@ export function registerLiveSettings(ctx, logger) {
|
|
|
42
62
|
cachedUnwatch = scope.watch((next) => {
|
|
43
63
|
const prev = current;
|
|
44
64
|
current = resolveSettings(next);
|
|
65
|
+
const b = current.distillBudgets;
|
|
66
|
+
const budgetNote = (b.extract || b.dedup || b.l2 || b.l3)
|
|
67
|
+
? `,输出预算=抽取 ${b.extract || '默认'}/去重 ${b.dedup || '默认'}/L2 ${b.l2 || '默认'}/L3 ${b.l3 || '默认'}`
|
|
68
|
+
: '';
|
|
69
|
+
const inputNote = current.distillMaxInputChars > 0 ? `,输入预算=${current.distillMaxInputChars}` : '';
|
|
45
70
|
logger.info(`[memory] 记忆模式开关更新:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}` +
|
|
46
|
-
`,蒸馏思考=${current.reasoningEffort || '跟随配置'}(此前 总=${prev.enabled})`
|
|
71
|
+
`,蒸馏思考=${current.reasoningEffort || '跟随配置'}(此前 总=${prev.enabled})` +
|
|
72
|
+
(current.distillProvider && current.distillModel
|
|
73
|
+
? `,蒸馏模型=${current.distillProvider}/${current.distillModel}`
|
|
74
|
+
: '') + budgetNote + inputNote);
|
|
47
75
|
});
|
|
48
76
|
return {
|
|
49
77
|
supported: true,
|
|
@@ -127,6 +155,8 @@ function resolveSettings(value) {
|
|
|
127
155
|
return { ...ALWAYS_ON };
|
|
128
156
|
const v = value;
|
|
129
157
|
const efforts = ['', 'off', 'high', 'max'];
|
|
158
|
+
const num = (x) => (typeof x === 'number' && Number.isFinite(x) && x >= 0 ? Math.floor(x) : 0);
|
|
159
|
+
const rawBudgets = (v.distillBudgets ?? {});
|
|
130
160
|
return {
|
|
131
161
|
enabled: v.enabled !== false,
|
|
132
162
|
capture: v.capture !== false,
|
|
@@ -135,5 +165,14 @@ function resolveSettings(value) {
|
|
|
135
165
|
reasoningEffort: typeof v.reasoningEffort === 'string' && efforts.includes(v.reasoningEffort)
|
|
136
166
|
? v.reasoningEffort
|
|
137
167
|
: '',
|
|
168
|
+
distillProvider: typeof v.distillProvider === 'string' ? v.distillProvider : '',
|
|
169
|
+
distillModel: typeof v.distillModel === 'string' ? v.distillModel : '',
|
|
170
|
+
distillBudgets: {
|
|
171
|
+
extract: num(rawBudgets.extract),
|
|
172
|
+
dedup: num(rawBudgets.dedup),
|
|
173
|
+
l2: num(rawBudgets.l2),
|
|
174
|
+
l3: num(rawBudgets.l3),
|
|
175
|
+
},
|
|
176
|
+
distillMaxInputChars: num(v.distillMaxInputChars),
|
|
138
177
|
};
|
|
139
178
|
}
|
package/dist/stats.js
CHANGED
|
@@ -10,6 +10,9 @@ import { createRequire } from 'node:module';
|
|
|
10
10
|
import { closeSync, openSync, readSync, statSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { resolveDataDir } from './config.js';
|
|
13
|
+
import { effectiveCfg } from './pipeline/runner.js';
|
|
14
|
+
import { LAYER_DEFAULT_BUDGETS, resolveModelRoute } from './llm.js';
|
|
15
|
+
import { errDetail } from './util/filelog.js';
|
|
13
16
|
const require = createRequire(import.meta.url);
|
|
14
17
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
15
18
|
/** 注册状态 RPC(web 侧 connection 服务可选,缺失时跳过,不影响插件主体)。 */
|
|
@@ -30,6 +33,7 @@ export function registerMemoryRpc(ctx, cfg, stores, logger, status, live, modes,
|
|
|
30
33
|
const dispose = connection.rpc.handle('/rpc', async (endpoint, payload) => {
|
|
31
34
|
try {
|
|
32
35
|
const value = await handleEndpoint(endpoint, payload, {
|
|
36
|
+
ctx,
|
|
33
37
|
cfg,
|
|
34
38
|
stores,
|
|
35
39
|
status,
|
|
@@ -157,9 +161,14 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
157
161
|
}
|
|
158
162
|
case 'dsh-memory/settings-get': {
|
|
159
163
|
const s = live?.get();
|
|
164
|
+
const budgets = s?.distillBudgets ?? { extract: 0, dedup: 0, l2: 0, l3: 0 };
|
|
160
165
|
return {
|
|
161
166
|
supported: live?.supported ?? false,
|
|
162
|
-
settings: s ?? {
|
|
167
|
+
settings: s ?? {
|
|
168
|
+
enabled: true, capture: true, distill: true, recall: true,
|
|
169
|
+
reasoningEffort: '', distillProvider: '', distillModel: '',
|
|
170
|
+
distillBudgets: { extract: 0, dedup: 0, l2: 0, l3: 0 }, distillMaxInputChars: 0,
|
|
171
|
+
},
|
|
163
172
|
// 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
|
|
164
173
|
ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
|
|
165
174
|
// 蒸馏思考档位:current 是运行时覆盖('' = 跟随配置),effective 是实际生效值
|
|
@@ -168,6 +177,23 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
168
177
|
effective: s?.reasoningEffort || cfg.llm.reasoningEffort,
|
|
169
178
|
fallback: cfg.llm.reasoningEffort,
|
|
170
179
|
},
|
|
180
|
+
// 分层输出预算:current 是运行时覆盖(0 = 跟随默认),defaults 是内置默认(UI 占位/提示用)
|
|
181
|
+
budgets: {
|
|
182
|
+
current: budgets,
|
|
183
|
+
defaults: { ...LAYER_DEFAULT_BUDGETS },
|
|
184
|
+
effective: {
|
|
185
|
+
extract: budgets.extract > 0 ? budgets.extract : LAYER_DEFAULT_BUDGETS.extract,
|
|
186
|
+
dedup: budgets.dedup > 0 ? budgets.dedup : LAYER_DEFAULT_BUDGETS.dedup,
|
|
187
|
+
l2: budgets.l2 > 0 ? budgets.l2 : LAYER_DEFAULT_BUDGETS.l2,
|
|
188
|
+
l3: budgets.l3 > 0 ? budgets.l3 : LAYER_DEFAULT_BUDGETS.l3,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
// 输入预算(字符):current 是运行时覆盖(0 = 跟随配置),fallback 是静态配置值
|
|
192
|
+
inputBudget: {
|
|
193
|
+
current: s?.distillMaxInputChars ?? 0,
|
|
194
|
+
fallback: cfg.llm.maxInputChars,
|
|
195
|
+
effective: s && s.distillMaxInputChars > 0 ? s.distillMaxInputChars : cfg.llm.maxInputChars,
|
|
196
|
+
},
|
|
171
197
|
};
|
|
172
198
|
}
|
|
173
199
|
case 'dsh-memory/settings-set': {
|
|
@@ -186,6 +212,37 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
186
212
|
}
|
|
187
213
|
clean.reasoningEffort = v;
|
|
188
214
|
}
|
|
215
|
+
// 蒸馏模型运行时覆盖:供应商/模型 id 原样接受(不在此校验存在性——
|
|
216
|
+
// 供应商可被用户随后删除,解析侧按存在性回退并提示)
|
|
217
|
+
for (const key of ['distillProvider', 'distillModel']) {
|
|
218
|
+
if (patch[key] !== undefined) {
|
|
219
|
+
const v = String(patch[key]);
|
|
220
|
+
if (v.length > 200)
|
|
221
|
+
throw new Error(`${key} 过长(≤200 字符)`);
|
|
222
|
+
clean[key] = v;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 分层输出预算:四键一起校验,非负整数 ≤ 100 万;0 = 跟随内置默认
|
|
226
|
+
if (patch.distillBudgets !== undefined) {
|
|
227
|
+
const raw = (patch.distillBudgets ?? {});
|
|
228
|
+
const budgets = {};
|
|
229
|
+
for (const key of ['extract', 'dedup', 'l2', 'l3']) {
|
|
230
|
+
const n = Number(raw[key] ?? 0);
|
|
231
|
+
if (!Number.isInteger(n) || n < 0 || n > 1_000_000) {
|
|
232
|
+
throw new Error(`distillBudgets.${key} 须为 0~1000000 的整数(0 = 跟随默认)`);
|
|
233
|
+
}
|
|
234
|
+
budgets[key] = n;
|
|
235
|
+
}
|
|
236
|
+
clean.distillBudgets = budgets;
|
|
237
|
+
}
|
|
238
|
+
// 输入预算(字符):0 = 跟随静态配置;正值须落在静态 schema 同款范围(1000~100 万)
|
|
239
|
+
if (patch.distillMaxInputChars !== undefined) {
|
|
240
|
+
const n = Number(patch.distillMaxInputChars);
|
|
241
|
+
if (!Number.isInteger(n) || n < 0 || n > 1_000_000 || (n > 0 && n < 1000)) {
|
|
242
|
+
throw new Error('distillMaxInputChars 须为 0 或 1000~1000000 的整数(0 = 跟随配置)');
|
|
243
|
+
}
|
|
244
|
+
clean.distillMaxInputChars = n;
|
|
245
|
+
}
|
|
189
246
|
if (Object.keys(clean).length === 0)
|
|
190
247
|
throw new Error('开关更新载荷为空');
|
|
191
248
|
await live.update(clean);
|
|
@@ -270,6 +327,61 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
270
327
|
throw new Error('重建控制器未初始化');
|
|
271
328
|
return rebuild.requestCancel();
|
|
272
329
|
}
|
|
330
|
+
// ── 蒸馏模型选择器(用户已配置的供应商路由) ──
|
|
331
|
+
case 'dsh-memory/llm-providers': {
|
|
332
|
+
// 供应商目录(已注册适配器的活动路由)+ 默认选择 + 当前覆盖与实际生效路由
|
|
333
|
+
let providers = [];
|
|
334
|
+
try {
|
|
335
|
+
providers = deps.ctx.llm.listProviders();
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
deps.logger.warn(`[memory] 供应商列表读取失败: ${errDetail(err)}`);
|
|
339
|
+
}
|
|
340
|
+
let def = null;
|
|
341
|
+
try {
|
|
342
|
+
const sel = deps.ctx.get('agentDefaultModel')?.currentSelection?.();
|
|
343
|
+
if (sel?.provider && sel?.model)
|
|
344
|
+
def = { provider: sel.provider, model: sel.model };
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
/* 可选服务缺失 = 无默认选择 */
|
|
348
|
+
}
|
|
349
|
+
const s = live?.get();
|
|
350
|
+
const current = { provider: s?.distillProvider ?? '', model: s?.distillModel ?? '' };
|
|
351
|
+
let effective = null;
|
|
352
|
+
try {
|
|
353
|
+
effective = await resolveModelRoute(deps.ctx, effectiveCfg(cfg, live));
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
effective = null; // 无法解析(无默认选择且未覆盖)时 UI 显示占位
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
supported: true,
|
|
360
|
+
providers,
|
|
361
|
+
default: def,
|
|
362
|
+
// 部署静态 pin(provider+model 双字段)优先于运行时选择,UI 据此禁用选择器
|
|
363
|
+
pinned: Boolean(cfg.llm.provider && cfg.llm.model),
|
|
364
|
+
current,
|
|
365
|
+
// 所选供应商是否仍在已注册路由中(用户删掉供应商后提示回退)
|
|
366
|
+
currentRegistered: current.provider === '' || providers.some((p) => p.id === current.provider),
|
|
367
|
+
effective,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
case 'dsh-memory/llm-models': {
|
|
371
|
+
const p = (payload ?? {});
|
|
372
|
+
if (typeof p.provider !== 'string' || !p.provider)
|
|
373
|
+
throw new Error('provider 缺失');
|
|
374
|
+
// 两个内置适配器(deepseek/pi-ai)的 listModels 都读本地快照不触网;
|
|
375
|
+
// 仍加超时兜底,防第三方适配器实现为远端查询拖死 RPC 轮询
|
|
376
|
+
const models = await Promise.race([
|
|
377
|
+
deps.ctx.llm.listModels(p.provider),
|
|
378
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('模型列表查询超时')), 8000)),
|
|
379
|
+
]);
|
|
380
|
+
return {
|
|
381
|
+
provider: p.provider,
|
|
382
|
+
models: models.map((m) => ({ id: m.id, name: m.name, description: m.description ?? null })),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
273
385
|
// ── 嵌入源(远程/本地/关闭 三态)与模型管理 ──
|
|
274
386
|
case 'dsh-memory/embedding-state-get': {
|
|
275
387
|
if (!embedManager)
|
|
@@ -31,10 +31,18 @@ export interface DownloaderOptions {
|
|
|
31
31
|
/** 下载镜像根(默认 https://hf-mirror.com,可配回 https://huggingface.co)。 */
|
|
32
32
|
mirror: string;
|
|
33
33
|
logger?: MemoryLogger;
|
|
34
|
-
/**
|
|
34
|
+
/** 测试注入;默认 undici fetch(按需挂代理 dispatcher)。 */
|
|
35
35
|
fetchImpl?: FetchLike;
|
|
36
36
|
/** 测试注入磁盘剩余字节;默认 statfs。 */
|
|
37
37
|
freeBytes?: () => Promise<number | null>;
|
|
38
|
+
/** 单文件失败的自动重试间隔(毫秒),长度即重试次数;默认 [1000, 3000]。
|
|
39
|
+
* sha256 失配重试从零开始(污染断点已删除);数量不吻合/网络错误保留断点续传。 */
|
|
40
|
+
retryDelaysMs?: number[];
|
|
41
|
+
/** 显式代理三态:''(默认)= 探测代理环境变量;'none' = 禁用强制直连;
|
|
42
|
+
* 其他值 = 代理 URL(如 http://127.0.0.1:7890)。镜像直连在国内网络
|
|
43
|
+
* 间歇不可达(真实事故:直连超时与污染字节交替),与 curl/npm 同语义地
|
|
44
|
+
* 走用户已配置的代理是可达性的底线。 */
|
|
45
|
+
proxy?: string;
|
|
38
46
|
}
|
|
39
47
|
export declare class ModelDownloadQueue {
|
|
40
48
|
private readonly dataDir;
|
|
@@ -42,7 +50,15 @@ export declare class ModelDownloadQueue {
|
|
|
42
50
|
private progress;
|
|
43
51
|
private busy;
|
|
44
52
|
private abort;
|
|
53
|
+
/** 代理 dispatcher(按需创建;dispose 关闭连接池)。 */
|
|
54
|
+
private agent;
|
|
55
|
+
/** 默认 fetch:undici(可挂代理 dispatcher);测试注入优先。 */
|
|
56
|
+
private readonly defaultFetch;
|
|
45
57
|
constructor(dataDir: string, opts: DownloaderOptions);
|
|
58
|
+
/** 镜像根(无尾斜杠)。 */
|
|
59
|
+
private mirrorUrl;
|
|
60
|
+
/** 释放代理连接池(插件 dispose 链调用;无代理时幂等无操作)。 */
|
|
61
|
+
dispose(): void;
|
|
46
62
|
/** 当前进度快照(无任务时 null)。 */
|
|
47
63
|
getProgress(): DownloadProgress | null;
|
|
48
64
|
/** 是否有任务在跑(含校验阶段)。 */
|
|
@@ -64,8 +80,25 @@ export declare class ModelDownloadQueue {
|
|
|
64
80
|
/** 取消当前任务:中断 fetch,保留 .part 断点。 */
|
|
65
81
|
cancel(): boolean;
|
|
66
82
|
private run;
|
|
67
|
-
/** 下载单文件到最终路径(含续传与校验),返回该文件贡献的字节数。
|
|
83
|
+
/** 下载单文件到最终路径(含续传与校验),返回该文件贡献的字节数。
|
|
84
|
+
* 单文件失败自动重试(默认 2 次)+ **重试换缓存键**:镜像链路
|
|
85
|
+
* (Caddy×3 → CloudFront → Cloudflare)存在缓存对象污染窗口——同一时间窗内
|
|
86
|
+
* 同一 URL 确定性拿到错误字节(2026-08-19 embeddinggemma generation_config.json
|
|
87
|
+
* 连续错哈希的真实事故),普通重试会全打同一污染缓存;每次重试追加
|
|
88
|
+
* `?dshmem-retry=N` 参数绕开缓存键另取对象,窗口期也能自愈。
|
|
89
|
+
* - sha256 失配:downloadFileOnce 已删除断点 → 从零重下;
|
|
90
|
+
* - 数量不吻合/网络错误:断点保留 → Range 续传重试;
|
|
91
|
+
* - 取消:立即上抛不重试。 */
|
|
68
92
|
private downloadFile;
|
|
93
|
+
/** 单次尝试:续传探测 → fetch(attempt>0 追加缓存键参数)→ 落盘 → 尺寸与 sha256 校验 → rename。 */
|
|
94
|
+
private downloadFileOnce;
|
|
69
95
|
private freeBytes;
|
|
70
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* 解析下载代理(三态):`''`(默认)= 探测代理环境变量(HTTPS_PROXY > ALL_PROXY >
|
|
99
|
+
* HTTP_PROXY,大小写双形态,尊重 NO_PROXY);`'none'` = 禁用代理强制直连;
|
|
100
|
+
* 其他值 = 显式代理 URL。与 curl/npm 同语义——用户配置了代理 env 的机器上
|
|
101
|
+
* 镜像直连往往间歇不可达(真实事故:直连超时与污染字节交替出现)。
|
|
102
|
+
*/
|
|
103
|
+
export declare function resolveProxyUrl(setting: string | undefined, host: string): string;
|
|
71
104
|
export {};
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
* - 断点续传:写 .part 旁车文件,重试从断点 Range 续传;服务器不支持 Range(回 200)
|
|
7
7
|
* 则从头重写;取消保留断点;
|
|
8
8
|
* - 完整性:每文件下满后流式哈希整文件比对目录 sha256(续传无法增量哈希,落盘后
|
|
9
|
-
*
|
|
9
|
+
* 单遍校验最简单且正确);失配删文件整体重下;单文件失败自动重试(默认 2 次,
|
|
10
|
+
* 吸收镜像瞬态污染——sha 失配从零重下、网络类错误保留断点续传);
|
|
10
11
|
* - 磁盘门禁:下载前检查数据目录所在卷剩余空间 ≥ 模型体积 × 1.2(statfs 不可用时跳过);
|
|
11
12
|
* - 同一时刻只跑一个下载任务(串行队列),后续请求直接拒绝并说明。
|
|
12
13
|
*/
|
|
13
14
|
import { createHash } from 'node:crypto';
|
|
14
15
|
import { promises as fs } from 'node:fs';
|
|
15
16
|
import * as path from 'node:path';
|
|
17
|
+
import { fetch as undiciFetch, ProxyAgent } from 'undici';
|
|
16
18
|
import { catalogById, catalogTotalBytes, MODEL_CATALOG } from './model-catalog.js';
|
|
17
19
|
const DISK_HEADROOM = 1.2;
|
|
18
20
|
export class ModelDownloadQueue {
|
|
@@ -21,9 +23,40 @@ export class ModelDownloadQueue {
|
|
|
21
23
|
progress = null;
|
|
22
24
|
busy = false;
|
|
23
25
|
abort = null;
|
|
26
|
+
/** 代理 dispatcher(按需创建;dispose 关闭连接池)。 */
|
|
27
|
+
agent;
|
|
28
|
+
/** 默认 fetch:undici(可挂代理 dispatcher);测试注入优先。 */
|
|
29
|
+
defaultFetch;
|
|
24
30
|
constructor(dataDir, opts) {
|
|
25
31
|
this.dataDir = dataDir;
|
|
26
32
|
this.opts = opts;
|
|
33
|
+
// 畸形 mirror(无 scheme 等)只跳过代理解析,不炸构造器(下载本身还会报清晰的 URL 错)
|
|
34
|
+
let host = '';
|
|
35
|
+
try {
|
|
36
|
+
host = new URL(this.mirrorUrl()).host;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* ignore */
|
|
40
|
+
}
|
|
41
|
+
const proxy = resolveProxyUrl(opts.proxy, host);
|
|
42
|
+
if (proxy) {
|
|
43
|
+
this.agent = new ProxyAgent(proxy);
|
|
44
|
+
opts.logger?.info(`[memory] 模型下载走代理 ${proxy}(镜像直连在国内网络间歇不可达)`);
|
|
45
|
+
}
|
|
46
|
+
this.defaultFetch = ((u, init) => {
|
|
47
|
+
const dispatch = this.agent;
|
|
48
|
+
// undici fetch 的 init 接受 dispatcher;RequestInit 类型无此字段,断言透传
|
|
49
|
+
return undiciFetch(u, { ...init, ...(dispatch ? { dispatcher: dispatch } : {}) });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** 镜像根(无尾斜杠)。 */
|
|
53
|
+
mirrorUrl() {
|
|
54
|
+
return this.opts.mirror.replace(/\/+$/, '');
|
|
55
|
+
}
|
|
56
|
+
/** 释放代理连接池(插件 dispose 链调用;无代理时幂等无操作)。 */
|
|
57
|
+
dispose() {
|
|
58
|
+
void this.agent?.close().catch(() => { });
|
|
59
|
+
this.agent = undefined;
|
|
27
60
|
}
|
|
28
61
|
/** 当前进度快照(无任务时 null)。 */
|
|
29
62
|
getProgress() {
|
|
@@ -187,13 +220,46 @@ export class ModelDownloadQueue {
|
|
|
187
220
|
prog.overallReceived = overall;
|
|
188
221
|
}
|
|
189
222
|
}
|
|
190
|
-
/** 下载单文件到最终路径(含续传与校验),返回该文件贡献的字节数。
|
|
223
|
+
/** 下载单文件到最终路径(含续传与校验),返回该文件贡献的字节数。
|
|
224
|
+
* 单文件失败自动重试(默认 2 次)+ **重试换缓存键**:镜像链路
|
|
225
|
+
* (Caddy×3 → CloudFront → Cloudflare)存在缓存对象污染窗口——同一时间窗内
|
|
226
|
+
* 同一 URL 确定性拿到错误字节(2026-08-19 embeddinggemma generation_config.json
|
|
227
|
+
* 连续错哈希的真实事故),普通重试会全打同一污染缓存;每次重试追加
|
|
228
|
+
* `?dshmem-retry=N` 参数绕开缓存键另取对象,窗口期也能自愈。
|
|
229
|
+
* - sha256 失配:downloadFileOnce 已删除断点 → 从零重下;
|
|
230
|
+
* - 数量不吻合/网络错误:断点保留 → Range 续传重试;
|
|
231
|
+
* - 取消:立即上抛不重试。 */
|
|
191
232
|
async downloadFile(entry, f, dir, onBytes) {
|
|
233
|
+
const delays = this.opts.retryDelaysMs ?? [1000, 3000];
|
|
234
|
+
let lastErr;
|
|
235
|
+
for (let attempt = 0;; attempt++) {
|
|
236
|
+
if (this.progress?.phase === 'cancelled')
|
|
237
|
+
throw new Error('已取消');
|
|
238
|
+
try {
|
|
239
|
+
return await this.downloadFileOnce(entry, f, dir, attempt, onBytes);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
lastErr = err;
|
|
243
|
+
// as 断言绕开 CFA 窄化——await 期间 cancel() 可能已改写 phase(同 downloadFileOnce 末段)
|
|
244
|
+
if (this.progress?.phase === 'cancelled')
|
|
245
|
+
throw err;
|
|
246
|
+
if (attempt >= delays.length)
|
|
247
|
+
throw err;
|
|
248
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
249
|
+
this.opts.logger?.warn(`[memory] 文件 ${f.path} 第 ${attempt + 1} 次尝试失败(${msg}),${delays[attempt]}ms 后自动重试(换缓存键)`);
|
|
250
|
+
await new Promise((r) => setTimeout(r, delays[attempt]));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** 单次尝试:续传探测 → fetch(attempt>0 追加缓存键参数)→ 落盘 → 尺寸与 sha256 校验 → rename。 */
|
|
255
|
+
async downloadFileOnce(entry, f, dir, attempt, onBytes) {
|
|
192
256
|
const finalPath = path.join(dir, f.path);
|
|
193
257
|
const partPath = finalPath + '.part';
|
|
194
|
-
const base = this.
|
|
195
|
-
|
|
196
|
-
const
|
|
258
|
+
const base = this.mirrorUrl();
|
|
259
|
+
// 重试追加缓存键参数:绕开镜像 CDN 的污染缓存对象(同窗口普通重试全打同一对象)
|
|
260
|
+
const cacheBust = attempt > 0 ? `?dshmem-retry=${attempt}` : '';
|
|
261
|
+
const url = `${base}/${entry.repo}/resolve/${entry.revision}/${f.path}${cacheBust}`;
|
|
262
|
+
const fetchImpl = this.opts.fetchImpl ?? this.defaultFetch;
|
|
197
263
|
const prog = this.progress;
|
|
198
264
|
let resumeFrom = 0;
|
|
199
265
|
const partSize = await fileSize(partPath);
|
|
@@ -303,6 +369,37 @@ async function fileSize(p) {
|
|
|
303
369
|
return null;
|
|
304
370
|
}
|
|
305
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* 解析下载代理(三态):`''`(默认)= 探测代理环境变量(HTTPS_PROXY > ALL_PROXY >
|
|
374
|
+
* HTTP_PROXY,大小写双形态,尊重 NO_PROXY);`'none'` = 禁用代理强制直连;
|
|
375
|
+
* 其他值 = 显式代理 URL。与 curl/npm 同语义——用户配置了代理 env 的机器上
|
|
376
|
+
* 镜像直连往往间歇不可达(真实事故:直连超时与污染字节交替出现)。
|
|
377
|
+
*/
|
|
378
|
+
export function resolveProxyUrl(setting, host) {
|
|
379
|
+
const value = (setting ?? '').trim();
|
|
380
|
+
if (value.toLowerCase() === 'none')
|
|
381
|
+
return '';
|
|
382
|
+
if (value)
|
|
383
|
+
return value;
|
|
384
|
+
const noProxy = process.env.NO_PROXY ?? process.env.no_proxy ?? '';
|
|
385
|
+
if (noProxy) {
|
|
386
|
+
for (const raw of noProxy.split(',')) {
|
|
387
|
+
const entry = raw.trim().replace(/^\./, '').toLowerCase();
|
|
388
|
+
if (entry && (host.toLowerCase() === entry || host.toLowerCase().endsWith(`.${entry}`)))
|
|
389
|
+
return '';
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const candidates = [
|
|
393
|
+
process.env.HTTPS_PROXY, process.env.https_proxy,
|
|
394
|
+
process.env.ALL_PROXY, process.env.all_proxy,
|
|
395
|
+
process.env.HTTP_PROXY, process.env.http_proxy,
|
|
396
|
+
];
|
|
397
|
+
for (const c of candidates) {
|
|
398
|
+
if (c && c.trim())
|
|
399
|
+
return c.trim();
|
|
400
|
+
}
|
|
401
|
+
return '';
|
|
402
|
+
}
|
|
306
403
|
async function sha256File(p) {
|
|
307
404
|
const { createReadStream } = await import('node:fs');
|
|
308
405
|
const hash = createHash('sha256');
|
|
@@ -43,7 +43,7 @@ export const MODEL_CATALOG = [
|
|
|
43
43
|
{ path: 'added_tokens.json', size: 35, sha256: '50b2f405ba56a26d4913fd772089992252d7f942123cc0a034d96424221ba946' },
|
|
44
44
|
{ path: 'special_tokens_map.json', size: 662, sha256: '2f7b0adf4fb469770bb1490e3e35df87b1dc578246c5e7e6fc76ecf33213a397' },
|
|
45
45
|
{ path: 'tokenizer_config.json', size: 1156830, sha256: '3ca953eea6c3c9fcda9cf3df22949ff18b216f7c74bd6459230f3f1013953f3a' },
|
|
46
|
-
{ path: 'generation_config.json', size: 133, sha256: '
|
|
46
|
+
{ path: 'generation_config.json', size: 133, sha256: '1fb1efd221c1ca88a736d1b36cb47d754c177677e222acb3b1e5424c5d664870' },
|
|
47
47
|
{ path: 'tokenizer.json', size: 20323312, sha256: '4dda02faaf32bc91031dc8c88457ac272b00c1016cc679757d1c441b248b9c47' },
|
|
48
48
|
{ path: 'onnx/model_quantized.onnx', size: 567874, sha256: '172efde319fe1542dc41f31be6154910b05b78f7a861c265c4600eec906bd6d8' },
|
|
49
49
|
{ path: 'onnx/model_quantized.onnx_data', size: 308890624, sha256: '705626e28e4c23c82ade34566b4197d97f534c12275fa406dfb71e9937d388c0' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-layered-memory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsc -p tsconfig.json && node scripts/copy-client.mjs",
|
|
35
|
-
"smoke": "node dist-smoke/smoke.js"
|
|
35
|
+
"smoke": "node dist-smoke/smoke.js",
|
|
36
|
+
"verify-catalog": "npm run build && node scripts/verify-catalog.mjs"
|
|
36
37
|
},
|
|
37
38
|
"engines": {
|
|
38
39
|
"node": ">=22.16.0"
|
|
@@ -52,7 +53,8 @@
|
|
|
52
53
|
"dependencies": {
|
|
53
54
|
"@deepseek-ai/schemastery": "3.18.1",
|
|
54
55
|
"@node-rs/jieba": "^2.0.2",
|
|
55
|
-
"sqlite-vec": "^0.1.7-alpha.2"
|
|
56
|
+
"sqlite-vec": "^0.1.7-alpha.2",
|
|
57
|
+
"undici": "^7.29.0"
|
|
56
58
|
},
|
|
57
59
|
"peerDependencies": {
|
|
58
60
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -66,15 +68,15 @@
|
|
|
66
68
|
},
|
|
67
69
|
"devDependencies": {
|
|
68
70
|
"@deepseek-ai/cordis": "4.0.1",
|
|
69
|
-
"@deepseek-ai/dsh-agent": "0.1.0-rc.
|
|
70
|
-
"@deepseek-ai/dsh-agent-default-model": "0.1.0-rc.
|
|
71
|
-
"@deepseek-ai/dsh-client-connection": "0.1.0-rc.
|
|
72
|
-
"@deepseek-ai/dsh-home-paths": "0.1.0-rc.
|
|
73
|
-
"@deepseek-ai/dsh-llm": "0.1.0-rc.
|
|
74
|
-
"@deepseek-ai/dsh-session": "0.1.0-rc.
|
|
75
|
-
"@deepseek-ai/dsh-settings": "0.1.0-rc.
|
|
76
|
-
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.
|
|
77
|
-
"@deepseek-ai/dsh-tools": "0.1.0-rc.
|
|
71
|
+
"@deepseek-ai/dsh-agent": "0.1.0-rc.8",
|
|
72
|
+
"@deepseek-ai/dsh-agent-default-model": "0.1.0-rc.8",
|
|
73
|
+
"@deepseek-ai/dsh-client-connection": "0.1.0-rc.8",
|
|
74
|
+
"@deepseek-ai/dsh-home-paths": "0.1.0-rc.8",
|
|
75
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
76
|
+
"@deepseek-ai/dsh-session": "0.1.0-rc.8",
|
|
77
|
+
"@deepseek-ai/dsh-settings": "0.1.0-rc.8",
|
|
78
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.8",
|
|
79
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.8",
|
|
78
80
|
"@types/node": "^22.0.0",
|
|
79
81
|
"typescript": "^5.6.0"
|
|
80
82
|
}
|