@migor/cli 0.2.1 → 0.2.2

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.
@@ -0,0 +1,6 @@
1
+ /* @migor/trace-view —— Agentia trace 调用树渲染器(框架无关、零依赖)。
2
+ * 官网 playground 与 CLI inspector 共用同一份,避免两处渲染漂移。 */
3
+
4
+ export { createTraceView, capabilityTypeOf, CAP_ICO, fmtArg, fmtNum, fmtMs } from './view.js';
5
+ export { playTrace } from './fromTrace.js';
6
+ export { summarizeTrace, renderSummary } from './summary.js';
@@ -0,0 +1,106 @@
1
+ /* 能力排行(G2)—— 「哪个能力慢 / 贵 / 爱失败」。
2
+ *
3
+ * 与 trace 调用树同属**框架无关**的展示层:入参是 duck-typed 的 trace 形状,零依赖。
4
+ * CLI inspector 与官网 playground 共用同一份实现,避免两处聚合口径漂移。
5
+ *
6
+ * 数据来源(与框架侧的具名约定一致):
7
+ * - `capability` span(skill / subagent)→ 调用耗时与子孙 usage 聚合(tokens / costUsd);
8
+ * - `llm.turn` span 上的 `tool.output` 事件(普通工具不建 span)→ 工具耗时与成败,
9
+ * 读 `body.durationMs` / `body.ok`。
10
+ *
11
+ * ⚠️ 单条 run 内样本常 < 5,分位没有意义 —— 排行以 total/max 为主,不给 p50/p95
12
+ * (要看分位请用框架的 `buildRunReport` + `mergeRunReports`)。
13
+ */
14
+
15
+ function kindOf(span) {
16
+ if (span.attributes && span.attributes.skill !== undefined) return 'skill';
17
+ if (span.attributes && span.attributes.subagent !== undefined) return 'subagent';
18
+ return 'capability';
19
+ }
20
+
21
+ /**
22
+ * 从一条 trace 算能力排行。
23
+ * @param {any} trace Trace 形状(spans[] / events[] / usage?)
24
+ * @returns {Array<{capability:string,calls:number,errors:number,totalMs:number,maxMs:number,tokens:number|null,costUsd:number|null}>}
25
+ * 按 totalMs 降序
26
+ */
27
+ export function summarizeTrace(trace) {
28
+ const acc = new Map();
29
+ const get = (capability) => {
30
+ let a = acc.get(capability);
31
+ if (!a) {
32
+ a = { capability, calls: 0, errors: 0, totalMs: 0, maxMs: 0, tokens: null, costUsd: null };
33
+ acc.set(capability, a);
34
+ }
35
+ return a;
36
+ };
37
+ const addMs = (a, ms) => {
38
+ const v = Number.isFinite(ms) && ms > 0 ? ms : 0;
39
+ a.totalMs += v;
40
+ if (v > a.maxMs) a.maxMs = v;
41
+ };
42
+
43
+ const spans = (trace && trace.spans) || [];
44
+ for (const span of spans) {
45
+ if (span.kind === 'capability') {
46
+ const a = get(`${kindOf(span)}:${span.name}`);
47
+ a.calls += 1;
48
+ if (span.status === 'error') a.errors += 1;
49
+ if (span.endedAt != null) addMs(a, span.endedAt - span.startedAt);
50
+ const u = span.usage;
51
+ if (u) {
52
+ a.tokens =
53
+ (a.tokens || 0) +
54
+ (u.inputTokens || 0) +
55
+ (u.outputTokens || 0) +
56
+ (u.cacheReadTokens || 0) +
57
+ (u.cacheCreationTokens || 0);
58
+ if (u.costEstimate != null) a.costUsd = (a.costUsd || 0) + u.costEstimate;
59
+ }
60
+ continue;
61
+ }
62
+ if (span.kind !== 'llm.turn') continue;
63
+ for (const e of span.events || []) {
64
+ if (e.name !== 'tool.output') continue;
65
+ const b = e.body;
66
+ if (!b || typeof b !== 'object' || typeof b.tool !== 'string') continue;
67
+ const a = get(`tool:${b.tool}`);
68
+ a.calls += 1;
69
+ if (b.ok === false) a.errors += 1;
70
+ addMs(a, typeof b.durationMs === 'number' ? b.durationMs : 0);
71
+ }
72
+ }
73
+
74
+ return [...acc.values()].sort((x, y) => y.totalMs - x.totalMs || y.calls - x.calls);
75
+ }
76
+
77
+ /** 把排行渲染成一个小表格(返回 HTML 字符串;样式由 trace-view.css 提供) */
78
+ export function renderSummary(rows) {
79
+ if (!rows || rows.length === 0) {
80
+ return '<div class="tv-sum-empty">// 没有可归因的能力(没有 capability span,也没有 tool.output 事件)</div>';
81
+ }
82
+ const fmtMs = (ms) => (ms >= 1000 ? (ms / 1000).toFixed(2) + 's' : ms + 'ms');
83
+ const body = rows
84
+ .map((r) => {
85
+ const err = r.errors > 0 ? `<span class="tv-sum-err">${r.errors}</span>` : '0';
86
+ const cost =
87
+ r.costUsd != null
88
+ ? r.costUsd < 0.000001
89
+ ? r.costUsd.toExponential(2)
90
+ : r.costUsd.toFixed(6)
91
+ : '-';
92
+ return `<tr><td class="tv-sum-capability">${escapeHtml(r.capability)}</td><td>${r.calls}</td><td>${err}</td><td>${fmtMs(r.totalMs)}</td><td>${fmtMs(r.maxMs)}</td><td>${r.tokens != null ? r.tokens : '-'}</td><td>${cost}</td></tr>`;
93
+ })
94
+ .join('');
95
+ return (
96
+ '<table class="tv-sum"><thead><tr><th>capability</th><th>calls</th><th>err</th><th>total</th><th>max</th><th>tokens</th><th>cost</th></tr></thead>' +
97
+ `<tbody>${body}</tbody></table>`
98
+ );
99
+ }
100
+
101
+ function escapeHtml(s) {
102
+ return String(s).replace(
103
+ /[&<>"']/g,
104
+ (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c],
105
+ );
106
+ }
@@ -0,0 +1,166 @@
1
+ /* Agentia trace 调用树样式 —— 从 packages/website/src/styles/global.css 的 .tr-* 段原样抽出。
2
+ * 宿主需提供这些 CSS 变量:--ice(accent)--faint(次要文本)--text(主文本)。
3
+ * 官网用站点调色板;CLI inspector 页面自带同名变量,视觉一致。 */
4
+
5
+ .tr-row {
6
+ display: flex;
7
+ align-items: baseline;
8
+ gap: 8px;
9
+ padding: 3px 0;
10
+ white-space: nowrap;
11
+ }
12
+ .tr-pre {
13
+ color: var(--faint);
14
+ white-space: pre;
15
+ }
16
+ .tr-dot {
17
+ color: var(--ice);
18
+ }
19
+ .tr-row.running .tr-dot {
20
+ animation: tr-blink 0.9s steps(1) infinite;
21
+ }
22
+ .tr-name {
23
+ color: #d4d4d8;
24
+ overflow: hidden;
25
+ text-overflow: ellipsis;
26
+ }
27
+ /* span 入参摘要:让同名调用(如两次 tool:get_weather)可区分 */
28
+ .tr-arg {
29
+ color: var(--faint);
30
+ font-size: 11.5px;
31
+ overflow: hidden;
32
+ text-overflow: ellipsis;
33
+ min-width: 0;
34
+ }
35
+ /* 失败 span:点变红、名字与 meta 走告警色 */
36
+ .tr-row.error .tr-dot {
37
+ color: #f7768e;
38
+ }
39
+ .tr-row.error .tr-name {
40
+ color: #f7768e;
41
+ }
42
+ .tr-row.error .tr-meta {
43
+ color: rgba(247, 118, 142, 0.9);
44
+ }
45
+ .tr-row[data-kind="run"] .tr-name {
46
+ color: var(--text);
47
+ font-weight: 500;
48
+ }
49
+ .tr-row[data-kind="llm.turn"] .tr-name {
50
+ color: var(--ice);
51
+ }
52
+ .tr-meta {
53
+ margin-left: auto;
54
+ color: var(--faint);
55
+ font-size: 11px;
56
+ padding-left: 10px;
57
+ }
58
+ .tr-empty {
59
+ color: var(--faint);
60
+ }
61
+
62
+ /* 四类能力标识符(⚙ tool / ◆ skill / ¶ prompt / ⊕ subagent):只靠字形区分,
63
+ 不上类型色 —— 站点只有一支青色 accent,加四种颜色会打散近黑钛银的克制感。
64
+ 有内部结构的 skill / subagent 用 accent 点亮,工具类保持灰,层级一眼可辨。 */
65
+ .tr-ico {
66
+ flex: none;
67
+ color: var(--faint);
68
+ font-size: 10px;
69
+ }
70
+ .tr-row[data-capability="skill"] .tr-ico,
71
+ .tr-row[data-capability="subagent"] .tr-ico {
72
+ color: var(--ice);
73
+ }
74
+
75
+ /* span 事件行:普通工具 / @Prompt 调用是 turn 上的【事件】而非 span ——
76
+ 没有状态圈与耗时,视觉上比 span 行更轻,入参/出参摘要靠省略号收敛。 */
77
+ .tr-ev {
78
+ gap: 6px;
79
+ font-size: 11.5px;
80
+ }
81
+ .tr-ev .tr-evv {
82
+ flex: none;
83
+ color: var(--ice);
84
+ }
85
+ .tr-ev .tr-evtype {
86
+ flex: none;
87
+ color: var(--faint);
88
+ font-size: 10.5px;
89
+ }
90
+ .tr-ev .tr-name {
91
+ flex: none;
92
+ color: #a1a1aa;
93
+ }
94
+ .tr-io {
95
+ flex: 1 1 auto;
96
+ min-width: 0;
97
+ overflow: hidden;
98
+ text-overflow: ellipsis;
99
+ color: var(--faint);
100
+ }
101
+ .tr-ev.error .tr-evv,
102
+ .tr-ev.error .tr-name,
103
+ .tr-ev.error .tr-io {
104
+ color: #f7768e;
105
+ }
106
+
107
+ @keyframes tr-blink {
108
+ 0%,
109
+ 100% {
110
+ opacity: 1;
111
+ }
112
+ 50% {
113
+ opacity: 0.25;
114
+ }
115
+ }
116
+
117
+ /* —— 能力排行(G2):哪个能力慢 / 贵 / 爱失败 —— */
118
+ .tv-sum {
119
+ width: 100%;
120
+ border-collapse: collapse;
121
+ font-family: var(--tv-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
122
+ font-size: 11.5px;
123
+ margin-top: 10px;
124
+ }
125
+ .tv-sum th {
126
+ text-align: left;
127
+ color: #71717a;
128
+ font-weight: 500;
129
+ border-bottom: 1px solid #27272a;
130
+ padding: 4px 8px 4px 0;
131
+ white-space: nowrap;
132
+ }
133
+ .tv-sum td {
134
+ padding: 3px 8px 3px 0;
135
+ color: #a1a1aa;
136
+ white-space: nowrap;
137
+ }
138
+ .tv-sum td.tv-sum-capability {
139
+ color: #e4e4e7;
140
+ }
141
+ .tv-sum tbody tr:hover td {
142
+ background: #17171a;
143
+ }
144
+ .tv-sum-err {
145
+ color: #f7768e;
146
+ }
147
+ .tv-sum-empty {
148
+ color: #71717a;
149
+ margin-top: 10px;
150
+ font-size: 11.5px;
151
+ }
152
+
153
+ /* 失败行的错误原文:单起一行(.tr-row 是 nowrap,挤在同一行会把行撑爆) */
154
+ .tr-errm {
155
+ display: flex;
156
+ gap: 8px;
157
+ padding: 1px 0 4px;
158
+ white-space: normal;
159
+ }
160
+ .tr-errm-msg {
161
+ color: #f7768e;
162
+ font-size: 11.5px;
163
+ white-space: pre-wrap;
164
+ word-break: break-word;
165
+ min-width: 0;
166
+ }
@@ -0,0 +1,274 @@
1
+ /* Agentia trace 调用树渲染器 —— 数据无关、零依赖。
2
+ *
3
+ * 只认五个动作:reset 开树 / start 开 span / event 记事件 / end 收 span / finish 收 run。
4
+ * 谁喂它都行:官网 playground 的模拟回放脚本,或 fromTrace.js 的 playTrace(真实 Trace)。
5
+ *
6
+ * 视觉语言与语义(逐字取自 packages/website/src/scripts/playground.js,行为不变):
7
+ * - 事件行(tool.input / tool.output)没有状态圈、没有耗时,比 span 行更轻;
8
+ * - 事件与子 span 按发生顺序混排(input 先于它触发的 capability span、output 后于它)——
9
+ * 这个先后本身就是语义,不能拍平成一类;
10
+ * - 四类能力标识符(⚙ tool / ◆ skill / ¶ prompt / ⊕ subagent)只靠字形区分、不上类型色。
11
+ *
12
+ * 宿主需提供的 CSS 变量:--ice --faint --text(见 trace-view.css)。
13
+ */
14
+
15
+ export const CAP_ICO = { tool: '⚙', skill: '◆', prompt: '¶', subagent: '⊕' };
16
+
17
+ /** 能力类型取自名字前缀(tool: / skill: / prompt: / subagent:) */
18
+ export function capabilityTypeOf(name) {
19
+ const t = String(name || '').split(':')[0];
20
+ return CAP_ICO[t] ? t : '';
21
+ }
22
+
23
+ export const fmtNum = (n) => n.toLocaleString('en-US');
24
+ export const fmtMs = (ms) => (ms >= 1000 ? (ms / 1000).toFixed(2) + 's' : ms + 'ms');
25
+
26
+ /* span 入参摘要:写进 trace 行,否则同名 capability(如两次 tool:get_weather)无法区分 */
27
+ export function fmtArg(v) {
28
+ if (v == null) return '';
29
+ let s;
30
+ if (typeof v === 'object') {
31
+ const keys = Object.keys(v);
32
+ if (!keys.length) return ''; // 无入参就不显示,避免行里多一个 "{}"
33
+ s =
34
+ '{ ' +
35
+ keys
36
+ .slice(0, 4)
37
+ .map((k) => {
38
+ let sv = typeof v[k] === 'object' ? JSON.stringify(v[k]) : String(v[k]);
39
+ if (sv.length > 22) sv = sv.slice(0, 21) + '…';
40
+ return k + ': ' + sv;
41
+ })
42
+ .join(', ') +
43
+ (keys.length > 4 ? ', …' : '') +
44
+ ' }';
45
+ } else s = String(v);
46
+ return s.length > 62 ? s.slice(0, 61) + '…' : s;
47
+ }
48
+
49
+ function el(tag, cls, text) {
50
+ const node = document.createElement(tag);
51
+ if (cls) node.className = cls;
52
+ if (text !== undefined) node.textContent = text;
53
+ return node;
54
+ }
55
+
56
+ /**
57
+ * 建一个绑定到 rootEl 的 trace 视图。
58
+ * opts.price = { input, output }($/M tokens)或 null(无公开单价则不估算成本)
59
+ * opts.usage = { in, out, cost } 三个 DOM 节点(可选;给了才更新计数器)
60
+ * opts.onUsage = (acc) => void(可选;每次累计后回调,便于宿主自定义展示)
61
+ */
62
+ export function createTraceView(rootEl, opts = {}) {
63
+ let price = opts.price ?? null;
64
+ const onUsage = opts.onUsage ?? null;
65
+ const usageEls = opts.usage ?? null;
66
+
67
+ let traceRoot = null;
68
+ const spanMap = new Map();
69
+ const usageAcc = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
70
+
71
+ function renderTrace() {
72
+ rootEl.innerHTML = '';
73
+ const rows = [];
74
+ /* 每个节点下【事件】与【子 span】按发生顺序混排:tool.input 先于它触发的 capability span、
75
+ tool.output 后于它,这个先后本身就是语义,不能拍平成一类。 */
76
+ (function walk(node, prefix, isLast, isRoot) {
77
+ rows.push({ node, prefix, isRoot, last: isLast, ev: null });
78
+ const items = node.order || [];
79
+ items.forEach((it, i) => {
80
+ const last = i === items.length - 1;
81
+ const next = isRoot ? '' : prefix + (isLast ? ' ' : '│ ');
82
+ if (it.t === 'span') walk(it.n, next, last, false);
83
+ else rows.push({ node, prefix: next, isRoot: false, last, ev: it.e });
84
+ });
85
+ })(traceRoot, '', true, true);
86
+
87
+ rows.forEach(({ node, prefix, isRoot, last, ev }) => {
88
+ const branch = isRoot ? '' : prefix + (last ? '└─ ' : '├─ ');
89
+
90
+ /* 事件行:没有 status 圈、没有耗时、不参与 usage 计数,只带入参/出参摘要 */
91
+ if (ev) {
92
+ const row = el('div', 'tr-row tr-ev' + (ev.ok === false ? ' error' : ''));
93
+ row.dataset.ev = ev.type;
94
+ row.dataset.capability = capabilityTypeOf(ev.tool);
95
+ row.appendChild(el('span', 'tr-pre', branch));
96
+ row.appendChild(el('span', 'tr-evv', ev.type === 'tool.output' ? '◂' : '▸'));
97
+ row.appendChild(el('span', 'tr-evtype', ev.type));
98
+ // 非 tool.* 事件(usage.unpriced / llm.retry / budget.* / context.budget)没有工具名 ——
99
+ // 这一格整个不渲染,别把「无工具」显示成假工具名(.tr-name 是 flex:none,.tr-io 自然占满)
100
+ if (ev.tool) row.appendChild(el('span', 'tr-name', ev.tool));
101
+ const io = el('span', 'tr-io', ev.text || '');
102
+ io.title = ev.text || '';
103
+ row.appendChild(io);
104
+ rootEl.appendChild(row);
105
+ return;
106
+ }
107
+
108
+ const bad = node.done && node.status === 'error';
109
+ const row = el('div', 'tr-row' + (node.done ? '' : ' running') + (bad ? ' error' : ''));
110
+ row.dataset.kind = node.kind;
111
+ const ut = node.kind === 'capability' ? capabilityTypeOf(node.name) : '';
112
+ if (ut) row.dataset.capability = ut;
113
+ row.appendChild(el('span', 'tr-pre', branch));
114
+ row.appendChild(el('span', 'tr-dot', node.done ? (bad ? '✕' : '●') : '◌'));
115
+ if (ut) row.appendChild(el('span', 'tr-ico', CAP_ICO[ut]));
116
+ row.appendChild(el('span', 'tr-name', node.name));
117
+ if (node.arg) {
118
+ const arg = el('span', 'tr-arg', node.arg);
119
+ arg.title = node.arg;
120
+ row.appendChild(arg);
121
+ }
122
+ let meta;
123
+ if (node.done) {
124
+ const parts = [fmtMs(node.ms)];
125
+ if (node.usage) {
126
+ parts.push(fmtNum(node.usage.input + node.usage.output) + ' tok');
127
+ const cr = node.usage.cacheRead || 0;
128
+ const cc = node.usage.cacheCreation || 0;
129
+ if (cr || cc) parts.push('cache ↑' + fmtNum(cr) + ' ↓' + fmtNum(cc));
130
+ }
131
+ if (bad) parts.push((node.error && node.error.type) || 'error');
132
+ meta = parts.join(' · ');
133
+ } else meta = '…';
134
+ const metaEl = el('span', 'tr-meta', meta);
135
+ if (bad && node.error && node.error.message) metaEl.title = node.error.message;
136
+ row.appendChild(metaEl);
137
+ rootEl.appendChild(row);
138
+ // 失败原文必须**看得见** —— 只塞进 title(hover 才显)等于没显示。首次运行最常见的失败
139
+ // (没配 API key)就靠这一行定位;单起一行,不去挤占 nowrap 的 span 行。
140
+ if (bad && node.error && node.error.message) {
141
+ const errLine = el('div', 'tr-errm');
142
+ errLine.appendChild(el('span', 'tr-pre', branch + ' '));
143
+ errLine.appendChild(el('span', 'tr-errm-msg', node.error.message));
144
+ rootEl.appendChild(errLine);
145
+ }
146
+ });
147
+ }
148
+
149
+ function renderUsage(acc) {
150
+ if (onUsage) onUsage(acc);
151
+ if (!usageEls) return;
152
+ if (usageEls.in) usageEls.in.textContent = fmtNum(acc.input);
153
+ if (usageEls.out) usageEls.out.textContent = fmtNum(acc.output);
154
+ if (!usageEls.cost) return;
155
+ if (price == null || price.input == null || price.output == null) {
156
+ usageEls.cost.textContent = '—'; // 无公开单价的端点不做估算(如 DeepSeek)
157
+ return;
158
+ }
159
+ const cost = (acc.input * price.input + acc.output * price.output) / 1e6;
160
+ usageEls.cost.textContent = '$' + cost.toFixed(4);
161
+ }
162
+
163
+ /** 开树。rootName 为 run 根显示名(如 'run · 审查一份文档') */
164
+ function reset(rootName) {
165
+ traceRoot = {
166
+ id: 'root',
167
+ kind: 'run',
168
+ name: rootName || 'run',
169
+ children: [],
170
+ events: [],
171
+ order: [],
172
+ done: false,
173
+ ms: 0,
174
+ usage: { input: 0, output: 0 },
175
+ };
176
+ spanMap.clear();
177
+ spanMap.set('root', traceRoot);
178
+ usageAcc.input = 0;
179
+ usageAcc.output = 0;
180
+ usageAcc.cacheRead = 0;
181
+ usageAcc.cacheCreation = 0;
182
+ renderTrace();
183
+ }
184
+
185
+ function start(s) {
186
+ const node = {
187
+ id: s.id,
188
+ kind: s.kind,
189
+ name: s.name,
190
+ arg: s.arg || '',
191
+ children: [],
192
+ events: [],
193
+ order: [],
194
+ done: false,
195
+ ms: 0,
196
+ status: 'ok',
197
+ error: null,
198
+ usage: null,
199
+ };
200
+ spanMap.set(s.id, node);
201
+ const parent = spanMap.get(s.parent) || traceRoot;
202
+ parent.children.push(node);
203
+ parent.order.push({ t: 'span', n: node });
204
+ renderTrace();
205
+ }
206
+
207
+ /* span 事件:框架把普通工具 / @Prompt 调用记成【turn 上的事件】,不给它们建 capability span
208
+ (只有 skill / subagent 会 recorder.begin('capability', …),见 engine/loop.ts)。
209
+ events 与 order 并存:events 是数据、order 负责与子 span 的先后顺序。 */
210
+ function event(id, type, tool, text, ok) {
211
+ const node = spanMap.get(id);
212
+ if (!node) return;
213
+ const e = { type, tool, text, ok: ok !== false };
214
+ node.events.push(e);
215
+ node.order.push({ t: 'ev', e });
216
+ renderTrace();
217
+ }
218
+
219
+ function end(s) {
220
+ const node = spanMap.get(s.id);
221
+ if (!node) return;
222
+ node.done = true;
223
+ node.ms = s.ms || 0;
224
+ node.status = s.status || 'ok';
225
+ node.error = s.error || null;
226
+ if (s.arg) node.arg = s.arg;
227
+ node.usage = s.usage
228
+ ? {
229
+ input: s.usage.input || 0,
230
+ output: s.usage.output || 0,
231
+ cacheRead: s.usage.cacheRead || 0,
232
+ cacheCreation: s.usage.cacheCreation || 0,
233
+ }
234
+ : null;
235
+ // 计数器只累加 llm.turn(capability span 的 usage 是子 span 聚合,重复计入会双算)
236
+ if (s.usage && node.kind === 'llm.turn') {
237
+ usageAcc.input += node.usage.input;
238
+ usageAcc.output += node.usage.output;
239
+ usageAcc.cacheRead += node.usage.cacheRead;
240
+ usageAcc.cacheCreation += node.usage.cacheCreation;
241
+ renderUsage(usageAcc);
242
+ }
243
+ renderTrace();
244
+ }
245
+
246
+ function finish(ms, status, error) {
247
+ if (!traceRoot) return;
248
+ traceRoot.done = true;
249
+ traceRoot.ms = ms;
250
+ // run 失败时根 span 也要标红:原先 status 恒为 ok,错误只体现在子 span 上
251
+ traceRoot.status = status || 'ok';
252
+ traceRoot.error = error || null;
253
+ traceRoot.usage = { input: usageAcc.input, output: usageAcc.output };
254
+ renderTrace();
255
+ }
256
+
257
+ return {
258
+ reset,
259
+ start,
260
+ event,
261
+ end,
262
+ finish,
263
+ render: renderTrace,
264
+ setUsage: renderUsage,
265
+ /** 换单价(如真实模式切服务商):null 表示无公开单价 → 成本显示为 — */
266
+ setPrice(p) {
267
+ price = p ?? null;
268
+ renderUsage(usageAcc);
269
+ },
270
+ get usage() {
271
+ return usageAcc;
272
+ },
273
+ };
274
+ }