@xcanwin/manyoyo 6.2.7 → 6.2.9

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.
@@ -387,35 +387,6 @@
387
387
  return status;
388
388
  }
389
389
 
390
- function buildStructuredTraceResidualLines(message) {
391
- const lines = String(message && message.content ? message.content : '')
392
- .split('\n')
393
- .map(function (line) {
394
- return String(line || '').trim();
395
- })
396
- .filter(Boolean);
397
- const traceEvents = Array.isArray(message && message.traceEvents) ? message.traceEvents : [];
398
- const consumed = new Map();
399
- traceEvents.forEach(function (traceEvent) {
400
- const key = traceEvent && traceEvent.text ? String(traceEvent.text).trim() : '';
401
- if (!key) {
402
- return;
403
- }
404
- consumed.set(key, (consumed.get(key) || 0) + 1);
405
- });
406
- return lines.filter(function (line) {
407
- if (!line || line === '[执行过程]') {
408
- return false;
409
- }
410
- const remaining = consumed.get(line) || 0;
411
- if (remaining > 0) {
412
- consumed.set(line, remaining - 1);
413
- return false;
414
- }
415
- return true;
416
- });
417
- }
418
-
419
390
  function resolveTraceTone(traceEvent) {
420
391
  const kind = traceEvent && traceEvent.kind ? String(traceEvent.kind) : '';
421
392
  if (kind === 'command') return 'command';
@@ -586,20 +557,24 @@
586
557
  const container = document.createElement('div');
587
558
  container.className = 'trace-structured';
588
559
 
560
+ const messageId = message && message.id ? message.id : '';
561
+ const pending = Boolean(state.agentRun.active && state.agentRun.traceMessageId === messageId);
562
+
589
563
  const flow = document.createElement('div');
590
564
  flow.className = 'trace-flow';
591
- buildStructuredTraceResidualLines(message).forEach(function (line) {
592
- flow.appendChild(createResidualTraceCard(line));
593
- });
565
+ if (pending) {
566
+ window.ManyoyoChatBehavior.buildStructuredTraceResidualLines(message).forEach(function (line) {
567
+ flow.appendChild(createResidualTraceCard(line));
568
+ });
569
+ }
594
570
  const traceEvents = Array.isArray(message && message.traceEvents) ? message.traceEvents : [];
595
- traceEvents.forEach(function (traceEvent) {
571
+ const mergedTraceEvents = window.ManyoyoChatBehavior.mergeToolTraceEvents(traceEvents);
572
+ mergedTraceEvents.forEach(function (traceEvent) {
596
573
  flow.appendChild(createTraceEventCard(traceEvent));
597
574
  });
598
575
 
599
- const messageId = message && message.id ? message.id : '';
600
- const pending = Boolean(state.agentRun.active && state.agentRun.traceMessageId === messageId);
601
- const summaryInfo = window.ManyoyoChatBehavior.summarizeTraceFlow(traceEvents, { pending });
602
- const hasError = traceEvents.some(function (event) {
576
+ const summaryInfo = window.ManyoyoChatBehavior.summarizeTraceFlow(mergedTraceEvents, { pending });
577
+ const hasError = mergedTraceEvents.some(function (event) {
603
578
  return event && event.kind === 'error';
604
579
  });
605
580
  const defaultOpen = hasError;
@@ -2108,6 +2083,13 @@
2108
2083
  { label: '最近 resume', value: lastResumeText },
2109
2084
  { label: '最近结果', value: detail.lastResumeOk == null ? '暂无' : (detail.lastResumeOk ? '成功' : '失败'), tone: detail.lastResumeOk == null ? 'info' : (detail.lastResumeOk ? 'ok' : 'danger') }
2110
2085
  ]);
2086
+ renderKeyValueCard(detailSummary, '用量统计', detail.usageTotal ? [
2087
+ { label: '累计输入 tokens', value: String(detail.usageTotal.inputTokens) },
2088
+ { label: '累计输出 tokens', value: String(detail.usageTotal.outputTokens) },
2089
+ { label: '累计花费', value: typeof detail.usageTotal.costUsd === 'number' ? `$${detail.usageTotal.costUsd.toFixed(4)}` : '暂不支持' }
2090
+ ] : [
2091
+ { label: '状态', value: '暂无数据(当前 Agent 程序不支持用量统计,或还未执行过对话)', tone: 'info' }
2092
+ ]);
2111
2093
  renderKeyValueCard(detailSummary, '最近活动', [
2112
2094
  { label: '最近角色', value: latestRoleLabel },
2113
2095
  { label: '最近时间', value: latestTimestampText },
@@ -22,6 +22,62 @@
22
22
  return trimmed ? `${trimmed} · MANYOYO Web` : 'MANYOYO Web';
23
23
  }
24
24
 
25
+ function buildStructuredTraceResidualLines(message) {
26
+ const lines = String(message && message.content ? message.content : '')
27
+ .split('\n')
28
+ .map(line => String(line || '').trim())
29
+ .filter(Boolean);
30
+ const traceEvents = Array.isArray(message && message.traceEvents) ? message.traceEvents : [];
31
+ const consumed = new Map();
32
+ traceEvents.forEach(traceEvent => {
33
+ const text = traceEvent && traceEvent.text ? String(traceEvent.text) : '';
34
+ if (!text) {
35
+ return;
36
+ }
37
+ text.split('\n').forEach(subLine => {
38
+ const key = String(subLine || '').trim();
39
+ if (!key) {
40
+ return;
41
+ }
42
+ consumed.set(key, (consumed.get(key) || 0) + 1);
43
+ });
44
+ });
45
+ return lines.filter(line => {
46
+ if (!line || line === '[执行过程]') {
47
+ return false;
48
+ }
49
+ const remaining = consumed.get(line) || 0;
50
+ if (remaining > 0) {
51
+ consumed.set(line, remaining - 1);
52
+ return false;
53
+ }
54
+ return true;
55
+ });
56
+ }
57
+
58
+ const MERGEABLE_TRACE_KINDS = new Set(['tool', 'command', 'mcp']);
59
+
60
+ function mergeToolTraceEvents(traceEvents) {
61
+ const events = Array.isArray(traceEvents) ? traceEvents : [];
62
+ const result = [];
63
+ const indexByKey = new Map();
64
+ events.forEach(event => {
65
+ const kind = event && event.kind ? String(event.kind) : '';
66
+ const toolId = event && event.toolId ? String(event.toolId) : '';
67
+ if (MERGEABLE_TRACE_KINDS.has(kind) && toolId) {
68
+ const key = `${kind}:${toolId}`;
69
+ if (indexByKey.has(key)) {
70
+ const index = indexByKey.get(key);
71
+ result[index] = Object.assign({}, result[index], event);
72
+ return;
73
+ }
74
+ indexByKey.set(key, result.length);
75
+ }
76
+ result.push(event);
77
+ });
78
+ return result;
79
+ }
80
+
25
81
  function mergeTraceIntoReply(messages) {
26
82
  const list = Array.isArray(messages) ? messages : [];
27
83
  const result = [];
@@ -44,6 +100,8 @@
44
100
  isNearBottom,
45
101
  summarizeTraceFlow,
46
102
  buildDocumentTitle,
47
- mergeTraceIntoReply
103
+ mergeTraceIntoReply,
104
+ mergeToolTraceEvents,
105
+ buildStructuredTraceResidualLines
48
106
  };
49
107
  }());
package/lib/web/server.js CHANGED
@@ -189,7 +189,25 @@ function createEmptyWebAgentSession(agentId, agentName) {
189
189
  lastResumeAt: null,
190
190
  lastResumeOk: null,
191
191
  lastResumeError: '',
192
- engineSessionId: ''
192
+ engineSessionId: '',
193
+ usageTotal: null
194
+ };
195
+ }
196
+
197
+ function normalizeWebAgentUsageTotal(value) {
198
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
199
+ return null;
200
+ }
201
+ if (typeof value.inputTokens !== 'number' || typeof value.outputTokens !== 'number') {
202
+ return null;
203
+ }
204
+ if (typeof value.costUsd !== 'number' && value.costUsd !== null) {
205
+ return null;
206
+ }
207
+ return {
208
+ inputTokens: value.inputTokens,
209
+ outputTokens: value.outputTokens,
210
+ costUsd: typeof value.costUsd === 'number' ? value.costUsd : null
193
211
  };
194
212
  }
