dsh-prime-memory 0.11.0
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/CHANGELOG.en.md +28 -0
- package/CHANGELOG.ja.md +30 -0
- package/CHANGELOG.ko.md +30 -0
- package/CHANGELOG.md +1220 -0
- package/ENGINEERING-NOTES.md +452 -0
- package/INSTALL.en.md +92 -0
- package/INSTALL.ja.md +92 -0
- package/INSTALL.ko.md +92 -0
- package/INSTALL.md +92 -0
- package/LICENSE +21 -0
- package/README.en.md +458 -0
- package/README.ja.md +306 -0
- package/README.ko.md +306 -0
- package/README.md +425 -0
- package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
- package/assets/changelog/0.8.9/01-panel.png +0 -0
- package/assets/changelog/0.8.9/02-halo.png +0 -0
- package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
- package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
- package/assets/img/EmbeddingSource.png +0 -0
- package/assets/img/Hero.png +0 -0
- package/assets/img/Layers.png +0 -0
- package/assets/img/MemoryTools.png +0 -0
- package/assets/img/Modes.png +0 -0
- package/assets/img/ToolTrajectory.png +0 -0
- package/assets/img/ui-dark.jpg +0 -0
- package/assets/img/ui-light.jpg +0 -0
- package/assets/readme/bench-dialog.svg +70 -0
- package/assets/readme/bench-workflow.svg +79 -0
- package/assets/readme/flow.svg +189 -0
- package/assets/readme/storage.svg +115 -0
- package/cordis.patch.yml +16 -0
- package/dist/bench-control.d.ts +34 -0
- package/dist/bench-control.js +16 -0
- package/dist/client.js +4293 -0
- package/dist/config.d.ts +683 -0
- package/dist/config.js +129 -0
- package/dist/contract.d.ts +820 -0
- package/dist/contract.js +1 -0
- package/dist/embedding-worker.cjs +176 -0
- package/dist/graph/apply.d.ts +37 -0
- package/dist/graph/apply.js +270 -0
- package/dist/graph/constraints.d.ts +47 -0
- package/dist/graph/constraints.js +38 -0
- package/dist/graph/search.d.ts +16 -0
- package/dist/graph/search.js +115 -0
- package/dist/graph/types.d.ts +142 -0
- package/dist/graph/types.js +14 -0
- package/dist/hooks/capture.d.ts +32 -0
- package/dist/hooks/capture.js +194 -0
- package/dist/hooks/recall.d.ts +63 -0
- package/dist/hooks/recall.js +429 -0
- package/dist/index.d.ts +534 -0
- package/dist/index.js +344 -0
- package/dist/llm-usage.d.ts +26 -0
- package/dist/llm-usage.js +37 -0
- package/dist/llm.d.ts +153 -0
- package/dist/llm.js +530 -0
- package/dist/pipeline/graph.d.ts +35 -0
- package/dist/pipeline/graph.js +104 -0
- package/dist/pipeline/l1.d.ts +19 -0
- package/dist/pipeline/l1.js +271 -0
- package/dist/pipeline/l2.d.ts +13 -0
- package/dist/pipeline/l2.js +83 -0
- package/dist/pipeline/l3.d.ts +15 -0
- package/dist/pipeline/l3.js +78 -0
- package/dist/pipeline/rebuild.d.ts +61 -0
- package/dist/pipeline/rebuild.js +307 -0
- package/dist/pipeline/ruminate.d.ts +89 -0
- package/dist/pipeline/ruminate.js +298 -0
- package/dist/pipeline/runner.d.ts +167 -0
- package/dist/pipeline/runner.js +638 -0
- package/dist/pipeline/trigger.d.ts +40 -0
- package/dist/pipeline/trigger.js +75 -0
- package/dist/prompts/graph-projection.d.ts +70 -0
- package/dist/prompts/graph-projection.js +167 -0
- package/dist/prompts/l1-dedup.d.ts +22 -0
- package/dist/prompts/l1-dedup.js +251 -0
- package/dist/prompts/l1-extraction.d.ts +22 -0
- package/dist/prompts/l1-extraction.js +457 -0
- package/dist/prompts/persona.d.ts +23 -0
- package/dist/prompts/persona.js +240 -0
- package/dist/prompts/scene.d.ts +32 -0
- package/dist/prompts/scene.js +414 -0
- package/dist/runtime-package-lock.json +982 -0
- package/dist/settings.d.ts +50 -0
- package/dist/settings.js +355 -0
- package/dist/stats.d.ts +109 -0
- package/dist/stats.js +929 -0
- package/dist/store/bm25.d.ts +19 -0
- package/dist/store/bm25.js +63 -0
- package/dist/store/cost-ledger.d.ts +75 -0
- package/dist/store/cost-ledger.js +171 -0
- package/dist/store/download-queue.d.ts +79 -0
- package/dist/store/download-queue.js +424 -0
- package/dist/store/embedding-source.d.ts +118 -0
- package/dist/store/embedding-source.js +443 -0
- package/dist/store/embedding.d.ts +90 -0
- package/dist/store/embedding.js +206 -0
- package/dist/store/graph-store.d.ts +94 -0
- package/dist/store/graph-store.js +641 -0
- package/dist/store/l0.d.ts +40 -0
- package/dist/store/l0.js +197 -0
- package/dist/store/l1.d.ts +93 -0
- package/dist/store/l1.js +297 -0
- package/dist/store/local-embedding.d.ts +89 -0
- package/dist/store/local-embedding.js +227 -0
- package/dist/store/model-catalog.d.ts +48 -0
- package/dist/store/model-catalog.js +81 -0
- package/dist/store/occupancy.d.ts +30 -0
- package/dist/store/occupancy.js +134 -0
- package/dist/store/pending.d.ts +36 -0
- package/dist/store/pending.js +103 -0
- package/dist/store/persona.d.ts +15 -0
- package/dist/store/persona.js +60 -0
- package/dist/store/recall-dedupe.d.ts +26 -0
- package/dist/store/recall-dedupe.js +138 -0
- package/dist/store/runtime-installer.d.ts +59 -0
- package/dist/store/runtime-installer.js +243 -0
- package/dist/store/scenes.d.ts +24 -0
- package/dist/store/scenes.js +160 -0
- package/dist/store/search-utils.d.ts +38 -0
- package/dist/store/search-utils.js +100 -0
- package/dist/store/session-modes.d.ts +35 -0
- package/dist/store/session-modes.js +144 -0
- package/dist/store/sqlite.d.ts +246 -0
- package/dist/store/sqlite.js +1491 -0
- package/dist/store/state.d.ts +41 -0
- package/dist/store/state.js +72 -0
- package/dist/token-cost.d.ts +23 -0
- package/dist/token-cost.js +185 -0
- package/dist/tools/index.d.ts +34 -0
- package/dist/tools/index.js +758 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +38 -0
- package/dist/util/context-occupancy.d.ts +68 -0
- package/dist/util/context-occupancy.js +92 -0
- package/dist/util/filelog.d.ts +6 -0
- package/dist/util/filelog.js +108 -0
- package/dist/util/io.d.ts +18 -0
- package/dist/util/io.js +97 -0
- package/dist/util/recall-budget.d.ts +31 -0
- package/dist/util/recall-budget.js +84 -0
- package/dist/util/sanitize.d.ts +11 -0
- package/dist/util/sanitize.js +67 -0
- package/dist/util/text.d.ts +16 -0
- package/dist/util/text.js +61 -0
- package/dist/util/tokenizer.d.ts +9 -0
- package/dist/util/tokenizer.js +50 -0
- package/dsh.plugin.json +22 -0
- package/package.json +118 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Bm25Doc {
|
|
2
|
+
id: string;
|
|
3
|
+
text: string;
|
|
4
|
+
}
|
|
5
|
+
export interface Bm25Hit {
|
|
6
|
+
id: string;
|
|
7
|
+
score: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class Bm25Index {
|
|
10
|
+
private docs;
|
|
11
|
+
private termFreq;
|
|
12
|
+
private docLen;
|
|
13
|
+
private df;
|
|
14
|
+
private totalLen;
|
|
15
|
+
get size(): number;
|
|
16
|
+
/** 全量重建(小语料每轮重建,量级为个位数到百条)。 */
|
|
17
|
+
rebuild(docs: Bm25Doc[]): void;
|
|
18
|
+
search(query: string, topK: number, filter?: (id: string) => boolean): Bm25Hit[];
|
|
19
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量 BM25 检索:与 FTS 共用同一分词器(util/text.ts 的 tokenize,
|
|
3
|
+
* jieba 词 + CJK 二元组并集)。
|
|
4
|
+
* 仅用于 L2 场景摘要的上下文选取(小语料);L0/L1 检索走 SQLite FTS5/向量。
|
|
5
|
+
*/
|
|
6
|
+
import { tokenize } from '../util/text.js';
|
|
7
|
+
const K1 = 1.5;
|
|
8
|
+
const B = 0.75;
|
|
9
|
+
export class Bm25Index {
|
|
10
|
+
docs = [];
|
|
11
|
+
termFreq = [];
|
|
12
|
+
docLen = [];
|
|
13
|
+
df = new Map();
|
|
14
|
+
totalLen = 0;
|
|
15
|
+
get size() {
|
|
16
|
+
return this.docs.length;
|
|
17
|
+
}
|
|
18
|
+
/** 全量重建(小语料每轮重建,量级为个位数到百条)。 */
|
|
19
|
+
rebuild(docs) {
|
|
20
|
+
this.docs = docs;
|
|
21
|
+
this.termFreq = [];
|
|
22
|
+
this.docLen = [];
|
|
23
|
+
this.df = new Map();
|
|
24
|
+
this.totalLen = 0;
|
|
25
|
+
for (let i = 0; i < docs.length; i++) {
|
|
26
|
+
const terms = tokenize(docs[i].text);
|
|
27
|
+
const tf = new Map();
|
|
28
|
+
for (const t of terms)
|
|
29
|
+
tf.set(t, (tf.get(t) ?? 0) + 1);
|
|
30
|
+
this.termFreq.push(tf);
|
|
31
|
+
this.docLen.push(terms.length);
|
|
32
|
+
this.totalLen += terms.length;
|
|
33
|
+
for (const t of tf.keys())
|
|
34
|
+
this.df.set(t, (this.df.get(t) ?? 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
search(query, topK, filter) {
|
|
38
|
+
const terms = tokenize(query);
|
|
39
|
+
if (terms.length === 0 || this.docs.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
const avgdl = this.totalLen / this.docs.length || 1;
|
|
42
|
+
const n = this.docs.length;
|
|
43
|
+
const scores = [];
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
if (filter && !filter(this.docs[i].id))
|
|
46
|
+
continue;
|
|
47
|
+
const tf = this.termFreq[i];
|
|
48
|
+
const len = this.docLen[i];
|
|
49
|
+
let score = 0;
|
|
50
|
+
for (const t of new Set(terms)) {
|
|
51
|
+
const f = tf.get(t) ?? 0;
|
|
52
|
+
if (f === 0)
|
|
53
|
+
continue;
|
|
54
|
+
const idf = Math.log(1 + (n - (this.df.get(t) ?? 0) + 0.5) / ((this.df.get(t) ?? 0) + 0.5));
|
|
55
|
+
score += idf * ((f * (K1 + 1)) / (f + K1 * (1 - B + B * (len / avgdl))));
|
|
56
|
+
}
|
|
57
|
+
if (score > 0)
|
|
58
|
+
scores.push({ i, s: score });
|
|
59
|
+
}
|
|
60
|
+
scores.sort((a, b) => b.s - a.s);
|
|
61
|
+
return scores.slice(0, topK).map(({ i, s }) => ({ id: this.docs[i].id, score: s }));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蒸馏成本账本(token_cost 明细表)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:明细写入(写入时按保留期滚动清理)+ 四路聚合查询(单窗口总览 / 按模型 /
|
|
5
|
+
* 按层级归并 / 按时间桶)。与检索引擎零关系——唯一耦合是共享同一个 node:sqlite
|
|
6
|
+
* 连接。成本看板是增强能力:降级/异常一律返回零值,不向上抛错。
|
|
7
|
+
* MemoryDb 的四个同名公开方法保持签名做一行委托,调用方零改动。
|
|
8
|
+
*/
|
|
9
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
10
|
+
import type { CostByModel } from '../contract.js';
|
|
11
|
+
import type { MemoryLogger } from '../types.js';
|
|
12
|
+
/** token_cost 单窗口成本聚合(成本看板用)。 */
|
|
13
|
+
export interface CostAggregate {
|
|
14
|
+
calls: number;
|
|
15
|
+
inputChars: number;
|
|
16
|
+
outputTokens: number;
|
|
17
|
+
reasoningTokens: number;
|
|
18
|
+
/** 单次调用输出 token 均值(无数据为 0)。 */
|
|
19
|
+
avgOutputTokens: number;
|
|
20
|
+
/** 单次调用输出 token 中位数(无数据为 0)。 */
|
|
21
|
+
medianOutputTokens: number;
|
|
22
|
+
}
|
|
23
|
+
/** 按层级(l1/l2/l3 归并)分组的成本行。 */
|
|
24
|
+
export interface CostByLayer {
|
|
25
|
+
layer: string;
|
|
26
|
+
calls: number;
|
|
27
|
+
inputChars: number;
|
|
28
|
+
outputTokens: number;
|
|
29
|
+
reasoningTokens: number;
|
|
30
|
+
avgOutputTokens: number;
|
|
31
|
+
medianOutputTokens: number;
|
|
32
|
+
}
|
|
33
|
+
/** 按时间桶 + provider/model 聚合的扁平行(趋势图与日均/周均/月均 + 中位数统计共用)。 */
|
|
34
|
+
export interface BucketRow {
|
|
35
|
+
bucket: number;
|
|
36
|
+
provider: string;
|
|
37
|
+
model: string;
|
|
38
|
+
calls: number;
|
|
39
|
+
outputTokens: number;
|
|
40
|
+
reasoningTokens: number;
|
|
41
|
+
}
|
|
42
|
+
export declare class CostLedger {
|
|
43
|
+
private db;
|
|
44
|
+
private logger;
|
|
45
|
+
private stmtInsert;
|
|
46
|
+
private stmtDelete;
|
|
47
|
+
/** init 是否成功(未就绪 = 宿主库降级,方法全部返回零值不抛错)。 */
|
|
48
|
+
get ready(): boolean;
|
|
49
|
+
/** 建表 + 迁移 + 语句缓存(MemoryDb.initSchema 内调用;失败冒泡触发库级降级)。 */
|
|
50
|
+
init(db: DatabaseSync, logger?: MemoryLogger): void;
|
|
51
|
+
/**
|
|
52
|
+
* 记录一次蒸馏调用成本(明细表,写入时按 retentionDays 滚动清理;0 = 永久保留)。
|
|
53
|
+
* 失败/成功都记(token 照烧);记账失败记 warn 但不阻断蒸馏(成本看板是增强能力)。
|
|
54
|
+
*/
|
|
55
|
+
insertCostCall(provider: string, model: string, layer: string, inputChars: number, outputTokens: number, reasoningTokens: number, retentionDays: number): void;
|
|
56
|
+
/**
|
|
57
|
+
* 查询 token_cost 单窗口聚合(成本看板用;since 为毫秒起点,0 = 全量)。
|
|
58
|
+
* 输入口径:inputChars 是字符(llm 流拿不到输入 token,沿用 llm-usage 的字符折算口径)。
|
|
59
|
+
*/
|
|
60
|
+
aggregateCost(since: number): {
|
|
61
|
+
total: CostAggregate;
|
|
62
|
+
byModel: CostByModel[];
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* 按层级归并聚合(l1 = l1-extract + l1-dedup;成本看板层级表格用)。
|
|
66
|
+
* 降级/异常返回空数组,不抛错。
|
|
67
|
+
*/
|
|
68
|
+
aggregateCostByLayer(since: number): CostByLayer[];
|
|
69
|
+
/**
|
|
70
|
+
* 按时间桶(bucketMs 毫秒)+ model 聚合,返回扁平行。
|
|
71
|
+
* offsetMs 把桶边界对齐本地时区;layer 为空=全部,'l1' 归并 extract/dedup,其余精确匹配。
|
|
72
|
+
* 趋势图与「日均/周均/月均 + 中位数」统计共用:JS 侧按不同 bucketMs 调三次再聚合。
|
|
73
|
+
*/
|
|
74
|
+
aggregateByBucket(bucketMs: number, offsetMs: number, since: number, layer: string): BucketRow[];
|
|
75
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
function emptyCostAggregate() {
|
|
2
|
+
return { calls: 0, inputChars: 0, outputTokens: 0, reasoningTokens: 0, avgOutputTokens: 0, medianOutputTokens: 0 };
|
|
3
|
+
}
|
|
4
|
+
/** 已排序序列的中位数(偶数个取中间两者平均;空返回 0)。SQLite 无内置 median,序列须取回 JS 计算。 */
|
|
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
|
+
*/
|
|
65
|
+
aggregateCost(since) {
|
|
66
|
+
if (!this.ready || !this.db)
|
|
67
|
+
return { total: emptyCostAggregate(), byModel: [] };
|
|
68
|
+
try {
|
|
69
|
+
const total = this.db
|
|
70
|
+
.prepare(`SELECT COUNT(*) AS calls,
|
|
71
|
+
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
72
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
73
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
74
|
+
COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
|
|
75
|
+
FROM token_cost WHERE ts >= ?`)
|
|
76
|
+
.get(since);
|
|
77
|
+
const tokenRows = this.db
|
|
78
|
+
.prepare('SELECT output_tokens FROM token_cost WHERE ts >= ? ORDER BY output_tokens')
|
|
79
|
+
.all(since);
|
|
80
|
+
const byModel = this.db
|
|
81
|
+
.prepare(`SELECT provider, model, COUNT(*) AS calls,
|
|
82
|
+
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
83
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
84
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
|
|
85
|
+
FROM token_cost WHERE ts >= ? GROUP BY provider, model ORDER BY outputTokens DESC`)
|
|
86
|
+
.all(since);
|
|
87
|
+
return {
|
|
88
|
+
total: {
|
|
89
|
+
calls: total.calls,
|
|
90
|
+
inputChars: total.inputChars,
|
|
91
|
+
outputTokens: total.outputTokens,
|
|
92
|
+
reasoningTokens: total.reasoningTokens,
|
|
93
|
+
avgOutputTokens: total.avgOutputTokens,
|
|
94
|
+
medianOutputTokens: medianOf(tokenRows.map((r) => r.output_tokens)),
|
|
95
|
+
},
|
|
96
|
+
byModel,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { total: emptyCostAggregate(), byModel: [] };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 按层级归并聚合(l1 = l1-extract + l1-dedup;成本看板层级表格用)。
|
|
105
|
+
* 降级/异常返回空数组,不抛错。
|
|
106
|
+
*/
|
|
107
|
+
aggregateCostByLayer(since) {
|
|
108
|
+
if (!this.ready || !this.db)
|
|
109
|
+
return [];
|
|
110
|
+
try {
|
|
111
|
+
const rows = this.db
|
|
112
|
+
.prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
|
|
113
|
+
COUNT(*) AS calls,
|
|
114
|
+
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
115
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
116
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
117
|
+
COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
|
|
118
|
+
FROM token_cost WHERE ts >= ? GROUP BY layer ORDER BY layer`)
|
|
119
|
+
.all(since);
|
|
120
|
+
const tokenRows = this.db
|
|
121
|
+
.prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
|
|
122
|
+
output_tokens
|
|
123
|
+
FROM token_cost WHERE ts >= ? ORDER BY layer, output_tokens`)
|
|
124
|
+
.all(since);
|
|
125
|
+
const medianByLayer = new Map();
|
|
126
|
+
for (const r of tokenRows) {
|
|
127
|
+
const arr = medianByLayer.get(r.layer);
|
|
128
|
+
if (arr)
|
|
129
|
+
arr.push(r.output_tokens);
|
|
130
|
+
else
|
|
131
|
+
medianByLayer.set(r.layer, [r.output_tokens]);
|
|
132
|
+
}
|
|
133
|
+
return rows.map((r) => ({
|
|
134
|
+
...r,
|
|
135
|
+
medianOutputTokens: medianOf(medianByLayer.get(r.layer) ?? []),
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 按时间桶(bucketMs 毫秒)+ model 聚合,返回扁平行。
|
|
144
|
+
* offsetMs 把桶边界对齐本地时区;layer 为空=全部,'l1' 归并 extract/dedup,其余精确匹配。
|
|
145
|
+
* 趋势图与「日均/周均/月均 + 中位数」统计共用:JS 侧按不同 bucketMs 调三次再聚合。
|
|
146
|
+
*/
|
|
147
|
+
aggregateByBucket(bucketMs, offsetMs, since, layer) {
|
|
148
|
+
if (!this.ready || !this.db)
|
|
149
|
+
return [];
|
|
150
|
+
try {
|
|
151
|
+
let sql = `SELECT CAST((ts + ?) / ? AS INTEGER) AS bucket, provider, model,
|
|
152
|
+
COUNT(*) AS calls,
|
|
153
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
154
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
|
|
155
|
+
FROM token_cost WHERE ts >= ?`;
|
|
156
|
+
const params = [offsetMs, bucketMs, since];
|
|
157
|
+
if (layer === 'l1') {
|
|
158
|
+
sql += ` AND layer IN ('l1-extract','l1-dedup')`;
|
|
159
|
+
}
|
|
160
|
+
else if (layer) {
|
|
161
|
+
sql += ` AND layer = ?`;
|
|
162
|
+
params.push(layer);
|
|
163
|
+
}
|
|
164
|
+
sql += ` GROUP BY bucket, provider, model ORDER BY bucket, provider, model`;
|
|
165
|
+
return this.db.prepare(sql).all(...params);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { MemoryLogger } from '../types.js';
|
|
2
|
+
import { type CatalogEntry } from './model-catalog.js';
|
|
3
|
+
import type { DownloadProgress } from '../contract.js';
|
|
4
|
+
export type { DownloadPhase, DownloadProgress } from '../contract.js';
|
|
5
|
+
export interface ModelStatus {
|
|
6
|
+
id: string;
|
|
7
|
+
/** none=未下载;partial=有断点/不完整;downloaded=全部文件就位且尺寸吻合。 */
|
|
8
|
+
state: 'none' | 'partial' | 'downloaded';
|
|
9
|
+
bytesOnDisk: number;
|
|
10
|
+
totalBytes: number;
|
|
11
|
+
}
|
|
12
|
+
type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
13
|
+
export interface DownloaderOptions {
|
|
14
|
+
/** 下载镜像根(默认 https://hf-mirror.com,可配回 https://huggingface.co)。 */
|
|
15
|
+
mirror: string;
|
|
16
|
+
logger?: MemoryLogger;
|
|
17
|
+
/** 测试注入;默认 undici fetch(按需挂代理 dispatcher)。 */
|
|
18
|
+
fetchImpl?: FetchLike;
|
|
19
|
+
/** 测试注入磁盘剩余字节;默认 statfs。 */
|
|
20
|
+
freeBytes?: () => Promise<number | null>;
|
|
21
|
+
/** 单文件失败的自动重试间隔(毫秒),长度即重试次数;默认 [1000, 3000]。 */
|
|
22
|
+
retryDelaysMs?: number[];
|
|
23
|
+
/** 显式代理三态(见模块头注释)。 */
|
|
24
|
+
proxy?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class ModelDownloadQueue {
|
|
27
|
+
private readonly dataDir;
|
|
28
|
+
private readonly opts;
|
|
29
|
+
private progress;
|
|
30
|
+
private busy;
|
|
31
|
+
private abort;
|
|
32
|
+
/** 代理 dispatcher(按需创建;dispose 关闭连接池)。 */
|
|
33
|
+
private agent;
|
|
34
|
+
/** 默认 fetch:undici(可挂代理 dispatcher);测试注入优先。 */
|
|
35
|
+
private readonly defaultFetch;
|
|
36
|
+
constructor(dataDir: string, opts: DownloaderOptions);
|
|
37
|
+
/** 镜像根(无尾斜杠)。 */
|
|
38
|
+
private mirrorUrl;
|
|
39
|
+
/** 释放代理连接池(插件 dispose 链调用;无代理时幂等无操作)。 */
|
|
40
|
+
dispose(): void;
|
|
41
|
+
/** 当前进度快照(无任务时 null)。 */
|
|
42
|
+
getProgress(): DownloadProgress | null;
|
|
43
|
+
/** 是否有任务在跑(含校验阶段)。 */
|
|
44
|
+
isBusy(): boolean;
|
|
45
|
+
modelsDir(id: string): string;
|
|
46
|
+
/** 全目录状态扫描(设置页模型卡数据源)。 */
|
|
47
|
+
listStatus(): Promise<ModelStatus[]>;
|
|
48
|
+
/** 单模型是否已完整下载(尺寸口径,不做哈希复验——下载完成时已验过)。 */
|
|
49
|
+
isDownloaded(id: string): Promise<boolean>;
|
|
50
|
+
/** 删除已下载模型(切走后释放磁盘;正在使用/下载中的拒绝)。 */
|
|
51
|
+
deleteModel(id: string): Promise<{
|
|
52
|
+
ok: boolean;
|
|
53
|
+
error?: string;
|
|
54
|
+
}>;
|
|
55
|
+
/** 启动下载(串行队列:忙时直接拒绝)。resolve 在任务终态(done/error/cancelled)。 */
|
|
56
|
+
start(id: string): Promise<DownloadProgress>;
|
|
57
|
+
/** 按给定目录项启动(测试缝:合成目录项驱动状态机,不触网)。 */
|
|
58
|
+
startEntry(entry: CatalogEntry): Promise<DownloadProgress>;
|
|
59
|
+
/** 取消当前任务:中断 fetch,保留 .part 断点。 */
|
|
60
|
+
cancel(): boolean;
|
|
61
|
+
private run;
|
|
62
|
+
/** 下载单文件到最终路径(含续传与校验),返回该文件贡献的字节数。
|
|
63
|
+
* 单文件失败自动重试(默认 2 次)+ 重试换缓存键(见模块头注释):
|
|
64
|
+
* - sha256 失配:downloadFileOnce 已删除断点 → 从零重下;
|
|
65
|
+
* - 数量不吻合/网络错误:断点保留 → Range 续传重试;
|
|
66
|
+
* - 取消:立即上抛不重试。 */
|
|
67
|
+
private downloadFile;
|
|
68
|
+
/** 单次尝试:续传探测 → fetch(attempt>0 追加缓存键参数)→ 落盘 → 尺寸与 sha256 校验 → rename。 */
|
|
69
|
+
private downloadFileOnce;
|
|
70
|
+
private freeBytes;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 解析下载代理(三态):`''`(默认)= 探测代理环境变量(HTTPS_PROXY > ALL_PROXY >
|
|
74
|
+
* HTTP_PROXY,大小写双形态,尊重 NO_PROXY);`'none'` = 禁用代理强制直连;
|
|
75
|
+
* 其他值 = 显式代理 URL。与 curl/npm 同语义。
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveProxyUrl(setting: string | undefined, host: string): string;
|
|
78
|
+
/** 代理 URL 日志脱敏:剥掉 userinfo(内网代理常带 user:pass 凭据),只留 scheme//host。 */
|
|
79
|
+
export declare function maskProxyUrl(proxy: string): string;
|