dsh-plugin-mind-tree 0.3.0 → 0.4.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/README.md +19 -1
- package/lib/client.js +441 -20
- package/lib/index.js +19 -5
- package/lib/pricing.js +99 -0
- package/lib/reduce.js +9 -5
- package/package.json +38 -18
package/README.md
CHANGED
|
@@ -16,16 +16,25 @@
|
|
|
16
16
|
选择保存在本机(`localStorage`),随时可「恢复默认」;
|
|
17
17
|
默认色调经**对比度优化**(白字对比不足 4.5:1 时自动改用深色字),比原版更耐看而不刺眼;
|
|
18
18
|
- **气泡字号适中偏大**(主文字 13px / 摘要 12px,行高 34px),贴合 DSH 设计语言,窄侧栏里也清晰可读;
|
|
19
|
-
-
|
|
19
|
+
- **过程信息四档削减**(默认「精简」,工具栏可切换;导出跟随当前档位):
|
|
20
20
|
|
|
21
21
|
| 档位 | 保留内容 | 真实会话实测(2491 节点 / 15 轮) |
|
|
22
22
|
|---|---|---|
|
|
23
|
+
| **输入** | 轮次 + 你的真实发言 + 分支点(= 把整场对话压成需求清单) | 41 节点 |
|
|
23
24
|
| **精简**(默认) | 轮次 + 你的真实发言 + 全部分支点 + 每轮「工具活动 ×N」聚合行 + 每轮答复 | **119 节点**,大纲 8 KB |
|
|
24
25
|
| **标准** | 精简 + 实质性过程文本(≥60 字)与每条工具调用(不含工具输出) | 1259 节点,136 KB |
|
|
25
26
|
| **完整** | 全部节点,与事件日志一一对应 | 2491 节点,286 KB |
|
|
26
27
|
|
|
27
28
|
工具聚合行自带被折叠调用的清单(点它即展开)、可搜索、可定位到对应轮次;
|
|
28
29
|
削减只作用于展示与导出,**宿主始终返回完整数据**,随时可切回「完整」。
|
|
30
|
+
- **成本估算**:按**该轮实际模型**(`message.source.model`)与**高峰/空闲时段**把 token 折算成人民币:
|
|
31
|
+
每轮行尾显示 `¥`、详情面板给出模型与成本、搜索框下方显示整场合计;未知模型按兜底价并标 `≈`。
|
|
32
|
+
单价来源 DeepSeek 官方「模型 & 价格」页(`lib/pricing.js` 里带版本号,改价只改一处);
|
|
33
|
+
缓存命中/未命中、输出分别计价,缓存写入按未命中输入计费。
|
|
34
|
+
- **缩放 / 平移 / 图片导出**:`Ctrl/⌘ + 滚轮` 以光标为中心缩放、拖动平移、双击或「适配」回到整体;
|
|
35
|
+
「导出 ▾」新增 **SVG 矢量图** 与 **PNG 图片**(2 倍像素,超大画布自动降倍率),可直接贴进文档/聊天。
|
|
36
|
+
- **实时跟随**:会话产生新事件时**静默刷新**(不闪 loading、900ms 节流),工具栏显示「● 运行中」,
|
|
37
|
+
最新一轮自动加虚线环与脉动圆点;流中断自动重连。
|
|
29
38
|
- **导出 Markdown / Canvas**:把思维树导出为可归档文档(含分支标记、token 统计;大纲与 JSON 跟随当前档位);
|
|
30
39
|
- **分支节点识别**:
|
|
31
40
|
- 替换分支(消息被编辑/重试后旧内容脱离上下文,橙色虚线连接)
|
|
@@ -84,6 +93,15 @@
|
|
|
84
93
|
> 正确写法:`https://github.com/ShOpenMind/dsh-plugin-mind-tree/releases/latest/download/dsh-plugin-mind-tree.tgz`
|
|
85
94
|
> 已经被卡住的话:`cd ~/.dsh/profiles/<profile> && npx pnpm install --update-checksums`。
|
|
86
95
|
|
|
96
|
+
### 方式零(最省事):从 npm 装
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
dsh plugin install dsh-plugin-mind-tree
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
或在 Harness 的「插件」面板里搜索 `dsh-plugin-mind-tree`。
|
|
103
|
+
npm 上的版本号不可变,不存在「同一地址内容变了、哈希对不上」的问题。
|
|
104
|
+
|
|
87
105
|
### 方式一(推荐):粘贴一行地址,30 秒装好,**不需要 git**
|
|
88
106
|
|
|
89
107
|
在 DeepSeek Harness 的**插件页 → 添加插件**里粘贴下面的地址(**固定链接,始终指向最新版**):
|
package/lib/client.js
CHANGED
|
@@ -20955,6 +20955,7 @@ function reduceGraph(tree, level = "compact") {
|
|
|
20955
20955
|
if (level === "full") return tree;
|
|
20956
20956
|
const nodes = tree.nodes ?? [];
|
|
20957
20957
|
const keepIds = /* @__PURE__ */ new Set();
|
|
20958
|
+
const isUserOnly = level === "user";
|
|
20958
20959
|
const isCompact = level === "compact";
|
|
20959
20960
|
const minAssistant = isCompact ? Number.POSITIVE_INFINITY : MIN_ASSISTANT_STANDARD;
|
|
20960
20961
|
const lastOfTurn = lastAssistantIds(nodes);
|
|
@@ -20975,13 +20976,15 @@ function reduceGraph(tree, level = "compact") {
|
|
|
20975
20976
|
case "info":
|
|
20976
20977
|
continue;
|
|
20977
20978
|
// system / developer 注入内容
|
|
20978
|
-
case "user":
|
|
20979
|
-
if (isInjectedLabel(node2.label)) continue;
|
|
20980
|
-
break;
|
|
20981
20979
|
case "assistant":
|
|
20980
|
+
if (isUserOnly) continue;
|
|
20982
20981
|
if (!(lastOfTurn.has(node2.id) || charLength(node2.label) >= minAssistant)) continue;
|
|
20983
20982
|
break;
|
|
20983
|
+
case "user":
|
|
20984
|
+
if (isInjectedLabel(node2.label)) continue;
|
|
20985
|
+
break;
|
|
20984
20986
|
case "tool": {
|
|
20987
|
+
if (isUserOnly) continue;
|
|
20985
20988
|
if (!isCompact) break;
|
|
20986
20989
|
const key = node2.turn ?? -1;
|
|
20987
20990
|
if (!groupBuckets.has(key)) groupBuckets.set(key, []);
|
|
@@ -21071,7 +21074,7 @@ var DETAIL_LEVELS, MIN_ASSISTANT_STANDARD, GROUP_NAME_LIMIT, charLength, DETAIL_
|
|
|
21071
21074
|
var init_reduce = __esm({
|
|
21072
21075
|
"lib/reduce.js"() {
|
|
21073
21076
|
init_format();
|
|
21074
|
-
DETAIL_LEVELS = ["compact", "standard", "full"];
|
|
21077
|
+
DETAIL_LEVELS = ["user", "compact", "standard", "full"];
|
|
21075
21078
|
MIN_ASSISTANT_STANDARD = 60;
|
|
21076
21079
|
GROUP_NAME_LIMIT = 4;
|
|
21077
21080
|
charLength = (text) => [...String(text ?? "")].length;
|
|
@@ -21083,12 +21086,96 @@ var init_reduce = __esm({
|
|
|
21083
21086
|
}
|
|
21084
21087
|
});
|
|
21085
21088
|
|
|
21089
|
+
// lib/pricing.js
|
|
21090
|
+
var pricing_exports = {};
|
|
21091
|
+
__export(pricing_exports, {
|
|
21092
|
+
FALLBACK_PRICE: () => FALLBACK_PRICE,
|
|
21093
|
+
MODEL_PRICES: () => MODEL_PRICES,
|
|
21094
|
+
PRICING_VERSION: () => PRICING_VERSION,
|
|
21095
|
+
costOf: () => costOf,
|
|
21096
|
+
costOfTree: () => costOfTree,
|
|
21097
|
+
formatCny: () => formatCny,
|
|
21098
|
+
isPeakHour: () => isPeakHour,
|
|
21099
|
+
priceOf: () => priceOf,
|
|
21100
|
+
sumCost: () => sumCost
|
|
21101
|
+
});
|
|
21102
|
+
function isPeakHour(atMs) {
|
|
21103
|
+
const at = typeof atMs === "number" && Number.isFinite(atMs) ? atMs : Date.now();
|
|
21104
|
+
const shifted = new Date(at + BJ_OFFSET_MS);
|
|
21105
|
+
const day = shifted.getUTCDay();
|
|
21106
|
+
const minutes = shifted.getUTCHours() * 60 + shifted.getUTCMinutes();
|
|
21107
|
+
const weekday = day >= 1 && day <= 5;
|
|
21108
|
+
const inMorning = minutes >= 9 * 60 && minutes < 12 * 60;
|
|
21109
|
+
const inAfternoon = minutes >= 14 * 60 && minutes < 18 * 60;
|
|
21110
|
+
return weekday && (inMorning || inAfternoon);
|
|
21111
|
+
}
|
|
21112
|
+
function priceOf(model, atMs) {
|
|
21113
|
+
const table = MODEL_PRICES[model] ?? FALLBACK_PRICE;
|
|
21114
|
+
const slot = isPeakHour(atMs) ? 1 : 0;
|
|
21115
|
+
return {
|
|
21116
|
+
model: MODEL_PRICES[model] ? model : null,
|
|
21117
|
+
peak: slot === 1,
|
|
21118
|
+
hit: table.hit[slot],
|
|
21119
|
+
miss: table.miss[slot],
|
|
21120
|
+
out: table.out[slot]
|
|
21121
|
+
};
|
|
21122
|
+
}
|
|
21123
|
+
function costOf(usage, model, atMs) {
|
|
21124
|
+
const input2 = Math.max(0, Number(usage?.input) || 0);
|
|
21125
|
+
const output2 = Math.max(0, Number(usage?.output) || 0);
|
|
21126
|
+
const cacheRead = Math.max(0, Number(usage?.cacheRead) || 0);
|
|
21127
|
+
const cacheWrite = Math.max(0, Number(usage?.cacheWrite) || 0);
|
|
21128
|
+
const price = priceOf(model, atMs);
|
|
21129
|
+
const cny = ((input2 + cacheWrite) * price.miss + cacheRead * price.hit + output2 * price.out) / 1e6;
|
|
21130
|
+
return { cny, peak: price.peak, model: price.model, known: price.model !== null };
|
|
21131
|
+
}
|
|
21132
|
+
function sumCost(entries) {
|
|
21133
|
+
let cny = 0;
|
|
21134
|
+
let anyUnknown = false;
|
|
21135
|
+
for (const entry of entries ?? []) {
|
|
21136
|
+
if (!entry) continue;
|
|
21137
|
+
cny += Number(entry.cny) || 0;
|
|
21138
|
+
if (entry.known === false) anyUnknown = true;
|
|
21139
|
+
}
|
|
21140
|
+
return { cny, anyUnknown };
|
|
21141
|
+
}
|
|
21142
|
+
function formatCny(value) {
|
|
21143
|
+
const cny = Number(value) || 0;
|
|
21144
|
+
if (cny <= 0) return "\xA50";
|
|
21145
|
+
if (cny < 5e-3) return "<\xA50.01";
|
|
21146
|
+
if (cny < 1) return `\xA5${cny.toFixed(3)}`;
|
|
21147
|
+
return `\xA5${cny.toFixed(2)}`;
|
|
21148
|
+
}
|
|
21149
|
+
function costOfTree(tree) {
|
|
21150
|
+
const entries = [];
|
|
21151
|
+
for (const node2 of tree?.nodes ?? []) {
|
|
21152
|
+
if (node2?.kind !== "turn" || !node2.usage) continue;
|
|
21153
|
+
entries.push(costOf(node2.usage, node2.usage.model, node2.usage.startedAt));
|
|
21154
|
+
}
|
|
21155
|
+
return sumCost(entries);
|
|
21156
|
+
}
|
|
21157
|
+
var PRICING_VERSION, MODEL_PRICES, FALLBACK_PRICE, BJ_OFFSET_MS;
|
|
21158
|
+
var init_pricing = __esm({
|
|
21159
|
+
"lib/pricing.js"() {
|
|
21160
|
+
PRICING_VERSION = "2026-11-15";
|
|
21161
|
+
MODEL_PRICES = {
|
|
21162
|
+
"deepseek-flash": { hit: [0.02, 0.04], miss: [1, 2], out: [4, 8] },
|
|
21163
|
+
"deepseek-v4-flash": { hit: [0.02, 0.04], miss: [1, 2], out: [4, 8] },
|
|
21164
|
+
"deepseek-v4-flash-vision-exp": { hit: [0.02, 0.04], miss: [1, 2], out: [4, 8] },
|
|
21165
|
+
"deepseek-v4-pro": { hit: [0.15, 0.3], miss: [4.5, 9], out: [13.5, 27] }
|
|
21166
|
+
};
|
|
21167
|
+
FALLBACK_PRICE = MODEL_PRICES["deepseek-flash"];
|
|
21168
|
+
BJ_OFFSET_MS = 8 * 3600 * 1e3;
|
|
21169
|
+
}
|
|
21170
|
+
});
|
|
21171
|
+
|
|
21086
21172
|
// src/client.js
|
|
21087
21173
|
var React = require("react");
|
|
21088
21174
|
var { MenuItemButton } = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
21089
21175
|
var { TYPERT_REMOTE: TYPERT_REMOTE2 } = (init_remote(), __toCommonJS(remote_exports));
|
|
21090
21176
|
var FMT = (init_format(), __toCommonJS(format_exports));
|
|
21091
21177
|
var REDUCE = (init_reduce(), __toCommonJS(reduce_exports));
|
|
21178
|
+
var PRICING = (init_pricing(), __toCommonJS(pricing_exports));
|
|
21092
21179
|
var h = React.createElement;
|
|
21093
21180
|
var PACKAGE_ID = "dsh-plugin-mind-tree";
|
|
21094
21181
|
var REGISTRY_ID = `${PACKAGE_ID}/mind-tree`;
|
|
@@ -21190,13 +21277,29 @@ var MESSAGES = {
|
|
|
21190
21277
|
"export.cancelled": "\u5DF2\u53D6\u6D88",
|
|
21191
21278
|
"export.failed": "\u5BFC\u51FA\u5931\u8D25",
|
|
21192
21279
|
"warning": "\u517C\u5BB9\u6027\u63D0\u793A",
|
|
21280
|
+
"level.user": "\u8F93\u5165",
|
|
21193
21281
|
"level.compact": "\u7CBE\u7B80",
|
|
21194
21282
|
"level.standard": "\u6807\u51C6",
|
|
21195
21283
|
"level.full": "\u5B8C\u6574",
|
|
21196
|
-
"level.hint": "\u4FE1\u606F\u91CF",
|
|
21284
|
+
"level.hint": "\u4FE1\u606F\u91CF(\u8F93\u5165 / \u7CBE\u7B80 / \u6807\u51C6 / \u5B8C\u6574)",
|
|
21285
|
+
"level.user.hint": "\u53EA\u7559\u8F6E\u6B21\u3001\u4F60\u7684\u53D1\u8A00\u4E0E\u5206\u652F\u70B9 \u2014\u2014 \u76F8\u5F53\u4E8E\u628A\u6574\u573A\u5BF9\u8BDD\u538B\u6210\u9700\u6C42\u6E05\u5355",
|
|
21197
21286
|
"level.compact.hint": "\u53EA\u7559\u8F6E\u6B21\u3001\u4F60\u7684\u53D1\u8A00\u3001\u5206\u652F\u70B9\u3001\u5DE5\u5177\u6D3B\u52A8\u5408\u8BA1\u4E0E\u6BCF\u8F6E\u7B54\u590D",
|
|
21198
21287
|
"level.standard.hint": "\u5728\u7CBE\u7B80\u57FA\u7840\u4E0A,\u4FDD\u7559\u5B9E\u8D28\u8FC7\u7A0B\u6587\u672C\u4E0E\u6BCF\u6761\u5DE5\u5177\u8C03\u7528(\u4E0D\u542B\u5DE5\u5177\u8F93\u51FA)",
|
|
21199
21288
|
"level.full.hint": "\u5168\u90E8\u8282\u70B9,\u4E0E\u4E8B\u4EF6\u65E5\u5FD7\u4E00\u4E00\u5BF9\u5E94",
|
|
21289
|
+
"zoom.hint": "Ctrl/\u2318 + \u6EDA\u8F6E\u7F29\u653E,\u62D6\u52A8\u5E73\u79FB,\u53CC\u51FB\u56DE\u5230\u6574\u4F53",
|
|
21290
|
+
"zoom.in": "\u653E\u5927",
|
|
21291
|
+
"zoom.out": "\u7F29\u5C0F",
|
|
21292
|
+
"zoom.fit": "\u9002\u914D",
|
|
21293
|
+
"live.running": "\u8FD0\u884C\u4E2D",
|
|
21294
|
+
"live.hint": "\u8BE5\u4F1A\u8BDD\u6B63\u5728\u4EA7\u751F\u65B0\u4E8B\u4EF6,\u6811\u4F1A\u81EA\u52A8\u5237\u65B0\u5E76\u6807\u51FA\u6700\u65B0\u4E00\u8F6E",
|
|
21295
|
+
"total.turns": "\u8F6E",
|
|
21296
|
+
"total.costHint": "\u6309\u6A21\u578B\u5B98\u65B9\u5355\u4EF7\u4F30\u7B97(\u9AD8\u5CF0/\u7A7A\u95F2\u65F6\u6BB5\u81EA\u52A8\u5207\u6362)",
|
|
21297
|
+
"cost.label": "\u6210\u672C",
|
|
21298
|
+
"cost.model": "\u6A21\u578B",
|
|
21299
|
+
"cost.peak": "\u6309\u9AD8\u5CF0\u65F6\u6BB5\u5355\u4EF7",
|
|
21300
|
+
"cost.offPeak": "\u6309\u7A7A\u95F2\u65F6\u6BB5\u5355\u4EF7",
|
|
21301
|
+
"export.svg": "SVG \u77E2\u91CF\u56FE",
|
|
21302
|
+
"export.png": "PNG \u56FE\u7247",
|
|
21200
21303
|
"node.toolgroup": "\u5DE5\u5177\u6D3B\u52A8",
|
|
21201
21304
|
"detail.grouped": "\u6298\u53E0\u7684\u5DE5\u5177\u8C03\u7528",
|
|
21202
21305
|
"expand.short": "\u5C55\u5F00",
|
|
@@ -21313,13 +21416,29 @@ var MESSAGES = {
|
|
|
21313
21416
|
"export.cancelled": "Cancelled",
|
|
21314
21417
|
"export.failed": "Export failed",
|
|
21315
21418
|
"warning": "Compatibility",
|
|
21419
|
+
"level.user": "Input",
|
|
21316
21420
|
"level.compact": "Compact",
|
|
21317
21421
|
"level.standard": "Standard",
|
|
21318
21422
|
"level.full": "Full",
|
|
21319
|
-
"level.hint": "Detail level",
|
|
21423
|
+
"level.hint": "Detail level (Input / Compact / Standard / Full)",
|
|
21424
|
+
"level.user.hint": "Turns, your messages and branch points only \u2014 the session as a requirement list",
|
|
21320
21425
|
"level.compact.hint": "Turns, your messages, branch points, tool totals and each turn answer only",
|
|
21321
21426
|
"level.standard.hint": "Adds substantive process text and every tool call (no tool output)",
|
|
21322
21427
|
"level.full.hint": "Every node, one-to-one with the event log",
|
|
21428
|
+
"zoom.hint": "Ctrl/\u2318 + wheel to zoom, drag to pan, double-click to fit",
|
|
21429
|
+
"zoom.in": "Zoom in",
|
|
21430
|
+
"zoom.out": "Zoom out",
|
|
21431
|
+
"zoom.fit": "Fit",
|
|
21432
|
+
"live.running": "Live",
|
|
21433
|
+
"live.hint": "This session is producing new events; the tree refreshes and marks the latest turn",
|
|
21434
|
+
"total.turns": "turns",
|
|
21435
|
+
"total.costHint": "Estimated from official model prices (peak / off-peak aware)",
|
|
21436
|
+
"cost.label": "Cost",
|
|
21437
|
+
"cost.model": "Model",
|
|
21438
|
+
"cost.peak": "Priced at peak hour",
|
|
21439
|
+
"cost.offPeak": "Priced at off-peak hour",
|
|
21440
|
+
"export.svg": "SVG vector",
|
|
21441
|
+
"export.png": "PNG image",
|
|
21323
21442
|
"node.toolgroup": "Tool activity",
|
|
21324
21443
|
"detail.grouped": "Folded tool calls",
|
|
21325
21444
|
"expand.short": "Expand",
|
|
@@ -21872,7 +21991,7 @@ function flattenFiltered(data, query) {
|
|
|
21872
21991
|
for (const root of roots) walk(root, 0, null);
|
|
21873
21992
|
return { rows, index, matches };
|
|
21874
21993
|
}
|
|
21875
|
-
function OutlineRow({ row, width, selected, highlighted, onSelect, onToggle, onJump, t, palette }) {
|
|
21994
|
+
function OutlineRow({ row, width, selected, highlighted, onSelect, onToggle, onJump, t, palette, costs, live }) {
|
|
21876
21995
|
const { node: node2, depth, kidCount, collapsed } = row;
|
|
21877
21996
|
const x = PAD_X + depth * INDENT;
|
|
21878
21997
|
const railX = x + 7;
|
|
@@ -21885,7 +22004,10 @@ function OutlineRow({ row, width, selected, highlighted, onSelect, onToggle, onJ
|
|
|
21885
22004
|
const shadowed = node2.surface === "shadowed";
|
|
21886
22005
|
const isTurn = node2.kind === "turn";
|
|
21887
22006
|
const usage = node2.usage;
|
|
21888
|
-
const
|
|
22007
|
+
const cost = costs?.get(node2.id);
|
|
22008
|
+
const tokenText = isTurn && usage ? fmtTokens2(usage.total || usage.input + usage.output) : "";
|
|
22009
|
+
const costText = cost && cost.cny > 0 ? PRICING.formatCny(cost.cny) : "";
|
|
22010
|
+
const metaText = isTurn && usage ? `#${node2.seq >= 0 ? node2.seq : "\u2014"} \xB7 ${usage.messageCount ?? 0}\u6761${tokenText ? ` \xB7 ${tokenText} tok` : ""}${costText ? ` \xB7 ${costText}` : ""}` : `#${node2.seq >= 0 ? node2.seq : "\u2014"} ${t(`node.${node2.kind}`)}`;
|
|
21889
22011
|
const metaWidth = measureText(metaText, 10.5) + 9;
|
|
21890
22012
|
const metaX = rowWidth - 8 - metaWidth;
|
|
21891
22013
|
let cursor = metaX;
|
|
@@ -21953,6 +22075,27 @@ ${usage.messageCount ?? 0} \u6761\u6D88\u606F \xB7 ${fmtTokens2(usage.input)} \u
|
|
|
21953
22075
|
strokeWidth: 1.2,
|
|
21954
22076
|
strokeDasharray: "5 3"
|
|
21955
22077
|
}),
|
|
22078
|
+
// 实时高亮:正在跑的那一轮加虚线环 + 脉动圆点。
|
|
22079
|
+
live && h("rect", {
|
|
22080
|
+
x,
|
|
22081
|
+
y: 2,
|
|
22082
|
+
width: rowWidth,
|
|
22083
|
+
height: ROW_H - 6,
|
|
22084
|
+
rx: 6,
|
|
22085
|
+
ry: 6,
|
|
22086
|
+
fill: "none",
|
|
22087
|
+
stroke: "var(--mt-live, #ef4444)",
|
|
22088
|
+
strokeWidth: 1.4,
|
|
22089
|
+
strokeDasharray: "5 4",
|
|
22090
|
+
opacity: 0.9
|
|
22091
|
+
}),
|
|
22092
|
+
live && h("circle", {
|
|
22093
|
+
className: "mt-live-dot",
|
|
22094
|
+
cx: x + 4,
|
|
22095
|
+
cy: ROW_H / 2,
|
|
22096
|
+
r: 3.2,
|
|
22097
|
+
fill: "var(--mt-live, #ef4444)"
|
|
22098
|
+
}),
|
|
21956
22099
|
kidCount > 0 && h(
|
|
21957
22100
|
"g",
|
|
21958
22101
|
{
|
|
@@ -22045,7 +22188,7 @@ ${usage.messageCount ?? 0} \u6761\u6D88\u606F \xB7 ${fmtTokens2(usage.input)} \u
|
|
|
22045
22188
|
h("title", null, title)
|
|
22046
22189
|
);
|
|
22047
22190
|
}
|
|
22048
|
-
function TreeCanvas({ data, collapsed, query, onToggle, onSelect, onJump, selectedId, t, width, palette }) {
|
|
22191
|
+
function TreeCanvas({ data, collapsed, query, onToggle, onSelect, onJump, selectedId, t, width, palette, costs, liveTurn, zoom, onZoom, svgRef }) {
|
|
22049
22192
|
const searching = typeof query === "string" && query.trim() !== "";
|
|
22050
22193
|
let rows;
|
|
22051
22194
|
let index;
|
|
@@ -22104,12 +22247,86 @@ function TreeCanvas({ data, collapsed, query, onToggle, onSelect, onJump, select
|
|
|
22104
22247
|
opacity: 0.7
|
|
22105
22248
|
}));
|
|
22106
22249
|
}
|
|
22250
|
+
const contentH = Math.max(140, height);
|
|
22251
|
+
const zooming = zoom && zoom.mode === "custom";
|
|
22252
|
+
const scale = zooming ? Math.min(4, Math.max(0.4, zoom.scale)) : 1;
|
|
22253
|
+
const viewBox = zooming ? `${zoom.x} ${zoom.y} ${canvasWidth / scale} ${contentH / scale}` : `0 0 ${canvasWidth} ${contentH}`;
|
|
22254
|
+
const dragRef = React.useRef(null);
|
|
22255
|
+
const svgElRef = React.useRef(null);
|
|
22256
|
+
const attachRef = (element) => {
|
|
22257
|
+
svgElRef.current = element;
|
|
22258
|
+
if (typeof svgRef === "function") svgRef(element);
|
|
22259
|
+
else if (svgRef) svgRef.current = element;
|
|
22260
|
+
};
|
|
22261
|
+
const localPoint = (event) => {
|
|
22262
|
+
const element = svgElRef.current;
|
|
22263
|
+
if (!element) return { fx: 0.5, fy: 0.5 };
|
|
22264
|
+
const rect = element.getBoundingClientRect();
|
|
22265
|
+
if (rect.width === 0 || rect.height === 0) return { fx: 0.5, fy: 0.5 };
|
|
22266
|
+
return {
|
|
22267
|
+
fx: (event.clientX - rect.left) / rect.width,
|
|
22268
|
+
fy: (event.clientY - rect.top) / rect.height
|
|
22269
|
+
};
|
|
22270
|
+
};
|
|
22271
|
+
const handleWheel = (event) => {
|
|
22272
|
+
if (!(event.ctrlKey || event.metaKey)) return;
|
|
22273
|
+
event.preventDefault();
|
|
22274
|
+
const { fx, fy } = localPoint(event);
|
|
22275
|
+
const direction = event.deltaY < 0 ? 1 : -1;
|
|
22276
|
+
const nextScale = Math.min(4, Math.max(0.4, scale * (direction > 0 ? 1.15 : 1 / 1.15)));
|
|
22277
|
+
const vbW = canvasWidth / scale;
|
|
22278
|
+
const vbH = contentH / scale;
|
|
22279
|
+
const baseX = zooming ? zoom.x : 0;
|
|
22280
|
+
const baseY = zooming ? zoom.y : 0;
|
|
22281
|
+
const anchorX = baseX + fx * vbW;
|
|
22282
|
+
const anchorY = baseY + fy * vbH;
|
|
22283
|
+
const nextW = canvasWidth / nextScale;
|
|
22284
|
+
const nextH = contentH / nextScale;
|
|
22285
|
+
onZoom?.({ mode: "custom", scale: nextScale, x: anchorX - fx * nextW, y: anchorY - fy * nextH });
|
|
22286
|
+
};
|
|
22287
|
+
const handlePointerDown = (event) => {
|
|
22288
|
+
if (!zooming) return;
|
|
22289
|
+
if (event.button !== 0 && event.button !== 1) return;
|
|
22290
|
+
dragRef.current = { startX: event.clientX, startY: event.clientY, originX: zoom.x, originY: zoom.y };
|
|
22291
|
+
if (typeof event.currentTarget.setPointerCapture === "function") {
|
|
22292
|
+
try {
|
|
22293
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
22294
|
+
} catch {
|
|
22295
|
+
}
|
|
22296
|
+
}
|
|
22297
|
+
};
|
|
22298
|
+
const handlePointerMove = (event) => {
|
|
22299
|
+
const drag = dragRef.current;
|
|
22300
|
+
if (!drag) return;
|
|
22301
|
+
const element = svgElRef.current;
|
|
22302
|
+
const rect = element?.getBoundingClientRect();
|
|
22303
|
+
if (!rect || rect.width === 0) return;
|
|
22304
|
+
const unitsPerPxX = canvasWidth / scale / rect.width;
|
|
22305
|
+
const unitsPerPxY = contentH / scale / rect.height;
|
|
22306
|
+
onZoom?.({
|
|
22307
|
+
mode: "custom",
|
|
22308
|
+
scale,
|
|
22309
|
+
x: drag.originX - (event.clientX - drag.startX) * unitsPerPxX,
|
|
22310
|
+
y: drag.originY - (event.clientY - drag.startY) * unitsPerPxY
|
|
22311
|
+
});
|
|
22312
|
+
};
|
|
22313
|
+
const handlePointerUp = () => {
|
|
22314
|
+
dragRef.current = null;
|
|
22315
|
+
};
|
|
22107
22316
|
return h(
|
|
22108
22317
|
"svg",
|
|
22109
22318
|
{
|
|
22319
|
+
ref: attachRef,
|
|
22110
22320
|
width: canvasWidth,
|
|
22111
|
-
height:
|
|
22112
|
-
|
|
22321
|
+
height: contentH,
|
|
22322
|
+
viewBox,
|
|
22323
|
+
style: { display: "block", cursor: zooming ? "grab" : "default" },
|
|
22324
|
+
onWheel: handleWheel,
|
|
22325
|
+
onPointerDown: handlePointerDown,
|
|
22326
|
+
onPointerMove: handlePointerMove,
|
|
22327
|
+
onPointerUp: handlePointerUp,
|
|
22328
|
+
onPointerCancel: handlePointerUp,
|
|
22329
|
+
onDoubleClick: zooming ? () => onZoom?.({ mode: "fit", scale: 1, x: 0, y: 0 }) : void 0
|
|
22113
22330
|
},
|
|
22114
22331
|
h(
|
|
22115
22332
|
"g",
|
|
@@ -22125,7 +22342,9 @@ function TreeCanvas({ data, collapsed, query, onToggle, onSelect, onJump, select
|
|
|
22125
22342
|
onToggle,
|
|
22126
22343
|
onJump,
|
|
22127
22344
|
t,
|
|
22128
|
-
palette
|
|
22345
|
+
palette,
|
|
22346
|
+
costs,
|
|
22347
|
+
live: liveTurn != null && row.node.kind === "turn" && row.node.turn === liveTurn
|
|
22129
22348
|
})),
|
|
22130
22349
|
searching && rows.length === 0 && h("text", {
|
|
22131
22350
|
x: canvasWidth / 2,
|
|
@@ -22137,7 +22356,7 @@ function TreeCanvas({ data, collapsed, query, onToggle, onSelect, onJump, select
|
|
|
22137
22356
|
)
|
|
22138
22357
|
);
|
|
22139
22358
|
}
|
|
22140
|
-
function DetailPanel({ node: node2, detail, loadingDetail, onClose, onJump, onEvent, onOpenSession, onShowTree, onBack, canJump, showBack, t }) {
|
|
22359
|
+
function DetailPanel({ node: node2, detail, loadingDetail, onClose, onJump, onEvent, onOpenSession, onShowTree, onBack, canJump, showBack, t, cost }) {
|
|
22141
22360
|
const branch = node2?.branch;
|
|
22142
22361
|
return h(
|
|
22143
22362
|
"div",
|
|
@@ -22169,6 +22388,15 @@ function DetailPanel({ node: node2, detail, loadingDetail, onClose, onJump, onEv
|
|
|
22169
22388
|
node2.usage.messageCount != null && h("span", { className: "mt-chip" }, `${t("token.messages")} ${node2.usage.messageCount}`),
|
|
22170
22389
|
(node2.usage.durationMs ?? 0) > 0 && h("span", { className: "mt-chip" }, `${t("token.duration")} ${fmtDuration(node2.usage.durationMs)}`)
|
|
22171
22390
|
),
|
|
22391
|
+
(node2?.usage?.model || cost) && h(
|
|
22392
|
+
"div",
|
|
22393
|
+
{ className: "mt-detail-meta" },
|
|
22394
|
+
node2?.usage?.model && h("span", { className: "mt-chip" }, `${t("cost.model")} ${node2.usage.model}`),
|
|
22395
|
+
cost && h("span", {
|
|
22396
|
+
className: "mt-chip",
|
|
22397
|
+
title: cost.peak ? t("cost.peak") : t("cost.offPeak")
|
|
22398
|
+
}, `${t("cost.label")} ${cost.known === false ? "\u2248" : ""}${PRICING.formatCny(cost.cny)}`)
|
|
22399
|
+
),
|
|
22172
22400
|
branch && h(
|
|
22173
22401
|
"div",
|
|
22174
22402
|
{ className: "mt-detail-branch" },
|
|
@@ -22395,6 +22623,11 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22395
22623
|
if (exportNoticeTimer.current !== null) clearTimeout(exportNoticeTimer.current);
|
|
22396
22624
|
}, []);
|
|
22397
22625
|
const [exportMenuOpen, setExportMenuOpen] = React.useState(false);
|
|
22626
|
+
const [zoom, setZoom] = React.useState({ mode: "fit", scale: 1, x: 0, y: 0 });
|
|
22627
|
+
const svgRef = React.useRef(null);
|
|
22628
|
+
const [lastFrameAt, setLastFrameAt] = React.useState(0);
|
|
22629
|
+
const [liveNow, setLiveNow] = React.useState(false);
|
|
22630
|
+
const pendingRefresh = React.useRef(null);
|
|
22398
22631
|
const exportMenuRef = React.useRef(null);
|
|
22399
22632
|
const [palette, setPalette] = React.useState(() => loadPalette());
|
|
22400
22633
|
const [paletteMenuOpen, setPaletteMenuOpen] = React.useState(false);
|
|
@@ -22440,12 +22673,15 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22440
22673
|
return () => document.removeEventListener("pointerdown", close);
|
|
22441
22674
|
}, [exportMenuOpen]);
|
|
22442
22675
|
const activeSession = targetSession ?? sessionId;
|
|
22443
|
-
const load = React.useCallback((mode, session, abort) => {
|
|
22676
|
+
const load = React.useCallback((mode, session, abort, options) => {
|
|
22444
22677
|
if (!service) return;
|
|
22678
|
+
const silent = options?.silent === true;
|
|
22445
22679
|
DIAG.loadAttempts += 1;
|
|
22446
22680
|
const startedAt = Date.now();
|
|
22447
|
-
|
|
22448
|
-
|
|
22681
|
+
if (!silent) {
|
|
22682
|
+
setLoading(true);
|
|
22683
|
+
setError(null);
|
|
22684
|
+
}
|
|
22449
22685
|
const call = mode === "tree" ? service.tree(session, abort?.signal ?? void 0) : mode === "lineage" ? service.lineage(session, abort?.signal ?? void 0) : service.forest(abort?.signal ?? void 0);
|
|
22450
22686
|
withTimeout(Promise.resolve(call), FETCH_TIMEOUT_MS, `mindTree.${mode}`).then(unwrapResult).then((result) => {
|
|
22451
22687
|
if (abort?.signal?.aborted) return;
|
|
@@ -22473,10 +22709,29 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22473
22709
|
load(view, activeSession, controller);
|
|
22474
22710
|
return () => controller.abort();
|
|
22475
22711
|
}, [service, view, activeSession, tick]);
|
|
22712
|
+
const refreshRef = React.useRef(null);
|
|
22713
|
+
refreshRef.current = () => {
|
|
22714
|
+
const controller = new AbortController();
|
|
22715
|
+
load(view, activeSession, controller, { silent: true });
|
|
22716
|
+
};
|
|
22476
22717
|
React.useEffect(() => {
|
|
22477
22718
|
if (!service) return;
|
|
22478
22719
|
const controller = new AbortController();
|
|
22479
22720
|
let stopped = false;
|
|
22721
|
+
let timer = null;
|
|
22722
|
+
let dirty = false;
|
|
22723
|
+
const flush = () => {
|
|
22724
|
+
timer = null;
|
|
22725
|
+
if (stopped) return;
|
|
22726
|
+
if (!dirty) return;
|
|
22727
|
+
dirty = false;
|
|
22728
|
+
refreshRef.current?.();
|
|
22729
|
+
};
|
|
22730
|
+
const schedule = () => {
|
|
22731
|
+
setLastFrameAt(Date.now());
|
|
22732
|
+
dirty = true;
|
|
22733
|
+
if (timer === null) timer = setTimeout(flush, 900);
|
|
22734
|
+
};
|
|
22480
22735
|
const run = async () => {
|
|
22481
22736
|
let attempts = 0;
|
|
22482
22737
|
while (!stopped && attempts < 12) {
|
|
@@ -22485,7 +22740,7 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22485
22740
|
for await (const rawFrame of service.follow(controller.signal)) {
|
|
22486
22741
|
if (stopped) return;
|
|
22487
22742
|
const frame = rawFrame && typeof rawFrame === "object" && "ok" in rawFrame && rawFrame.ok === true ? rawFrame.value : rawFrame;
|
|
22488
|
-
if (frame?.sessionId === activeSession)
|
|
22743
|
+
if (frame?.sessionId === activeSession) schedule();
|
|
22489
22744
|
}
|
|
22490
22745
|
return;
|
|
22491
22746
|
} catch {
|
|
@@ -22497,9 +22752,16 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22497
22752
|
run();
|
|
22498
22753
|
return () => {
|
|
22499
22754
|
stopped = true;
|
|
22755
|
+
if (timer !== null) clearTimeout(timer);
|
|
22500
22756
|
controller.abort();
|
|
22501
22757
|
};
|
|
22502
22758
|
}, [service, activeSession]);
|
|
22759
|
+
React.useEffect(() => {
|
|
22760
|
+
if (!lastFrameAt) return void 0;
|
|
22761
|
+
setLiveNow(true);
|
|
22762
|
+
const timer = setTimeout(() => setLiveNow(false), 2500);
|
|
22763
|
+
return () => clearTimeout(timer);
|
|
22764
|
+
}, [lastFrameAt]);
|
|
22503
22765
|
const rawData = view === "tree" ? treeData : view === "lineage" ? lineageData : forestData;
|
|
22504
22766
|
const displayed = React.useMemo(() => view === "tree" ? rawData : sessionsAsDisplay(rawData), [view, rawData]);
|
|
22505
22767
|
const data = React.useMemo(
|
|
@@ -22511,13 +22773,55 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22511
22773
|
const nodeMap = React.useMemo(() => new Map(nodes.map((n) => [n.id, n])), [data]);
|
|
22512
22774
|
const selectedNode = selected ? nodeMap.get(selected) : null;
|
|
22513
22775
|
const canJump = activeSession === sessionId;
|
|
22776
|
+
const costs = React.useMemo(() => {
|
|
22777
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
22778
|
+
for (const node2 of nodes) {
|
|
22779
|
+
if (node2.kind !== "turn" || !node2.usage) continue;
|
|
22780
|
+
map2.set(node2.id, PRICING.costOf(node2.usage, node2.usage.model, node2.usage.startedAt));
|
|
22781
|
+
}
|
|
22782
|
+
return map2;
|
|
22783
|
+
}, [data]);
|
|
22784
|
+
const totals = React.useMemo(() => {
|
|
22785
|
+
if (view !== "tree") return null;
|
|
22786
|
+
let turns = 0;
|
|
22787
|
+
let tokens = 0;
|
|
22788
|
+
let cny = 0;
|
|
22789
|
+
let unknown2 = false;
|
|
22790
|
+
for (const node2 of nodes) {
|
|
22791
|
+
if (node2.kind !== "turn" || !node2.usage) continue;
|
|
22792
|
+
turns += 1;
|
|
22793
|
+
tokens += Number(node2.usage.total || node2.usage.input + node2.usage.output) || 0;
|
|
22794
|
+
const cost = costs.get(node2.id);
|
|
22795
|
+
if (cost) {
|
|
22796
|
+
cny += cost.cny;
|
|
22797
|
+
if (cost.known === false) unknown2 = true;
|
|
22798
|
+
}
|
|
22799
|
+
}
|
|
22800
|
+
return turns > 0 ? { turns, tokens, cny, unknown: unknown2 } : null;
|
|
22801
|
+
}, [view, data, costs]);
|
|
22802
|
+
const liveTurn = React.useMemo(() => {
|
|
22803
|
+
if (!liveNow || view !== "tree") return null;
|
|
22804
|
+
let max = null;
|
|
22805
|
+
for (const node2 of nodes) {
|
|
22806
|
+
if (node2.kind !== "turn" || typeof node2.turn !== "number") continue;
|
|
22807
|
+
if (max === null || node2.turn > max) max = node2.turn;
|
|
22808
|
+
}
|
|
22809
|
+
return max;
|
|
22810
|
+
}, [liveNow, view, data]);
|
|
22514
22811
|
const switchView = (next) => {
|
|
22515
22812
|
setView(next);
|
|
22516
22813
|
setSelected(null);
|
|
22517
22814
|
setDetail(null);
|
|
22518
22815
|
setQuery("");
|
|
22816
|
+
setZoom({ mode: "fit", scale: 1, x: 0, y: 0 });
|
|
22519
22817
|
if (next === "tree") setTargetSession(null);
|
|
22520
22818
|
};
|
|
22819
|
+
const zoomBy = (factor) => setZoom((current) => {
|
|
22820
|
+
const base = current.mode === "custom" ? current : { mode: "custom", scale: 1, x: 0, y: 0 };
|
|
22821
|
+
const nextScale = Math.min(4, Math.max(0.4, base.scale * factor));
|
|
22822
|
+
return { ...base, scale: nextScale };
|
|
22823
|
+
});
|
|
22824
|
+
const fitZoom = () => setZoom({ mode: "fit", scale: 1, x: 0, y: 0 });
|
|
22521
22825
|
const fetchDetail = (node2) => {
|
|
22522
22826
|
if (Array.isArray(node2.grouped) && node2.grouped.length > 0) {
|
|
22523
22827
|
setLoadingDetail(false);
|
|
@@ -22676,7 +22980,8 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22676
22980
|
title: `${t(`level.${level}`)} \u2014 ${t(`level.${level}.hint`)}`,
|
|
22677
22981
|
onClick: () => setDetailLevel(level)
|
|
22678
22982
|
}, t(`level.${level}`)));
|
|
22679
|
-
const saveExport = async (content, fileName, mime, description, extensions) => {
|
|
22983
|
+
const saveExport = async (content, fileName, mime, description, extensions, options) => {
|
|
22984
|
+
const binary = options?.binary === true;
|
|
22680
22985
|
if (typeof window !== "undefined" && typeof window.showSaveFilePicker === "function") {
|
|
22681
22986
|
try {
|
|
22682
22987
|
const handle = await window.showSaveFilePicker({
|
|
@@ -22698,7 +23003,7 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22698
23003
|
DIAG.lastExport = `picker-failed:${error63?.message ?? error63}`;
|
|
22699
23004
|
}
|
|
22700
23005
|
}
|
|
22701
|
-
const blob = new Blob([content], { type: `${mime};charset=utf-8` });
|
|
23006
|
+
const blob = content instanceof Blob ? content : new Blob([content], { type: binary ? mime : `${mime};charset=utf-8` });
|
|
22702
23007
|
const url2 = URL.createObjectURL(blob);
|
|
22703
23008
|
const anchor2 = document.createElement("a");
|
|
22704
23009
|
anchor2.href = url2;
|
|
@@ -22710,6 +23015,68 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22710
23015
|
DIAG.lastExport = "downloaded";
|
|
22711
23016
|
setExportNotice(`${t("export.saved")}: ${fileName}`);
|
|
22712
23017
|
};
|
|
23018
|
+
const imageBaseName = () => `mind-tree-${String(activeSession ?? "session").slice(0, 8)}`;
|
|
23019
|
+
const buildStandaloneSvg = () => {
|
|
23020
|
+
const element = svgRef.current;
|
|
23021
|
+
if (!element) throw new Error("\u753B\u5E03\u5C1A\u672A\u5C31\u7EEA");
|
|
23022
|
+
const clone2 = element.cloneNode(true);
|
|
23023
|
+
clone2.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
23024
|
+
const vars = COLOR_KINDS.map((key) => `${COLOR_VARS[key]}:${palette.colors[key]}`).join(";");
|
|
23025
|
+
const fontFamily = '-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif';
|
|
23026
|
+
clone2.setAttribute("style", `${vars};font-family:${fontFamily};background:#ffffff`);
|
|
23027
|
+
const width = Number(element.getAttribute("width")) || 800;
|
|
23028
|
+
const height = Number(element.getAttribute("height")) || 600;
|
|
23029
|
+
clone2.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
23030
|
+
clone2.setAttribute("width", String(width));
|
|
23031
|
+
clone2.setAttribute("height", String(height));
|
|
23032
|
+
const background = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
23033
|
+
background.setAttribute("x", "0");
|
|
23034
|
+
background.setAttribute("y", "0");
|
|
23035
|
+
background.setAttribute("width", "100%");
|
|
23036
|
+
background.setAttribute("height", "100%");
|
|
23037
|
+
background.setAttribute("fill", "#ffffff");
|
|
23038
|
+
clone2.insertBefore(background, clone2.firstChild);
|
|
23039
|
+
return { text: new XMLSerializer().serializeToString(clone2), width, height };
|
|
23040
|
+
};
|
|
23041
|
+
const exportImage = async (kind) => {
|
|
23042
|
+
try {
|
|
23043
|
+
const { text, width, height } = buildStandaloneSvg();
|
|
23044
|
+
if (kind === "svg") {
|
|
23045
|
+
await saveExport(text, `${imageBaseName()}.svg`, "image/svg+xml", "SVG \u77E2\u91CF\u56FE", [".svg"]);
|
|
23046
|
+
return;
|
|
23047
|
+
}
|
|
23048
|
+
const factor = Math.max(0.1, Math.min(2, 32e3 / Math.max(1, height)));
|
|
23049
|
+
const svgBlob = new Blob([text], { type: "image/svg+xml;charset=utf-8" });
|
|
23050
|
+
const url2 = URL.createObjectURL(svgBlob);
|
|
23051
|
+
let image;
|
|
23052
|
+
try {
|
|
23053
|
+
image = await new Promise((resolve, reject) => {
|
|
23054
|
+
const element = new Image();
|
|
23055
|
+
element.onload = () => resolve(element);
|
|
23056
|
+
element.onerror = () => reject(new Error("SVG \u89E3\u7801\u5931\u8D25"));
|
|
23057
|
+
element.src = url2;
|
|
23058
|
+
});
|
|
23059
|
+
} finally {
|
|
23060
|
+
setTimeout(() => URL.revokeObjectURL(url2), 5e3);
|
|
23061
|
+
}
|
|
23062
|
+
const canvas = document.createElement("canvas");
|
|
23063
|
+
canvas.width = Math.max(1, Math.round(width * factor));
|
|
23064
|
+
canvas.height = Math.max(1, Math.round(height * factor));
|
|
23065
|
+
const context = typeof canvas.getContext === "function" ? canvas.getContext("2d") : null;
|
|
23066
|
+
if (!context) throw new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 canvas");
|
|
23067
|
+
context.fillStyle = "#ffffff";
|
|
23068
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
23069
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
23070
|
+
const blob = await new Promise((resolve) => {
|
|
23071
|
+
canvas.toBlob((result) => resolve(result), "image/png");
|
|
23072
|
+
});
|
|
23073
|
+
if (!blob) throw new Error("PNG \u751F\u6210\u5931\u8D25");
|
|
23074
|
+
await saveExport(blob, `${imageBaseName()}.png`, "image/png", "PNG \u56FE\u7247", [".png"], { binary: true });
|
|
23075
|
+
} catch (error63) {
|
|
23076
|
+
DIAG.lastExport = `image-failed:${error63?.message ?? error63}`;
|
|
23077
|
+
setExportNotice(`${t("export.failed")}: ${error63?.message ?? error63}`);
|
|
23078
|
+
}
|
|
23079
|
+
};
|
|
22713
23080
|
const exportAs = async (format) => {
|
|
22714
23081
|
try {
|
|
22715
23082
|
if (displayed == null || (displayed.nodes ?? []).length === 0) return;
|
|
@@ -22739,7 +23106,19 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22739
23106
|
className: `mt-status-chip ${failed ? "mt-status-chip-bad" : service ? "mt-status-chip-ok" : ""}`,
|
|
22740
23107
|
title: failed ? String(DIAG.lastError ?? "") : ""
|
|
22741
23108
|
}, failed ? t("status.failed") : service ? t("status.ready") : t("status.connecting")),
|
|
23109
|
+
liveNow && h("span", { className: "mt-live-chip", title: t("live.hint") }, `\u25CF ${t("live.running")}`),
|
|
22742
23110
|
h("span", { className: "mt-level-group", title: t("level.hint") }, levelButtons),
|
|
23111
|
+
h(
|
|
23112
|
+
"span",
|
|
23113
|
+
{ className: "mt-zoom-group", title: t("zoom.hint") },
|
|
23114
|
+
h("button", { className: "mt-btn mt-zoom-btn", onClick: () => zoomBy(1 / 1.25), title: t("zoom.out") }, "\u2212"),
|
|
23115
|
+
h("button", {
|
|
23116
|
+
className: `mt-btn mt-zoom-btn ${zoom.mode === "custom" ? "mt-btn-on" : ""}`,
|
|
23117
|
+
onClick: fitZoom,
|
|
23118
|
+
title: t("zoom.fit")
|
|
23119
|
+
}, zoom.mode === "custom" ? `${Math.round(zoom.scale * 100)}%` : t("zoom.fit")),
|
|
23120
|
+
h("button", { className: "mt-btn mt-zoom-btn", onClick: () => zoomBy(1.25), title: t("zoom.in") }, "+")
|
|
23121
|
+
),
|
|
22743
23122
|
h("button", { className: "mt-btn", onClick: expandAll, title: t("expandAll") }, t("expand.short")),
|
|
22744
23123
|
h("button", { className: "mt-btn", onClick: collapseAll, title: t("collapseAll") }, t("collapse.short")),
|
|
22745
23124
|
h(
|
|
@@ -22828,6 +23207,21 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22828
23207
|
exportAs("canvas");
|
|
22829
23208
|
}
|
|
22830
23209
|
}, t("export.canvas")),
|
|
23210
|
+
h("div", { className: "mt-menu-divider" }),
|
|
23211
|
+
h("button", {
|
|
23212
|
+
className: "mt-menu-item",
|
|
23213
|
+
onClick: () => {
|
|
23214
|
+
setExportMenuOpen(false);
|
|
23215
|
+
exportImage("svg");
|
|
23216
|
+
}
|
|
23217
|
+
}, t("export.svg")),
|
|
23218
|
+
h("button", {
|
|
23219
|
+
className: "mt-menu-item",
|
|
23220
|
+
onClick: () => {
|
|
23221
|
+
setExportMenuOpen(false);
|
|
23222
|
+
exportImage("png");
|
|
23223
|
+
}
|
|
23224
|
+
}, t("export.png")),
|
|
22831
23225
|
h("div", { className: "mt-menu-hint" }, t("export.hint"))
|
|
22832
23226
|
)
|
|
22833
23227
|
),
|
|
@@ -22851,6 +23245,17 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22851
23245
|
}),
|
|
22852
23246
|
query && h("button", { className: "mt-btn", onClick: () => setQuery("") }, "\xD7")
|
|
22853
23247
|
),
|
|
23248
|
+
// 合计条:轮次 / token / 估算成本(单价随高峰-空闲时段自动切换)。
|
|
23249
|
+
totals && h(
|
|
23250
|
+
"div",
|
|
23251
|
+
{ className: "mt-total" },
|
|
23252
|
+
h("span", { className: "mt-total-item" }, `${totals.turns} ${t("total.turns")}`),
|
|
23253
|
+
h("span", { className: "mt-total-item" }, `${fmtTokens2(totals.tokens)} tok`),
|
|
23254
|
+
h("span", {
|
|
23255
|
+
className: "mt-total-item mt-total-cost",
|
|
23256
|
+
title: `${t("total.costHint")} (${PRICING.PRICING_VERSION})`
|
|
23257
|
+
}, `${totals.unknown ? "\u2248" : ""}${PRICING.formatCny(totals.cny)}`)
|
|
23258
|
+
),
|
|
22854
23259
|
activeSession && activeSession !== sessionId && h(
|
|
22855
23260
|
"div",
|
|
22856
23261
|
{ className: "mt-breadcrumb" },
|
|
@@ -22919,7 +23324,12 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22919
23324
|
},
|
|
22920
23325
|
selectedId: selected,
|
|
22921
23326
|
t,
|
|
22922
|
-
width: canvasWidth
|
|
23327
|
+
width: canvasWidth,
|
|
23328
|
+
costs,
|
|
23329
|
+
liveTurn,
|
|
23330
|
+
zoom,
|
|
23331
|
+
onZoom: setZoom,
|
|
23332
|
+
svgRef
|
|
22923
23333
|
}),
|
|
22924
23334
|
data != null && nodes.length > 0 && h("div", { className: "mt-hint" }, t("click.hint"))
|
|
22925
23335
|
),
|
|
@@ -22927,6 +23337,7 @@ function MindTreeBodyInner({ ctx, sessionId, signal, uiWorkspace }) {
|
|
|
22927
23337
|
node: selectedNode,
|
|
22928
23338
|
detail,
|
|
22929
23339
|
loadingDetail,
|
|
23340
|
+
cost: costs.get(selectedNode.id),
|
|
22930
23341
|
onClose: () => {
|
|
22931
23342
|
setSelected(null);
|
|
22932
23343
|
setDetail(null);
|
|
@@ -22992,6 +23403,16 @@ module.exports = {
|
|
|
22992
23403
|
.mt-warn{margin:4px 8px 0;padding:5px 8px;border-radius:6px;font-size:10.5px;line-height:1.5;color:var(--dsw-alias-state-warning-primary,#b45309);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary,#b45309) 12%, transparent);border:1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary,#b45309) 35%, transparent)}
|
|
22993
23404
|
.mt-warn-title{font-weight:600}
|
|
22994
23405
|
.mt-level-group{display:inline-flex;flex:0 0 auto;gap:2px;padding:1px;border-radius:7px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04))}
|
|
23406
|
+
.mt-zoom-group{display:inline-flex;flex:0 0 auto;gap:2px;padding:1px;border-radius:7px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04))}
|
|
23407
|
+
.mt-zoom-btn{padding:3px 6px;font-size:10.5px;min-width:24px;justify-content:center}
|
|
23408
|
+
.mt-zoom-btn.mt-btn-on{background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 2px rgba(0,0,0,.12)}
|
|
23409
|
+
.mt-live-chip{display:inline-flex;align-items:center;gap:3px;flex:0 0 auto;padding:2px 7px;border-radius:99px;font-size:10px;font-weight:600;color:#fff;background:var(--mt-live,#ef4444);animation:mt-live-breathe 1.6s ease-in-out infinite}
|
|
23410
|
+
@keyframes mt-live-breathe{0%,100%{opacity:.65}50%{opacity:1}}
|
|
23411
|
+
.mt-live-dot{animation:mt-live-breathe 1.3s ease-in-out infinite}
|
|
23412
|
+
.mt-total{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:3px 8px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.08));font-size:10.5px;color:var(--dsw-alias-label-secondary,#61666b);font-variant-numeric:tabular-nums}
|
|
23413
|
+
.mt-total-item{display:inline-flex;align-items:center;padding:1px 6px;border-radius:99px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04))}
|
|
23414
|
+
.mt-total-cost{font-weight:600;color:var(--dsw-alias-label-primary,#111827)}
|
|
23415
|
+
.mt-menu-divider{height:1px;margin:3px 6px;background:var(--dsw-alias-border-l1,rgba(0,0,0,.08))}
|
|
22995
23416
|
.mt-btn-level{padding:3px 6px;font-size:10.5px;opacity:.75}
|
|
22996
23417
|
.mt-btn-level.mt-btn-on{opacity:1;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 1px 2px rgba(0,0,0,.12)}
|
|
22997
23418
|
.mt-menu-wide{min-width:250px;max-height:70vh;overflow:auto}
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// 另注册 mind_tree 工具,让终端/GUI 里的 agent 也能直接导出思维树(不依赖网页客户端)。
|
|
5
5
|
import { buildExport } from './format.js';
|
|
6
6
|
import { reduceGraph, DETAIL_LEVELS } from './reduce.js';
|
|
7
|
+
import { costOfTree, formatCny } from './pricing.js';
|
|
7
8
|
|
|
8
9
|
export const name = 'mindTree';
|
|
9
10
|
export const inject = ['sessionQuery'];
|
|
@@ -361,6 +362,7 @@ export function apply(ctx) {
|
|
|
361
362
|
const turnAssistantText = new Map(); // turn -> 该轮首条中文助手文本(回退)
|
|
362
363
|
const turnUsage = new Map(); // turn -> 该轮 token 消耗聚合
|
|
363
364
|
const turnTimes = new Map(); // turn -> {start, end}
|
|
365
|
+
const turnModel = new Map(); // turn -> 该轮实际使用的模型(用于成本估算)
|
|
364
366
|
let nodeSeqCounter = 0;
|
|
365
367
|
|
|
366
368
|
const pushNode = (node) => {
|
|
@@ -436,7 +438,12 @@ export function apply(ctx) {
|
|
|
436
438
|
turnAssistantText.set(currentTurn, source);
|
|
437
439
|
}
|
|
438
440
|
}
|
|
439
|
-
if (role === 'assistant')
|
|
441
|
+
if (role === 'assistant') {
|
|
442
|
+
addTurnUsage(currentTurn, data.usage);
|
|
443
|
+
// 记录该轮使用的模型(成本估算按模型单价折算)。
|
|
444
|
+
const modelId = message?.source?.model ?? data?.source?.model;
|
|
445
|
+
if (typeof modelId === 'string' && modelId && currentTurn !== null) turnModel.set(currentTurn, modelId);
|
|
446
|
+
}
|
|
440
447
|
const baseLabel = squash(text, 160) || (role === 'user' ? '用户消息' : role === 'assistant' ? '助手回复' : role);
|
|
441
448
|
const node = {
|
|
442
449
|
id: `n${seq}`, seq, type: event.type, kind,
|
|
@@ -680,6 +687,8 @@ export function apply(ctx) {
|
|
|
680
687
|
cacheWrite: usage.cacheWrite,
|
|
681
688
|
reasoning: usage.reasoning,
|
|
682
689
|
messageCount: usage.messageCount,
|
|
690
|
+
...(turnModel.has(node.turn) ? { model: turnModel.get(node.turn) } : {}),
|
|
691
|
+
...(times?.start !== undefined ? { startedAt: times.start } : {}),
|
|
683
692
|
...(times?.start !== undefined && times?.end !== undefined && times.end > times.start
|
|
684
693
|
? { durationMs: times.end - times.start }
|
|
685
694
|
: {}),
|
|
@@ -901,8 +910,8 @@ export function apply(ctx) {
|
|
|
901
910
|
},
|
|
902
911
|
level: {
|
|
903
912
|
type: 'string',
|
|
904
|
-
enum: ['compact', 'standard', 'full'],
|
|
905
|
-
description: '过程信息量:compact(
|
|
913
|
+
enum: ['user', 'compact', 'standard', 'full'],
|
|
914
|
+
description: '过程信息量:user(只留轮次/你的发言/分支点,可当需求清单)、compact(默认,另加工具活动合计与每轮答复)、standard(加过程文本与工具明细)、full(全量)。',
|
|
906
915
|
},
|
|
907
916
|
},
|
|
908
917
|
},
|
|
@@ -958,6 +967,11 @@ export function apply(ctx) {
|
|
|
958
967
|
const keptCount = (reduced?.nodes ?? []).length;
|
|
959
968
|
const dropped = nodeCount - keptCount;
|
|
960
969
|
const levelNote = dropped > 0 ? `,已按 ${level} 档削减 ${dropped} 个过程节点` : '';
|
|
970
|
+
// 成本估算:只在"会话思维树"上有意义(轮次带 usage/model)。
|
|
971
|
+
const cost = view === 'tree' ? costOfTree(displayed) : null;
|
|
972
|
+
const costNote = cost && cost.cny > 0
|
|
973
|
+
? `,估算成本 ${cost.anyUnknown ? '≈' : ''}${formatCny(cost.cny)}`
|
|
974
|
+
: '';
|
|
961
975
|
const writeTo = typeof args?.writeTo === 'string' ? args.writeTo.trim() : '';
|
|
962
976
|
if (writeTo !== '') {
|
|
963
977
|
const fs = ctx.get('fs');
|
|
@@ -968,7 +982,7 @@ export function apply(ctx) {
|
|
|
968
982
|
await fs.writeText(target, exported.content);
|
|
969
983
|
const displayPath = typeof fs.processPath === 'function' ? fs.processPath(target) : writeTo;
|
|
970
984
|
return {
|
|
971
|
-
summary: `已导出 ${exported.format} 到 ${displayPath}(${exported.content.length} 字节,${turns} 轮 / ${keptCount} 节点${levelNote})`,
|
|
985
|
+
summary: `已导出 ${exported.format} 到 ${displayPath}(${exported.content.length} 字节,${turns} 轮 / ${keptCount} 节点${levelNote}${costNote})`,
|
|
972
986
|
format: exported.format,
|
|
973
987
|
sessionId: String(sessionId ?? 'forest'),
|
|
974
988
|
fileName: exported.fileName,
|
|
@@ -983,7 +997,7 @@ export function apply(ctx) {
|
|
|
983
997
|
? `${exported.content.slice(0, TOOL_CONTENT_LIMIT)}\n\n…(内容超过 ${TOOL_CONTENT_LIMIT} 字符已截断;完整内容请用 writeTo 写入文件)`
|
|
984
998
|
: exported.content;
|
|
985
999
|
return {
|
|
986
|
-
summary: `已生成 ${exported.format}(${exported.content.length} 字节,${turns} 轮 / ${keptCount} 节点${levelNote}${truncated ? ',已截断' : ''})`,
|
|
1000
|
+
summary: `已生成 ${exported.format}(${exported.content.length} 字节,${turns} 轮 / ${keptCount} 节点${levelNote}${costNote}${truncated ? ',已截断' : ''})`,
|
|
987
1001
|
format: exported.format,
|
|
988
1002
|
sessionId: String(sessionId ?? 'forest'),
|
|
989
1003
|
fileName: exported.fileName,
|
package/lib/pricing.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// 成本估算层:把 token 消耗折算成人民币。
|
|
2
|
+
// 单价来源:DeepSeek 官方「模型 & 价格」页(抓取于 2026-11-15),单位:元 / 百万 tokens。
|
|
3
|
+
// 高峰时段:北京时间 周一至周五(不含法定节假日)9:00–12:00、14:00–18:00;其余为**空闲时段**,价格减半。
|
|
4
|
+
// 说明:法定节假日无法从本地推断,此处只按"工作日 + 时段"近似。
|
|
5
|
+
//
|
|
6
|
+
// 纯函数、无依赖,客户端(界面显示)与宿主(mind_tree 工具)共用。
|
|
7
|
+
|
|
8
|
+
/** 单价表版本标记(改了数字就更新它,便于界面/文档追溯)。 */
|
|
9
|
+
export const PRICING_VERSION = '2026-11-15';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 每百万 token 单价(元)。每个字段是 [空闲时段, 高峰时段]。
|
|
13
|
+
* hit = 缓存命中输入;miss = 缓存未命中输入;out = 输出。
|
|
14
|
+
*/
|
|
15
|
+
export const MODEL_PRICES = {
|
|
16
|
+
'deepseek-flash': { hit: [0.02, 0.04], miss: [1, 2], out: [4, 8] },
|
|
17
|
+
'deepseek-v4-flash': { hit: [0.02, 0.04], miss: [1, 2], out: [4, 8] },
|
|
18
|
+
'deepseek-v4-flash-vision-exp': { hit: [0.02, 0.04], miss: [1, 2], out: [4, 8] },
|
|
19
|
+
'deepseek-v4-pro': { hit: [0.15, 0.3], miss: [4.5, 9], out: [13.5, 27] },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** 未知模型时的兜底单价(按 flash 计,界面用 ≈ 标注)。 */
|
|
23
|
+
export const FALLBACK_PRICE = MODEL_PRICES['deepseek-flash'];
|
|
24
|
+
|
|
25
|
+
const BJ_OFFSET_MS = 8 * 3600 * 1000;
|
|
26
|
+
|
|
27
|
+
/** 是否处于高峰时段(按北京时间近似判断,忽略法定节假日)。 */
|
|
28
|
+
export function isPeakHour(atMs) {
|
|
29
|
+
const at = typeof atMs === 'number' && Number.isFinite(atMs) ? atMs : Date.now();
|
|
30
|
+
const shifted = new Date(at + BJ_OFFSET_MS);
|
|
31
|
+
const day = shifted.getUTCDay(); // 0=周日 … 6=周六
|
|
32
|
+
const minutes = shifted.getUTCHours() * 60 + shifted.getUTCMinutes();
|
|
33
|
+
const weekday = day >= 1 && day <= 5;
|
|
34
|
+
const inMorning = minutes >= 9 * 60 && minutes < 12 * 60;
|
|
35
|
+
const inAfternoon = minutes >= 14 * 60 && minutes < 18 * 60;
|
|
36
|
+
return weekday && (inMorning || inAfternoon);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 取某个模型在某时段的单价三元组。 */
|
|
40
|
+
export function priceOf(model, atMs) {
|
|
41
|
+
const table = MODEL_PRICES[model] ?? FALLBACK_PRICE;
|
|
42
|
+
const slot = isPeakHour(atMs) ? 1 : 0;
|
|
43
|
+
return {
|
|
44
|
+
model: MODEL_PRICES[model] ? model : null,
|
|
45
|
+
peak: slot === 1,
|
|
46
|
+
hit: table.hit[slot],
|
|
47
|
+
miss: table.miss[slot],
|
|
48
|
+
out: table.out[slot],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 一次 token 消耗折算成元。
|
|
54
|
+
* @param usage - { input, output, cacheRead, cacheWrite }(单位 token;input 为**未命中缓存**部分)
|
|
55
|
+
* @param model - 模型 id,如 deepseek-v4-pro;未知模型按兜底价并标记 known=false
|
|
56
|
+
* @param atMs - 该次调用时间(毫秒),用于判断高峰/空闲;缺省按当前时间
|
|
57
|
+
* @returns { cny, peak, model, known }
|
|
58
|
+
*/
|
|
59
|
+
export function costOf(usage, model, atMs) {
|
|
60
|
+
const input = Math.max(0, Number(usage?.input) || 0);
|
|
61
|
+
const output = Math.max(0, Number(usage?.output) || 0);
|
|
62
|
+
const cacheRead = Math.max(0, Number(usage?.cacheRead) || 0);
|
|
63
|
+
const cacheWrite = Math.max(0, Number(usage?.cacheWrite) || 0);
|
|
64
|
+
const price = priceOf(model, atMs);
|
|
65
|
+
// 缓存写入按"未命中输入"计费;缓存读取按"命中输入"计费。
|
|
66
|
+
const cny = ((input + cacheWrite) * price.miss + cacheRead * price.hit + output * price.out) / 1_000_000;
|
|
67
|
+
return { cny, peak: price.peak, model: price.model, known: price.model !== null };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 汇总多份成本。 */
|
|
71
|
+
export function sumCost(entries) {
|
|
72
|
+
let cny = 0;
|
|
73
|
+
let anyUnknown = false;
|
|
74
|
+
for (const entry of entries ?? []) {
|
|
75
|
+
if (!entry) continue;
|
|
76
|
+
cny += Number(entry.cny) || 0;
|
|
77
|
+
if (entry.known === false) anyUnknown = true;
|
|
78
|
+
}
|
|
79
|
+
return { cny, anyUnknown };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 人民币显示:小于 1 分显示 <¥0.01,其余两位小数。 */
|
|
83
|
+
export function formatCny(value) {
|
|
84
|
+
const cny = Number(value) || 0;
|
|
85
|
+
if (cny <= 0) return '¥0';
|
|
86
|
+
if (cny < 0.005) return '<¥0.01';
|
|
87
|
+
if (cny < 1) return `¥${cny.toFixed(3)}`;
|
|
88
|
+
return `¥${cny.toFixed(2)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 从整棵树聚合成本(按轮 usage + 模型)。 */
|
|
92
|
+
export function costOfTree(tree) {
|
|
93
|
+
const entries = [];
|
|
94
|
+
for (const node of tree?.nodes ?? []) {
|
|
95
|
+
if (node?.kind !== 'turn' || !node.usage) continue;
|
|
96
|
+
entries.push(costOf(node.usage, node.usage.model, node.usage.startedAt));
|
|
97
|
+
}
|
|
98
|
+
return sumCost(entries);
|
|
99
|
+
}
|
package/lib/reduce.js
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
import { buildHierarchy, isInjectedLabel } from './format.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* 四档信息量(由轻到重):
|
|
8
|
+
* - user:只留 轮次 + 你的真实发言 + 分支节点 —— 相当于"把整场对话压成需求清单",可直接喂给别的模型;
|
|
8
9
|
* - compact (默认):只留 轮次 + 用户真实发言 + 分支节点 + 每轮工具活动聚合 1 行 + 每轮最终助手回复;
|
|
9
10
|
* - standard:在 compact 基础上保留实质性助手过程文本(≥ MIN_ASSISTANT_STANDARD 字)与工具明细行,去掉工具结果;
|
|
10
11
|
* - full:不做任何削减(与事件日志一一对应)。
|
|
11
12
|
*/
|
|
12
|
-
export const DETAIL_LEVELS = ['compact', 'standard', 'full'];
|
|
13
|
+
export const DETAIL_LEVELS = ['user', 'compact', 'standard', 'full'];
|
|
13
14
|
|
|
14
15
|
/** standard 档保留的助手文本最小字数。 */
|
|
15
16
|
export const MIN_ASSISTANT_STANDARD = 60;
|
|
@@ -50,6 +51,7 @@ export function reduceGraph(tree, level = 'compact') {
|
|
|
50
51
|
if (level === 'full') return tree;
|
|
51
52
|
const nodes = tree.nodes ?? [];
|
|
52
53
|
const keepIds = new Set();
|
|
54
|
+
const isUserOnly = level === 'user';
|
|
53
55
|
const isCompact = level === 'compact';
|
|
54
56
|
const minAssistant = isCompact ? Number.POSITIVE_INFINITY : MIN_ASSISTANT_STANDARD;
|
|
55
57
|
const lastOfTurn = lastAssistantIds(nodes);
|
|
@@ -66,13 +68,15 @@ export function reduceGraph(tree, level = 'compact') {
|
|
|
66
68
|
continue; // 工具输出:折叠掉(可在工具行详情中查看)
|
|
67
69
|
case 'info':
|
|
68
70
|
continue; // system / developer 注入内容
|
|
69
|
-
case 'user':
|
|
70
|
-
if (isInjectedLabel(node.label)) continue; // 模型切换、运行时上下文、后台任务通知
|
|
71
|
-
break;
|
|
72
71
|
case 'assistant':
|
|
72
|
+
if (isUserOnly) continue; // 「输入」档完全不要助手文本
|
|
73
73
|
if (!(lastOfTurn.has(node.id) || charLength(node.label) >= minAssistant)) continue;
|
|
74
74
|
break;
|
|
75
|
+
case 'user':
|
|
76
|
+
if (isInjectedLabel(node.label)) continue; // 模型切换、运行时上下文、后台任务通知
|
|
77
|
+
break;
|
|
75
78
|
case 'tool': {
|
|
79
|
+
if (isUserOnly) continue; // 「输入」档不要工具行(等同于"需求清单")
|
|
76
80
|
if (!isCompact) break; // standard 保留工具明细行
|
|
77
81
|
const key = node.turn ?? -1;
|
|
78
82
|
if (!groupBuckets.has(key)) groupBuckets.set(key, []);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-mind-tree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "遍历对话上下文,识别分支节点并渲染思维树/会话树的 DeepSeek Harness 插件(右侧栏 GUI + mind_tree 工具 + Obsidian 导出)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,9 +17,15 @@
|
|
|
17
17
|
"./typert": "./lib/typert.host.js",
|
|
18
18
|
"./remote": "./lib/remote.js",
|
|
19
19
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
20
|
-
"./package.json": "./package.json"
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
"./pricing": "./lib/pricing.js"
|
|
21
22
|
},
|
|
22
|
-
"files": [
|
|
23
|
+
"files": [
|
|
24
|
+
"lib",
|
|
25
|
+
"cordis.patch.yml",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
23
29
|
"repository": {
|
|
24
30
|
"type": "git",
|
|
25
31
|
"url": "git+https://github.com/ShOpenMind/dsh-plugin-mind-tree.git"
|
|
@@ -28,7 +34,17 @@
|
|
|
28
34
|
"bugs": {
|
|
29
35
|
"url": "https://github.com/ShOpenMind/dsh-plugin-mind-tree/issues"
|
|
30
36
|
},
|
|
31
|
-
"keywords": [
|
|
37
|
+
"keywords": [
|
|
38
|
+
"dsh",
|
|
39
|
+
"deepseek-harness",
|
|
40
|
+
"plugin",
|
|
41
|
+
"mindmap",
|
|
42
|
+
"session-tree",
|
|
43
|
+
"observability",
|
|
44
|
+
"dsh-plugin",
|
|
45
|
+
"dsh-bundle",
|
|
46
|
+
"dsh-market"
|
|
47
|
+
],
|
|
32
48
|
"dependencies": {
|
|
33
49
|
"zod": "^4.4.0"
|
|
34
50
|
},
|
|
@@ -39,17 +55,6 @@
|
|
|
39
55
|
"react": "^18.3.1",
|
|
40
56
|
"react-dom": "^18.3.1"
|
|
41
57
|
},
|
|
42
|
-
"scripts": {
|
|
43
|
-
"build": "node tools/build.mjs",
|
|
44
|
-
"test": "node tools/run-all.mjs",
|
|
45
|
-
"test:host": "node tools/host-test.mjs",
|
|
46
|
-
"test:reduce": "node tools/reduce-test.mjs",
|
|
47
|
-
"test:client": "node tools/client-test.mjs",
|
|
48
|
-
"test:mount": "node tools/mount-test.mjs",
|
|
49
|
-
"test:render": "node tools/render-test.mjs",
|
|
50
|
-
"test:mermaid": "node tools/mermaid-check.mjs",
|
|
51
|
-
"test:integration": "node tools/integration-test.mjs"
|
|
52
|
-
},
|
|
53
58
|
"peerDependencies": {
|
|
54
59
|
"@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
55
60
|
},
|
|
@@ -58,9 +63,13 @@
|
|
|
58
63
|
"optional": true
|
|
59
64
|
}
|
|
60
65
|
},
|
|
61
|
-
"engines": {
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=22"
|
|
68
|
+
},
|
|
62
69
|
"dsh": {
|
|
63
|
-
"bundle": {
|
|
70
|
+
"bundle": {
|
|
71
|
+
"patch": "./cordis.patch.yml"
|
|
72
|
+
},
|
|
64
73
|
"client": {
|
|
65
74
|
"platform": "web",
|
|
66
75
|
"inject": [
|
|
@@ -88,5 +97,16 @@
|
|
|
88
97
|
"@deepseek-ai/dsh-client-ui-workspace"
|
|
89
98
|
]
|
|
90
99
|
}
|
|
100
|
+
},
|
|
101
|
+
"scripts": {
|
|
102
|
+
"build": "node tools/build.mjs",
|
|
103
|
+
"test": "node tools/run-all.mjs",
|
|
104
|
+
"test:host": "node tools/host-test.mjs",
|
|
105
|
+
"test:reduce": "node tools/reduce-test.mjs",
|
|
106
|
+
"test:client": "node tools/client-test.mjs",
|
|
107
|
+
"test:mount": "node tools/mount-test.mjs",
|
|
108
|
+
"test:render": "node tools/render-test.mjs",
|
|
109
|
+
"test:mermaid": "node tools/mermaid-check.mjs",
|
|
110
|
+
"test:integration": "node tools/integration-test.mjs"
|
|
91
111
|
}
|
|
92
|
-
}
|
|
112
|
+
}
|