195
213
 
@@ -218,7 +236,8 @@ function normalizeWebAgentSessionRecord(agentId, rawAgent) {
218
236
  lastResumeAt: typeof source.lastResumeAt === 'string' ? source.lastResumeAt : null,
219
237
  lastResumeOk: typeof source.lastResumeOk === 'boolean' ? source.lastResumeOk : null,
220
238
  lastResumeError: typeof source.lastResumeError === 'string' ? source.lastResumeError : '',
221
- engineSessionId: typeof source.engineSessionId === 'string' ? source.engineSessionId : ''
239
+ engineSessionId: typeof source.engineSessionId === 'string' ? source.engineSessionId : '',
240
+ usageTotal: normalizeWebAgentUsageTotal(source.usageTotal)
222
241
  };
223
242
  }
224
243
 
@@ -1039,6 +1058,86 @@ function extractEngineSessionId(agentProgram, text) {
1039
1058
  return '';
1040
1059
  }
1041
1060
 
1061
+ function extractClaudeTurnUsage(text) {
1062
+ let result = null;
1063
+ for (const rawLine of String(text || '').split('\n')) {
1064
+ const payload = parseJsonObjectLine(rawLine);
1065
+ if (!payload || payload.type !== 'result') {
1066
+ continue;
1067
+ }
1068
+ const usage = toPlainObject(payload.usage);
1069
+ result = {
1070
+ inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : null,
1071
+ outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : null,
1072
+ costUsd: typeof payload.total_cost_usd === 'number' ? payload.total_cost_usd : null
1073
+ };
1074
+ }
1075
+ return result;
1076
+ }
1077
+
1078
+ function extractCodexTurnUsage(text) {
1079
+ let result = null;
1080
+ for (const rawLine of String(text || '').split('\n')) {
1081
+ const payload = parseJsonObjectLine(rawLine);
1082
+ if (!payload || payload.type !== 'turn.completed') {
1083
+ continue;
1084
+ }
1085
+ const usage = toPlainObject(payload.usage);
1086
+ result = {
1087
+ inputTokens: typeof usage.input_tokens === 'number' ? usage.input_tokens : null,
1088
+ outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : null,
1089
+ costUsd: null
1090
+ };
1091
+ }
1092
+ return result;
1093
+ }
1094
+
1095
+ function extractOpenCodeTurnUsage(text) {
1096
+ let result = null;
1097
+ for (const rawLine of String(text || '').split('\n')) {
1098
+ const payload = parseJsonObjectLine(rawLine);
1099
+ if (!payload || payload.type !== 'step_finish') {
1100
+ continue;
1101
+ }
1102
+ const part = toPlainObject(payload.part);
1103
+ const tokens = toPlainObject(part.tokens);
1104
+ result = {
1105
+ inputTokens: typeof tokens.input === 'number' ? tokens.input : null,
1106
+ outputTokens: typeof tokens.output === 'number' ? tokens.output : null,
1107
+ costUsd: typeof part.cost === 'number' ? part.cost : null
1108
+ };
1109
+ }
1110
+ return result;
1111
+ }
1112
+
1113
+ // Gemini 的非交互 stream-json 输出目前没有已确认的 usage/token 字段可解析,故不返回数据(而非猜测字段名)。
1114
+ function extractTurnUsage(agentProgram, text) {
1115
+ if (agentProgram === 'claude') {
1116
+ return extractClaudeTurnUsage(text);
1117
+ }
1118
+ if (agentProgram === 'codex') {
1119
+ return extractCodexTurnUsage(text);
1120
+ }
1121
+ if (agentProgram === 'opencode') {
1122
+ return extractOpenCodeTurnUsage(text);
1123
+ }
1124
+ return null;
1125
+ }
1126
+
1127
+ function accumulateUsageTotal(baseline, turnUsage) {
1128
+ const base = baseline && typeof baseline === 'object'
1129
+ ? baseline
1130
+ : { inputTokens: 0, outputTokens: 0, costUsd: null };
1131
+ const baseCost = typeof base.costUsd === 'number' ? base.costUsd : null;
1132
+ const turnCost = typeof turnUsage.costUsd === 'number' ? turnUsage.costUsd : null;
1133
+ return {
1134
+ inputTokens: (base.inputTokens || 0) + (turnUsage.inputTokens || 0),
1135
+ outputTokens: (base.outputTokens || 0) + (turnUsage.outputTokens || 0),
1136
+ // 引擎从未上报过费用(如 Codex)时保持 null,不要伪装成"花费为 0"
1137
+ costUsd: baseCost === null && turnCost === null ? null : (baseCost || 0) + (turnCost || 0)
1138
+ };
1139
+ }
1140
+
1042
1141
  function getAgentRuntimeMeta(template) {
1043
1142
  const normalizedTemplate = normalizeAgentPromptCommandTemplate(template, 'agentPromptCommand');
1044
1143
  const agentProgram = resolveAgentProgram(normalizedTemplate);
@@ -1483,6 +1582,7 @@ function prepareCodexTraceEvent(payload) {
1483
1582
  const mcpServer = pickFirstString(item.server);
1484
1583
  const mcpTool = pickFirstString(item.tool);
1485
1584
  const itemStatus = pickFirstString(item.status);
1585
+ const toolId = pickFirstString(item.id);
1486
1586
 
1487
1587
  function shortenText(value, maxChars = 140) {
1488
1588
  const raw = clipText(stripAnsi(String(value || '')).replace(/\s+/g, ' ').trim(), maxChars);
@@ -1558,14 +1658,16 @@ function prepareCodexTraceEvent(payload) {
1558
1658
  return createTraceEvent('tool', `[工具开始] ${toolName || 'tool_call'}`, {
1559
1659
  phase: 'started',
1560
1660
  status: pickDisplayStatus('in_progress'),
1561
- toolName: toolName || 'tool_call'
1661
+ toolName: toolName || 'tool_call',
1662
+ toolId
1562
1663
  });
1563
1664
  }
1564
1665
  if (itemType === 'command_execution') {
1565
1666
  return createTraceEvent('command', `[命令开始] ${commandText || 'command_execution'}`, {
1566
1667
  phase: 'started',
1567
1668
  status: pickDisplayStatus('in_progress'),
1568
- command: commandText || 'command_execution'
1669
+ command: commandText || 'command_execution',
1670
+ toolId
1569
1671
  });
1570
1672
  }
1571
1673
  if (itemType === 'mcp_tool_call') {
@@ -1583,7 +1685,8 @@ function prepareCodexTraceEvent(payload) {
1583
1685
  arguments: item.arguments && typeof item.arguments === 'object' && !Array.isArray(item.arguments)
1584
1686
  ? item.arguments
1585
1687
  : null,
1586
- argumentSummary: summary
1688
+ argumentSummary: summary,
1689
+ toolId
1587
1690
  }
1588
1691
  );
1589
1692
  }
@@ -1612,7 +1715,8 @@ function prepareCodexTraceEvent(payload) {
1612
1715
  return createTraceEvent('tool', `[工具完成] ${toolName || 'tool_call'}`, {
1613
1716
  phase: 'completed',
1614
1717
  status: pickDisplayStatus('completed'),
1615
- toolName: toolName || 'tool_call'
1718
+ toolName: toolName || 'tool_call',
1719
+ toolId
1616
1720
  });
1617
1721
  }
1618
1722
  if (itemType === 'command_execution') {
@@ -1622,7 +1726,8 @@ function prepareCodexTraceEvent(payload) {
1622
1726
  status: pickDisplayStatus(suffix),
1623
1727
  command: commandText || 'command_execution',
1624
1728
  exitCode: typeof item.exit_code === 'number' ? item.exit_code : null,
1625
- result: item.aggregated_output !== undefined ? item.aggregated_output : null
1729
+ result: item.aggregated_output !== undefined ? item.aggregated_output : null,
1730
+ toolId
1626
1731
  });
1627
1732
  }
1628
1733
  if (itemType === 'mcp_tool_call') {
@@ -1642,7 +1747,8 @@ function prepareCodexTraceEvent(payload) {
1642
1747
  : null,
1643
1748
  argumentSummary: summary,
1644
1749
  result: item.result !== undefined ? item.result : null,
1645
- error: item.error !== undefined ? item.error : null
1750
+ error: item.error !== undefined ? item.error : null,
1751
+ toolId
1646
1752
  }
1647
1753
  );
1648
1754
  }
