dsh-layered-memory 0.8.6 → 0.8.7
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 +10 -0
- package/README.md +8 -0
- package/dist/client.js +317 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +4 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +3 -0
- package/dist/llm.js +5 -0
- package/dist/stats.js +11 -0
- package/dist/store/sqlite.d.ts +68 -0
- package/dist/store/sqlite.js +162 -0
- package/dist/token-cost.d.ts +90 -0
- package/dist/token-cost.js +178 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -103,6 +103,15 @@ long-lived facts never sink); tune via `recall.decayHalfLifeDays`, `0` disables.
|
|
|
103
103
|
also registers three model-callable memory tools: `memory_search` /
|
|
104
104
|
`conversation_search` / `memory_read_scene`.
|
|
105
105
|
|
|
106
|
+
**Cost dashboard**: every distillation LLM call (extract / dedup / L2 / L3) writes its
|
|
107
|
+
token cost to a SQLite detail table keyed by `provider/model` (configurable retention,
|
|
108
|
+
default 365 days with rolling cleanup on write; accounting failures only log a warning
|
|
109
|
+
and never block distillation). Visualize it under Settings → Memory → the **Cost** tab:
|
|
110
|
+
per-model trend lines (day/week/month granularity + last-N-days window + L1/L2/L3 layer
|
|
111
|
+
filter), a layer × time-window table (calls / output & reasoning tokens / mean / median),
|
|
112
|
+
and per-model totals — distillation overhead at a glance. Input is counted in characters
|
|
113
|
+
(DSH streaming usage carries no input tokens); output and reasoning in tokens.
|
|
114
|
+
|
|
106
115
|
In action: the "Context injection · memory" row surfaces relevant memories first, and
|
|
107
116
|
the model then calls `memory_read_scene` directly to read scene blocks before answering
|
|
108
117
|
from memory:
|
|
@@ -269,6 +278,7 @@ the bundle layer appends and causes `duplicate loader entry id` startup failure)
|
|
|
269
278
|
| `llm.temperature` | `0.3` | Distillation temperature |
|
|
270
279
|
| `llm.maxInputChars` | `700000` | Input character budget per distillation call (over-budget L1 inputs are chunked automatically); runtime-adjustable in Settings → distillation parameters → input budget (empty/0 = follow this value) |
|
|
271
280
|
| `llm.timeoutMs` | `120000` | Per-call distillation timeout (ms) |
|
|
281
|
+
| `tokenCost.retentionDays` | `365` | Retention (days) for distillation cost details (the `token_cost` table); rows older than this are rolled away on write. `0` = keep forever. Also the upper bound of the cost dashboard's "last N days" window |
|
|
272
282
|
| `tools` | `true` | Whether to register model-callable memory tools |
|
|
273
283
|
| `benchControl` | `false` | Register the in-process bench control service (rebuild trigger / session-mode setting / distillation usage snapshot — used by the benchmark's lifecycle track). Off by default — zero surface in production deployments; do not enable casually |
|
|
274
284
|
|
package/README.md
CHANGED
|
@@ -76,6 +76,13 @@ npx tsc src/smoke.ts --outDir dist-smoke --module nodenext --moduleResolution no
|
|
|
76
76
|
|
|
77
77
|
插件挂在 dsh 原生事件上(`session/event` 捕获、`agent/pre-step` 注入),蒸馏调用复用宿主 `ctx.llm`。召回以**消息侧注入**呈现:相关记忆作为一条合成消息排在用户新消息之前,会话流里显示为**"上下文注入 · memory"**行(点开看命中内容)——用户能直接看到"记忆生效了";注入内容有长度预算与时间预算,超限截断/超时跳过,绝不拖慢对话。**同会话去重**:已注入过的记忆不再重复注入(模型上下文里已经有了,追问同类问题时省 token);上下文被 `/compact` 压缩或清空时自动重置,记忆可重新注入;被更新的记忆(内容变化换新 id)不受旧压制。**时效加权**:召回排序按 `相关度 × max(0.5, 0.5^(距上次更新天数/30))` 软加权——相关度相近的候选之间新鲜记忆优先(名额自然轮转),相关度足够高的老记忆照常召回(地板保证最多损失一半排序分,长期事实不沉底);`recall.decayHalfLifeDays` 可调,0=关闭。
|
|
78
78
|
|
|
79
|
+
**成本看板**:每次蒸馏 LLM 调用(抽取/去重/L2/L3)的 token 成本按 `provider/model` 写入
|
|
80
|
+
SQLite 明细表(保留期可配置,默认 365 天,写入时滚动清理;记账失败只告警、绝不阻塞蒸馏),
|
|
81
|
+
设置页 → 记忆 → **成本** Tab 可视化:按模型分色的趋势折线(日/周/月粒度 + 近 N 天窗口 +
|
|
82
|
+
L1/L2/L3 层级过滤)、层级 × 时间窗口表格(调用数 / 输出与思考 token / 均值 / 中位数)、
|
|
83
|
+
按模型累计——蒸馏开销一目了然。输入按字符计(dsh 流式 usage 不含输入 token),
|
|
84
|
+
输出与思考按 token 计。
|
|
85
|
+
|
|
79
86
|
**记忆工具(3):**
|
|
80
87
|
- memory_search
|
|
81
88
|
- conversation_search
|
|
@@ -274,6 +281,7 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
|
|
|
274
281
|
| `llm.temperature` | `0.3` | 蒸馏温度 |
|
|
275
282
|
| `llm.maxInputChars` | `700000` | 单次蒸馏输入字符预算(超限的 L1 输入自动分块抽取);运行时可在设置页 → 蒸馏参数 → 输入预算调整(留空/0 = 跟随本值) |
|
|
276
283
|
| `llm.timeoutMs` | `120000` | 单次蒸馏调用超时(ms) |
|
|
284
|
+
| `tokenCost.retentionDays` | `365` | 蒸馏成本明细(token_cost 表)保留天数,写入时滚动清理更早行;`0` = 永久保留。成本看板「近 N 天」窗口上限同此值 |
|
|
277
285
|
| `tools` | `true` | 是否注册模型可调用的记忆工具 |
|
|
278
286
|
| `benchControl` | `false` | 注册 bench 控制服务(进程内 rebuild 触发/会话档位设置/蒸馏用量快照,供基准 lifecycle 赛道)。默认关——生产部署零表面积,勿随意开启 |
|
|
279
287
|
|
package/dist/client.js
CHANGED
|
@@ -877,6 +877,15 @@ window.__ModuleLoader__.load({
|
|
|
877
877
|
" --dsh-mem-text-2: var(--dsw-alias-label-secondary, #61666b);",
|
|
878
878
|
" --dsh-mem-text-3: var(--dsw-alias-label-tertiary, #6e7781);",
|
|
879
879
|
" --dsh-mem-danger: var(--dsw-alias-state-error-primary, #d0403f);",
|
|
880
|
+
// 图表系列(成本折线图):8 档固定色,PALETTE 只引用 var();1 档锚品牌蓝,8 档中性"其他"
|
|
881
|
+
" --dsh-mem-chart-1: #4d6bfe;",
|
|
882
|
+
" --dsh-mem-chart-2: #0e9c8f;",
|
|
883
|
+
" --dsh-mem-chart-3: #1f9d55;",
|
|
884
|
+
" --dsh-mem-chart-4: #a8821c;",
|
|
885
|
+
" --dsh-mem-chart-5: #d97a0d;",
|
|
886
|
+
" --dsh-mem-chart-6: #d64570;",
|
|
887
|
+
" --dsh-mem-chart-7: #7c5cff;",
|
|
888
|
+
" --dsh-mem-chart-8: #61666b;",
|
|
880
889
|
// 档位色 = 灰 → 品牌蓝 的渐变阶(chat/work 过渡蓝 / auto 品牌蓝);
|
|
881
890
|
// 文字对比度按 pill 真实底色(流光内底 = bg-card 97% + 档位色 3%)复算 AA 达标
|
|
882
891
|
" --dsh-mem-mode-chat: #5a69b0;",
|
|
@@ -908,6 +917,14 @@ window.__ModuleLoader__.load({
|
|
|
908
917
|
" --dsh-mem-text-2: var(--dsw-alias-label-secondary, #cfd3d6);",
|
|
909
918
|
" --dsh-mem-text-3: var(--dsw-alias-label-tertiary, #8892a6);",
|
|
910
919
|
" --dsh-mem-danger: var(--dsw-alias-state-error-primary, #f4707b);",
|
|
920
|
+
" --dsh-mem-chart-1: #6e85ff;",
|
|
921
|
+
" --dsh-mem-chart-2: #35c4b5;",
|
|
922
|
+
" --dsh-mem-chart-3: #52c98d;",
|
|
923
|
+
" --dsh-mem-chart-4: #d9b23e;",
|
|
924
|
+
" --dsh-mem-chart-5: #f59e5b;",
|
|
925
|
+
" --dsh-mem-chart-6: #f47ba2;",
|
|
926
|
+
" --dsh-mem-chart-7: #a78bfa;",
|
|
927
|
+
" --dsh-mem-chart-8: #8892a6;",
|
|
911
928
|
" --dsh-mem-mode-chat: #97a4ff;",
|
|
912
929
|
" --dsh-mem-mode-work: #8295ff;",
|
|
913
930
|
" --dsh-mem-mode-auto: #7b90ff;",
|
|
@@ -2824,12 +2841,311 @@ window.__ModuleLoader__.load({
|
|
|
2824
2841
|
);
|
|
2825
2842
|
}
|
|
2826
2843
|
|
|
2844
|
+
// ── 折线图 helper:把桶序列画成 SVG 多模型折线 ──
|
|
2845
|
+
function renderCostChart(buckets, models, maxY, fmtDate, fmtInt, palette) {
|
|
2846
|
+
var W = 600, H = 200, L = 46, R = 10, T = 10, B = 26;
|
|
2847
|
+
var iw = W - L - R;
|
|
2848
|
+
var ih = H - T - B;
|
|
2849
|
+
var n = buckets.length;
|
|
2850
|
+
var x = function (i) { return L + (n <= 1 ? iw / 2 : (i / (n - 1)) * iw); };
|
|
2851
|
+
var y = function (v) { return T + ih - (v / maxY) * ih; };
|
|
2852
|
+
var yTicks = [0, maxY / 2, maxY];
|
|
2853
|
+
var xIdx = n > 2 ? [0, Math.floor((n - 1) / 2), n - 1] : n === 2 ? [0, 1] : [0];
|
|
2854
|
+
return react.createElement(
|
|
2855
|
+
"svg",
|
|
2856
|
+
{ viewBox: "0 0 " + W + " " + H, style: { width: "100%", height: "auto", display: "block" } },
|
|
2857
|
+
react.createElement("line", { x1: L, y1: y(0), x2: W - R, y2: y(0), stroke: "var(--dsh-mem-border)", strokeWidth: 1 }),
|
|
2858
|
+
yTicks.map(function (v) {
|
|
2859
|
+
return react.createElement("text", { key: "yt" + v, x: L - 6, y: y(v) + 4, textAnchor: "end", fontSize: 10, fill: "var(--dsh-mem-text-3)" }, fmtInt(v));
|
|
2860
|
+
}),
|
|
2861
|
+
xIdx.map(function (i) {
|
|
2862
|
+
return react.createElement("text", { key: "xt" + i, x: x(i), y: H - 8, textAnchor: "middle", fontSize: 10, fill: "var(--dsh-mem-text-3)" }, fmtDate(buckets[i] ? buckets[i].ts : 0));
|
|
2863
|
+
}),
|
|
2864
|
+
models.map(function (m, mi) {
|
|
2865
|
+
var pts = buckets.map(function (b, i) { return x(i) + "," + y(b.byModel[m] || 0); }).join(" ");
|
|
2866
|
+
return react.createElement("polyline", { key: "pl" + m, points: pts, fill: "none", stroke: palette[mi % palette.length], strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round" });
|
|
2867
|
+
}),
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// ── Tab:蒸馏成本看板(折线图 + 层级×窗口表格 + 总览) ──
|
|
2872
|
+
function CostTab(props) {
|
|
2873
|
+
var rpc = props.rpc;
|
|
2874
|
+
var dataState = react.useState(null);
|
|
2875
|
+
var data = dataState[0];
|
|
2876
|
+
var setData = dataState[1];
|
|
2877
|
+
var errState = react.useState(null);
|
|
2878
|
+
var error = errState[0];
|
|
2879
|
+
var setError = errState[1];
|
|
2880
|
+
var granState = react.useState("day");
|
|
2881
|
+
var granularity = granState[0];
|
|
2882
|
+
var setGranularity = granState[1];
|
|
2883
|
+
var layerState = react.useState("");
|
|
2884
|
+
var layer = layerState[0];
|
|
2885
|
+
var setLayer = layerState[1];
|
|
2886
|
+
var rangeState = react.useState(0);
|
|
2887
|
+
var rangeDays = rangeState[0];
|
|
2888
|
+
var setRangeDays = rangeState[1];
|
|
2889
|
+
var rangeOpenState = react.useState(false);
|
|
2890
|
+
var rangeOpen = rangeOpenState[0];
|
|
2891
|
+
var setRangeOpen = rangeOpenState[1];
|
|
2892
|
+
|
|
2893
|
+
var load = react.useCallback(function () {
|
|
2894
|
+
setError(null);
|
|
2895
|
+
rpc("dsh-memory/token-cost", { granularity: granularity, rangeDays: rangeDays })
|
|
2896
|
+
.then(function (r) {
|
|
2897
|
+
if (r && r.ok) setData(r.value);
|
|
2898
|
+
else setError(r && r.error ? r.error.message : "RPC error");
|
|
2899
|
+
})
|
|
2900
|
+
.catch(function (e) { setError(String((e && e.message) || e)); });
|
|
2901
|
+
}, [rpc, granularity, rangeDays]);
|
|
2902
|
+
|
|
2903
|
+
react.useEffect(function () {
|
|
2904
|
+
load();
|
|
2905
|
+
var timer = setInterval(load, 5000);
|
|
2906
|
+
return function () { clearInterval(timer); };
|
|
2907
|
+
}, [load]);
|
|
2908
|
+
|
|
2909
|
+
var fmtInt = function (n) { return String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ","); };
|
|
2910
|
+
var fmtModel = function (p, m) { return p ? p + "/" + m : m; };
|
|
2911
|
+
var fmtDate = function (ts) {
|
|
2912
|
+
try {
|
|
2913
|
+
var d = new Date(ts);
|
|
2914
|
+
// 近 N 天时后端强制日粒度,用 trend 实际粒度(而非用户选的周/月)格式化横轴
|
|
2915
|
+
var g = data && data.trend ? data.trend.granularity : granularity;
|
|
2916
|
+
if (g === "month") return (d.getMonth() + 1) + "月";
|
|
2917
|
+
return (d.getMonth() + 1) + "/" + d.getDate();
|
|
2918
|
+
} catch (e) { return ""; }
|
|
2919
|
+
};
|
|
2920
|
+
var RANGE_LABELS = { day: "今日", week: "本周", month: "本月", all: "累计" };
|
|
2921
|
+
var LAYER_OPTS = [{ key: "", label: "全部" }, { key: "l1", label: "L1" }, { key: "l2", label: "L2" }, { key: "l3", label: "L3" }];
|
|
2922
|
+
var GRAN_OPTS = [{ key: "day", label: "日" }, { key: "week", label: "周" }, { key: "month", label: "月" }];
|
|
2923
|
+
var PALETTE = ["var(--dsh-mem-chart-1)", "var(--dsh-mem-chart-2)", "var(--dsh-mem-chart-3)", "var(--dsh-mem-chart-4)", "var(--dsh-mem-chart-5)", "var(--dsh-mem-chart-6)", "var(--dsh-mem-chart-7)", "var(--dsh-mem-chart-8)"];
|
|
2924
|
+
|
|
2925
|
+
var windows = (data && data.windows) || [];
|
|
2926
|
+
var byModel = (data && data.byModel) || [];
|
|
2927
|
+
var byLayer = (data && data.byLayer) || [];
|
|
2928
|
+
var trend = data && data.trend ? data.trend : null;
|
|
2929
|
+
|
|
2930
|
+
// 趋势桶(按 layer 过滤/合并;全部 = l1+l2+l3 逐桶相加)
|
|
2931
|
+
var buckets = [];
|
|
2932
|
+
if (trend && trend.byLayer) {
|
|
2933
|
+
if (layer === "l1" || layer === "l2" || layer === "l3") {
|
|
2934
|
+
buckets = trend.byLayer[layer] || [];
|
|
2935
|
+
} else {
|
|
2936
|
+
var seqs = [trend.byLayer.l1 || [], trend.byLayer.l2 || [], trend.byLayer.l3 || []];
|
|
2937
|
+
var n = seqs[0].length;
|
|
2938
|
+
for (var i = 0; i < n; i++) {
|
|
2939
|
+
var merged = { ts: 0, total: 0, byModel: {} };
|
|
2940
|
+
for (var s = 0; s < seqs.length; s++) {
|
|
2941
|
+
var seq = seqs[s];
|
|
2942
|
+
if (seq && seq[i]) {
|
|
2943
|
+
if (merged.ts === 0) merged.ts = seq[i].ts;
|
|
2944
|
+
merged.total += seq[i].total;
|
|
2945
|
+
Object.keys(seq[i].byModel).forEach(function (m) {
|
|
2946
|
+
merged.byModel[m] = (merged.byModel[m] || 0) + seq[i].byModel[m];
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
buckets.push(merged);
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// 折线模型 + maxY
|
|
2956
|
+
var models = [];
|
|
2957
|
+
var seen = {};
|
|
2958
|
+
buckets.forEach(function (b) {
|
|
2959
|
+
Object.keys(b.byModel).forEach(function (m) {
|
|
2960
|
+
if (!seen[m]) { seen[m] = true; models.push(m); }
|
|
2961
|
+
});
|
|
2962
|
+
});
|
|
2963
|
+
models.sort();
|
|
2964
|
+
var maxY = 1;
|
|
2965
|
+
buckets.forEach(function (b) {
|
|
2966
|
+
if (b.total > maxY) maxY = b.total;
|
|
2967
|
+
models.forEach(function (m) { if ((b.byModel[m] || 0) > maxY) maxY = b.byModel[m]; });
|
|
2968
|
+
});
|
|
2969
|
+
|
|
2970
|
+
// 层级×窗口表格数据(layer → {range: 格子})
|
|
2971
|
+
var layerTable = byLayer.map(function (lc) {
|
|
2972
|
+
var win = {};
|
|
2973
|
+
lc.windows.forEach(function (w) { win[w.range] = w; });
|
|
2974
|
+
return { layer: lc.layer, win: win };
|
|
2975
|
+
});
|
|
2976
|
+
|
|
2977
|
+
var thFirst = { fontSize: 12, fontWeight: 600, color: "var(--dsh-mem-text-3)", textAlign: "left", padding: "4px 10px", borderBottom: "1px solid var(--dsh-mem-border)" };
|
|
2978
|
+
var thStyle = { fontSize: 12, fontWeight: 600, color: "var(--dsh-mem-text-3)", textAlign: "right", padding: "4px 10px", borderBottom: "1px solid var(--dsh-mem-border)" };
|
|
2979
|
+
var tdFirst = { fontSize: 12.5, fontWeight: 600, color: "var(--dsh-mem-text-1)", textAlign: "left", padding: "4px 10px" };
|
|
2980
|
+
var tdStyle = { fontSize: 12.5, color: "var(--dsh-mem-text-1)", textAlign: "right", padding: "4px 10px", fontFamily: "ui-monospace, Consolas, monospace" };
|
|
2981
|
+
|
|
2982
|
+
return react.createElement(
|
|
2983
|
+
"div", null,
|
|
2984
|
+
react.createElement(
|
|
2985
|
+
"div", { style: Object.assign({}, S.flexRow, { marginBottom: 10 }) },
|
|
2986
|
+
react.createElement(Segmented, { value: layer, options: LAYER_OPTS, onChange: setLayer }),
|
|
2987
|
+
react.createElement(Segmented, { value: granularity, options: GRAN_OPTS, onChange: setGranularity }),
|
|
2988
|
+
react.createElement(NButton, { onClick: function () { setRangeOpen(!rangeOpen); } }, rangeDays > 0 ? "近 " + rangeDays + " 天" : "近N天"),
|
|
2989
|
+
react.createElement("div", { style: S.grow }),
|
|
2990
|
+
react.createElement(NButton, { onClick: load }, "刷新"),
|
|
2991
|
+
),
|
|
2992
|
+
rangeOpen
|
|
2993
|
+
? react.createElement(
|
|
2994
|
+
"div", { style: Object.assign({}, S.flexRow, { marginBottom: 10 }) },
|
|
2995
|
+
react.createElement("span", { style: S.muted }, "展示近 N 天(正整数,清空=默认窗口;超出保留期后端自动回退)"),
|
|
2996
|
+
react.createElement(NInput, {
|
|
2997
|
+
value: rangeDays === 0 ? "" : String(rangeDays),
|
|
2998
|
+
placeholder: "如 30",
|
|
2999
|
+
style: { width: 90 },
|
|
3000
|
+
onChange: function (e) {
|
|
3001
|
+
var v = String(e.target.value || "").trim();
|
|
3002
|
+
if (v === "") { setRangeDays(0); return; }
|
|
3003
|
+
var n = Number(v);
|
|
3004
|
+
if (Number.isInteger(n) && n > 0 && n <= 3650) setRangeDays(n);
|
|
3005
|
+
},
|
|
3006
|
+
}),
|
|
3007
|
+
)
|
|
3008
|
+
: null,
|
|
3009
|
+
error
|
|
3010
|
+
? react.createElement("div", { style: S.error }, "成本读取失败:" + error)
|
|
3011
|
+
: null,
|
|
3012
|
+
react.createElement("div", { style: S.panelLabel }, "成本趋势(按模型)"),
|
|
3013
|
+
buckets.length > 0
|
|
3014
|
+
? react.createElement(
|
|
3015
|
+
"div", null,
|
|
3016
|
+
renderCostChart(buckets, models, maxY, fmtDate, fmtInt, PALETTE),
|
|
3017
|
+
react.createElement(
|
|
3018
|
+
"div", { style: { display: "flex", flexWrap: "wrap", gap: "4px 12px", margin: "6px 0 14px" } },
|
|
3019
|
+
models.map(function (m, mi) {
|
|
3020
|
+
return react.createElement(
|
|
3021
|
+
"span", { key: "lg" + m, style: { display: "inline-flex", alignItems: "center", gap: 4, fontSize: 12, color: "var(--dsh-mem-text-2)" } },
|
|
3022
|
+
react.createElement("span", { style: { width: 10, height: 10, borderRadius: 4, background: PALETTE[mi % PALETTE.length], display: "inline-block" } }),
|
|
3023
|
+
m,
|
|
3024
|
+
);
|
|
3025
|
+
}),
|
|
3026
|
+
),
|
|
3027
|
+
)
|
|
3028
|
+
: react.createElement("p", { style: S.muted }, data ? "暂无成本数据(触发一次蒸馏后这里会出现趋势)。" : "加载中…"),
|
|
3029
|
+
react.createElement("div", { style: S.panelLabel }, "层级成本(输出 token)"),
|
|
3030
|
+
react.createElement(
|
|
3031
|
+
"table", { style: { width: "100%", borderCollapse: "collapse", marginBottom: 14 } },
|
|
3032
|
+
react.createElement(
|
|
3033
|
+
"thead", null,
|
|
3034
|
+
react.createElement(
|
|
3035
|
+
"tr", null,
|
|
3036
|
+
react.createElement("th", { style: thFirst }, "层级"),
|
|
3037
|
+
["day", "week", "month", "all"].map(function (r) {
|
|
3038
|
+
return react.createElement("th", { key: r, style: thStyle }, RANGE_LABELS[r]);
|
|
3039
|
+
}),
|
|
3040
|
+
),
|
|
3041
|
+
),
|
|
3042
|
+
react.createElement(
|
|
3043
|
+
"tbody", null,
|
|
3044
|
+
layerTable.map(function (lc) {
|
|
3045
|
+
return react.createElement(
|
|
3046
|
+
"tr", { key: lc.layer },
|
|
3047
|
+
react.createElement("td", { style: tdFirst }, lc.layer.toUpperCase()),
|
|
3048
|
+
["day", "week", "month", "all"].map(function (r) {
|
|
3049
|
+
var w = lc.win[r];
|
|
3050
|
+
return react.createElement("td", { key: r, style: tdStyle }, w ? fmtInt(w.outputTokens) : "0");
|
|
3051
|
+
}),
|
|
3052
|
+
);
|
|
3053
|
+
}),
|
|
3054
|
+
),
|
|
3055
|
+
),
|
|
3056
|
+
react.createElement("div", { style: S.panelLabel }, "层级成本(单次 avg)"),
|
|
3057
|
+
react.createElement(
|
|
3058
|
+
"table", { style: { width: "100%", borderCollapse: "collapse", marginBottom: 14 } },
|
|
3059
|
+
react.createElement(
|
|
3060
|
+
"thead", null,
|
|
3061
|
+
react.createElement(
|
|
3062
|
+
"tr", null,
|
|
3063
|
+
react.createElement("th", { style: thFirst }, "层级"),
|
|
3064
|
+
["day", "week", "month", "all"].map(function (r) {
|
|
3065
|
+
return react.createElement("th", { key: r, style: thStyle }, RANGE_LABELS[r] + "-avg");
|
|
3066
|
+
}),
|
|
3067
|
+
),
|
|
3068
|
+
),
|
|
3069
|
+
react.createElement(
|
|
3070
|
+
"tbody", null,
|
|
3071
|
+
layerTable.map(function (lc) {
|
|
3072
|
+
return react.createElement(
|
|
3073
|
+
"tr", { key: lc.layer },
|
|
3074
|
+
react.createElement("td", { style: tdFirst }, lc.layer.toUpperCase()),
|
|
3075
|
+
["day", "week", "month", "all"].map(function (r) {
|
|
3076
|
+
var w = lc.win[r];
|
|
3077
|
+
return react.createElement("td", { key: r, style: tdStyle }, w ? fmtInt(w.avgOutputTokens) : "0");
|
|
3078
|
+
}),
|
|
3079
|
+
);
|
|
3080
|
+
}),
|
|
3081
|
+
),
|
|
3082
|
+
),
|
|
3083
|
+
react.createElement("div", { style: S.panelLabel }, "层级成本(单次 median)"),
|
|
3084
|
+
react.createElement(
|
|
3085
|
+
"table", { style: { width: "100%", borderCollapse: "collapse", marginBottom: 14 } },
|
|
3086
|
+
react.createElement(
|
|
3087
|
+
"thead", null,
|
|
3088
|
+
react.createElement(
|
|
3089
|
+
"tr", null,
|
|
3090
|
+
react.createElement("th", { style: thFirst }, "层级"),
|
|
3091
|
+
["day", "week", "month", "all"].map(function (r) {
|
|
3092
|
+
return react.createElement("th", { key: r, style: thStyle }, RANGE_LABELS[r] + "-median");
|
|
3093
|
+
}),
|
|
3094
|
+
),
|
|
3095
|
+
),
|
|
3096
|
+
react.createElement(
|
|
3097
|
+
"tbody", null,
|
|
3098
|
+
layerTable.map(function (lc) {
|
|
3099
|
+
return react.createElement(
|
|
3100
|
+
"tr", { key: lc.layer },
|
|
3101
|
+
react.createElement("td", { style: tdFirst }, lc.layer.toUpperCase()),
|
|
3102
|
+
["day", "week", "month", "all"].map(function (r) {
|
|
3103
|
+
var w = lc.win[r];
|
|
3104
|
+
return react.createElement("td", { key: r, style: tdStyle }, w ? fmtInt(w.medianOutputTokens) : "0");
|
|
3105
|
+
}),
|
|
3106
|
+
);
|
|
3107
|
+
}),
|
|
3108
|
+
),
|
|
3109
|
+
),
|
|
3110
|
+
react.createElement("div", { style: S.panelLabel }, "时间窗口总览"),
|
|
3111
|
+
react.createElement(
|
|
3112
|
+
"div", { style: S.statGrid },
|
|
3113
|
+
windows.map(function (w) {
|
|
3114
|
+
return react.createElement(
|
|
3115
|
+
"div", { key: w.range, className: "dsh-mem-card", style: S.statTile },
|
|
3116
|
+
react.createElement("div", { style: S.statNum }, fmtInt(w.outputTokens + w.reasoningTokens)),
|
|
3117
|
+
react.createElement("div", { style: S.statLabel }, RANGE_LABELS[w.range] + " · 总输出 token"),
|
|
3118
|
+
react.createElement("div", { style: S.muted }, "文字 " + fmtInt(w.outputTokens) + " · 思考 " + fmtInt(w.reasoningTokens) + " · " + w.calls + " 次调用"),
|
|
3119
|
+
);
|
|
3120
|
+
}),
|
|
3121
|
+
),
|
|
3122
|
+
byModel.length > 0
|
|
3123
|
+
? react.createElement(
|
|
3124
|
+
"div", null,
|
|
3125
|
+
react.createElement("div", { style: S.panelLabel }, "按模型(累计)"),
|
|
3126
|
+
byModel.map(function (m) {
|
|
3127
|
+
var label = fmtModel(m.provider, m.model);
|
|
3128
|
+
return react.createElement(
|
|
3129
|
+
"div", { key: "m-" + label, style: S.infoRow },
|
|
3130
|
+
react.createElement("span", { style: S.infoKey }, label),
|
|
3131
|
+
react.createElement("span", { style: S.infoVal }, m.calls + " 次 · 输出 " + fmtInt(m.outputTokens) + " · 思考 " + fmtInt(m.reasoningTokens)),
|
|
3132
|
+
);
|
|
3133
|
+
}),
|
|
3134
|
+
)
|
|
3135
|
+
: null,
|
|
3136
|
+
data
|
|
3137
|
+
? react.createElement("p", { style: S.hint }, "输入按字符、输出/思考按 token 计;趋势图 Y 轴为输出 token,上方可切换层级与颗粒度。" )
|
|
3138
|
+
: null,
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
|
|
2827
3142
|
// ── 主面板:Tab 框架 ──
|
|
2828
3143
|
var TABS = [
|
|
2829
3144
|
["overview", "概览"],
|
|
2830
3145
|
["records", "记忆"],
|
|
2831
3146
|
["scenes", "场景"],
|
|
2832
3147
|
["persona", "画像"],
|
|
3148
|
+
["cost", "成本"],
|
|
2833
3149
|
["log", "日志"],
|
|
2834
3150
|
];
|
|
2835
3151
|
|
|
@@ -2898,6 +3214,7 @@ window.__ModuleLoader__.load({
|
|
|
2898
3214
|
else if (tab === "records") body = react.createElement(RecordsTab, { rpc: rpc });
|
|
2899
3215
|
else if (tab === "scenes") body = react.createElement(ScenesTab, { rpc: rpc });
|
|
2900
3216
|
else if (tab === "persona") body = react.createElement(PersonaTab, { rpc: rpc });
|
|
3217
|
+
else if (tab === "cost") body = react.createElement(CostTab, { rpc: rpc });
|
|
2901
3218
|
else body = react.createElement(LogTab, { rpc: rpc });
|
|
2902
3219
|
|
|
2903
3220
|
return react.createElement(
|
package/dist/config.d.ts
CHANGED
|
@@ -114,6 +114,10 @@ export interface MemoryConfig {
|
|
|
114
114
|
l3: number;
|
|
115
115
|
}>;
|
|
116
116
|
};
|
|
117
|
+
tokenCost: {
|
|
118
|
+
/** token_cost 明细保留天数;写入时滚动清理更早行。0 = 永久保留。 */
|
|
119
|
+
retentionDays: number;
|
|
120
|
+
};
|
|
117
121
|
/** 是否注册模型可调用的记忆工具。 */
|
|
118
122
|
tools: boolean;
|
|
119
123
|
/** 注册 bench 控制服务(dsh-memory-bench,进程内 rebuild 触发面)。
|
|
@@ -226,6 +230,11 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
226
230
|
maxInputChars: Schema<number, number>;
|
|
227
231
|
timeoutMs: Schema<number, number>;
|
|
228
232
|
}>>;
|
|
233
|
+
tokenCost: Schema<Schemastery.ObjectS<{
|
|
234
|
+
retentionDays: Schema<number, number>;
|
|
235
|
+
}>, Schemastery.ObjectT<{
|
|
236
|
+
retentionDays: Schema<number, number>;
|
|
237
|
+
}>>;
|
|
229
238
|
tools: Schema<boolean, boolean>;
|
|
230
239
|
benchControl: Schema<boolean, boolean>;
|
|
231
240
|
}>, Schemastery.ObjectT<{
|
|
@@ -334,6 +343,11 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
334
343
|
maxInputChars: Schema<number, number>;
|
|
335
344
|
timeoutMs: Schema<number, number>;
|
|
336
345
|
}>>;
|
|
346
|
+
tokenCost: Schema<Schemastery.ObjectS<{
|
|
347
|
+
retentionDays: Schema<number, number>;
|
|
348
|
+
}>, Schemastery.ObjectT<{
|
|
349
|
+
retentionDays: Schema<number, number>;
|
|
350
|
+
}>>;
|
|
337
351
|
tools: Schema<boolean, boolean>;
|
|
338
352
|
benchControl: Schema<boolean, boolean>;
|
|
339
353
|
}>>;
|
package/dist/config.js
CHANGED
|
@@ -80,6 +80,10 @@ export const memorySchema = Schema.object({
|
|
|
80
80
|
maxInputChars: Schema.number().min(1000).max(1_000_000).default(700_000),
|
|
81
81
|
timeoutMs: Schema.number().min(1000).max(600_000).default(120_000),
|
|
82
82
|
}),
|
|
83
|
+
// token_cost 明细保留期(写入时滚动清理;0 = 永久保留)。成本看板的「近 N 天」窗口上限也取此值
|
|
84
|
+
tokenCost: Schema.object({
|
|
85
|
+
retentionDays: Schema.number().min(0).max(3650).default(365),
|
|
86
|
+
}),
|
|
83
87
|
tools: Schema.boolean().default(true),
|
|
84
88
|
benchControl: Schema.boolean().default(false),
|
|
85
89
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,11 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
114
114
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
115
115
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
116
116
|
}>>;
|
|
117
|
+
tokenCost: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
118
|
+
retentionDays: import("@deepseek-ai/schemastery").default<number, number>;
|
|
119
|
+
}>, Schemastery.ObjectT<{
|
|
120
|
+
retentionDays: import("@deepseek-ai/schemastery").default<number, number>;
|
|
121
|
+
}>>;
|
|
117
122
|
tools: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
118
123
|
benchControl: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
119
124
|
}>, Schemastery.ObjectT<{
|
|
@@ -222,6 +227,11 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
222
227
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
223
228
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
224
229
|
}>>;
|
|
230
|
+
tokenCost: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
231
|
+
retentionDays: import("@deepseek-ai/schemastery").default<number, number>;
|
|
232
|
+
}>, Schemastery.ObjectT<{
|
|
233
|
+
retentionDays: import("@deepseek-ai/schemastery").default<number, number>;
|
|
234
|
+
}>>;
|
|
225
235
|
tools: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
226
236
|
benchControl: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
227
237
|
}>>;
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import { registerMemoryTools } from './tools/index.js';
|
|
|
41
41
|
import { errDetail, withFileLog } from './util/filelog.js';
|
|
42
42
|
import { resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
43
43
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
44
|
+
import { initTokenCost, resetTokenCost } from './token-cost.js';
|
|
44
45
|
export const name = 'dsh-memory-plugin';
|
|
45
46
|
/** 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。 */
|
|
46
47
|
export const inject = ['llm', 'tools', 'systemPrompt'];
|
|
@@ -113,6 +114,7 @@ export async function apply(ctx, config) {
|
|
|
113
114
|
// ── 检索引擎(memory.db)与向量服务 ──
|
|
114
115
|
const embed = initial.svc;
|
|
115
116
|
const db = new MemoryDb(path.join(dataDir, 'memory.db'), initial.dims, logger);
|
|
117
|
+
initTokenCost(db, config.tokenCost.retentionDays);
|
|
116
118
|
// 插件卸载时关闭连接(WAL 落盘),注册一次即可
|
|
117
119
|
ctx.effect(() => () => db.close());
|
|
118
120
|
let dbInit = { needsReindex: false };
|
|
@@ -325,6 +327,7 @@ export async function apply(ctx, config) {
|
|
|
325
327
|
return (async () => {
|
|
326
328
|
await flushL0?.();
|
|
327
329
|
db.close();
|
|
330
|
+
resetTokenCost();
|
|
328
331
|
})();
|
|
329
332
|
});
|
|
330
333
|
}
|
package/dist/llm.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
2
2
|
import { errDetail } from './util/filelog.js';
|
|
3
3
|
import { recordDistillCall } from './llm-usage.js';
|
|
4
|
+
import { recordCostCall } from './token-cost.js';
|
|
4
5
|
// ── 分层输出预算(规格 C 节):结构化蒸馏远用不到总闸级预算,逐层设护栏;
|
|
5
6
|
// 模型跑偏时单次损失有界。数值"先试跑"状态,按线上截断率调整。
|
|
6
7
|
/** L1 抽取(大输入块的 JSON 记忆数组输出)。 */
|
|
@@ -203,11 +204,15 @@ export async function callLLM(ctx, cfg, opts) {
|
|
|
203
204
|
// 记账含失败路径(failures 计数;tokens 尽当时流内已到的 usage)
|
|
204
205
|
if (opts.layer)
|
|
205
206
|
recordDistillCall(opts.layer, user.length, outputTokens, reasoningTokens, true);
|
|
207
|
+
if (opts.layer)
|
|
208
|
+
recordCostCall(provider, model, opts.layer, user.length, outputTokens, reasoningTokens);
|
|
206
209
|
opts.logger?.warn(`[memory] LLM 调用失败 ${provider}/${model}(${((Date.now() - startedAt) / 1000).toFixed(1)}s): ${errDetail(err)}`);
|
|
207
210
|
throw err;
|
|
208
211
|
}
|
|
209
212
|
if (opts.layer)
|
|
210
213
|
recordDistillCall(opts.layer, user.length, outputTokens, reasoningTokens, false);
|
|
214
|
+
if (opts.layer)
|
|
215
|
+
recordCostCall(provider, model, opts.layer, user.length, outputTokens, reasoningTokens);
|
|
211
216
|
const out = (blockText || deltaText).trim();
|
|
212
217
|
if (out.length === 0) {
|
|
213
218
|
// 空输出是最难排查的失败:流正常结束但一个字没吐。必须记录 finish 原因、
|
package/dist/stats.js
CHANGED
|
@@ -14,6 +14,7 @@ import { effectiveCfg } from './pipeline/runner.js';
|
|
|
14
14
|
import { emptyRecallStats } from './hooks/recall.js';
|
|
15
15
|
import { decideSendableEffort, LAYER_DEFAULT_BUDGETS, resolveModelEfforts, resolveModelRoute } from './llm.js';
|
|
16
16
|
import { errDetail } from './util/filelog.js';
|
|
17
|
+
import { snapshotTokenCost } from './token-cost.js';
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
18
19
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
19
20
|
/** 注册状态 RPC(web 侧 connection 服务可选,缺失时跳过,不影响插件主体)。 */
|
|
@@ -148,6 +149,16 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
148
149
|
switch (endpoint) {
|
|
149
150
|
case 'dsh-memory/stats':
|
|
150
151
|
return buildStats(cfg, stores, status);
|
|
152
|
+
case 'dsh-memory/token-cost': {
|
|
153
|
+
const p = (payload ?? {});
|
|
154
|
+
const granularity = p.granularity === 'week' || p.granularity === 'month' ? p.granularity : 'day';
|
|
155
|
+
// rangeDays 须为正整数且不超过明细保留期(tokenCost.retentionDays,0=永久保留则放行 1~3650),否则回退默认窗口
|
|
156
|
+
const retention = cfg.tokenCost.retentionDays;
|
|
157
|
+
const upper = retention > 0 ? retention : 3650;
|
|
158
|
+
const rawDays = p.rangeDays;
|
|
159
|
+
const rangeDays = typeof rawDays === 'number' && Number.isInteger(rawDays) && rawDays > 0 && rawDays <= upper ? rawDays : 0;
|
|
160
|
+
return snapshotTokenCost(granularity, rangeDays);
|
|
161
|
+
}
|
|
151
162
|
case 'dsh-memory/session-mode-get': {
|
|
152
163
|
if (!modes)
|
|
153
164
|
throw new Error('档位存储未初始化');
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -9,6 +9,45 @@ export interface StoreCapabilities {
|
|
|
9
9
|
ftsSearch: boolean;
|
|
10
10
|
vectorSearch: boolean;
|
|
11
11
|
}
|
|
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
|
+
/** 按 model 分组的成本行(成本看板用)。 */
|
|
24
|
+
export interface CostByModel {
|
|
25
|
+
provider: string;
|
|
26
|
+
model: string;
|
|
27
|
+
calls: number;
|
|
28
|
+
inputChars: number;
|
|
29
|
+
outputTokens: number;
|
|
30
|
+
reasoningTokens: number;
|
|
31
|
+
}
|
|
32
|
+
/** 按层级(l1/l2/l3 归并)分组的成本行。 */
|
|
33
|
+
export interface CostByLayer {
|
|
34
|
+
layer: string;
|
|
35
|
+
calls: number;
|
|
36
|
+
inputChars: number;
|
|
37
|
+
outputTokens: number;
|
|
38
|
+
reasoningTokens: number;
|
|
39
|
+
avgOutputTokens: number;
|
|
40
|
+
medianOutputTokens: number;
|
|
41
|
+
}
|
|
42
|
+
/** 按时间桶 + provider/model 聚合的扁平行(趋势图与日均/周均/月均 + 中位数统计共用)。 */
|
|
43
|
+
export interface BucketRow {
|
|
44
|
+
bucket: number;
|
|
45
|
+
provider: string;
|
|
46
|
+
model: string;
|
|
47
|
+
calls: number;
|
|
48
|
+
outputTokens: number;
|
|
49
|
+
reasoningTokens: number;
|
|
50
|
+
}
|
|
12
51
|
/** L1 检索命中(含 BM25/余弦归一分数)。 */
|
|
13
52
|
export interface L1SearchHit {
|
|
14
53
|
id: string;
|
|
@@ -44,6 +83,9 @@ export declare class MemoryDb {
|
|
|
44
83
|
private stmtL1FtsDelete;
|
|
45
84
|
private stmtL1FtsSearch;
|
|
46
85
|
private stmtL1FtsSearchFamily;
|
|
86
|
+
/** token_cost 明细写入 / 滚动清理语句(构造期 prepare 缓存)。 */
|
|
87
|
+
private stmtInsertCost;
|
|
88
|
+
private stmtDeleteCost;
|
|
47
89
|
private stmtUpsertL0;
|
|
48
90
|
private stmtGetL0;
|
|
49
91
|
/** 主表存在性点查(同 L1:防御性 FTS 删除的前置判断)。 */
|
|
@@ -145,6 +187,32 @@ export declare class MemoryDb {
|
|
|
145
187
|
searchL1Vector(embedding: Float32Array, topK: number, family?: string): L1SearchHit[];
|
|
146
188
|
/** 批量 upsert L0 消息(元数据 + FTS;embeddings 与 records 等长,可省略)。 */
|
|
147
189
|
upsertL0Batch(records: L0MessageRecord[], embeddings?: Array<Float32Array | undefined>): boolean;
|
|
190
|
+
/**
|
|
191
|
+
* 记录一次蒸馏调用成本(明细表,写入时按 retentionDays 滚动清理;0 = 永久保留)。
|
|
192
|
+
* 失败/成功都记(token 照烧);记账失败记 warn 但不阻断蒸馏(成本看板是增强能力)。
|
|
193
|
+
*/
|
|
194
|
+
insertCostCall(provider: string, model: string, layer: string, inputChars: number, outputTokens: number, reasoningTokens: number, retentionDays: number): void;
|
|
195
|
+
/**
|
|
196
|
+
* 查询 token_cost 单窗口聚合(成本看板用;since 为毫秒起点,0 = 全量)。
|
|
197
|
+
* 输入口径:inputChars 是字符(llm 流拿不到输入 token,沿用 llm-usage 的字符折算口径)。
|
|
198
|
+
* 成本看板是增强能力:降级态/查询异常一律返回零值,不向上抛错。
|
|
199
|
+
* median 需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median 函数)。
|
|
200
|
+
*/
|
|
201
|
+
aggregateCost(since: number): {
|
|
202
|
+
total: CostAggregate;
|
|
203
|
+
byModel: CostByModel[];
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* 按层级归并聚合(l1 = l1-extract + l1-dedup;成本看板层级表格用)。
|
|
207
|
+
* 降级/异常返回空数组,不抛错。
|
|
208
|
+
*/
|
|
209
|
+
aggregateCostByLayer(since: number): CostByLayer[];
|
|
210
|
+
/**
|
|
211
|
+
* 按时间桶(bucketMs 毫秒)+ model 聚合,返回扁平行。
|
|
212
|
+
* offsetMs 把桶边界对齐本地时区;layer 为空=全部,'l1' 归并 extract/dedup,其余精确匹配。
|
|
213
|
+
* 趋势图与「日均/周均/月均 + 中位数」统计共用:JS 侧按不同 bucketMs 调三次再聚合。
|
|
214
|
+
*/
|
|
215
|
+
aggregateByBucket(bucketMs: number, offsetMs: number, since: number, layer: string): BucketRow[];
|
|
148
216
|
countL0(): number;
|
|
149
217
|
/** 统计 recorded_at >= iso 的消息数(状态面板"今日捕获"用)。 */
|
|
150
218
|
countL0Since(iso: string): number;
|
package/dist/store/sqlite.js
CHANGED
|
@@ -35,6 +35,17 @@ function chunkIds(ids) {
|
|
|
35
35
|
out.push(ids.slice(i, i + IN_CHUNK));
|
|
36
36
|
return out;
|
|
37
37
|
}
|
|
38
|
+
function emptyCostAggregate() {
|
|
39
|
+
return { calls: 0, inputChars: 0, outputTokens: 0, reasoningTokens: 0, avgOutputTokens: 0, medianOutputTokens: 0 };
|
|
40
|
+
}
|
|
41
|
+
/** 已排序序列的中位数(偶数个取中间两者平均;空返回 0)。 */
|
|
42
|
+
function medianOf(sorted) {
|
|
43
|
+
const n = sorted.length;
|
|
44
|
+
if (n === 0)
|
|
45
|
+
return 0;
|
|
46
|
+
const mid = Math.floor(n / 2);
|
|
47
|
+
return n % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
48
|
+
}
|
|
38
49
|
export class MemoryDb {
|
|
39
50
|
db;
|
|
40
51
|
degraded = false;
|
|
@@ -56,6 +67,9 @@ export class MemoryDb {
|
|
|
56
67
|
stmtL1FtsDelete;
|
|
57
68
|
stmtL1FtsSearch;
|
|
58
69
|
stmtL1FtsSearchFamily;
|
|
70
|
+
/** token_cost 明细写入 / 滚动清理语句(构造期 prepare 缓存)。 */
|
|
71
|
+
stmtInsertCost;
|
|
72
|
+
stmtDeleteCost;
|
|
59
73
|
stmtUpsertL0;
|
|
60
74
|
stmtGetL0;
|
|
61
75
|
/** 主表存在性点查(同 L1:防御性 FTS 删除的前置判断)。 */
|
|
@@ -325,6 +339,25 @@ export class MemoryDb {
|
|
|
325
339
|
this.stmtGetL0 = this.db.prepare('SELECT session_id, role, message_text, recorded_at, timestamp FROM l0_conversations WHERE record_id = ?');
|
|
326
340
|
this.stmtL0Exists = this.db.prepare('SELECT 1 FROM l0_conversations WHERE record_id = ?');
|
|
327
341
|
this.prepareL0VecStatements();
|
|
342
|
+
// ── token_cost:蒸馏成本明细表(成本看板用;保留期经 retentionDays 滚动清理) ──
|
|
343
|
+
this.db.exec(`
|
|
344
|
+
CREATE TABLE IF NOT EXISTS token_cost (
|
|
345
|
+
ts INTEGER NOT NULL,
|
|
346
|
+
provider TEXT NOT NULL,
|
|
347
|
+
model TEXT NOT NULL,
|
|
348
|
+
layer TEXT NOT NULL,
|
|
349
|
+
input_chars INTEGER NOT NULL DEFAULT 0,
|
|
350
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
351
|
+
reasoning_tokens INTEGER NOT NULL DEFAULT 0
|
|
352
|
+
)
|
|
353
|
+
`);
|
|
354
|
+
// 迁移:provider/model 复合键引入前的旧表补 provider 列(历史行回填 unknown)
|
|
355
|
+
if (this.tableExists('token_cost') && !this.hasColumn('token_cost', 'provider')) {
|
|
356
|
+
this.db.exec("ALTER TABLE token_cost ADD COLUMN provider TEXT NOT NULL DEFAULT 'unknown'");
|
|
357
|
+
}
|
|
358
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_token_cost_ts ON token_cost(ts)');
|
|
359
|
+
this.stmtInsertCost = this.db.prepare('INSERT INTO token_cost (ts, provider, model, layer, input_chars, output_tokens, reasoning_tokens) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
360
|
+
this.stmtDeleteCost = this.db.prepare('DELETE FROM token_cost WHERE ts < ?');
|
|
328
361
|
// ── FTS5 全文索引(建表失败仅停用 FTS,不降级整个库) ──
|
|
329
362
|
try {
|
|
330
363
|
// 索引重建判据(FTS5 无法 ALTER,只能 drop 后从源表全量回灌):
|
|
@@ -978,6 +1011,135 @@ export class MemoryDb {
|
|
|
978
1011
|
return false;
|
|
979
1012
|
}
|
|
980
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* 记录一次蒸馏调用成本(明细表,写入时按 retentionDays 滚动清理;0 = 永久保留)。
|
|
1016
|
+
* 失败/成功都记(token 照烧);记账失败记 warn 但不阻断蒸馏(成本看板是增强能力)。
|
|
1017
|
+
*/
|
|
1018
|
+
insertCostCall(provider, model, layer, inputChars, outputTokens, reasoningTokens, retentionDays) {
|
|
1019
|
+
if (this.degraded)
|
|
1020
|
+
return;
|
|
1021
|
+
try {
|
|
1022
|
+
this.stmtInsertCost.run(Date.now(), provider, model, layer, Math.max(0, Math.round(inputChars)), Math.max(0, Math.round(outputTokens)), Math.max(0, Math.round(reasoningTokens)));
|
|
1023
|
+
if (retentionDays > 0)
|
|
1024
|
+
this.stmtDeleteCost.run(Date.now() - retentionDays * 24 * 3600_000);
|
|
1025
|
+
}
|
|
1026
|
+
catch (err) {
|
|
1027
|
+
this.logger?.warn(`${TAG} token_cost 记账失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* 查询 token_cost 单窗口聚合(成本看板用;since 为毫秒起点,0 = 全量)。
|
|
1032
|
+
* 输入口径:inputChars 是字符(llm 流拿不到输入 token,沿用 llm-usage 的字符折算口径)。
|
|
1033
|
+
* 成本看板是增强能力:降级态/查询异常一律返回零值,不向上抛错。
|
|
1034
|
+
* median 需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median 函数)。
|
|
1035
|
+
*/
|
|
1036
|
+
aggregateCost(since) {
|
|
1037
|
+
if (this.degraded)
|
|
1038
|
+
return { total: emptyCostAggregate(), byModel: [] };
|
|
1039
|
+
try {
|
|
1040
|
+
const total = this.db
|
|
1041
|
+
.prepare(`SELECT COUNT(*) AS calls,
|
|
1042
|
+
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
1043
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
1044
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
1045
|
+
COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
|
|
1046
|
+
FROM token_cost WHERE ts >= ?`)
|
|
1047
|
+
.get(since);
|
|
1048
|
+
const tokenRows = this.db
|
|
1049
|
+
.prepare('SELECT output_tokens FROM token_cost WHERE ts >= ? ORDER BY output_tokens')
|
|
1050
|
+
.all(since);
|
|
1051
|
+
const byModel = this.db
|
|
1052
|
+
.prepare(`SELECT provider, model, COUNT(*) AS calls,
|
|
1053
|
+
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
1054
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
1055
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
|
|
1056
|
+
FROM token_cost WHERE ts >= ? GROUP BY provider, model ORDER BY outputTokens DESC`)
|
|
1057
|
+
.all(since);
|
|
1058
|
+
return {
|
|
1059
|
+
total: {
|
|
1060
|
+
calls: total.calls,
|
|
1061
|
+
inputChars: total.inputChars,
|
|
1062
|
+
outputTokens: total.outputTokens,
|
|
1063
|
+
reasoningTokens: total.reasoningTokens,
|
|
1064
|
+
avgOutputTokens: total.avgOutputTokens,
|
|
1065
|
+
medianOutputTokens: medianOf(tokenRows.map((r) => r.output_tokens)),
|
|
1066
|
+
},
|
|
1067
|
+
byModel,
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
catch {
|
|
1071
|
+
return { total: emptyCostAggregate(), byModel: [] };
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* 按层级归并聚合(l1 = l1-extract + l1-dedup;成本看板层级表格用)。
|
|
1076
|
+
* 降级/异常返回空数组,不抛错。
|
|
1077
|
+
*/
|
|
1078
|
+
aggregateCostByLayer(since) {
|
|
1079
|
+
if (this.degraded)
|
|
1080
|
+
return [];
|
|
1081
|
+
try {
|
|
1082
|
+
const rows = this.db
|
|
1083
|
+
.prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
|
|
1084
|
+
COUNT(*) AS calls,
|
|
1085
|
+
COALESCE(SUM(input_chars), 0) AS inputChars,
|
|
1086
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
1087
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
1088
|
+
COALESCE(AVG(output_tokens), 0) AS avgOutputTokens
|
|
1089
|
+
FROM token_cost WHERE ts >= ? GROUP BY layer ORDER BY layer`)
|
|
1090
|
+
.all(since);
|
|
1091
|
+
// 中位数需取 output_tokens 序列在 JS 侧算(SQLite 无内置 median)
|
|
1092
|
+
const tokenRows = this.db
|
|
1093
|
+
.prepare(`SELECT CASE WHEN layer IN ('l1-extract','l1-dedup') THEN 'l1' ELSE layer END AS layer,
|
|
1094
|
+
output_tokens
|
|
1095
|
+
FROM token_cost WHERE ts >= ? ORDER BY layer, output_tokens`)
|
|
1096
|
+
.all(since);
|
|
1097
|
+
const medianByLayer = new Map();
|
|
1098
|
+
for (const r of tokenRows) {
|
|
1099
|
+
const arr = medianByLayer.get(r.layer);
|
|
1100
|
+
if (arr)
|
|
1101
|
+
arr.push(r.output_tokens);
|
|
1102
|
+
else
|
|
1103
|
+
medianByLayer.set(r.layer, [r.output_tokens]);
|
|
1104
|
+
}
|
|
1105
|
+
return rows.map((r) => ({
|
|
1106
|
+
...r,
|
|
1107
|
+
medianOutputTokens: medianOf(medianByLayer.get(r.layer) ?? []),
|
|
1108
|
+
}));
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
return [];
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* 按时间桶(bucketMs 毫秒)+ model 聚合,返回扁平行。
|
|
1116
|
+
* offsetMs 把桶边界对齐本地时区;layer 为空=全部,'l1' 归并 extract/dedup,其余精确匹配。
|
|
1117
|
+
* 趋势图与「日均/周均/月均 + 中位数」统计共用:JS 侧按不同 bucketMs 调三次再聚合。
|
|
1118
|
+
*/
|
|
1119
|
+
aggregateByBucket(bucketMs, offsetMs, since, layer) {
|
|
1120
|
+
if (this.degraded)
|
|
1121
|
+
return [];
|
|
1122
|
+
try {
|
|
1123
|
+
let sql = `SELECT CAST((ts + ?) / ? AS INTEGER) AS bucket, provider, model,
|
|
1124
|
+
COUNT(*) AS calls,
|
|
1125
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
1126
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens
|
|
1127
|
+
FROM token_cost WHERE ts >= ?`;
|
|
1128
|
+
const params = [offsetMs, bucketMs, since];
|
|
1129
|
+
if (layer === 'l1') {
|
|
1130
|
+
sql += ` AND layer IN ('l1-extract','l1-dedup')`;
|
|
1131
|
+
}
|
|
1132
|
+
else if (layer) {
|
|
1133
|
+
sql += ` AND layer = ?`;
|
|
1134
|
+
params.push(layer);
|
|
1135
|
+
}
|
|
1136
|
+
sql += ` GROUP BY bucket, provider, model ORDER BY bucket, provider, model`;
|
|
1137
|
+
return this.db.prepare(sql).all(...params);
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
return [];
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
981
1143
|
countL0() {
|
|
982
1144
|
if (this.degraded)
|
|
983
1145
|
return 0;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蒸馏成本看板账本:把每次蒸馏调用(model × layer)的 token 成本写入 SQLite 明细表。
|
|
3
|
+
*
|
|
4
|
+
* 模块级单例 + init 注入 db:callLLM 拿不到 db(db 在 index.ts 运行时创建),
|
|
5
|
+
* 故通过 initTokenCost(db, retentionDays) 在插件启动时注入;recordCostCall 每次调用写一行明细。
|
|
6
|
+
*
|
|
7
|
+
* 与作者 llm-usage.ts 的关系:作者是"按 layer 累计的纯内存计数器"(给 bench 用);
|
|
8
|
+
* 本模块补上三个缺口——按 model 分组、持久化(保留期可配置,默认 365 天)、面向 UI 成本看板。
|
|
9
|
+
*/
|
|
10
|
+
import type { DistillLayer } from './llm-usage.js';
|
|
11
|
+
import type { CostByModel, MemoryDb } from './store/sqlite.js';
|
|
12
|
+
/** 插件启动时注入 db 与明细保留期(index.ts 调用;retentionDays 0 = 永久保留)。 */
|
|
13
|
+
export declare function initTokenCost(d: MemoryDb, retention: number): void;
|
|
14
|
+
/** 插件卸载时清空 db 引用(index.ts 的 ctx.effect 清理里调用,防悬空引用)。 */
|
|
15
|
+
export declare function resetTokenCost(): void;
|
|
16
|
+
/** 记录一次蒸馏调用成本(callLLM 出口调用;provider/model 由调用方传入)。 */
|
|
17
|
+
export declare function recordCostCall(provider: string, model: string, layer: DistillLayer, inputChars: number, outputTokens: number, reasoningTokens: number): void;
|
|
18
|
+
/** 成本看板单个时间窗口(day/week/month/all)。 */
|
|
19
|
+
export interface CostWindow {
|
|
20
|
+
range: 'day' | 'week' | 'month' | 'all';
|
|
21
|
+
/** 窗口起点(毫秒;all 为 0)。 */
|
|
22
|
+
since: number;
|
|
23
|
+
calls: number;
|
|
24
|
+
inputChars: number;
|
|
25
|
+
outputTokens: number;
|
|
26
|
+
reasoningTokens: number;
|
|
27
|
+
avgOutputTokens: number;
|
|
28
|
+
medianOutputTokens: number;
|
|
29
|
+
}
|
|
30
|
+
/** 成本看板时间粒度(趋势图 + 统计口径共用)。 */
|
|
31
|
+
export type Granularity = 'day' | 'week' | 'month';
|
|
32
|
+
/** 每模型统计指标(层级表格行;均值/中位数按 since=0 全量历史的活跃桶口径,非「最近窗口」)。 */
|
|
33
|
+
export interface ModelMetrics {
|
|
34
|
+
model: string;
|
|
35
|
+
dayCalls: number;
|
|
36
|
+
weekCalls: number;
|
|
37
|
+
monthCalls: number;
|
|
38
|
+
dayOutput: number;
|
|
39
|
+
dayMedian: number;
|
|
40
|
+
weekOutput: number;
|
|
41
|
+
weekMedian: number;
|
|
42
|
+
monthOutput: number;
|
|
43
|
+
monthMedian: number;
|
|
44
|
+
}
|
|
45
|
+
/** 每层级(l1/l2/l3)的模型统计(层级表格)。 */
|
|
46
|
+
export interface LayerMetrics {
|
|
47
|
+
layer: 'l1' | 'l2' | 'l3';
|
|
48
|
+
models: ModelMetrics[];
|
|
49
|
+
}
|
|
50
|
+
/** 层级 × 窗口矩阵里的单个窗口格子。 */
|
|
51
|
+
export interface LayerWindow {
|
|
52
|
+
range: 'day' | 'week' | 'month' | 'all';
|
|
53
|
+
calls: number;
|
|
54
|
+
inputChars: number;
|
|
55
|
+
outputTokens: number;
|
|
56
|
+
reasoningTokens: number;
|
|
57
|
+
avgOutputTokens: number;
|
|
58
|
+
medianOutputTokens: number;
|
|
59
|
+
}
|
|
60
|
+
/** 单个层级在四个窗口的聚合(层级×窗口表格行)。 */
|
|
61
|
+
export interface LayerCost {
|
|
62
|
+
layer: 'l1' | 'l2' | 'l3';
|
|
63
|
+
windows: LayerWindow[];
|
|
64
|
+
}
|
|
65
|
+
/** 趋势单桶。 */
|
|
66
|
+
export interface TrendBucket {
|
|
67
|
+
ts: number;
|
|
68
|
+
total: number;
|
|
69
|
+
byModel: Record<string, number>;
|
|
70
|
+
}
|
|
71
|
+
/** 趋势快照:三个层级各一份连续桶序列。 */
|
|
72
|
+
export interface TrendSnapshot {
|
|
73
|
+
granularity: Granularity;
|
|
74
|
+
byLayer: Record<'l1' | 'l2' | 'l3', TrendBucket[]>;
|
|
75
|
+
}
|
|
76
|
+
/** 成本看板快照(dsh-memory/token-cost 端点返回值)。 */
|
|
77
|
+
export interface CostSnapshot {
|
|
78
|
+
/** day/week/month/all 四窗口聚合。 */
|
|
79
|
+
windows: CostWindow[];
|
|
80
|
+
/** 全量窗口(all)按 model 分组。 */
|
|
81
|
+
byModel: CostByModel[];
|
|
82
|
+
/** 按层级(l1/l2/l3 归并)在四个窗口的聚合(层级×窗口表格)。 */
|
|
83
|
+
byLayer: LayerCost[];
|
|
84
|
+
/** 每层级的模型统计指标(层级表格)。 */
|
|
85
|
+
byLayerStats: LayerMetrics[];
|
|
86
|
+
/** 趋势图数据。 */
|
|
87
|
+
trend: TrendSnapshot;
|
|
88
|
+
}
|
|
89
|
+
/** 读成本看板快照(db 未注入/降级时返回全零结构,不抛错;rangeDays>0 = 趋势展示近 N 天)。 */
|
|
90
|
+
export declare function snapshotTokenCost(granularity: Granularity, rangeDays: number): CostSnapshot;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
let db = null;
|
|
2
|
+
let retentionDays = 365;
|
|
3
|
+
/** 插件启动时注入 db 与明细保留期(index.ts 调用;retentionDays 0 = 永久保留)。 */
|
|
4
|
+
export function initTokenCost(d, retention) {
|
|
5
|
+
db = d;
|
|
6
|
+
retentionDays = Math.max(0, Math.round(retention));
|
|
7
|
+
}
|
|
8
|
+
/** 插件卸载时清空 db 引用(index.ts 的 ctx.effect 清理里调用,防悬空引用)。 */
|
|
9
|
+
export function resetTokenCost() {
|
|
10
|
+
db = null;
|
|
11
|
+
}
|
|
12
|
+
/** 记录一次蒸馏调用成本(callLLM 出口调用;provider/model 由调用方传入)。 */
|
|
13
|
+
export function recordCostCall(provider, model, layer, inputChars, outputTokens, reasoningTokens) {
|
|
14
|
+
if (!db)
|
|
15
|
+
return;
|
|
16
|
+
db.insertCostCall(provider, model, layer, inputChars, outputTokens, reasoningTokens, retentionDays);
|
|
17
|
+
}
|
|
18
|
+
/** 四窗口定义:range + 回看毫秒数(all 的 ms 恒 0)。 */
|
|
19
|
+
const WINDOW_DEFS = [
|
|
20
|
+
{ range: 'day', ms: 24 * 3600_000 },
|
|
21
|
+
{ range: 'week', ms: 7 * 24 * 3600_000 },
|
|
22
|
+
{ range: 'month', ms: 30 * 24 * 3600_000 },
|
|
23
|
+
{ range: 'all', ms: 0 },
|
|
24
|
+
];
|
|
25
|
+
/** 趋势桶宽(毫秒)与桶数。 */
|
|
26
|
+
const TREND_MS = { day: 24 * 3600_000, week: 7 * 24 * 3600_000, month: 30 * 24 * 3600_000 };
|
|
27
|
+
const TREND_COUNT = { day: 30, week: 12, month: 12 };
|
|
28
|
+
/** 三个归并层级。 */
|
|
29
|
+
const LAYERS = ['l1', 'l2', 'l3'];
|
|
30
|
+
/** 本地时区偏移(东正西负):把 SQL 端 UTC epoch 桶边界对齐到本地午夜。 */
|
|
31
|
+
function localOffsetMs() {
|
|
32
|
+
return -new Date().getTimezoneOffset() * 60_000;
|
|
33
|
+
}
|
|
34
|
+
/** 已排序序列的中位数(偶数取中间两者平均;空返回 0)。 */
|
|
35
|
+
function medianOf(sorted) {
|
|
36
|
+
const n = sorted.length;
|
|
37
|
+
if (n === 0)
|
|
38
|
+
return 0;
|
|
39
|
+
const mid = Math.floor(n / 2);
|
|
40
|
+
return n % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
41
|
+
}
|
|
42
|
+
/** provider/model 复合键(避免跨 provider 同名 model 在聚合里混淆)。 */
|
|
43
|
+
function modelKey(provider, model) {
|
|
44
|
+
return provider + '/' + model;
|
|
45
|
+
}
|
|
46
|
+
/** 从某模型某粒度的 bucket 序列算均值/中位数(活跃桶口径:只除有调用的桶数)。 */
|
|
47
|
+
function statOf(rows) {
|
|
48
|
+
const active = rows.filter((r) => r.calls > 0);
|
|
49
|
+
if (active.length === 0)
|
|
50
|
+
return { avgCalls: 0, avgOutput: 0, medianOutput: 0 };
|
|
51
|
+
const calls = active.reduce((s, r) => s + r.calls, 0);
|
|
52
|
+
const output = active.reduce((s, r) => s + r.outputTokens, 0);
|
|
53
|
+
return {
|
|
54
|
+
avgCalls: calls / active.length,
|
|
55
|
+
avgOutput: output / active.length,
|
|
56
|
+
medianOutput: medianOf(active.map((r) => r.outputTokens).sort((a, b) => a - b)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** 从 day/week/month 三组 bucket 行组装每个模型的统计指标(provider/model 复合键)。 */
|
|
60
|
+
function buildModelMetrics(dayRows, weekRows, monthRows) {
|
|
61
|
+
const keys = new Set();
|
|
62
|
+
for (const r of dayRows)
|
|
63
|
+
keys.add(modelKey(r.provider, r.model));
|
|
64
|
+
for (const r of weekRows)
|
|
65
|
+
keys.add(modelKey(r.provider, r.model));
|
|
66
|
+
for (const r of monthRows)
|
|
67
|
+
keys.add(modelKey(r.provider, r.model));
|
|
68
|
+
return Array.from(keys)
|
|
69
|
+
.sort()
|
|
70
|
+
.map((key) => {
|
|
71
|
+
const d = statOf(dayRows.filter((r) => modelKey(r.provider, r.model) === key));
|
|
72
|
+
const w = statOf(weekRows.filter((r) => modelKey(r.provider, r.model) === key));
|
|
73
|
+
const m = statOf(monthRows.filter((r) => modelKey(r.provider, r.model) === key));
|
|
74
|
+
return {
|
|
75
|
+
model: key,
|
|
76
|
+
dayCalls: d.avgCalls,
|
|
77
|
+
weekCalls: w.avgCalls,
|
|
78
|
+
monthCalls: m.avgCalls,
|
|
79
|
+
dayOutput: d.avgOutput,
|
|
80
|
+
dayMedian: d.medianOutput,
|
|
81
|
+
weekOutput: w.avgOutput,
|
|
82
|
+
weekMedian: w.medianOutput,
|
|
83
|
+
monthOutput: m.avgOutput,
|
|
84
|
+
monthMedian: m.medianOutput,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** 从某层级某粒度的 bucket 行生成连续趋势桶(空桶补 0;count 为桶数,由调用方按展示范围算)。 */
|
|
89
|
+
function buildTrend(rows, granularity, now, count) {
|
|
90
|
+
const bucketMs = TREND_MS[granularity];
|
|
91
|
+
const offset = localOffsetMs();
|
|
92
|
+
const cur = Math.floor((now + offset) / bucketMs);
|
|
93
|
+
const buckets = [];
|
|
94
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
95
|
+
const b = cur - i;
|
|
96
|
+
buckets.push({ ts: b * bucketMs - offset, total: 0, byModel: {} });
|
|
97
|
+
}
|
|
98
|
+
for (const r of rows) {
|
|
99
|
+
const idx = count - 1 - (cur - r.bucket);
|
|
100
|
+
if (idx < 0 || idx >= count)
|
|
101
|
+
continue;
|
|
102
|
+
const tb = buckets[idx];
|
|
103
|
+
tb.total += r.outputTokens;
|
|
104
|
+
const key = modelKey(r.provider, r.model);
|
|
105
|
+
tb.byModel[key] = (tb.byModel[key] ?? 0) + r.outputTokens;
|
|
106
|
+
}
|
|
107
|
+
return buckets;
|
|
108
|
+
}
|
|
109
|
+
/** 读成本看板快照(db 未注入/降级时返回全零结构,不抛错;rangeDays>0 = 趋势展示近 N 天)。 */
|
|
110
|
+
export function snapshotTokenCost(granularity, rangeDays) {
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const emptyWindow = (range, ms) => ({
|
|
113
|
+
range,
|
|
114
|
+
since: ms === 0 ? 0 : now - ms,
|
|
115
|
+
calls: 0,
|
|
116
|
+
inputChars: 0,
|
|
117
|
+
outputTokens: 0,
|
|
118
|
+
reasoningTokens: 0,
|
|
119
|
+
avgOutputTokens: 0,
|
|
120
|
+
medianOutputTokens: 0,
|
|
121
|
+
});
|
|
122
|
+
if (!db) {
|
|
123
|
+
return {
|
|
124
|
+
windows: WINDOW_DEFS.map((w) => emptyWindow(w.range, w.ms)),
|
|
125
|
+
byModel: [],
|
|
126
|
+
byLayer: [],
|
|
127
|
+
byLayerStats: [],
|
|
128
|
+
trend: { granularity, byLayer: { l1: [], l2: [], l3: [] } },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// 四窗口总聚合(累计窗口顺带取 byModel)
|
|
132
|
+
const d = db;
|
|
133
|
+
const windows = [];
|
|
134
|
+
let byModel = [];
|
|
135
|
+
for (const w of WINDOW_DEFS) {
|
|
136
|
+
const since = w.ms === 0 ? 0 : now - w.ms;
|
|
137
|
+
const agg = d.aggregateCost(since);
|
|
138
|
+
windows.push({ range: w.range, since, ...agg.total });
|
|
139
|
+
if (w.range === 'all')
|
|
140
|
+
byModel = agg.byModel;
|
|
141
|
+
}
|
|
142
|
+
// 按层级 × 窗口聚合(层级×窗口表格):每个窗口调一次 aggregateCostByLayer,再按层组装
|
|
143
|
+
const layerByWindow = WINDOW_DEFS.map((w) => {
|
|
144
|
+
const since = w.ms === 0 ? 0 : now - w.ms;
|
|
145
|
+
return { range: w.range, rows: d.aggregateCostByLayer(since) };
|
|
146
|
+
});
|
|
147
|
+
const byLayer = LAYERS.map((layer) => ({
|
|
148
|
+
layer,
|
|
149
|
+
windows: layerByWindow.map(({ range, rows }) => {
|
|
150
|
+
const row = rows.find((r) => r.layer === layer);
|
|
151
|
+
return {
|
|
152
|
+
range,
|
|
153
|
+
calls: row?.calls ?? 0,
|
|
154
|
+
inputChars: row?.inputChars ?? 0,
|
|
155
|
+
outputTokens: row?.outputTokens ?? 0,
|
|
156
|
+
reasoningTokens: row?.reasoningTokens ?? 0,
|
|
157
|
+
avgOutputTokens: row?.avgOutputTokens ?? 0,
|
|
158
|
+
medianOutputTokens: row?.medianOutputTokens ?? 0,
|
|
159
|
+
};
|
|
160
|
+
}),
|
|
161
|
+
}));
|
|
162
|
+
// 每层级模型统计(全量历史口径)+ 趋势(按展示范围)
|
|
163
|
+
const offset = localOffsetMs();
|
|
164
|
+
const byLayerStats = [];
|
|
165
|
+
const trendByLayer = { l1: [], l2: [], l3: [] };
|
|
166
|
+
// 趋势展示范围:rangeDays > 0 = 近 N 天(强制按「日」粒度出 N 个日桶,不受周/月聚合影响);否则用默认窗口
|
|
167
|
+
const trendGranularity = rangeDays > 0 ? 'day' : granularity;
|
|
168
|
+
const trendSince = rangeDays > 0 ? now - rangeDays * 24 * 3600_000 : 0;
|
|
169
|
+
const trendCount = rangeDays > 0 ? rangeDays : TREND_COUNT[granularity];
|
|
170
|
+
for (const layer of LAYERS) {
|
|
171
|
+
const dayRows = d.aggregateByBucket(TREND_MS.day, offset, 0, layer);
|
|
172
|
+
const weekRows = d.aggregateByBucket(TREND_MS.week, offset, 0, layer);
|
|
173
|
+
const monthRows = d.aggregateByBucket(TREND_MS.month, offset, 0, layer);
|
|
174
|
+
byLayerStats.push({ layer, models: buildModelMetrics(dayRows, weekRows, monthRows) });
|
|
175
|
+
trendByLayer[layer] = buildTrend(d.aggregateByBucket(TREND_MS[trendGranularity], offset, trendSince, layer), trendGranularity, now, trendCount);
|
|
176
|
+
}
|
|
177
|
+
return { windows, byModel, byLayer, byLayerStats, trend: { granularity: trendGranularity, byLayer: trendByLayer } };
|
|
178
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-layered-memory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.7",
|
|
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",
|