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/stats.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import { type MemoryConfig } from './config.js';
|
|
3
3
|
import { type RecallSessionStats } from './hooks/recall.js';
|
|
4
4
|
import type { RebuildController } from './pipeline/rebuild.js';
|
|
5
|
-
import type
|
|
5
|
+
import { type LiveSettingsHandle } from './settings.js';
|
|
6
6
|
import type { L0Store } from './store/l0.js';
|
|
7
7
|
import type { L1Store } from './store/l1.js';
|
|
8
8
|
import type { PersonaStore } from './store/persona.js';
|
|
@@ -44,31 +44,7 @@ export interface SessionInfoSource {
|
|
|
44
44
|
vectorSearch: boolean;
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
-
export
|
|
48
|
-
ok: boolean;
|
|
49
|
-
dataDir: string;
|
|
50
|
-
/** 新会话默认记忆档位(auto/chat/work)。 */
|
|
51
|
-
family: string;
|
|
52
|
-
version: string;
|
|
53
|
-
l0Today: number;
|
|
54
|
-
l1Count: number;
|
|
55
|
-
l1TotalExtracted: number;
|
|
56
|
-
sceneCount: number;
|
|
57
|
-
personaChars: number;
|
|
58
|
-
hasPersona: boolean;
|
|
59
|
-
lastExtractAt: string | null;
|
|
60
|
-
lastL2At: string | null;
|
|
61
|
-
lastL3At: string | null;
|
|
62
|
-
memoriesSinceL2: number;
|
|
63
|
-
memoriesSinceL3: number;
|
|
64
|
-
pendingExtract: number;
|
|
65
|
-
message: string;
|
|
66
|
-
/** 实际生效的阈值(概览进度分母用,避免 UI 硬编码与部署配置脱节)。 */
|
|
67
|
-
thresholds: {
|
|
68
|
-
l2MinNewMemories: number;
|
|
69
|
-
l3Interval: number;
|
|
70
|
-
};
|
|
71
|
-
}
|
|
47
|
+
export type { MemoryStats } from './contract.js';
|
|
72
48
|
/** 注册状态 RPC(web 侧 connection 服务可选,缺失时跳过,不影响插件主体)。 */
|
|
73
49
|
export declare function registerMemoryRpc(ctx: Context, cfg: MemoryConfig, stores: {
|
|
74
50
|
l0: L0Store;
|
package/dist/stats.js
CHANGED
|
@@ -12,7 +12,8 @@ import { join } from 'node:path';
|
|
|
12
12
|
import { EFFORT_CHOICES, resolveDataDir } from './config.js';
|
|
13
13
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
14
14
|
import { emptyRecallStats } from './hooks/recall.js';
|
|
15
|
-
import { decideSendableEffort, LAYER_DEFAULT_BUDGETS, resolveModelEfforts, resolveModelRoute } from './llm.js';
|
|
15
|
+
import { buildRouteChain, decideSendableEffort, LAYER_DEFAULT_BUDGETS, resolveModelEfforts, resolveModelRoute } from './llm.js';
|
|
16
|
+
import { projectDistillChain, validateDistillChain } from './settings.js';
|
|
16
17
|
import { errDetail } from './util/filelog.js';
|
|
17
18
|
import { snapshotTokenCost } from './token-cost.js';
|
|
18
19
|
const require = createRequire(import.meta.url);
|
|
@@ -164,7 +165,8 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
164
165
|
throw new Error('档位存储未初始化');
|
|
165
166
|
const p = (payload ?? {});
|
|
166
167
|
const sessionId = expectSessionId(p.sessionId);
|
|
167
|
-
|
|
168
|
+
const v = { sessionId, mode: modes.get(sessionId), defaultMode: modes.default };
|
|
169
|
+
return v;
|
|
168
170
|
}
|
|
169
171
|
case 'dsh-memory/session-mode-set': {
|
|
170
172
|
if (!modes)
|
|
@@ -177,7 +179,8 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
177
179
|
}
|
|
178
180
|
modes.set(sessionId, p.mode);
|
|
179
181
|
deps.logger.info(`[memory] 会话档位设置 session=${sessionId} mode=${p.mode}`);
|
|
180
|
-
|
|
182
|
+
const v = { sessionId, mode: p.mode };
|
|
183
|
+
return v;
|
|
181
184
|
}
|
|
182
185
|
// ── 会话级统计(悬浮卡信息区;热路径端点,见 SessionInfoSource 的零 I/O 硬规则) ──
|
|
183
186
|
case 'dsh-memory/session-stats': {
|
|
@@ -196,7 +199,7 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
196
199
|
const chat = stores.state.forFamily('chat');
|
|
197
200
|
const work = stores.state.forFamily('work');
|
|
198
201
|
const lastAt = Math.max(chat.lastExtractAt, work.lastExtractAt);
|
|
199
|
-
|
|
202
|
+
const v = {
|
|
200
203
|
supported: true,
|
|
201
204
|
sessionId,
|
|
202
205
|
mode,
|
|
@@ -211,13 +214,16 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
211
214
|
lastExtractAt: lastAt ? new Date(lastAt).toISOString() : null,
|
|
212
215
|
},
|
|
213
216
|
};
|
|
217
|
+
return v;
|
|
214
218
|
}
|
|
215
219
|
case 'dsh-memory/settings-get': {
|
|
216
220
|
const s = live?.get();
|
|
217
221
|
const budgets = s?.distillBudgets ?? { extract: 0, dedup: 0, l2: 0, l3: 0 };
|
|
218
222
|
// 蒸馏思考档位:current 是运行时值('' = 自动);effective 是能力探询后实际发送值
|
|
219
223
|
// ('' = 不传,跟随模型默认);options 是当前生效模型声明的档位表(空声明 → 只显示
|
|
220
|
-
// high,用户规则:无声明默认 high
|
|
224
|
+
// high,用户规则:无声明默认 high),fallback 是静态部署值。注:旧「蒸馏思考」
|
|
225
|
+
// 选择器已删,本块保留给旧 client 版本与 smoke 兼容;新 UI(路由链编辑器)
|
|
226
|
+
// 的逐行档位词表走 llm-models 的 efforts 字段
|
|
221
227
|
let effortEffective = s?.reasoningEffort || cfg.llm.reasoningEffort;
|
|
222
228
|
let effortOptions = ['high'];
|
|
223
229
|
let effortRoute = null;
|
|
@@ -234,17 +240,18 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
234
240
|
catch {
|
|
235
241
|
/* 路由解析/探询失败保持占位(effective 用运行时||静态值) */
|
|
236
242
|
}
|
|
237
|
-
|
|
243
|
+
const resp = {
|
|
238
244
|
supported: live?.supported ?? false,
|
|
239
245
|
settings: s ?? {
|
|
240
246
|
enabled: true, capture: true, distill: true, recall: true,
|
|
241
|
-
reasoningEffort: '', distillProvider: '', distillModel: '',
|
|
247
|
+
reasoningEffort: '', distillProvider: '', distillModel: '', distillChain: [],
|
|
242
248
|
distillBudgets: { extract: 0, dedup: 0, l2: 0, l3: 0 }, distillMaxInputChars: 0,
|
|
243
249
|
},
|
|
244
250
|
// 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
|
|
245
251
|
ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
|
|
246
252
|
effort: {
|
|
247
253
|
current: s?.reasoningEffort ?? '',
|
|
254
|
+
// 静态 schema 与 settings-set 写入门都以 EFFORT_CHOICES 白名单校验,这里断言回窄类型
|
|
248
255
|
effective: effortEffective,
|
|
249
256
|
fallback: cfg.llm.reasoningEffort,
|
|
250
257
|
options: effortOptions,
|
|
@@ -268,6 +275,7 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
268
275
|
effective: s && s.distillMaxInputChars > 0 ? s.distillMaxInputChars : cfg.llm.maxInputChars,
|
|
269
276
|
},
|
|
270
277
|
};
|
|
278
|
+
return resp;
|
|
271
279
|
}
|
|
272
280
|
case 'dsh-memory/settings-set': {
|
|
273
281
|
if (!live)
|
|
@@ -278,6 +286,13 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
278
286
|
if (typeof patch[key] === 'boolean')
|
|
279
287
|
clean[key] = patch[key];
|
|
280
288
|
}
|
|
289
|
+
// 运行时统一路由链:结构校验后整体写入(空数组 = 回到跟随部署配置)
|
|
290
|
+
if (patch.distillChain !== undefined) {
|
|
291
|
+
const err = validateDistillChain(patch.distillChain);
|
|
292
|
+
if (err)
|
|
293
|
+
throw new Error(err);
|
|
294
|
+
clean.distillChain = patch.distillChain;
|
|
295
|
+
}
|
|
281
296
|
if (patch.reasoningEffort !== undefined) {
|
|
282
297
|
const v = String(patch.reasoningEffort);
|
|
283
298
|
// 白名单与 schema/settings 同源(config.ts EFFORT_CHOICES)——此前此处漏扩词表,
|
|
@@ -322,7 +337,8 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
322
337
|
throw new Error('开关更新载荷为空');
|
|
323
338
|
await live.update(clean);
|
|
324
339
|
deps.logger.info(`[memory] 设置更新:${JSON.stringify(clean)}`);
|
|
325
|
-
|
|
340
|
+
const v = { ok: true, settings: live.get() };
|
|
341
|
+
return v;
|
|
326
342
|
}
|
|
327
343
|
case 'dsh-memory/list-records': {
|
|
328
344
|
const p = (payload ?? {});
|
|
@@ -338,22 +354,24 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
338
354
|
const wanted = offset + limit + 1;
|
|
339
355
|
const hits = await stores.l1.search(p.query, Math.min(wanted, SEARCH_CAP), { type: p.type || undefined });
|
|
340
356
|
const filtered = p.scene ? hits.filter((h) => h.scene_name === p.scene) : hits;
|
|
341
|
-
|
|
357
|
+
const resp = {
|
|
342
358
|
items: filtered.slice(offset, offset + limit).map(hitToUiRecord),
|
|
343
359
|
hasMore: filtered.length > offset + limit,
|
|
344
360
|
total: null,
|
|
345
361
|
truncated: wanted > SEARCH_CAP,
|
|
346
362
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
347
363
|
};
|
|
364
|
+
return resp;
|
|
348
365
|
}
|
|
349
366
|
const { items, total } = stores.l1.list({ type: p.type || undefined, scene: p.scene || undefined, limit, offset });
|
|
350
|
-
|
|
367
|
+
const resp = {
|
|
351
368
|
items: items.map(hitToUiRecord),
|
|
352
369
|
hasMore: offset + items.length < total,
|
|
353
370
|
total,
|
|
354
371
|
truncated: false,
|
|
355
372
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
356
373
|
};
|
|
374
|
+
return resp;
|
|
357
375
|
}
|
|
358
376
|
case 'dsh-memory/scenes': {
|
|
359
377
|
// 两族拼接展示(浏览器保持混合视图;路径冲突时后写入的族覆盖显示名,读取仍各自独立)
|
|
@@ -381,8 +399,10 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
381
399
|
return { lines: readLogTail(join(dataDir, 'memory.log'), Math.min(Math.max(Number(p.lines) || 200, 1), 1000)) };
|
|
382
400
|
}
|
|
383
401
|
case 'dsh-memory/rebuild-status': {
|
|
384
|
-
if (!rebuild)
|
|
385
|
-
|
|
402
|
+
if (!rebuild) {
|
|
403
|
+
const v = { supported: false, running: false, phase: 'idle' };
|
|
404
|
+
return v;
|
|
405
|
+
}
|
|
386
406
|
return rebuild.getStatus();
|
|
387
407
|
}
|
|
388
408
|
case 'dsh-memory/rebuild-start': {
|
|
@@ -407,6 +427,7 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
407
427
|
// ── 蒸馏模型选择器(用户已配置的供应商路由) ──
|
|
408
428
|
case 'dsh-memory/llm-providers': {
|
|
409
429
|
// 供应商目录(已注册适配器的活动路由)+ 默认选择 + 当前覆盖与实际生效路由
|
|
430
|
+
// ——蒸馏路由链编辑器的数据源(供应商下拉/默认模型展示/链状态 chain 块)
|
|
410
431
|
let providers = [];
|
|
411
432
|
try {
|
|
412
433
|
providers = deps.ctx.llm.listProviders();
|
|
@@ -425,14 +446,21 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
425
446
|
}
|
|
426
447
|
const s = live?.get();
|
|
427
448
|
const current = { provider: s?.distillProvider ?? '', model: s?.distillModel ?? '' };
|
|
449
|
+
// 统一路由链块:current = 运行时链(含旧键投影);static = 部署静态回退链;
|
|
450
|
+
// effective = buildRouteChain 语义的实际链(主路由 + 有效条目去重,每条带档位候选);
|
|
451
|
+
// source 标记当前链来自运行时还是部署静态(UI 的跟随态/接管态判定)
|
|
452
|
+
const chainCurrent = projectDistillChain(s);
|
|
453
|
+
let effectiveChain = [];
|
|
428
454
|
let effective = null;
|
|
429
455
|
try {
|
|
430
|
-
|
|
456
|
+
const cfgView = effectiveCfg(cfg, live);
|
|
457
|
+
effective = await resolveModelRoute(deps.ctx, cfgView);
|
|
458
|
+
effectiveChain = buildRouteChain({ provider: effective.provider, model: effective.model, effort: cfgView.llm.primaryEffort || '' }, cfgView.llm.fallbacks, cfgView.llm.reasoningEffort);
|
|
431
459
|
}
|
|
432
460
|
catch {
|
|
433
461
|
effective = null; // 无法解析(无默认选择且未覆盖)时 UI 显示占位
|
|
434
462
|
}
|
|
435
|
-
|
|
463
|
+
const resp = {
|
|
436
464
|
supported: true,
|
|
437
465
|
providers,
|
|
438
466
|
default: def,
|
|
@@ -442,7 +470,14 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
442
470
|
// 所选供应商是否仍在已注册路由中(用户删掉供应商后提示回退)
|
|
443
471
|
currentRegistered: current.provider === '' || providers.some((p) => p.id === current.provider),
|
|
444
472
|
effective,
|
|
473
|
+
chain: {
|
|
474
|
+
current: chainCurrent,
|
|
475
|
+
static: cfg.llm.fallbacks ?? [],
|
|
476
|
+
effectiveChain,
|
|
477
|
+
source: chainCurrent.length ? 'runtime' : 'static',
|
|
478
|
+
},
|
|
445
479
|
};
|
|
480
|
+
return resp;
|
|
446
481
|
}
|
|
447
482
|
case 'dsh-memory/llm-models': {
|
|
448
483
|
const p = (payload ?? {});
|
|
@@ -456,16 +491,40 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
456
491
|
deps.ctx.llm.listModels(p.provider),
|
|
457
492
|
new Promise((_, reject) => setTimeout(() => reject(new Error('模型列表查询超时')), 8000)),
|
|
458
493
|
]);
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
494
|
+
// 每个模型附思考档位能力表(resolveModelInfo 复用 effortCache,本地快照不触
|
|
495
|
+
// 网):统一路由链编辑器的逐行档位下拉数据源。整体限时限流——第三方适配器的
|
|
496
|
+
// resolveModelInfo 若为远端查询会拖死端点(client 5s 轮询放大),超时降级空表
|
|
497
|
+
// (探询失败/未声明同样 → 空表,UI 只显示「跟随部署配置」)
|
|
498
|
+
const baseModels = models.map((m) => ({ id: m.id, name: m.name, description: m.description ?? null, efforts: [] }));
|
|
499
|
+
const providerId = p.provider;
|
|
500
|
+
const withEfforts = await Promise.race([
|
|
501
|
+
(async () => {
|
|
502
|
+
const out = [];
|
|
503
|
+
for (const m of models) {
|
|
504
|
+
let efforts = [];
|
|
505
|
+
try {
|
|
506
|
+
efforts = (await resolveModelEfforts(deps.ctx, providerId, m.id))?.efforts ?? [];
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
efforts = [];
|
|
510
|
+
}
|
|
511
|
+
out.push({ id: m.id, name: m.name, description: m.description ?? null, efforts });
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
})(),
|
|
515
|
+
new Promise((resolve) => setTimeout(() => resolve(baseModels), 4000)),
|
|
516
|
+
]);
|
|
517
|
+
const resp = { provider: p.provider, models: withEfforts };
|
|
518
|
+
return resp;
|
|
463
519
|
}
|
|
464
520
|
// ── 嵌入源(远程/本地/关闭 三态)与模型管理 ──
|
|
465
521
|
case 'dsh-memory/embedding-state-get': {
|
|
466
|
-
if (!embedManager)
|
|
467
|
-
|
|
468
|
-
|
|
522
|
+
if (!embedManager) {
|
|
523
|
+
const v = { supported: false };
|
|
524
|
+
return v;
|
|
525
|
+
}
|
|
526
|
+
const v = { supported: true, ...(await embedManager.snapshot()) };
|
|
527
|
+
return v;
|
|
469
528
|
}
|
|
470
529
|
case 'dsh-memory/embedding-source-set': {
|
|
471
530
|
if (!embedManager)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蒸馏成本账本(token_cost 明细表):从 sqlite.ts 巨石拆出的第一刀(体检 P1)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:明细写入(写入时按保留期滚动清理)+ 四路聚合查询(单窗口总览 / 按模型 /
|
|
5
|
+
* 按层级归并 / 按时间桶)。与检索引擎零关系——唯一的耦合是共享同一个
|
|
6
|
+
* node:sqlite 连接。成本看板是增强能力:降级/异常一律返回零值,不向上抛错。
|
|
7
|
+
*
|
|
8
|
+
* 接口策略:MemoryDb 的四个同名公开方法保持原签名做一行委托("拆文件不拆接口"),
|
|
9
|
+
* 既有调用方(token-cost.ts / smoke)零改动。
|
|
10
|
+
*/
|
|
11
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
12
|
+
import type { CostByModel } from '../contract.js';
|
|
13
|
+
import type { MemoryLogger } from '../types.js';
|
|
14
|
+
/** token_cost 单窗口成本聚合(成本看板用)。 */
|
|
15
|
+
export interface CostAggregate {
|
|
16
|
+
calls: number;
|
|
17
|
+
inputChars: number;
|
|
18
|
+
outputTokens: number;
|
|
19
|
+
reasoningTokens: number;
|
|
20
|
+
/** 单次调用输出 token 均值(无数据为 0)。 */
|
|
21
|
+
avgOutputTokens: number;
|
|
22
|
+
/** 单次调用输出 token 中位数(无数据为 0)。 */
|
|
23
|
+
medianOutputTokens: number;
|
|
24
|
+
}
|
|
25
|
+
/** 按层级(l1/l2/l3 归并)分组的成本行。 */
|
|
26
|
+
export interface CostByLayer {
|
|
27
|
+
layer: string;
|
|
28
|
+
calls: number;
|
|
29
|
+
inputChars: number;
|
|
30
|
+
outputTokens: number;
|
|
31
|
+
reasoningTokens: number;
|
|
32
|
+
avgOutputTokens: number;
|
|
33
|
+
medianOutputTokens: number;
|
|
34
|
+
}
|
|
35
|
+
/** 按时间桶 + provider/model 聚合的扁平行(趋势图与日均/周均/月均 + 中位数统计共用)。 */
|
|
36
|
+
export interface BucketRow {
|
|
37
|
+
bucket: number;
|
|
38
|
+
provider: string;
|
|
39
|
+
model: string;
|
|
40
|
+
calls: number;
|
|
41
|
+
outputTokens: number;
|
|
42
|
+
reasoningTokens: number;
|
|
43
|
+
}
|
|
44
|
+
export declare class CostLedger {
|
|
45
|
+
private db;
|
|
46
|
+
private logger;
|
|
47
|
+
private stmtInsert;
|
|
48
|
+
private stmtDelete;
|
|
49
|
+
/** init 是否成功(未就绪 = 宿主库降级,方法全部返回零值不抛错)。 */
|
|
50
|
+
get ready(): boolean;
|
|
51
|
+
/** 建表 + 迁移 + 语句缓存(MemoryDb.initSchema 内调用;失败冒泡触发库级降级)。 */
|
|
52
|
+
init(db: DatabaseSync, logger?: MemoryLogger): void;
|
|
53
|
+
/**
|
|
54
|
+
* 记录一次蒸馏调用成本(明细表,写入时按 retentionDays 滚动清理;0 = 永久保留)。
|
|
55
|
+
* 失败/成功都记(token 照烧);记账失败记 warn 但不阻断蒸馏(成本看板是增强能力)。
|
|
56
|
+
*/
|
|
57
|
+
insertCostCall(provider: string, model: string, layer: string, inputChars: number, outputTokens: number, reasoningTokens: number, retentionDays: number): void;
|
|
58
|
+
/**
|
|
59
|
+
* 查询 token_cost 单窗口聚合(成本看板用;since 为毫秒起点,0 = 全量)。
|
|
60
|
+
* 输入口径:inputChars 是字符(llm 流拿不到输入 token,沿用 llm-usage 的字符折算口径)。
|
|
61
|
+
* median 需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median 函数)。
|
|
62
|
+
*/
|
|
63
|
+
aggregateCost(since: number): {
|
|
64
|
+
total: CostAggregate;
|
|
65
|
+
byModel: CostByModel[];
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* 按层级归并聚合(l1 = l1-extract + l1-dedup;成本看板层级表格用)。
|
|
69
|
+
* 降级/异常返回空数组,不抛错。
|
|
70
|
+
*/
|
|
71
|
+
aggregateCostByLayer(since: number): CostByLayer[];
|
|
72
|
+
/**
|
|
73
|
+
* 按时间桶(bucketMs 毫秒)+ model 聚合,返回扁平行。
|
|
74
|
+
* offsetMs 把桶边界对齐本地时区;layer 为空=全部,'l1' 归并 extract/dedup,其余精确匹配。
|
|
75
|
+
* 趋势图与「日均/周均/月均 + 中位数」统计共用:JS 侧按不同 bucketMs 调三次再聚合。
|
|
76
|
+
*/
|
|
77
|
+
aggregateByBucket(bucketMs: number, offsetMs: number, since: number, layer: string): BucketRow[];
|
|
78
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
function emptyCostAggregate() {
|
|
2
|
+
return { calls: 0, inputChars: 0, outputTokens: 0, reasoningTokens: 0, avgOutputTokens: 0, medianOutputTokens: 0 };
|
|
3
|
+
}
|
|
4
|
+
/** 已排序序列的中位数(偶数个取中间两者平均;空返回 0)。 */
|
|
5
|
+
function medianOf(sorted) {
|
|
6
|
+
const n = sorted.length;
|
|
7
|
+
if (n === 0)
|
|
8
|
+
return 0;
|
|
9
|
+
const mid = Math.floor(n / 2);
|
|
10
|
+
return n % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
11
|
+
}
|
|
12
|
+
export class CostLedger {
|
|
13
|
+
db = null;
|
|
14
|
+
logger;
|
|
15
|
+
stmtInsert = null;
|
|
16
|
+
stmtDelete = null;
|
|
17
|
+
/** init 是否成功(未就绪 = 宿主库降级,方法全部返回零值不抛错)。 */
|
|
18
|
+
get ready() {
|
|
19
|
+
return this.stmtInsert !== null;
|
|
20
|
+
}
|
|
21
|
+
/** 建表 + 迁移 + 语句缓存(MemoryDb.initSchema 内调用;失败冒泡触发库级降级)。 */
|
|
22
|
+
init(db, logger) {
|
|
23
|
+
this.db = db;
|
|
24
|
+
this.logger = logger;
|
|
25
|
+
db.exec(`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS token_cost (
|
|
27
|
+
ts INTEGER NOT NULL,
|
|
28
|
+
provider TEXT NOT NULL,
|
|
29
|
+
model TEXT NOT NULL,
|
|
30
|
+
layer TEXT NOT NULL,
|
|
31
|
+
input_chars INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
reasoning_tokens INTEGER NOT NULL DEFAULT 0
|
|
34
|
+
)
|
|
35
|
+
`);
|
|
36
|
+
// 迁移:provider/model 复合键引入前的旧表补 provider 列(历史行回填 unknown)
|
|
37
|
+
const cols = db.prepare('PRAGMA table_info(token_cost)').all();
|
|
38
|
+
if (!cols.some((c) => c.name === 'provider')) {
|
|
39
|
+
db.exec("ALTER TABLE token_cost ADD COLUMN provider TEXT NOT NULL DEFAULT 'unknown'");
|
|
40
|
+
}
|
|
41
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_token_cost_ts ON token_cost(ts)');
|
|
42
|
+
this.stmtInsert = db.prepare('INSERT INTO token_cost (ts, provider, model, layer, input_chars, output_tokens, reasoning_tokens) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
43
|
+
this.stmtDelete = db.prepare('DELETE FROM token_cost WHERE ts < ?');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 记录一次蒸馏调用成本(明细表,写入时按 retentionDays 滚动清理;0 = 永久保留)。
|
|
47
|
+
* 失败/成功都记(token 照烧);记账失败记 warn 但不阻断蒸馏(成本看板是增强能力)。
|
|
48
|
+
*/
|
|
49
|
+
insertCostCall(provider, model, layer, inputChars, outputTokens, reasoningTokens, retentionDays) {
|
|
50
|
+
if (!this.db || !this.stmtInsert || !this.stmtDelete)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
this.stmtInsert.run(Date.now(), provider, model, layer, Math.max(0, Math.round(inputChars)), Math.max(0, Math.round(outputTokens)), Math.max(0, Math.round(reasoningTokens)));
|
|
54
|
+
if (retentionDays > 0)
|
|
55
|
+
this.stmtDelete.run(Date.now() - retentionDays * 24 * 3600_000);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
this.logger?.warn(`[memory][sqlite] token_cost 记账失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 查询 token_cost 单窗口聚合(成本看板用;since 为毫秒起点,0 = 全量)。
|
|
63
|
+
* 输入口径:inputChars 是字符(llm 流拿不到输入 token,沿用 llm-usage 的字符折算口径)。
|
|
64
|
+
* median 需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median 函数)。
|
|
65
|
+
*/
|
|
66
|
+
aggregateCost(since) {
|
|
67
|
+
if (!this.ready || !this.db)
|
|
68
|
+
return { total: emptyCostAggregate(), byModel: [] };
|
|
69
|
+
try {
|
|
70
|
+
const total = this.db
|
|
71
|
+
.prepare(`SELECT COUNT(*) AS calls,
|
|
72
|
+
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
73
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
74
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
75
|
+
COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
|
|
76
|
+
FROM token_cost WHERE ts >= ?`)
|
|
77
|
+
.get(since);
|
|
78
|
+
const tokenRows = this.db
|
|
79
|
+
.prepare('SELECT output_tokens FROM token_cost WHERE ts >= ? ORDER BY output_tokens')
|
|
80
|
+
.all(since);
|
|
81
|
+
const byModel = this.db
|
|
82
|
+
.prepare(`SELECT provider, model, COUNT(*) AS calls,
|
|
83
|
+
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
84
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
85
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
|
|
86
|
+
FROM token_cost WHERE ts >= ? GROUP BY provider, model ORDER BY outputTokens DESC`)
|
|
87
|
+
.all(since);
|
|
88
|
+
return {
|
|
89
|
+
total: {
|
|
90
|
+
calls: total.calls,
|
|
91
|
+
inputChars: total.inputChars,
|
|
92
|
+
outputTokens: total.outputTokens,
|
|
93
|
+
reasoningTokens: total.reasoningTokens,
|
|
94
|
+
avgOutputTokens: total.avgOutputTokens,
|
|
95
|
+
medianOutputTokens: medianOf(tokenRows.map((r) => r.output_tokens)),
|
|
96
|
+
},
|
|
97
|
+
byModel,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return { total: emptyCostAggregate(), byModel: [] };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 按层级归并聚合(l1 = l1-extract + l1-dedup;成本看板层级表格用)。
|
|
106
|
+
* 降级/异常返回空数组,不抛错。
|
|
107
|
+
*/
|
|
108
|
+
aggregateCostByLayer(since) {
|
|
109
|
+
if (!this.ready || !this.db)
|
|
110
|
+
return [];
|
|
111
|
+
try {
|
|
112
|
+
const rows = this.db
|
|
113
|
+
.prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
|
|
114
|
+
COUNT(*) AS calls,
|
|
115
|
+
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
116
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
117
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
118
|
+
COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
|
|
119
|
+
FROM token_cost WHERE ts >= ? GROUP BY layer ORDER BY layer`)
|
|
120
|
+
.all(since);
|
|
121
|
+
// 中位数需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median)
|
|
122
|
+
const tokenRows = this.db
|
|
123
|
+
.prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
|
|
124
|
+
output_tokens
|
|
125
|
+
FROM token_cost WHERE ts >= ? ORDER BY layer, output_tokens`)
|
|
126
|
+
.all(since);
|
|
127
|
+
const medianByLayer = new Map();
|
|
128
|
+
for (const r of tokenRows) {
|
|
129
|
+
const arr = medianByLayer.get(r.layer);
|
|
130
|
+
if (arr)
|
|
131
|
+
arr.push(r.output_tokens);
|
|
132
|
+
else
|
|
133
|
+
medianByLayer.set(r.layer, [r.output_tokens]);
|
|
134
|
+
}
|
|
135
|
+
return rows.map((r) => ({
|
|
136
|
+
...r,
|
|
137
|
+
medianOutputTokens: medianOf(medianByLayer.get(r.layer) ?? []),
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 按时间桶(bucketMs 毫秒)+ model 聚合,返回扁平行。
|
|
146
|
+
* offsetMs 把桶边界对齐本地时区;layer 为空=全部,'l1' 归并 extract/dedup,其余精确匹配。
|
|
147
|
+
* 趋势图与「日均/周均/月均 + 中位数」统计共用:JS 侧按不同 bucketMs 调三次再聚合。
|
|
148
|
+
*/
|
|
149
|
+
aggregateByBucket(bucketMs, offsetMs, since, layer) {
|
|
150
|
+
if (!this.ready || !this.db)
|
|
151
|
+
return [];
|
|
152
|
+
try {
|
|
153
|
+
let sql = `SELECT CAST((ts + ?) / ? AS INTEGER) AS bucket, provider, model,
|
|
154
|
+
COUNT(*) AS calls,
|
|
155
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
156
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
|
|
157
|
+
FROM token_cost WHERE ts >= ?`;
|
|
158
|
+
const params = [offsetMs, bucketMs, since];
|
|
159
|
+
if (layer === 'l1') {
|
|
160
|
+
sql += ` AND layer IN ('l1-extract','l1-dedup')`;
|
|
161
|
+
}
|
|
162
|
+
else if (layer) {
|
|
163
|
+
sql += ` AND layer = ?`;
|
|
164
|
+
params.push(layer);
|
|
165
|
+
}
|
|
166
|
+
sql += ` GROUP BY bucket, provider, model ORDER BY bucket, provider, model`;
|
|
167
|
+
return this.db.prepare(sql).all(...params);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -1,24 +1,7 @@
|
|
|
1
1
|
import type { MemoryLogger } from '../types.js';
|
|
2
2
|
import { type CatalogEntry } from './model-catalog.js';
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
modelId: string;
|
|
6
|
-
phase: DownloadPhase;
|
|
7
|
-
/** 当前文件序号(1-based)。 */
|
|
8
|
-
fileIndex: number;
|
|
9
|
-
fileCount: number;
|
|
10
|
-
/** 当前文件已收字节(含续传基线)。 */
|
|
11
|
-
fileReceived: number;
|
|
12
|
-
fileTotal: number;
|
|
13
|
-
/** 整模型累计字节(含已完成文件;分母 = 目录总大小)。 */
|
|
14
|
-
overallReceived: number;
|
|
15
|
-
overallTotal: number;
|
|
16
|
-
/** EMA 平滑速度(字节/秒)。 */
|
|
17
|
-
speedBps: number;
|
|
18
|
-
startedAt: number;
|
|
19
|
-
/** phase=error 时的原因。 */
|
|
20
|
-
error?: string;
|
|
21
|
-
}
|
|
3
|
+
import type { DownloadProgress } from '../contract.js';
|
|
4
|
+
export type { DownloadPhase, DownloadProgress } from '../contract.js';
|
|
22
5
|
export interface ModelStatus {
|
|
23
6
|
id: string;
|
|
24
7
|
/** none=未下载;partial=有断点/不完整;downloaded=全部文件就位且尺寸吻合。 */
|
|
@@ -104,4 +87,3 @@ export declare function resolveProxyUrl(setting: string | undefined, host: strin
|
|
|
104
87
|
/** 代理 URL 日志脱敏:剥掉 userinfo(内网代理常带 user:pass 凭据),只留 scheme//host;
|
|
105
88
|
* 解析失败的串原样也可能是凭据形态,返回占位符。 */
|
|
106
89
|
export declare function maskProxyUrl(proxy: string): string;
|
|
107
|
-
export {};
|
|
@@ -7,7 +7,8 @@ import { LocalEmbeddingService } from './local-embedding.js';
|
|
|
7
7
|
import { ModelDownloadQueue } from './download-queue.js';
|
|
8
8
|
import { RuntimeInstaller } from './runtime-installer.js';
|
|
9
9
|
import type { MemoryDb } from './sqlite.js';
|
|
10
|
-
|
|
10
|
+
import type { EmbeddingSourceKind, EmbeddingStateView } from '../contract.js';
|
|
11
|
+
export type { ApplyPhase, EmbeddingSourceKind, EmbeddingStateView, ReindexProgressState } from '../contract.js';
|
|
11
12
|
export interface EmbeddingSourceState {
|
|
12
13
|
source: EmbeddingSourceKind;
|
|
13
14
|
/** source=local 时启用的目录模型 id。 */
|
|
@@ -38,17 +39,6 @@ export declare function resolveInitialEmbedding(cfg: MemoryConfig, sourceStore:
|
|
|
38
39
|
/** 本地服务构造工厂(index.ts 的初始解析与 Manager 共用一份实现,防漂移)。
|
|
39
40
|
* 推理在 worker 线程(见 local-embedding.ts);此处只传 runtime 目录与模型目录。 */
|
|
40
41
|
export declare function makeLocalServiceFactory(installer: RuntimeInstaller, downloader: ModelDownloadQueue, logger?: MemoryLogger, maxInputChars?: number): (modelId: string) => LocalEmbeddingService | null;
|
|
41
|
-
export type ApplyPhase = 'idle' | 'installing-runtime' | 'warming' | 'switching' | 'reindexing' | 'done' | 'error';
|
|
42
|
-
export interface ReindexProgressState {
|
|
43
|
-
running: boolean;
|
|
44
|
-
l1Done: number;
|
|
45
|
-
l1Total: number;
|
|
46
|
-
l0Done: number;
|
|
47
|
-
l0Total: number;
|
|
48
|
-
startedAt: number;
|
|
49
|
-
cancelled: boolean;
|
|
50
|
-
error?: string;
|
|
51
|
-
}
|
|
52
42
|
export interface EmbeddingManagerDeps {
|
|
53
43
|
dataDir: string;
|
|
54
44
|
cfg: MemoryConfig;
|
|
@@ -119,43 +109,3 @@ export declare class EmbeddingManager {
|
|
|
119
109
|
/** RPC 快照(设置页嵌入区块数据源;client 忙时 1s 轮询)。 */
|
|
120
110
|
snapshot(): Promise<EmbeddingStateView>;
|
|
121
111
|
}
|
|
122
|
-
export interface EmbeddingStateView {
|
|
123
|
-
source: EmbeddingSourceKind;
|
|
124
|
-
activeModel: string | null;
|
|
125
|
-
ceilings: {
|
|
126
|
-
remote: boolean;
|
|
127
|
-
local: boolean;
|
|
128
|
-
};
|
|
129
|
-
runtime: {
|
|
130
|
-
targetVersion: string;
|
|
131
|
-
phase: 'idle' | 'installing' | 'ready' | 'error' | 'cancelled';
|
|
132
|
-
installedVersion: string | null;
|
|
133
|
-
elapsedMs: number;
|
|
134
|
-
lastLines: string[];
|
|
135
|
-
error?: string;
|
|
136
|
-
};
|
|
137
|
-
models: Array<{
|
|
138
|
-
id: string;
|
|
139
|
-
name: string;
|
|
140
|
-
dims: number;
|
|
141
|
-
contextTokens: number;
|
|
142
|
-
tags: string[];
|
|
143
|
-
description: string;
|
|
144
|
-
totalBytes: number;
|
|
145
|
-
bytesOnDisk: number;
|
|
146
|
-
state: 'none' | 'partial' | 'downloaded';
|
|
147
|
-
}>;
|
|
148
|
-
download: ReturnType<ModelDownloadQueue['getProgress']>;
|
|
149
|
-
apply: {
|
|
150
|
-
phase: ApplyPhase;
|
|
151
|
-
message: string;
|
|
152
|
-
startedAt: number;
|
|
153
|
-
busy: boolean;
|
|
154
|
-
};
|
|
155
|
-
local: {
|
|
156
|
-
state: 'idle' | 'loading' | 'ready' | 'failed' | 'terminated';
|
|
157
|
-
error: string | null;
|
|
158
|
-
} | null;
|
|
159
|
-
reindex: ReindexProgressState;
|
|
160
|
-
activeNote?: string;
|
|
161
|
-
}
|
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import type { MemoryLogger } from '../types.js';
|
|
2
2
|
/** 钉死的 transformers.js 版本(D8:精确版本,升级插件时在此变更并重装运行时)。 */
|
|
3
3
|
export declare const PINNED_TRANSFORMERS_VERSION = "4.2.0";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/** 插件钉死的目标版本。 */
|
|
7
|
-
targetVersion: string;
|
|
8
|
-
/** runtime 里实际就位的版本(未安装为 null)。 */
|
|
9
|
-
installedVersion: string | null;
|
|
10
|
-
startedAt: number;
|
|
11
|
-
/** 已运行毫秒(读时计算)。 */
|
|
12
|
-
elapsedMs: number;
|
|
13
|
-
/** npm 输出尾行(最近 5 行,让用户看到 npm 在干什么)。 */
|
|
14
|
-
lastLines: string[];
|
|
15
|
-
error?: string;
|
|
16
|
-
}
|
|
4
|
+
import type { RuntimeProgress } from '../contract.js';
|
|
5
|
+
export type { RuntimeProgress } from '../contract.js';
|
|
17
6
|
/** 子进程抽象(测试注入缝):注册输出行回调 + 终止 + 终态。 */
|
|
18
7
|
export interface SpawnedProcess {
|
|
19
8
|
onStdout(cb: (line: string) => void): void;
|