@@ -1755,13 +1861,15 @@ async function prepareWebAgentExecution(ctx, state, sessionRef, prompt) {
1755
1861
  }
1756
1862
 
1757
1863
  function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, meta, result) {
1864
+ const turnUsage = extractTurnUsage(agentMeta.agentProgram, result.stdout);
1758
1865
  appendWebSessionMessage(state.webHistoryDir, sessionRef, 'assistant', result.output, {
1759
1866
  exitCode: result.exitCode,
1760
1867
  mode: 'agent',
1761
1868
  contextMode: meta.contextMode,
1762
1869
  resumeAttempted: meta.resumeAttempted,
1763
1870
  resumeSucceeded: meta.resumeSucceeded,
1764
- interrupted: result.interrupted === true
1871
+ interrupted: result.interrupted === true,
1872
+ ...(turnUsage ? { usage: turnUsage } : {})
1765
1873
  });
1766
1874
  const patch = {
1767
1875
  lastResumeAt: meta.resumeAttempted ? new Date().toISOString() : (agentSession.lastResumeAt || null),
@@ -1772,6 +1880,9 @@ function finalizeWebAgentExecution(state, sessionRef, agentSession, agentMeta, m
1772
1880
  if (engineSessionId) {
1773
1881
  patch.engineSessionId = engineSessionId;
1774
1882
  }
1883
+ if (turnUsage) {
1884
+ patch.usageTotal = accumulateUsageTotal(agentSession.usageTotal, turnUsage);
1885
+ }
1775
1886
  patchWebAgentSessionState(state.webHistoryDir, sessionRef, patch);
1776
1887
  }
1777
1888
 
@@ -3619,6 +3730,7 @@ function buildSessionDetail(ctx, state, containerMap, name) {
3619
3730
  lastResumeAt: agentSession.lastResumeAt || null,
3620
3731
  lastResumeOk: typeof agentSession.lastResumeOk === 'boolean' ? agentSession.lastResumeOk : null,
3621
3732
  lastResumeError: agentSession.lastResumeError || '',
3733
+ usageTotal: agentSession.usageTotal || null,
3622
3734
  applied
3623
3735
  };
3624
3736
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "6.2.7",
3
+ "version": "6.2.9",
4
4
  "imageVersion": "1.9.1-common",
5
5
  "playwrightCliVersion": "0.1.18",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",