dsh-layered-memory 0.8.5 → 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 +25 -2
- package/README.md +12 -2
- package/dist/client.js +317 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +7 -0
- package/dist/embedding-worker.cjs +176 -0
- package/dist/hooks/recall.d.ts +10 -6
- package/dist/hooks/recall.js +40 -8
- package/dist/index.d.ts +14 -0
- package/dist/index.js +5 -2
- package/dist/llm.js +5 -0
- package/dist/pipeline/runner.d.ts +5 -0
- package/dist/pipeline/runner.js +23 -4
- package/dist/pipeline/trigger.d.ts +2 -0
- package/dist/pipeline/trigger.js +11 -0
- package/dist/stats.js +11 -0
- package/dist/store/embedding-source.d.ts +2 -1
- package/dist/store/embedding-source.js +7 -2
- package/dist/store/embedding.d.ts +2 -1
- package/dist/store/l0.js +10 -4
- package/dist/store/l1.d.ts +11 -1
- package/dist/store/l1.js +26 -6
- package/dist/store/local-embedding.d.ts +69 -46
- package/dist/store/local-embedding.js +179 -75
- package/dist/store/recall-dedupe.d.ts +26 -0
- package/dist/store/recall-dedupe.js +138 -0
- package/dist/store/runtime-installer.d.ts +0 -2
- package/dist/store/runtime-installer.js +0 -6
- package/dist/store/search-utils.d.ts +17 -0
- package/dist/store/search-utils.js +29 -0
- package/dist/store/sqlite.d.ts +68 -0
- package/dist/store/sqlite.js +185 -9
- package/dist/token-cost.d.ts +90 -0
- package/dist/token-cost.js +178 -0
- package/dist/util/recall-budget.d.ts +2 -2
- package/dist/util/recall-budget.js +2 -2
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -90,10 +90,28 @@ synthetic message placed right before the user's new message, rendered as a
|
|
|
90
90
|
**"Context injection · memory"** row in the chat flow (expand to see the hits) — so you
|
|
91
91
|
can see "memory at work" directly. Injected content is bounded by length and
|
|
92
92
|
time budgets — oversized lines are truncated (pointing the model at the memory tools for
|
|
93
|
-
the full text) and a timed-out recall silently skips that turn, never slowing the chat.
|
|
93
|
+
the full text) and a timed-out recall silently skips that turn, never slowing the chat.
|
|
94
|
+
**Per-session dedupe**: a memory already injected in this session is not injected again
|
|
95
|
+
(the model's context already holds it — follow-up questions on the same topic save
|
|
96
|
+
tokens); the record resets when the context is compacted or cleared, so memories can
|
|
97
|
+
flow back in, and an updated memory (new id after a content change) is never held back
|
|
98
|
+
by the old suppression. **Freshness weighting**: recall ranking applies a soft weight
|
|
99
|
+
`relevance × max(0.5, 0.5^(days since last update / 30))` — among candidates of similar
|
|
100
|
+
relevance the fresh one wins (slots rotate naturally), while a strongly relevant old
|
|
101
|
+
memory still recalls fine (the floor caps its loss at half a ranking score, so
|
|
102
|
+
long-lived facts never sink); tune via `recall.decayHalfLifeDays`, `0` disables. It
|
|
94
103
|
also registers three model-callable memory tools: `memory_search` /
|
|
95
104
|
`conversation_search` / `memory_read_scene`.
|
|
96
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
|
+
|
|
97
115
|
In action: the "Context injection · memory" row surfaces relevant memories first, and
|
|
98
116
|
the model then calls `memory_read_scene` directly to read scene blocks before answering
|
|
99
117
|
from memory:
|
|
@@ -243,6 +261,7 @@ the bundle layer appends and causes `duplicate loader entry id` startup failure)
|
|
|
243
261
|
| `recall.includeSceneNav` | `true` | Inject scene navigation into the system prompt (`<scene-navigation>`, stable zone) |
|
|
244
262
|
| `recall.strategy` | `hybrid` | Retrieval strategy: `keyword` / `embedding` / `hybrid` |
|
|
245
263
|
| `recall.scoreThreshold` | `0.3` | Recall score threshold (below is not injected; applies to keyword/embedding only, not pre-fusion hybrid; tool path unfiltered) |
|
|
264
|
+
| `recall.decayHalfLifeDays` | `30` | Freshness-decay half-life for recall ranking (days, 0=off): ranking applies `relevance × max(0.5, 0.5^(days since last update / half-life))` — among similarly relevant candidates the fresh one wins (slots rotate), and an old memory loses at most half its ranking score (floor keeps long-lived facts afloat) |
|
|
246
265
|
| `embedding.enabled` | `false` | Vector retrieval switch; off = pure FTS |
|
|
247
266
|
| `embedding.baseUrl` | empty | OpenAI-compatible /embeddings endpoint (e.g. `https://api.siliconflow.cn/v1`) |
|
|
248
267
|
| `embedding.apiKey` | empty | API key |
|
|
@@ -259,6 +278,7 @@ the bundle layer appends and causes `duplicate loader entry id` startup failure)
|
|
|
259
278
|
| `llm.temperature` | `0.3` | Distillation temperature |
|
|
260
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) |
|
|
261
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 |
|
|
262
282
|
| `tools` | `true` | Whether to register model-callable memory tools |
|
|
263
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 |
|
|
264
284
|
|
|
@@ -299,7 +319,10 @@ with per-file sha256; arbitrary repos cannot be downloaded).
|
|
|
299
319
|
under `models/<id>/` in the data directory, deletable from the settings page at any time;
|
|
300
320
|
- **On-demand runtime**: the inference runtime (transformers.js, ~100–200MB) is
|
|
301
321
|
installed only on first switch to the local tier, into `runtime/` in the data
|
|
302
|
-
directory — never in the plugin's dependency tree or install directory;
|
|
322
|
+
directory — never in the plugin's dependency tree or install directory; model
|
|
323
|
+
loading and inference run on a **dedicated worker thread**, so the host event
|
|
324
|
+
loop is never frozen (conversations and page interactions stay responsive while
|
|
325
|
+
text is being embedded);
|
|
303
326
|
- **Live switching**: one click to swap sources — everything is re-embedded in the
|
|
304
327
|
background (visible progress, cancellable; retrieval silently degrades to keywords
|
|
305
328
|
in the meantime, conversations unaffected; a dimension change rebuilds the vector
|
package/README.md
CHANGED
|
@@ -74,7 +74,14 @@ npx tsc src/smoke.ts --outDir dist-smoke --module nodenext --moduleResolution no
|
|
|
74
74
|
alt="dsh-layered-memory 运行时数据流:左侧 User 与 Assistant 的会话事件流入插件(L0 捕获、L1–L3 蒸馏、检索召回、记忆工具),插件经 agent/pre-step 把相关记忆注入右侧 DSH 核心;蒸馏复用核心的 ctx.llm,数据双写 ~/.dsh/memory/">
|
|
75
75
|
</p>
|
|
76
76
|
|
|
77
|
-
插件挂在 dsh 原生事件上(`session/event` 捕获、`agent/pre-step` 注入),蒸馏调用复用宿主 `ctx.llm`。召回以**消息侧注入**呈现:相关记忆作为一条合成消息排在用户新消息之前,会话流里显示为**"上下文注入 · memory"**行(点开看命中内容)——用户能直接看到"记忆生效了"
|
|
77
|
+
插件挂在 dsh 原生事件上(`session/event` 捕获、`agent/pre-step` 注入),蒸馏调用复用宿主 `ctx.llm`。召回以**消息侧注入**呈现:相关记忆作为一条合成消息排在用户新消息之前,会话流里显示为**"上下文注入 · memory"**行(点开看命中内容)——用户能直接看到"记忆生效了";注入内容有长度预算与时间预算,超限截断/超时跳过,绝不拖慢对话。**同会话去重**:已注入过的记忆不再重复注入(模型上下文里已经有了,追问同类问题时省 token);上下文被 `/compact` 压缩或清空时自动重置,记忆可重新注入;被更新的记忆(内容变化换新 id)不受旧压制。**时效加权**:召回排序按 `相关度 × max(0.5, 0.5^(距上次更新天数/30))` 软加权——相关度相近的候选之间新鲜记忆优先(名额自然轮转),相关度足够高的老记忆照常召回(地板保证最多损失一半排序分,长期事实不沉底);`recall.decayHalfLifeDays` 可调,0=关闭。
|
|
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 计。
|
|
78
85
|
|
|
79
86
|
**记忆工具(3):**
|
|
80
87
|
- memory_search
|
|
@@ -206,7 +213,8 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
|
|
|
206
213
|
绕开镜像 CDN 偶发的坏缓存对象),校验失配从零重下、网络错误保留断点续传;
|
|
207
214
|
落盘数据目录 `models/<id>/`,不用了随时在设置页删除;
|
|
208
215
|
- **按需运行时**:首次切换本地档才安装推理运行时(transformers.js,约 100~200MB,
|
|
209
|
-
装进数据目录 `runtime
|
|
216
|
+
装进数据目录 `runtime/`——不进插件依赖树,不碰插件安装目录);模型加载与推理在
|
|
217
|
+
**独立 worker 线程**执行,不冻结宿主事件循环(嵌入计算期间对话与页面交互照常);
|
|
210
218
|
- **活切换**:一键换源——自动后台全量重嵌(进度可见、可取消,期间检索自动降级
|
|
211
219
|
关键词,不影响对话;维度变化时向量表按新维度重建);切换失败保持旧源,重启仍按原源运行;
|
|
212
220
|
- **生效规则 = 部署上限 AND 运行时选择**:`embedding.allowLocalModels=false` 可整体
|
|
@@ -256,6 +264,7 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
|
|
|
256
264
|
| `recall.includeSceneNav` | `true` | 系统提示注入场景导航(`<scene-navigation>`,稳定区) |
|
|
257
265
|
| `recall.strategy` | `hybrid` | 检索策略:`keyword` / `embedding` / `hybrid` |
|
|
258
266
|
| `recall.scoreThreshold` | `0.3` | 召回分数阈值(低于不注入;仅 keyword/embedding 策略生效,hybrid 融合前不过滤;工具路径不过滤) |
|
|
267
|
+
| `recall.decayHalfLifeDays` | `30` | 召回时效衰减半衰期(天,0=关):排序按 `相关度 × max(0.5, 0.5^(距更新天数/半衰期))` 软加权——相关度相近的候选间新鲜记忆优先(名额轮转),老记忆最多损失一半排序分(地板兜底,长期事实不沉底) |
|
|
259
268
|
| `embedding.enabled` | `false` | 向量检索开关;关闭即纯 FTS 运行 |
|
|
260
269
|
| `embedding.baseUrl` | 空 | OpenAI 兼容 /embeddings 地址(如 `https://api.siliconflow.cn/v1`) |
|
|
261
270
|
| `embedding.apiKey` | 空 | API Key |
|
|
@@ -272,6 +281,7 @@ ONNX 量化 **CPU 推理**——无需 API Key,数据不出本机)。本地
|
|
|
272
281
|
| `llm.temperature` | `0.3` | 蒸馏温度 |
|
|
273
282
|
| `llm.maxInputChars` | `700000` | 单次蒸馏输入字符预算(超限的 L1 输入自动分块抽取);运行时可在设置页 → 蒸馏参数 → 输入预算调整(留空/0 = 跟随本值) |
|
|
274
283
|
| `llm.timeoutMs` | `120000` | 单次蒸馏调用超时(ms) |
|
|
284
|
+
| `tokenCost.retentionDays` | `365` | 蒸馏成本明细(token_cost 表)保留天数,写入时滚动清理更早行;`0` = 永久保留。成本看板「近 N 天」窗口上限同此值 |
|
|
275
285
|
| `tools` | `true` | 是否注册模型可调用的记忆工具 |
|
|
276
286
|
| `benchControl` | `false` | 注册 bench 控制服务(进程内 rebuild 触发/会话档位设置/蒸馏用量快照,供基准 lifecycle 赛道)。默认关——生产部署零表面积,勿随意开启 |
|
|
277
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
|
@@ -66,6 +66,9 @@ export interface MemoryConfig {
|
|
|
66
66
|
strategy: 'keyword' | 'embedding' | 'hybrid';
|
|
67
67
|
/** 召回路径分数阈值(0~1,低于该分不注入;工具路径不过滤)。 */
|
|
68
68
|
scoreThreshold: number;
|
|
69
|
+
/** 时效衰减半衰期(天,0=关):召回排序的 freshness 加权——score × max(0.5, 0.5^(Δ天/半衰期)),
|
|
70
|
+
* 只影响相关度相近候选间的名次(老记忆最多损失一半排序分,不淘汰)。 */
|
|
71
|
+
decayHalfLifeDays: number;
|
|
69
72
|
};
|
|
70
73
|
embedding: {
|
|
71
74
|
/** 向量检索总开关;关闭时纯 FTS 运行(官方 provider="none" 同款)。 */
|
|
@@ -111,6 +114,10 @@ export interface MemoryConfig {
|
|
|
111
114
|
l3: number;
|
|
112
115
|
}>;
|
|
113
116
|
};
|
|
117
|
+
tokenCost: {
|
|
118
|
+
/** token_cost 明细保留天数;写入时滚动清理更早行。0 = 永久保留。 */
|
|
119
|
+
retentionDays: number;
|
|
120
|
+
};
|
|
114
121
|
/** 是否注册模型可调用的记忆工具。 */
|
|
115
122
|
tools: boolean;
|
|
116
123
|
/** 注册 bench 控制服务(dsh-memory-bench,进程内 rebuild 触发面)。
|
|
@@ -170,6 +177,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
170
177
|
includeSceneNav: Schema<boolean, boolean>;
|
|
171
178
|
strategy: Schema<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
172
179
|
scoreThreshold: Schema<number, number>;
|
|
180
|
+
decayHalfLifeDays: Schema<number, number>;
|
|
173
181
|
}>, Schemastery.ObjectT<{
|
|
174
182
|
enabled: Schema<boolean, boolean>;
|
|
175
183
|
maxResults: Schema<number, number>;
|
|
@@ -180,6 +188,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
180
188
|
includeSceneNav: Schema<boolean, boolean>;
|
|
181
189
|
strategy: Schema<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
182
190
|
scoreThreshold: Schema<number, number>;
|
|
191
|
+
decayHalfLifeDays: Schema<number, number>;
|
|
183
192
|
}>>;
|
|
184
193
|
embedding: Schema<Schemastery.ObjectS<{
|
|
185
194
|
enabled: Schema<boolean, boolean>;
|
|
@@ -221,6 +230,11 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
221
230
|
maxInputChars: Schema<number, number>;
|
|
222
231
|
timeoutMs: Schema<number, number>;
|
|
223
232
|
}>>;
|
|
233
|
+
tokenCost: Schema<Schemastery.ObjectS<{
|
|
234
|
+
retentionDays: Schema<number, number>;
|
|
235
|
+
}>, Schemastery.ObjectT<{
|
|
236
|
+
retentionDays: Schema<number, number>;
|
|
237
|
+
}>>;
|
|
224
238
|
tools: Schema<boolean, boolean>;
|
|
225
239
|
benchControl: Schema<boolean, boolean>;
|
|
226
240
|
}>, Schemastery.ObjectT<{
|
|
@@ -276,6 +290,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
276
290
|
includeSceneNav: Schema<boolean, boolean>;
|
|
277
291
|
strategy: Schema<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
278
292
|
scoreThreshold: Schema<number, number>;
|
|
293
|
+
decayHalfLifeDays: Schema<number, number>;
|
|
279
294
|
}>, Schemastery.ObjectT<{
|
|
280
295
|
enabled: Schema<boolean, boolean>;
|
|
281
296
|
maxResults: Schema<number, number>;
|
|
@@ -286,6 +301,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
286
301
|
includeSceneNav: Schema<boolean, boolean>;
|
|
287
302
|
strategy: Schema<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
288
303
|
scoreThreshold: Schema<number, number>;
|
|
304
|
+
decayHalfLifeDays: Schema<number, number>;
|
|
289
305
|
}>>;
|
|
290
306
|
embedding: Schema<Schemastery.ObjectS<{
|
|
291
307
|
enabled: Schema<boolean, boolean>;
|
|
@@ -327,6 +343,11 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
327
343
|
maxInputChars: Schema<number, number>;
|
|
328
344
|
timeoutMs: Schema<number, number>;
|
|
329
345
|
}>>;
|
|
346
|
+
tokenCost: Schema<Schemastery.ObjectS<{
|
|
347
|
+
retentionDays: Schema<number, number>;
|
|
348
|
+
}>, Schemastery.ObjectT<{
|
|
349
|
+
retentionDays: Schema<number, number>;
|
|
350
|
+
}>>;
|
|
330
351
|
tools: Schema<boolean, boolean>;
|
|
331
352
|
benchControl: Schema<boolean, boolean>;
|
|
332
353
|
}>>;
|
package/dist/config.js
CHANGED
|
@@ -48,6 +48,9 @@ export const memorySchema = Schema.object({
|
|
|
48
48
|
includeSceneNav: Schema.boolean().default(true),
|
|
49
49
|
strategy: Schema.union(['keyword', 'embedding', 'hybrid']).default('hybrid'),
|
|
50
50
|
scoreThreshold: Schema.number().min(0).max(1).default(0.3),
|
|
51
|
+
// 时效衰减(#29):乘法软加权 + 地板 0.5(老记忆最多损失一半排序分),只轮转
|
|
52
|
+
// 相关度相近候选的名次;0=关(bench 基线可比性可 pin 0)
|
|
53
|
+
decayHalfLifeDays: Schema.number().min(0).max(3650).default(30),
|
|
51
54
|
}),
|
|
52
55
|
embedding: Schema.object({
|
|
53
56
|
enabled: Schema.boolean().default(false),
|
|
@@ -77,6 +80,10 @@ export const memorySchema = Schema.object({
|
|
|
77
80
|
maxInputChars: Schema.number().min(1000).max(1_000_000).default(700_000),
|
|
78
81
|
timeoutMs: Schema.number().min(1000).max(600_000).default(120_000),
|
|
79
82
|
}),
|
|
83
|
+
// token_cost 明细保留期(写入时滚动清理;0 = 永久保留)。成本看板的「近 N 天」窗口上限也取此值
|
|
84
|
+
tokenCost: Schema.object({
|
|
85
|
+
retentionDays: Schema.number().min(0).max(3650).default(365),
|
|
86
|
+
}),
|
|
80
87
|
tools: Schema.boolean().default(true),
|
|
81
88
|
benchControl: Schema.boolean().default(false),
|
|
82
89
|
});
|