cc-viewer 1.7.0 → 1.7.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.
Files changed (123) hide show
  1. package/dist/assets/App-B224r-8M.js +2 -0
  2. package/dist/assets/{MdxEditorPanel-C5Q6uUyH.js → MdxEditorPanel-BUubiE6Y.js} +1 -1
  3. package/dist/assets/{Mobile-MoBp9c8k.js → Mobile-CKmfRRvo.js} +1 -1
  4. package/dist/assets/index-CjsQEQc2.js +2 -0
  5. package/dist/assets/seqResourceLoaders-C-T_EMiO.js +2 -0
  6. package/dist/assets/{seqResourceLoaders-CxuD1CYl.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
  7. package/dist/index.html +1 -1
  8. package/package.json +2 -1
  9. package/server/lib/log-management.js +1 -0
  10. package/server/lib/log-watcher.js +29 -3
  11. package/server/lib/stats-worker.js +21 -2
  12. package/server/lib/v2/adapter.js +123 -18
  13. package/server/lib/v2/journal.js +9 -5
  14. package/server/lib/v2/jsonl-read.js +127 -0
  15. package/server/lib/v2/live-feed.js +222 -20
  16. package/server/lib/v2/meta-rows.js +361 -0
  17. package/server/lib/v2/replay.js +5 -5
  18. package/server/lib/v2/session-select.js +69 -2
  19. package/server/lib/v2/v2-writer.js +15 -0
  20. package/server/lib/wire-compress.js +167 -0
  21. package/server/routes/events.js +96 -26
  22. package/server/routes/im.js +1 -1
  23. package/server/routes/logs.js +15 -12
  24. package/server/routes/v2.js +60 -0
  25. package/server/routes/workspaces.js +7 -6
  26. package/server/server.js +23 -1
  27. package/server/workspace-registry.js +5 -0
  28. package/src/utils/apiUrl.js +26 -0
  29. package/src/utils/askFallback.js +21 -0
  30. package/src/utils/askOptionDesc.js +55 -0
  31. package/src/utils/askPortalMatcher.js +54 -0
  32. package/src/utils/autoApproveOptions.js +27 -0
  33. package/src/utils/avatarAnimationPostPass.js +49 -0
  34. package/src/utils/builtinPresets.js +18 -0
  35. package/src/utils/clearCheckpoint.js +7 -0
  36. package/src/utils/commandValidator.js +45 -0
  37. package/src/utils/contentFilter.js +620 -0
  38. package/src/utils/contextRaw.js +18 -0
  39. package/src/utils/contextSidebarNavigation.js +16 -0
  40. package/src/utils/contextTurns.js +117 -0
  41. package/src/utils/displayScaleHelper.js +47 -0
  42. package/src/utils/effectiveModel.js +13 -0
  43. package/src/utils/entry-slim.js +563 -0
  44. package/src/utils/entryCache.js +144 -0
  45. package/src/utils/errorReport.js +6 -0
  46. package/src/utils/fileExpandedPathsStorage.js +70 -0
  47. package/src/utils/fileIcons.jsx +36 -0
  48. package/src/utils/fileOpen.js +37 -0
  49. package/src/utils/formatters.js +48 -0
  50. package/src/utils/gitApi.js +50 -0
  51. package/src/utils/gitTreeBuilder.js +19 -0
  52. package/src/utils/helpers.js +846 -0
  53. package/src/utils/identityHeal.js +99 -0
  54. package/src/utils/imConnState.js +59 -0
  55. package/src/utils/imOrigin.js +24 -0
  56. package/src/utils/imTr.js +7 -0
  57. package/src/utils/imageCompress.js +83 -0
  58. package/src/utils/imageDownscale.js +28 -0
  59. package/src/utils/imageResize.js +112 -0
  60. package/src/utils/ingestPipeline.js +47 -0
  61. package/src/utils/lazyWithReload.js +53 -0
  62. package/src/utils/markdown.js +30 -0
  63. package/src/utils/markdownIncremental.js +39 -0
  64. package/src/utils/markdownProfiler.js +113 -0
  65. package/src/utils/mdExtensionDetect.js +64 -0
  66. package/src/utils/memoryLinkParser.js +44 -0
  67. package/src/utils/modalMask.js +15 -0
  68. package/src/utils/monotime.js +6 -0
  69. package/src/utils/pinnedMenu.js +55 -0
  70. package/src/utils/presetShortcuts.js +14 -0
  71. package/src/utils/projectAlias.js +135 -0
  72. package/src/utils/promptClassifier.js +112 -0
  73. package/src/utils/promptDetect.js +196 -0
  74. package/src/utils/promptNav.js +60 -0
  75. package/src/utils/ptyChunkBuilder.js +251 -0
  76. package/src/utils/quickMenuHoverIntent.js +35 -0
  77. package/src/utils/rateLimitParser.js +119 -0
  78. package/src/utils/readResultPool.js +106 -0
  79. package/src/utils/refreshCachedItemProp.js +30 -0
  80. package/src/utils/requestType.js +208 -0
  81. package/src/utils/resizeCalc.js +21 -0
  82. package/src/utils/resolveLocalized.js +17 -0
  83. package/src/utils/searchApi.js +47 -0
  84. package/src/utils/searchReplace.js +51 -0
  85. package/src/utils/seqResourceLoaders.js +79 -0
  86. package/src/utils/sessionManager.js +471 -0
  87. package/src/utils/sessionMerge.js +180 -0
  88. package/src/utils/skillModalController.js +111 -0
  89. package/src/utils/skillsParser.js +116 -0
  90. package/src/utils/slashCommandLabels.js +80 -0
  91. package/src/utils/splitDragCalc.js +73 -0
  92. package/src/utils/stickyBottomController.js +643 -0
  93. package/src/utils/svgSanitize.js +48 -0
  94. package/src/utils/systemTags.js +47 -0
  95. package/src/utils/tClaude.js +20 -0
  96. package/src/utils/teamModalBuilder.js +351 -0
  97. package/src/utils/teamSessionParser.js +190 -0
  98. package/src/utils/teammateAvatars.js +194 -0
  99. package/src/utils/teammateDetector.js +121 -0
  100. package/src/utils/termDiag.js +114 -0
  101. package/src/utils/terminalClipboard.js +117 -0
  102. package/src/utils/terminalWriteQueue.js +398 -0
  103. package/src/utils/thinkingMerge.js +49 -0
  104. package/src/utils/toolCatalog.js +29 -0
  105. package/src/utils/toolResultBuilder.js +393 -0
  106. package/src/utils/toolResultClassifier.js +16 -0
  107. package/src/utils/toolResultCore.js +167 -0
  108. package/src/utils/toolsDiff.js +44 -0
  109. package/src/utils/toolsXmlFormatter.js +3 -0
  110. package/src/utils/ultraplanController.js +109 -0
  111. package/src/utils/ultraplanExperts.js +86 -0
  112. package/src/utils/ultraplanTemplates.js +154 -0
  113. package/src/utils/userImageRefs.js +49 -0
  114. package/src/utils/v3Assembler.js +112 -0
  115. package/src/utils/v3Rows.js +48 -0
  116. package/src/utils/voicePackPlayer.js +233 -0
  117. package/src/utils/webSearchGrouping.js +111 -0
  118. package/src/utils/workflowFormat.js +52 -0
  119. package/src/utils/workflowRuns.js +118 -0
  120. package/src/utils/workflowStore.js +94 -0
  121. package/dist/assets/App-DTkacfRW.js +0 -2
  122. package/dist/assets/index-Ac8vnV3v.js +0 -2
  123. package/dist/assets/seqResourceLoaders-Cd_TAXTb.js +0 -2
@@ -0,0 +1,846 @@
1
+ import { isSkillText, isMainAgent } from './contentFilter.js';
2
+ import { restoreSlimmedEntry } from './entry-slim.js';
3
+ import { formatToolAsXml } from './toolsXmlFormatter.js';
4
+ import modelClaudeUrl from '../img/model-claude.svg';
5
+ import modelOpenaiUrl from '../img/model-openai.svg';
6
+ import modelGeminiUrl from '../img/model-gemini.svg';
7
+ import modelQwenUrl from '../img/model-qwen.svg';
8
+ import modelKimiUrl from '../img/model-kimi.svg';
9
+ import modelGlmUrl from '../img/model-glm.svg';
10
+ import modelMinimaxUrl from '../img/model-minimax.svg';
11
+ import modelDeepseekUrl from '../img/model-deepseek.svg';
12
+ import modelClaudeAnimatedSvg from '../img/claude/writing.svg?raw';
13
+
14
+ // 上下文窗口规则唯一事实源在 server/lib/context-rules.js(CLIENT-SAFE,无 node 依赖,
15
+ // Vite 跨目录打包,先例见 toolsXmlFormatter.js)。前端一律经此处 re-export 取用,
16
+ // 保证血条档位/纠偏/usage 分子与服务端 SSE 路径同源,杜绝三份规则表漂移的旧病。
17
+ export {
18
+ getModelMaxTokens,
19
+ adaptContextWindow,
20
+ sumCacheCreationTokens,
21
+ sumUsageInputTokens,
22
+ sumUsageContextTokens,
23
+ } from '../../server/lib/context-rules.js';
24
+ import { classifyContextWindow, adaptContextWindow } from '../../server/lib/context-rules.js';
25
+
26
+ // getEffectiveModel moved to ./effectiveModel.js (pure, node-testable — sessionMerge/sessionManager
27
+ // import it without helpers' Vite-only svg imports); re-exported here to keep import paths stable.
28
+ export { getEffectiveModel } from './effectiveModel.js';
29
+ import { getEffectiveModel } from './effectiveModel.js';
30
+
31
+ // 校准尺寸 → token 数集中映射。
32
+ // 注意:扩档(如加 500K)必须同步改 src/config.json 的 calibrationModels(label/value)
33
+ // 与 i18n 文案;只改这里 UI 不会出现新选项。
34
+ const CALIBRATION_TOKEN_MAP = {
35
+ '1m': 1000000,
36
+ '200k': 200000,
37
+ };
38
+
39
+ /**
40
+ * 把用户在 popover 里选的"上下文窗口尺寸"换算成 token 数。
41
+ * - calibrationModel === '1m' / '200k':直接查表
42
+ * - 其它(含 'auto' 与所有兜底情况):按以下优先级判定
43
+ * 1) lastMainAgent.model 真实信号(但跳过 haiku — 启动期 init ping 噪声)
44
+ * 2) projectModelHint(来自 ~/.claude.json projects[cwd].lastModelUsage,
45
+ * 由 /api/claude-settings 与 workspace_started SSE 携带;反映用户在
46
+ * claude 里实际偏好的 model)
47
+ * 3) 冷启动兜底 → 1M(用户规约)
48
+ * 分类规则(context-rules.classifyContextWindow,前后端同源):
49
+ * · model 含 1m 子串(含 [1m] 后缀与裸 1m,如 deepseek-v3-1m)→ 1M
50
+ * · 其余按统一档位表归并:opus-4-6+/mythons/fable-5/deepseek-v4 → 1M;
51
+ * haiku/旧 opus(4-0/4-1/4-5)/3-opus/裸 claude/gpt/deepseek 等 → 200K 桶
52
+ * haiku 跳过的代价:纯 haiku 子任务期会落到 projectModelHint;该路径下 hint
53
+ * 缺失时仍按冷启动 1M 处理,不会突然变 200K。
54
+ *
55
+ * 不变量:永远返回 1000000 或 200000;调用方可放心当真值用。
56
+ */
57
+ export function resolveCalibrationTokens(calibrationModel, lastMainAgent, projectModelHint = null) {
58
+ const direct = CALIBRATION_TOKEN_MAP[calibrationModel];
59
+ if (direct) return direct;
60
+ const lastModel = lastMainAgent ? getEffectiveModel(lastMainAgent) : null;
61
+ // 优先用真实 mainAgent 信号;haiku 一律视为 init ping 噪声,跳过
62
+ if (typeof lastModel === 'string' && lastModel && !/haiku/i.test(lastModel)) {
63
+ return classifyContextWindow(lastModel);
64
+ }
65
+ // 回落 1:claude 自己存的项目偏好 model
66
+ if (typeof projectModelHint === 'string' && projectModelHint) {
67
+ return classifyContextWindow(projectModelHint);
68
+ }
69
+ // 回落 2:冷启动 1M
70
+ return 1000000;
71
+ }
72
+
73
+ // Pre-1.6.243 stored values (calibrated per concrete model) → the new size buckets;
74
+ // keeps upgrading users' calibration semantics instead of degrading them to 'auto'.
75
+ // (If a stale localStorage value misses both this map and calibrationModels, fall back to 'auto'.)
76
+ export const LEGACY_CALIBRATION_MIGRATION = {
77
+ 'opus-4.7-1m': '1m',
78
+ 'sonnet-4.6': '200k',
79
+ 'glm5': '200k',
80
+ 'kimi-k2.5': '200k',
81
+ 'minimax-2.1': '200k',
82
+ 'Qwen 3.5': '200k',
83
+ };
84
+
85
+ /**
86
+ * Read the persisted calibration choice ('ccv_calibrationModel'), applying the
87
+ * legacy-value migration and validating against the configured size options.
88
+ * Shared by the desktop header and the mobile shell so both apply identical
89
+ * migration semantics. Safe in non-browser environments (returns 'auto').
90
+ */
91
+ export function readCalibrationModel(calibrationModels) {
92
+ let raw = 'auto';
93
+ try {
94
+ raw = localStorage.getItem('ccv_calibrationModel') || 'auto';
95
+ } catch {}
96
+ const migrated = LEGACY_CALIBRATION_MIGRATION[raw] || raw;
97
+ return calibrationModels.some(m => m.value === migrated) ? migrated : 'auto';
98
+ }
99
+
100
+ /**
101
+ * Single source of truth for the context-bar percentage. Extracted from the
102
+ * desktop header math so mobile cannot drift (mobile previously ignored the
103
+ * user's calibration choice and trusted the server's used_percentage raw).
104
+ *
105
+ * Inputs:
106
+ * - calibrationModel: '1m' | '200k' | 'auto' (user's popover choice)
107
+ * - lastMainAgent / lastTotalTokens / lastInputTokens: the caller's reverse
108
+ * scan for the latest main-agent usage (tokens via sumUsageContextTokens /
109
+ * sumUsageInputTokens)
110
+ * - projectModelHint: claudeProjectModel from app state (see resolveCalibrationTokens)
111
+ * - contextWindow: server-pushed { used_percentage, context_window_size, total_input_tokens }
112
+ *
113
+ * Callers keep local-log gating, contextBarLocked/Optimistic handling and the
114
+ * _lastContextPercent memo outside this function.
115
+ */
116
+ export function computeContextPercent({ calibrationModel, lastMainAgent, projectModelHint = null, contextWindow, lastTotalTokens = 0, lastInputTokens = 0 }) {
117
+ let calibrationTokens = resolveCalibrationTokens(calibrationModel, lastMainAgent, projectModelHint);
118
+ // Adaptive correction: a window classified as 200K that has demonstrably held
119
+ // more than 200K of input must actually be 1M. Skipped when the user pinned '200k'.
120
+ if (calibrationModel !== '200k') {
121
+ const usedContextTokens = lastInputTokens > 0 ? lastInputTokens : (contextWindow?.total_input_tokens || 0);
122
+ calibrationTokens = adaptContextWindow(calibrationTokens, usedContextTokens);
123
+ }
124
+ let percent = 0;
125
+ if (contextWindow?.used_percentage != null) {
126
+ if (lastTotalTokens > 0) {
127
+ percent = Math.round(lastTotalTokens / calibrationTokens * 100);
128
+ } else {
129
+ // No usable usage on the last main agent: rebase the server percentage
130
+ // from its original window onto the calibrated one.
131
+ const origMax = contextWindow.context_window_size || 200000;
132
+ percent = Math.round(contextWindow.used_percentage * origMax / calibrationTokens);
133
+ }
134
+ } else if (lastMainAgent && lastTotalTokens > 0) {
135
+ percent = Math.round(lastTotalTokens / calibrationTokens * 100);
136
+ }
137
+ return Math.min(100, Math.max(0, percent));
138
+ }
139
+
140
+ /**
141
+ * Per-message model info resolver. 1v1 严格遵从:每条消息的 modelInfo 来自
142
+ * 它自己那条 request 的 effectiveModel,不回落到全局最新 model。
143
+ *
144
+ * 关键 off-by-one:assistant message 的 _timestamp 在 _processEntries 中
145
+ * 被赋值为【下一轮 entry】的 ts(详见 src/AppBase.jsx:184-186 与
146
+ * src/utils/sessionMerge.js:44/50),所以 producer req idx = idx - 1;
147
+ * user message 的 _timestamp 与生产者 req 一致,producer idx = idx。
148
+ *
149
+ * mid-session 启动边界:cc-viewer 在 mid-conversation 才启动时,第一个 entry
150
+ * 的 body.messages 已包含历史 [user, assistant, user, ...],那条 assistant
151
+ * 的 _timestamp = T0、idx=0、producerIdx=-1。其真实 producer 不在 server 视野内,
152
+ * 退而求其次用当前 entry 的 model(modelNameByReqIdx[0])作为最佳估计 ——
153
+ * 同一段对话通常 model 一致,比显示中性 'MainAgent' 更接近用户预期。
154
+ *
155
+ * @param {string|null} ts message._timestamp
156
+ * @param {string} role original msg.role ('user' | 'assistant');ChatMessage 内部
157
+ * 派生 role(plan-prompt / teammate-message 等)不传入此函数。
158
+ * @param {Object} tsToIndex map req.timestamp → request 数组下标
159
+ * @param {Array<string|null>} modelNameByReqIdx 按 request idx 存储的"当时活跃" model name
160
+ * @returns {Object|null} modelInfo (svg/color/name/...) 或 null(caller 应渲染中性 'MainAgent' 头像)
161
+ */
162
+ export function resolveProducerModelInfo(ts, role, tsToIndex, modelNameByReqIdx) {
163
+ if (!ts) return null;
164
+ const idx = tsToIndex[ts];
165
+ if (idx == null) return null;
166
+ const producerIdx = role === 'assistant' ? Math.max(idx - 1, 0) : idx;
167
+ const name = modelNameByReqIdx[producerIdx];
168
+ return name ? getModelInfo(name) : null;
169
+ }
170
+
171
+ /**
172
+ * Incrementally append cache loss entries to an existing map.
173
+ * Scans from startIndex to end; uses prevMainAgent as comparison baseline.
174
+ * Returns the last mainAgent entry encountered (for next incremental call).
175
+ */
176
+ export function appendCacheLossMap(map, requests, startIndex, prevMainAgent) {
177
+ for (let i = startIndex; i < requests.length; i++) {
178
+ const req = requests[i];
179
+ if (!isMainAgent(req)) continue;
180
+
181
+ if (prevMainAgent) {
182
+ const usage = req.response?.body?.usage;
183
+ if (usage) {
184
+ const cacheCreate = usage.cache_creation_input_tokens || 0;
185
+ const cacheRead = usage.cache_read_input_tokens || 0;
186
+ if (cacheCreate > 0 && cacheCreate > cacheRead) {
187
+ const prev = prevMainAgent._slimmed
188
+ ? restoreSlimmedEntry(prevMainAgent, requests)
189
+ : prevMainAgent;
190
+ const loss = _computeCacheLoss(prev, req);
191
+ if (loss) map.set(i, loss);
192
+ }
193
+ }
194
+ }
195
+ prevMainAgent = req;
196
+ }
197
+ return prevMainAgent;
198
+ }
199
+
200
+ /**
201
+ * Pre-compute cache loss analysis for all requests in a single forward pass.
202
+ * Returns Map<index, { reason, reasons }> — O(n) total instead of O(n²).
203
+ */
204
+ export function buildCacheLossMap(requests) {
205
+ const map = new Map();
206
+ appendCacheLossMap(map, requests, 0, null);
207
+ return map;
208
+ }
209
+
210
+ // WeakMap cache for stringified system/tools — avoids mutating request objects
211
+ const _jsonCache = new WeakMap();
212
+ function _getCachedJson(entry, key) {
213
+ let cache = _jsonCache.get(entry);
214
+ if (!cache) { cache = {}; _jsonCache.set(entry, cache); }
215
+ if (cache[key] === undefined) {
216
+ const body = stripPrivateKeys(entry.body);
217
+ cache[key] = JSON.stringify(body?.[key]);
218
+ }
219
+ return cache[key];
220
+ }
221
+
222
+ /** Internal: compare two adjacent MainAgent entries for cache loss reason. */
223
+ function _computeCacheLoss(prevMainAgent, req) {
224
+ const gap = req.timestamp - prevMainAgent.timestamp;
225
+ if (gap > 5 * 60 * 1000) return { reason: 'ttl', reasons: ['ttl'] };
226
+
227
+ const prev = stripPrivateKeys(prevMainAgent.body);
228
+ const curr = stripPrivateKeys(req.body);
229
+ if (!prev || !curr) return { reason: 'key_change', reasons: ['key_change'] };
230
+
231
+ const reasons = [];
232
+
233
+ if (prev.model !== curr.model) reasons.push('model_change');
234
+
235
+ // Use WeakMap cache to avoid re-serialization without mutating entry objects
236
+ const prevSystem = _getCachedJson(prevMainAgent, 'system');
237
+ const currSystem = _getCachedJson(req, 'system');
238
+ if (prevSystem !== currSystem) reasons.push('system_change');
239
+
240
+ const prevTools = _getCachedJson(prevMainAgent, 'tools');
241
+ const currTools = _getCachedJson(req, 'tools');
242
+ if (prevTools !== currTools) reasons.push('tools_change');
243
+
244
+ const prevMsgs = prev.messages || [];
245
+ const currMsgs = curr.messages || [];
246
+ if (currMsgs.length < prevMsgs.length) {
247
+ reasons.push('msg_truncated');
248
+ } else {
249
+ const prefixLen = Math.min(prevMsgs.length, currMsgs.length);
250
+ let prefixMatch = true;
251
+ for (let j = 0; j < prefixLen; j++) {
252
+ if (JSON.stringify(prevMsgs[j]) !== JSON.stringify(currMsgs[j])) {
253
+ prefixMatch = false;
254
+ break;
255
+ }
256
+ }
257
+ if (!prefixMatch) reasons.push('msg_modified');
258
+ }
259
+
260
+ if (reasons.length === 0) reasons.push('key_change');
261
+ return { reason: reasons[0], reasons };
262
+ }
263
+
264
+ export function escapeHtml(str) {
265
+ if (!str) return '';
266
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
267
+ }
268
+
269
+ export function truncateText(text, maxLen) {
270
+ if (!text) return '';
271
+ return text.length > maxLen ? text.substring(0, maxLen) + '...' : text;
272
+ }
273
+
274
+ // 文件内私有,仅供 L755 自调用使用。公共定义在 src/utils/toolResultCore.js,
275
+ // 跨模块请 import 那里的版本(本文件因 SVG/i18n 依赖链不能被 node --test 直接加载)。
276
+ function extractToolResultText(toolResult) {
277
+ if (!toolResult.content) return String(toolResult.content ?? '');
278
+ if (typeof toolResult.content === 'string') return toolResult.content;
279
+ if (Array.isArray(toolResult.content)) {
280
+ return toolResult.content
281
+ .filter(b => b.type === 'text')
282
+ .map(b => b.text)
283
+ .join('\n');
284
+ }
285
+ return JSON.stringify(toolResult.content);
286
+ }
287
+
288
+ // 模型配置:匹配规则、显示名、品牌色、SVG logo。
289
+ // 字段约定:
290
+ // match : 匹配模型名的正则(按数组顺序首次命中即止)
291
+ // name : 用于 avatar label 的短名
292
+ // color : avatar 背景色(CSS 变量或 literal)
293
+ // svg : 必填。静态 logo 字符串,由 dangerouslySetInnerHTML 渲染
294
+ // svgAnimated : 可选。流式响应期间显示的动画 logo;缺省时 ModelAvatar 自动回退到 svg。
295
+ // 添加新动画版只需设置此字段,其他消费者无感。
296
+ // Claude starburst logo path — single source for the two in-file renderings (the
297
+ // MODEL_PROVIDERS Claude avatar and getSvgAvatar('system')); a logo revision must not
298
+ // be able to silently diverge between them. src/img/model-claude.svg is a build asset
299
+ // and keeps its own copy.
300
+ const CLAUDE_STARBURST_PATH = 'M198.4 678.4l198.4-115.2 6.4-12.8H243.2l-96-6.4-102.4-6.4-19.2-6.4-25.6-25.6v-12.8l19.2-12.8h32l64 6.4 96 6.4 70.4 6.4L384 512h19.2V492.8l-6.4-6.4-102.4-64-108.8-76.8-51.2-38.4-32-19.2-19.2-25.6-6.4-38.4 32-32h44.8l38.4 32 83.2 64L384 364.8l12.8 12.8 6.4-6.4-6.4-12.8L339.2 256l-64-108.8-25.6-38.4-6.4-25.6c0-12.8-6.4-19.2-6.4-32l32-44.8 19.2-6.4 44.8 6.4 19.2 12.8 25.6 57.6 44.8 96 64 128 19.2 38.4 6.4 38.4 6.4 12.8h6.4V384l6.4-70.4 12.8-89.6 12.8-115.2 6.4-32 19.2-38.4 32-19.2 25.6 12.8 19.2 32v19.2l-32 70.4-19.2 121.6-19.2 83.2h6.4l12.8-12.8 44.8-57.6 70.4-89.6 32-32 38.4-38.4 25.6-19.2h44.8l32 51.2-12.8 51.2-51.2 57.6-38.4 51.2-51.2 70.4-38.4 57.6v6.4h6.4l121.6-25.6 64-12.8 76.8-12.8 38.4 19.2 6.4 19.2-12.8 32-83.2 19.2-96 19.2-147.2 32 64 6.4h96l128 6.4 32 19.2 25.6 38.4-6.4 19.2-51.2 25.6-70.4-12.8-160-38.4-57.6-12.8h-6.4v6.4l44.8 44.8 83.2 76.8 108.8 102.4 6.4 25.6-12.8 19.2h-12.8l-96-70.4-38.4-32-83.2-70.4h-6.4v6.4l19.2 25.6 102.4 147.2 6.4 44.8-6.4 12.8-25.6 6.4-25.6-6.4-57.6-83.2-64-83.2-51.2-83.2-6.4 6.4-25.6 307.2-12.8 12.8-32 12.8-25.6-19.2-12.8-32 12.8-64 19.2-83.2 12.8-64 12.8-83.2 6.4-25.6h-6.4l-64 83.2-96 128-70.4 76.8-19.2 6.4-32-12.8v-25.6l19.2-25.6 102.4-128 64-83.2 38.4-51.2v-6.4l-268.8 172.8-51.2 12.8-19.2-19.2v-32l12.8-12.8 76.8-57.6z m0 0';
301
+
302
+ const MODEL_PROVIDERS = [
303
+ {
304
+ match: /claude/i,
305
+ name: 'Claude',
306
+ color: 'var(--bg-model-avatar)',
307
+ svg: `<svg t="1771495509570" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1570" width="200" height="200"><path d="${CLAUDE_STARBURST_PATH}" fill="#D97757" p-id="1571"></path></svg>`,
308
+ svgAnimated: modelClaudeAnimatedSvg,
309
+ },
310
+ {
311
+ match: /gpt|o1|o3|o4/i,
312
+ name: 'OpenAI',
313
+ color: 'var(--bg-model-avatar)',
314
+ svg: '<svg t="1771495569354" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2752" width="200" height="200"><path d="M565.265178 954.519092c-22.289934 0-48.400999-8.151747-67.952455-14.838727a103.425292 103.425292 0 0 1-26.875292-11.272338c-12.737105-7.769634-15.411897-8.342804-19.806198-24.773669 21.971506-5.158528 81.581157-41.905075 103.871091-55.342721 148.896757-89.159735 119.028246-10.444426 119.028246-364.981743 15.029784 3.566389 82.791182 32.415932 82.791182 57.316972 0 133.102747 20.570425 273.847757-52.604243 354.91943-22.799418 25.47421-91.834527 58.972796-138.388646 58.972796z m-433.061569-299.321966c258.626916 136.860193 184.369594 157.048504 357.721593 52.094759 44.579867-27.193719 90.433445-49.292596 132.593263-77.568969v101.896839c-95.528287 22.226248-272.319304 227.038896-419.050754 100.686815l-25.856323-25.155782c-37.319718-43.943012-45.344094-72.410442-45.344094-151.889977z m375.744596-19.105658c-19.933569-13.310275-79.479535-51.330533-101.896839-57.316972v-133.739602a1158.312326 1158.312326 0 0 0 101.896839-57.316973c43.943012 10.189684 70.690933 47.063603 114.633945 57.316973v127.371049c-17.831947 12.10025-95.846715 58.718054-114.633945 63.749211z m-426.693016-178.31947c0-63.685525-4.26693-90.306074 38.84817-145.776166 23.945757-30.88748 47.509402-39.612396 82.090641-57.953828v261.110652c44.134069 23.372588 83.873836 49.037854 129.345301 74.448379l131.765351 78.396881c-59.545966 15.921381-63.685525 61.32916-109.602788 33.753328-104.699003-62.730242-272.38299-129.345301-272.38299-243.91556z m866.123138 127.37105c0 79.543221-47.573087 161.188063-121.002497 178.31947v-165.582365c0-82.791182 9.743885-84.574377-48.910483-116.608196L565.265178 362.371082c15.79401-23.62733 22.608361-19.105657 48.146256-34.835982 41.714019-25.47421 39.039227-16.112438 117.053995 28.785857 94.190891 54.196382 216.849212 100.559444 216.849212 228.885777z m-541.326961-197.425127v-95.528287c43.751956-23.181531 90.688187-50.94842 133.357489-76.804743 82.154327-49.547338 95.528287-63.303412 185.006449-63.303412 48.464684 0 102.533695 36.746548 125.651541 65.405034 42.223503 52.22213 39.930824 92.662439 39.930824 151.125751-31.078536-16.494551-192.393971-121.002497-222.899337-121.002498s-229.650003 123.422547-261.110652 140.108155z m-50.94842 159.213812c-16.36718-10.95391-63.112355-39.99451-82.791183-44.579867 0-168.320842-33.880699-314.606493 67.952455-390.519639 56.043262-41.714019 113.169178-53.814269 181.376375-30.696423 25.47421 8.661231 35.536523 20.888852 56.36169 26.429493-11.781822 16.048752-80.68956 50.311565-102.278953 63.303412-154.564769 93.235608-120.620384 7.451206-120.620384 376.063024z m-292.953415 121.002498c0 169.148754 115.143429 280.853165 274.293556 273.847756 59.800708-2.674792 26.811606-7.705949 69.417222 25.47421 97.820966 76.741057 228.822091 73.747838 319.637649 1.018969a251.939936 251.939936 0 0 0 52.604244-55.661149c58.20857-85.274918-10.95391-45.598836 81.96327-83.619094 130.236898-53.241099 199.399378-217.358696 128.64476-355.428914-27.448461-53.559526-40.249252-28.849543-28.276373-104.699003 18.723544-118.582447-63.176041-230.032116-157.621674-269.771884-98.90362-41.586648-129.090559 12.737105-178.892639-37.574459A161.888604 161.888604 0 0 0 580.103905 28.913674C474.003821-27.766443 331.284559 1.528898 258.428319 93.681853c-81.326415 102.979494 9.489143 54.705866-92.407697 98.648878C15.149614 257.353652-33.251385 439.175825 41.579107 561.06992c56.807488 92.598753 20.570425 4.967471 20.570424 106.800625z" fill="#EFA474" p-id="2753"></path></svg>',
315
+ },
316
+ {
317
+ match: /gemini/i,
318
+ name: 'Gemini',
319
+ color: 'var(--bg-model-avatar)',
320
+ svg: '<svg t="1771495622863" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4556" width="200" height="200"><path d="M960 512.896A477.248 477.248 0 0 0 512.896 960h-1.792A477.184 477.184 0 0 0 64 512.896v-1.792A477.184 477.184 0 0 0 511.104 64h1.792A477.248 477.248 0 0 0 960 511.104z" fill="#448AFF" p-id="4557"></path></svg>',
321
+ },
322
+ {
323
+ match: /qwen/i,
324
+ name: 'Qwen',
325
+ color: 'var(--bg-model-avatar)',
326
+ svg: '<svg t="1771495640319" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5634" width="200" height="200"><path d="M537.782857 57.161143c16.749714 29.44 33.426286 58.953143 50.102857 88.502857a7.68 7.68 0 0 0 6.692572 3.913143h236.873143c7.424 0 13.750857 4.681143 19.017142 13.933714l62.061715 109.677714c8.082286 14.372571 10.24 20.370286 1.024 35.693715a2262.893714 2262.893714 0 0 0-32.438857 55.478857l-15.652572 28.086857c-4.534857 8.338286-9.508571 11.922286-1.718857 21.833143l113.152 197.851428c7.314286 12.8 4.754286 21.065143-1.828571 32.841143-18.651429 33.499429-37.632 66.742857-56.978286 99.84-6.765714 11.593143-14.994286 16.018286-29.001143 15.798857a1833.398857 1833.398857 0 0 0-99.291429 0.658286 4.242286 4.242286 0 0 0-3.437714 2.157714 24571.830857 24571.830857 0 0 1-115.419428 202.24c-7.204571 12.507429-16.201143 15.469714-30.939429 15.506286-42.532571 0.146286-85.430857 0.182857-128.731429 0.109714a22.930286 22.930286 0 0 1-19.821714-11.556571l-56.978286-99.108571a3.84 3.84 0 0 0-3.547428-2.121143h-218.331429a76.288 76.288 0 0 1-34.377143-3.913143l-68.388571-118.198857a23.149714 23.149714 0 0 1-0.073143-23.04l51.492572-90.441143a8.448 8.448 0 0 0 0-8.411429 23594.496 23594.496 0 0 1-80.018286-139.593143L47.542857 425.325714c-6.838857-13.202286-7.387429-21.138286 4.022857-41.179428 19.858286-34.669714 39.570286-69.302857 59.209143-103.936 5.632-9.984 12.946286-14.262857 24.905143-14.262857 36.827429-0.182857 73.654857-0.182857 110.445714-0.073143 1.901714 0 3.657143-1.024 4.571429-2.669715l119.734857-208.859428a20.845714 20.845714 0 0 1 17.993143-10.496c22.381714-0.036571 44.946286 0 67.547428-0.256l43.410286-0.987429c14.555429-0.109714 30.866286 1.389714 38.4 14.518857z m-146.432 17.188571a2.56 2.56 0 0 0-2.194286 1.28l-122.331428 213.942857a6.692571 6.692571 0 0 1-5.741714 3.364572H138.788571c-2.377143 0-2.962286 1.060571-1.755428 3.145143l247.917714 433.334857c1.060571 1.792 0.548571 2.633143-1.462857 2.669714l-119.222857 0.658286a9.289143 9.289143 0 0 0-8.557714 4.937143l-56.32 98.56c-1.865143 3.328-0.914286 5.046857 2.925714 5.046857l243.858286 0.365714c1.974857 0 3.401143 0.841143 4.425142 2.56l59.867429 104.704c1.974857 3.474286 3.913143 3.510857 5.924571 0l213.577143-373.76 33.426286-58.953143a2.340571 2.340571 0 0 1 4.096 0l60.745143 107.958857a5.193143 5.193143 0 0 0 4.571428 2.633143l117.906286-0.841143a1.718857 1.718857 0 0 0 1.462857-2.56l-123.721143-217.014857a4.608 4.608 0 0 1 0-4.827428l12.507429-21.613715 47.798857-84.370285c1.024-1.755429 0.512-2.633143-1.499428-2.633143H392.521143c-2.486857 0-3.108571-1.097143-1.828572-3.291429l61.184-106.861714a4.571429 4.571429 0 0 0 0-4.864l-58.258285-102.253714a2.56 2.56 0 0 0-2.267429-1.316572z m268.361143 342.198857c1.974857 0 2.486857 0.841143 1.462857 2.56l-35.474286 62.500572-111.542857 195.620571a2.377143 2.377143 0 0 1-2.121143 1.243429 2.486857 2.486857 0 0 1-2.121142-1.243429l-147.346286-257.353143c-0.841143-1.462857-0.402286-2.230857 1.206857-2.304l9.216-0.512 286.793143-0.512h-0.073143z" fill="#7D5DED" p-id="5635"></path></svg>',
327
+ },
328
+ {
329
+ match: /kimi|moonshot/i,
330
+ name: 'Kimi',
331
+ color: 'var(--bg-model-avatar)',
332
+ svg: '<svg t="1771495664798" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6649" width="200" height="200"><path d="M731.062857 590.262857v318.317714h-148.589714V443.977143a146.285714 146.285714 0 0 1-146.285714 146.285714l-214.491429-0.036571v318.354285H73.142857V165.814857h148.553143v275.858286h180.882286l119.771428-275.858286h167.753143l-67.84 156.269714a255.524571 255.524571 0 0 1-106.678857 119.588572h66.925714a148.553143 148.553143 0 0 1 148.516572 148.589714z m120.758857-473.965714a99.035429 99.035429 0 0 1 0 198.070857h-99.035428V215.332571a99.035429 99.035429 0 0 1 99.035428-99.035428z" fill="currentColor" p-id="6650"></path></svg>',
333
+ },
334
+ {
335
+ match: /glm|chatglm/i,
336
+ name: 'GLM',
337
+ color: 'var(--bg-model-avatar)',
338
+ svg: '<svg data-name="glm" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 217.2 200"> <defs> <style> .st0 { fill: currentColor; } </style> </defs> <path class="st0" d="M86.8,190.4H0L131.3,4.8h85.8L86.8,190.4h0ZM212.8,190h-107.9l14.9-21.6c2.3-3.3,6-5.2,9.9-5.2h83.1v26.8h0ZM99.4,26.3c-2.3,3.3-6,5.2-9.9,5.2H6.4V4.8h107.9l-14.9,21.6Z"/> </svg>',
339
+ },
340
+ {
341
+ match: /minimax|abab/i,
342
+ name: 'MiniMax',
343
+ color: 'var(--bg-model-avatar)',
344
+ svg: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 8l4-4 4 4 4-4 4 4v8l-4 4-4-4-4 4-4-4z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>',
345
+ },
346
+ {
347
+ match: /deepseek/i,
348
+ name: 'DeepSeek',
349
+ color: 'var(--bg-model-avatar)',
350
+ svg: '<svg t="1771495947541" class="icon" viewBox="0 0 1391 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11539" width="200" height="200"><path d="M1326.70201318 112.02090779c-12.94676356-6.47485468-18.53640721 5.86654827-26.0997327 12.13814316-2.57756953 2.02376031-4.77807747 4.65435413-6.9785854 7.08168819-18.91788751 20.63528526-41.02017805 34.19182812-69.92725218 32.57311444-42.23384508-2.42733405-78.29772513 11.12773591-110.16384908 44.10589703-6.77679853-40.66668282-29.28560862-64.94444203-63.55255448-80.52327231-17.95608583-8.09209545-36.06388005-16.1856638-48.63358202-33.78825438-8.74900746-12.5431898-11.12773591-26.50330641-15.52727888-40.26016327-2.7823022-8.29535522-5.56460442-16.79249731-14.92044538-18.20942411-10.19244639-1.61871367-14.16337637 7.08168818-18.15934561 14.36516325-15.93232553 29.74073376-22.12880269 62.51710799-21.49692994 95.69705596 1.36684831 74.65672404 32.27117059 134.13819156 93.62469006 176.42358804 6.9800583 4.8546681 8.77551959 9.71080912 6.57501165 16.79102439-4.17271686 14.56695011-9.18056624 28.7303265-13.55506995 43.29727663-2.78377509 9.30723537-6.95501906 11.32952278-16.74241882 7.28347507-33.66158524-14.36516325-62.745407-35.60875491-88.46513227-61.30196806-43.62425973-43.09548975-83.0772755-90.64060097-132.26613965-127.86659667a581.67054343 581.67054343 0 0 0-35.07703915-24.48101901c-50.20074429-49.77065841 6.57501167-90.63912807 19.72650789-95.49526908 13.73181759-5.05792788 4.77955037-22.4572587-39.65480265-22.25547181s-85.07599655 15.3770434-136.87041526 35.60875491c-7.58541892 3.03416756-15.55379103 5.25971475-23.69596499 7.08168819-47.03990759-9.10544849-95.84876434-11.12773591-146.8596019-5.26118765-96.02551195 10.92594905-172.73103555 57.25739323-229.12678412 136.36373875-67.72674425 95.09022244-83.68558192 203.13015422-64.13582165 315.82149435 20.48504978 118.76262107 79.88992666 217.09027084 171.13736114 293.97106918 94.6350973 79.71317902 203.61031862 118.76262107 327.93607117 111.27735913 75.51542292-4.45256725 159.57953934-14.77020989 254.41642354-96.71040901 23.92426399 12.13961607 49.03715574 16.99575708 90.66564019 20.63675815 32.09295007 3.03416756 62.97223311-1.61871367 86.87145788-6.67664152 37.45429471-8.09209545 34.84874013-43.49906349 21.31870939-49.97244528-109.7838417-52.19946535-85.68283009-30.95587367-107.58333373-48.15194474 55.78891504-67.37324899 139.85303146-137.3770918 172.73103555-364.17669887 2.60408167-18.00616433 0.40357375-29.33568711 0-43.90263724-0.20325977-8.90218873 1.76894914-12.34287584 11.75960867-13.35328309 27.49014733-3.23742733 54.19671351-10.92594905 78.70277179-24.68280588 71.11440706-39.65480265 99.81822142-104.80250446 106.59649284-182.89844271 1.01188015-11.9363563-0.20178686-24.27775924-12.56970195-30.54935413M706.87018452 814.87993343c-106.36966673-85.37941333-157.98733782-113.5014334-179.30604722-112.28776638-19.92829476 1.21366703-16.33737217 24.48101902-11.96286844 39.65480264 4.5777635 14.97199677 10.57098089 25.2896394 18.94145384 38.44113563 5.76786417 8.70040186 9.76383341 21.64863831-5.78995762 31.35944742-34.26841876 21.64863831-93.82647692-7.28347506-96.60730623-8.69892897-69.3454579-41.67856295-127.33635378-96.710409-168.1797842-171.97249368-39.45154288-72.43117687-62.33888747-150.1220685-66.13306983-233.07267483-1.01188015-20.02992464 4.77955037-27.11161282 24.27923214-30.75408682a235.19659216 235.19659216 0 0 1 77.91771774-2.02228741c108.59668681 16.18713669 201.05631542 65.75453531 278.54394728 144.25552022 44.23256614 44.71125762 77.69089163 98.12439001 112.1861365 150.32385536 36.64567431 55.43394691 76.0986901 108.24024576 126.29943438 151.53604947 17.72778682 15.17378365 31.86465106 26.7065662 45.41972102 35.20370829-40.84343042 4.65435413-108.99878765 5.66476139-155.60860936-31.96628094m51.00936468-334.83953883c0-8.90218873 6.9815312-15.98387693 15.75557791-15.98387693q2.98408908 0.05155138 5.36134463 1.01188017a15.85720779 15.85720779 0 0 1 10.16740714 14.97199676 15.78061715 15.78061715 0 0 1-15.73053865 15.98387691 15.60386953 15.60386953 0 0 1-15.55379103-15.98387691m158.39238446 82.95060635c-10.1423679 4.24930749-20.30830215 7.89178146-30.09570191 8.29535522-15.12370514 0.81009328-31.66286419-5.46150162-40.61513142-13.15002332-13.96011661-11.93782919-23.92573689-18.61447073-28.09845373-39.45301577-1.79546129-8.90218873-0.81009328-22.65904557 0.78505403-30.54935414 3.59092258-16.99575708-0.40504665-27.92023321-12.13961606-37.8343021-9.55910075-8.09356835-21.72522894-10.31911553-35.07703915-10.31911552-4.98281013 0-9.55910075-2.22407429-12.94823645-4.04604774a13.27669246 13.27669246 0 0 1-5.76639128-18.61299783c1.39041466-2.8323807 8.16868608-9.71228202 9.7623605-10.92594905 18.13136058-10.52090241 39.04649623-7.08021529 58.36795748 0.81009328 17.93104659 7.48526194 31.46107731 21.24359167 51.01083757 40.6666828 19.92829476 23.46913885 23.51921733 29.94399353 34.84874013 47.54511123 8.97877936 13.75685685 17.14746545 27.92023321 22.71206987 44.10442409 3.41270206 10.11732865-0.98684091 18.41121097-12.74644958 23.46913885" fill="#4D6BFE" p-id="11540"></path></svg>',
351
+ },
352
+ ];
353
+
354
+ // 按 modelName 缓存 modelInfo 对象,保证同名返回同引用。
355
+ // React.memo(ModelAvatar) 和 ChatMessage.shouldComponentUpdate 依赖引用稳定性跳过 re-render。
356
+ const _modelInfoCache = new Map();
357
+
358
+ export function getModelInfo(modelName) {
359
+ if (!modelName) return null;
360
+ const cached = _modelInfoCache.get(modelName);
361
+ if (cached) return cached;
362
+ let info = null;
363
+ for (const provider of MODEL_PROVIDERS) {
364
+ if (provider.match.test(modelName)) {
365
+ info = {
366
+ name: modelName.replace(/^claude-/, '').replace(/-\d{8,}$/, ''),
367
+ provider: provider.name,
368
+ color: provider.color,
369
+ svg: provider.svg,
370
+ svgAnimated: provider.svgAnimated || null,
371
+ };
372
+ break;
373
+ }
374
+ }
375
+ if (!info) {
376
+ // 未知模型,不设 svg,由 renderModelAvatar 使用兜底头像
377
+ info = { name: modelName, provider: modelName, color: 'var(--bg-model-avatar)', svg: null };
378
+ }
379
+ _modelInfoCache.set(modelName, info);
380
+ return info;
381
+ }
382
+
383
+ // 自动审批三态语义与档位的唯一事实源迁至 ./autoApproveOptions.js(node 可直接测试,
384
+ // 无 Vite 专属依赖);这里 re-export 维持既有 import 路径不变。
385
+ export { AUTO_APPROVE_INSTANT } from './autoApproveOptions.js';
386
+
387
+ export function getSvgAvatar(type) {
388
+ if (type === 'user') {
389
+ return '<svg viewBox="0 0 24 24"><path d="M12 12c2.7 0 4.8-2.1 4.8-4.8S14.7 2.4 12 2.4 7.2 4.5 7.2 7.2 9.3 12 12 12zm0 2.4c-3.2 0-9.6 1.6-9.6 4.8v2.4h19.2v-2.4c0-3.2-6.4-4.8-9.6-4.8z"/></svg>';
390
+ }
391
+ if (type === 'agent') {
392
+ return '<svg viewBox="0 0 24 24"><path d="M12 2a2 2 0 012 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 017 7h1a1 1 0 011 1v3a1 1 0 01-1 1h-1.17A7 7 0 0113 22h-2a7 7 0 01-6.83-3H3a1 1 0 01-1-1v-3a1 1 0 011-1h1a7 7 0 017-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 012-2zM9.5 13a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm5 0a1.5 1.5 0 100 3 1.5 1.5 0 000-3z"/></svg>';
393
+ }
394
+ // sub-agent: 根据类型匹配不同图标
395
+ if (type === 'sub-search' || type === 'sub-explore') {
396
+ return '<svg viewBox="0 0 1024 1024"><path d="M128.957606 76.608479v495.401496c0 20.428928-17.875312 38.304239-38.304239 38.304239S52.349127 592.438903 52.349127 572.009975V38.304239c0-20.428928 17.875312-38.304239 38.30424-38.304239h426.453865c20.428928 0 38.304239 17.875312 38.304239 38.304239s-17.875312 38.304239-38.304239 38.30424H128.957606z m127.680798 127.680798v495.401496c0 20.428928-17.875312 38.304239-38.304239 38.304239S180.029925 720.119701 180.029925 699.690773V165.985037c0-20.428928 17.875312-38.304239 38.30424-38.304239h426.453865c20.428928 0 38.304239 17.875312 38.304239 38.304239s-17.875312 38.304239-38.304239 38.30424H256.638404z m579.670823 291.112219v-150.663341H399.640898v515.830424h76.608479c0 38.304239 30.643392 68.947631 43.411471 89.376558h-145.55611c-33.197007 0-61.286783-28.089776-61.286783-63.840399V321.755611c0-35.750623 28.089776-63.840399 61.286783-63.840399h490.294264c33.197007 0 61.286783 28.089776 61.286784 63.840399V564.349127c-20.428928-22.982544-58.733167-56.179551-89.376559-68.947631zM486.46384 411.13217h255.361596c15.321696 0 25.53616 10.214464 25.53616 25.536159s-10.214464 25.53616-25.53616 25.53616H486.46384c-15.321696 0-25.53616-10.214464-25.536159-25.53616s10.214464-25.53616 25.536159-25.536159z m0 102.144638h102.144639c15.321696 0 25.53616 10.214464 25.536159 25.53616s-10.214464 25.53616-25.536159 25.536159h-102.144639c-15.321696 0-25.53616-10.214464-25.536159-25.536159s10.214464-25.53616 25.536159-25.53616z m503.062345 492.84788c-2.553616 2.553616-7.660848 10.214464-12.76808 10.214464-10.214464 10.214464-35.750623 10.214464-45.965088 0l-81.71571-71.501247c-28.089776 17.875312-63.840399 28.089776-99.591023 28.089776-102.144638 0-186.413965-84.269327-186.413965-186.413965s84.269327-186.413965 186.413965-186.413965 186.413965 84.269327 186.413965 186.413965c0 38.304239-10.214464 71.501247-30.643391 99.591022l81.715711 71.501247c12.76808 12.76808 12.76808 35.750623 2.553616 48.518703z m-240.039901-324.309227c-58.733167 0-104.698254 48.518703-104.698254 104.698255 0 58.733167 48.518703 104.698254 104.698254 104.698254s104.698254-48.518703 104.698255-104.698254c2.553616-58.733167-45.965087-104.698254-104.698255-104.698255z" fill="currentColor"/></svg>';
397
+ }
398
+ if (type === 'sub-plan') {
399
+ return '<svg viewBox="0 0 1024 1024"><path d="M674.507676 598.338391 287.079856 598.338391c-14.136975 0-25.638937 11.499915-25.638937 25.63689 0 14.137998 11.501962 25.640983 25.638937 25.640983l387.42782 0c14.137998 0 25.640983-11.501962 25.640983-25.640983C700.148659 609.838306 688.646698 598.338391 674.507676 598.338391zM794.746154 384.583029c-14.137998 0-25.640983 11.501962-25.640983 25.63996l0 494.299873c0 0.130983 0 0.280386-0.001023 0.433882-0.899486 0.279363-2.665713 0.644683-5.764284 0.644683L200.047664 905.601426c-4.312212 0-7.564279-2.971681-7.564279-6.913457L192.483384 191.851493c0-4.493338 0.974188-8.28878 1.811252-9.821693l373.337917 0c14.136975 0 25.63996-11.501962 25.63996-25.63996 0-14.136975-11.501962-25.638937-25.63996-25.638937l-374.069581 0c-30.336936 0-52.357462 25.696242-52.357462 61.099566l0 706.836477c0 32.086789 26.396183 58.19133 58.84113 58.19133l563.293223 0c34.119075 0 57.042157-21.041222 57.042157-52.356438L820.382021 410.224012C820.383044 396.086014 808.883129 384.583029 794.746154 384.583029zM674.507676 745.295394 287.079856 745.295394c-14.136975 0-25.638937 11.499915-25.638937 25.63689 0 14.137998 11.501962 25.640983 25.638937 25.640983l387.42782 0c14.137998 0 25.640983-11.501962 25.640983-25.640983C700.148659 756.795309 688.646698 745.295394 674.507676 745.295394zM879.595634 157.107177c-3.77293-14.409175-12.871145-28.250414-24.964578-37.975916l-43.827181-35.240619c-12.880355-10.352789-29.27679-16.290011-44.986587-16.290011-16.876366 0-31.594579 6.747681-41.439808 18.995633l-19.624966 24.381293c-6.380314 7.968485-7.987928 17.577331-5.222955 26.916023l-114.241904 142.063553-2.486634 3.090385-22.812565 85.303828c-9.749038 30.859845 4.449335 48.075949 13.806447 55.608505 6.783497 5.450129 14.468526 8.728802 22.851451 9.754155 0.984421 0.246617 2.125407 0.488117 3.440355 0.632403 1.003863 0.110517 2.053776 0.167822 3.118014 0.167822l0.014326 0c4.588505 0 10.047844-0.9967 17.180288-3.13541l2.877537-1.065262 83.402525-41.897226 119.874181-149.07934c0.440022 0.017396 0.880043 0.032746 1.320065 0.032746 9.218966 0 17.411555-3.862981 23.077601-10.892071l19.562545-24.342408C880.924909 191.1976 884.15037 174.495196 879.595634 157.107177zM674.469813 348.564697l-56.738235 28.537963c-2.958378 0.579191-5.734608 1.684362-8.277524 3.296069-0.313132 0.198521-0.615007 0.385786-0.905626 0.562818 0.12382-0.318248 0.25378-0.639566 0.39295-0.971118 0.99977-2.437515 1.588171-5.022387 1.753947-7.702426l17.732873-66.078974 105.73209-131.488707 48.647977 39.113833L674.469813 348.564697zM831.385633 172.919305l-7.679913 9.550517-68.139913-54.779627 7.687076-9.56382c0.291642-0.132006 1.144056-0.38374 2.604314-0.38374 4.28356 0 9.735735 2.071172 13.570063 5.15644l43.832298 35.245735c3.493567 2.805906 6.421246 7.018857 7.642051 10.993378C831.497173 171.081447 831.492056 172.446537 831.385633 172.919305zM487.474932 291.067168l-200.394053 0c-14.136975 0-25.638937 11.501962-25.638937 25.63996 0 14.136975 11.501962 25.637913 25.638937 25.637913l200.394053 0c14.137998 0 25.63996-11.500938 25.63996-25.637913C513.114892 302.56913 501.611907 291.067168 487.474932 291.067168zM674.507676 438.023148 287.079856 438.023148c-14.136975 0-25.638937 11.500938-25.638937 25.638937s11.501962 25.638937 25.638937 25.638937l387.42782 0c14.137998 0 25.640983-11.501962 25.640983-25.638937S688.646698 438.023148 674.507676 438.023148z" fill="currentColor"/></svg>';
400
+ }
401
+ // system: Claude starburst logo (official brand color #D97757) for mid-conversation
402
+ // role:"system" messages — the "Append System Prompt" meta rows and their role-filter chip.
403
+ // Shares the path with MODEL_PROVIDERS' Claude entry above.
404
+ if (type === 'system') {
405
+ return `<svg viewBox="0 0 1024 1024"><path d="${CLAUDE_STARBURST_PATH}" fill="#D97757"/></svg>`;
406
+ }
407
+ // teammate: team/collaboration icon (two people)
408
+ if (type === 'teammate') {
409
+ return '<svg viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z" fill="currentColor"/></svg>';
410
+ }
411
+ // default: other sub-agent
412
+ return '<svg viewBox="0 0 1024 1024"><path d="M810.666667 384l53.333333-117.333333L981.333333 213.333333l-117.333333-53.333333L810.666667 42.666667l-53.333334 117.333333L640 213.333333l117.333333 53.333334L810.666667 384zM810.666667 640l-53.333334 117.333333L640 810.666667l117.333333 53.333333L810.666667 981.333333l53.333333-117.333333L981.333333 810.666667l-117.333333-53.333334L810.666667 640zM490.666667 405.333333L384 170.666667 277.333333 405.333333 42.666667 512l234.666666 106.666667L384 853.333333l106.666667-234.666666L725.333333 512l-234.666666-106.666667z m-64.426667 148.906667L384 647.253333l-42.24-93.013333L248.746667 512l93.013333-42.24L384 376.746667l42.24 93.013333 93.013333 42.24-93.013333 42.24z" fill="currentColor"/></svg>';
413
+ }
414
+
415
+ export function formatTokenCount(n) {
416
+ if (n == null || n === 0) return '0';
417
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
418
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
419
+ return String(n);
420
+ }
421
+
422
+ /** 递归过滤对象中 _ 前缀的私有属性(由前端注入,非原始数据) */
423
+ export function stripPrivateKeys(obj) {
424
+ if (Array.isArray(obj)) return obj.map(stripPrivateKeys);
425
+ if (obj && typeof obj === 'object') {
426
+ const result = {};
427
+ for (const key of Object.keys(obj)) {
428
+ if (key.startsWith('_')) continue;
429
+ result[key] = stripPrivateKeys(obj[key]);
430
+ }
431
+ return result;
432
+ }
433
+ return obj;
434
+ }
435
+
436
+ export function computeTokenStats(requests) {
437
+ const byModel = {};
438
+ for (const req of requests) {
439
+ const usage = req.response?.body?.usage;
440
+ if (!usage) continue;
441
+ const model = req.body?.model || 'unknown';
442
+ if (!byModel[model]) {
443
+ byModel[model] = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
444
+ }
445
+ const s = byModel[model];
446
+ s.input += (usage.input_tokens || 0);
447
+ s.output += (usage.output_tokens || 0);
448
+ s.cacheCreation += (usage.cache_creation_input_tokens || 0);
449
+ s.cacheRead += (usage.cache_read_input_tokens || 0);
450
+ }
451
+ return byModel;
452
+ }
453
+
454
+ export function computeCacheRebuildStats(requests) {
455
+ const stats = {
456
+ ttl: { count: 0, cacheCreate: 0 },
457
+ system_change: { count: 0, cacheCreate: 0 },
458
+ tools_change: { count: 0, cacheCreate: 0 },
459
+ model_change: { count: 0, cacheCreate: 0 },
460
+ msg_truncated: { count: 0, cacheCreate: 0 },
461
+ msg_modified: { count: 0, cacheCreate: 0 },
462
+ key_change: { count: 0, cacheCreate: 0 },
463
+ };
464
+ const lossMap = buildCacheLossMap(requests);
465
+ for (const [i, result] of lossMap) {
466
+ const cacheCreate = requests[i].response?.body?.usage?.cache_creation_input_tokens || 0;
467
+ const reasons = result.reasons || [result.reason];
468
+ for (const r of reasons) {
469
+ if (stats[r]) {
470
+ stats[r].count++;
471
+ stats[r].cacheCreate += cacheCreate;
472
+ }
473
+ }
474
+ }
475
+ return stats;
476
+ }
477
+
478
+ export function computeToolUsageStats(requests) {
479
+ const toolCounts = {};
480
+ for (const req of requests) {
481
+ const content = req.response?.body?.content;
482
+ if (!Array.isArray(content)) continue;
483
+ for (const block of content) {
484
+ if (block.type === 'tool_use' && block.name) {
485
+ toolCounts[block.name] = (toolCounts[block.name] || 0) + 1;
486
+ }
487
+ }
488
+ }
489
+ return Object.entries(toolCounts)
490
+ .sort((a, b) => b[1] - a[1]);
491
+ }
492
+
493
+ export function computeSkillUsageStats(requests) {
494
+ const skillCounts = {};
495
+ for (const req of requests) {
496
+ const messages = req.body?.messages;
497
+ if (!Array.isArray(messages)) continue;
498
+ for (const msg of messages) {
499
+ if (msg.role !== 'user') continue;
500
+ const content = msg.content;
501
+ if (!Array.isArray(content)) continue;
502
+ for (const block of content) {
503
+ if (block.type !== 'text' || !isSkillText(block.text)) continue;
504
+ const nameMatch = block.text.match(/^#\s+(.+)$/m);
505
+ const skillName = nameMatch ? nameMatch[1] : 'Skill';
506
+ skillCounts[skillName] = (skillCounts[skillName] || 0) + 1;
507
+ }
508
+ }
509
+ }
510
+ return Object.entries(skillCounts)
511
+ .sort((a, b) => b[1] - a[1]);
512
+ }
513
+
514
+ export function isClaudeMdReminder(text) {
515
+ if (typeof text !== 'string') return false;
516
+ return text.includes('<system-reminder>') && text.includes('# claudeMd');
517
+ }
518
+
519
+ export function isSkillsReminder(text) {
520
+ if (typeof text !== 'string') return false;
521
+ return text.includes('<system-reminder>') && text.includes('skills are available');
522
+ }
523
+
524
+ export function hasClaudeMdReminder(body) {
525
+ const messages = body?.messages;
526
+ if (!Array.isArray(messages)) return false;
527
+ for (const msg of messages) {
528
+ const content = msg?.content;
529
+ if (typeof content === 'string') {
530
+ if (isClaudeMdReminder(content)) return true;
531
+ } else if (Array.isArray(content)) {
532
+ for (const block of content) {
533
+ if (block.type === 'text' && isClaudeMdReminder(block.text)) {
534
+ return true;
535
+ }
536
+ }
537
+ }
538
+ }
539
+ return false;
540
+ }
541
+
542
+ export function hasSkillsReminder(body) {
543
+ // 与 extractLoadedSkills 的扫描范围对齐:system[] + messages[]
544
+ if (Array.isArray(body?.system)) {
545
+ for (const blk of body.system) {
546
+ if (blk?.type === 'text' && isSkillsReminder(blk.text)) return true;
547
+ }
548
+ }
549
+ const messages = body?.messages;
550
+ if (!Array.isArray(messages)) return false;
551
+ for (const msg of messages) {
552
+ const content = msg?.content;
553
+ if (typeof content === 'string') {
554
+ if (isSkillsReminder(content)) return true;
555
+ } else if (Array.isArray(content)) {
556
+ for (const block of content) {
557
+ if (block.type === 'text' && isSkillsReminder(block.text)) {
558
+ return true;
559
+ }
560
+ }
561
+ }
562
+ }
563
+ return false;
564
+ }
565
+
566
+ // parseLoadedSkills 拆到独立文件 skillsParser.js(纯函数、无传递依赖),
567
+ // 方便 node --test 直接导入(contentFilter.js 的 ./teammateDetector import
568
+ // 未写 .js 后缀,会让任何 helpers.js 的传递测试挂掉)。
569
+ export { parseLoadedSkills } from './skillsParser.js';
570
+ import { parseLoadedSkills as _parseLoadedSkills } from './skillsParser.js';
571
+
572
+ // 从 requests 中挑出当前生效的 skills 列表。
573
+ // 策略:最新 MainAgent;单请求直取。不依赖 response.usage(skills 只来自 body)。
574
+ // 只扫 body.system[] 与 body.messages[*].content[*] 中 type==='text' 的块。
575
+ export function extractLoadedSkills(requests) {
576
+ if (!Array.isArray(requests) || requests.length === 0) return [];
577
+ let chosen = null;
578
+ if (requests.length === 1) {
579
+ chosen = requests[0];
580
+ } else {
581
+ for (let i = requests.length - 1; i >= 0; i--) {
582
+ if (isMainAgent(requests[i])) { chosen = requests[i]; break; }
583
+ }
584
+ }
585
+ if (!chosen || !chosen.body) return [];
586
+ const body = chosen.body;
587
+
588
+ let last = null;
589
+ if (Array.isArray(body.system)) {
590
+ for (const blk of body.system) {
591
+ if (blk?.type === 'text' && isSkillsReminder(blk.text)) last = blk.text;
592
+ }
593
+ }
594
+ if (Array.isArray(body.messages)) {
595
+ for (const msg of body.messages) {
596
+ const c = msg?.content;
597
+ if (typeof c === 'string') {
598
+ if (isSkillsReminder(c)) last = c;
599
+ } else if (Array.isArray(c)) {
600
+ for (const blk of c) {
601
+ if (blk?.type === 'text' && isSkillsReminder(blk.text)) last = blk.text;
602
+ }
603
+ }
604
+ }
605
+ }
606
+ if (!last) return [];
607
+
608
+ const m = /<system-reminder>([\s\S]*?)<\/system-reminder>/i.exec(last);
609
+ return _parseLoadedSkills(m ? m[1] : last);
610
+ }
611
+
612
+ export function getModelShort(model) {
613
+ if (!model) return null;
614
+ return model
615
+ .replace(/^claude-/, '')
616
+ .replace(/-\d{8,}$/, '');
617
+ }
618
+
619
+ // ============== 请求过滤相关 ==============
620
+
621
+ /**
622
+ * 判断请求是否为相关请求(过滤掉心跳、token计数、eval/sdk 请求、在途请求、历史遗留的状态0请求)
623
+ */
624
+ export function isRelevantRequest(request) {
625
+ if (!request) return false;
626
+ const url = request.url || '';
627
+ return !(
628
+ request.ccvRotationContext || // rotation-context sentinel: metadata frame, never a renderable request
629
+ request.isHeartbeat ||
630
+ request.isCountTokens ||
631
+ /\/api\/eval\/sdk-/.test(url) ||
632
+ /\/messages\/count_tokens/.test(url) ||
633
+ request.inProgress === true || // 过滤在途请求
634
+ (request.response && request.response.status === 0) || // 兼容历史日志:过滤状态为0的请求
635
+ (request.body?.max_tokens === 1 && !request.body?.system && (!request.body?.tools || request.body.tools.length === 0)) // 配额检查请求(单消息 "quota",max_tokens=1)
636
+ );
637
+ }
638
+
639
+ /**
640
+ * 过滤出相关请求
641
+ */
642
+ export function filterRelevantRequests(requests) {
643
+ return requests.filter(isRelevantRequest);
644
+ }
645
+
646
+ /**
647
+ * The single source of truth for the request list a user can SEE and that
648
+ * selectedIndex indexes. showAll bypasses relevance filtering but must still
649
+ * hide rotation-context sentinels (metadata frames kept inside state.requests
650
+ * for positional-index integrity). Every selectedIndex-coupled call site must
651
+ * use this helper — a sentinel-inclusive array desynchronizes the selection.
652
+ */
653
+ export function visibleRequests(requests, showAll) {
654
+ return showAll
655
+ ? requests.filter((r) => !r?.ccvRotationContext)
656
+ : filterRelevantRequests(requests);
657
+ }
658
+
659
+ /**
660
+ * 从请求列表中查找前一个 MainAgent 请求的时间戳
661
+ */
662
+ export function findPrevMainAgentTimestamp(requests, startIndex) {
663
+ for (let i = startIndex - 1; i >= 0; i--) {
664
+ if (isMainAgent(requests[i]) && requests[i].timestamp) {
665
+ return requests[i].timestamp;
666
+ }
667
+ }
668
+ return null;
669
+ }
670
+
671
+ /**
672
+ * 从请求列表中提取最新 MainAgent 请求的 KV-cache 缓存内容
673
+ * cache_control 是缓存断点标记,表示从开始到该标记位置的所有内容都被缓存
674
+ * @param {Array} requests - 请求列表
675
+ * @returns {Object|null} - { system: string[], messages: string[], tools: string[], cacheCreateTokens: number, cacheReadTokens: number }
676
+ */
677
+ export function extractCachedContent(requests) {
678
+ if (!Array.isArray(requests) || requests.length === 0) return null;
679
+
680
+ // 单请求(DetailPanel 场景):直接使用,不做类型过滤
681
+ // 多请求(AppHeader fallback):逆序找最新 MainAgent,避免 teammate/subAgent 劫持
682
+ let chosen = null;
683
+ if (requests.length === 1) {
684
+ chosen = requests[0];
685
+ } else {
686
+ let latestMA = null;
687
+ let latestMAWithUsage = null;
688
+ for (let i = requests.length - 1; i >= 0; i--) {
689
+ if (isMainAgent(requests[i])) {
690
+ if (!latestMA) latestMA = requests[i];
691
+ if (requests[i].response?.body?.usage) {
692
+ latestMAWithUsage = requests[i];
693
+ break;
694
+ }
695
+ }
696
+ }
697
+ chosen = latestMAWithUsage || latestMA;
698
+ }
699
+
700
+ if (!chosen || !chosen.body) return null;
701
+
702
+ const body = chosen.body;
703
+ const usage = chosen.response?.body?.usage;
704
+
705
+ const result = {
706
+ system: [],
707
+ messages: [],
708
+ tools: [],
709
+ cacheCreateTokens: usage?.cache_creation_input_tokens || 0,
710
+ cacheReadTokens: usage?.cache_read_input_tokens || 0,
711
+ };
712
+
713
+ // 提取 system:找到最后一个带 cache_control 的位置,提取从开始到该位置的所有内容
714
+ if (Array.isArray(body.system)) {
715
+ let lastCacheIndex = -1;
716
+ for (let i = body.system.length - 1; i >= 0; i--) {
717
+ if (body.system[i].cache_control) {
718
+ lastCacheIndex = i;
719
+ break;
720
+ }
721
+ }
722
+ if (lastCacheIndex >= 0) {
723
+ for (let i = 0; i <= lastCacheIndex; i++) {
724
+ const block = body.system[i];
725
+ if (block.type === 'text' && block.text) {
726
+ result.system.push(block.text);
727
+ }
728
+ }
729
+ }
730
+ }
731
+
732
+ // 提取 messages:找到最后一个带 cache_control 的消息,提取从开始到该位置的所有消息内容
733
+ // 如果没有 cache_control 标记但有 cache_read(delta 重建/slim 还原后标记丢失),提取全部 messages
734
+ if (Array.isArray(body.messages)) {
735
+ let lastCacheIndex = -1;
736
+ for (let i = body.messages.length - 1; i >= 0; i--) {
737
+ const content = body.messages[i].content;
738
+ if (Array.isArray(content)) {
739
+ for (const block of content) {
740
+ if (block.cache_control) {
741
+ lastCacheIndex = i;
742
+ break;
743
+ }
744
+ }
745
+ if (lastCacheIndex >= 0) break;
746
+ }
747
+ }
748
+ // Fallback: delta 重建 + slim 还原后 cache_control 标记可能丢失,
749
+ // 但 cache_read > 0 说明 messages 确实被缓存了,提取全部
750
+ if (lastCacheIndex < 0 && result.cacheReadTokens > 0 && body.messages.length > 0) {
751
+ lastCacheIndex = body.messages.length - 1;
752
+ }
753
+ if (lastCacheIndex >= 0) {
754
+ for (let i = 0; i <= lastCacheIndex; i++) {
755
+ const msg = body.messages[i];
756
+ const content = msg.content;
757
+ if (typeof content === 'string') {
758
+ result.messages.push(`[${msg.role}] ${content}`);
759
+ } else if (Array.isArray(content)) {
760
+ for (const block of content) {
761
+ if (block.type === 'text' && block.text) {
762
+ result.messages.push(`[${msg.role}] ${block.text}`);
763
+ } else if (block.type === 'tool_use') {
764
+ const inputStr = block.input ? JSON.stringify(block.input) : '';
765
+ const preview = inputStr.length > 200 ? inputStr.substring(0, 200) + '...' : inputStr;
766
+ result.messages.push(`[${msg.role}] ${block.name}(${preview})`);
767
+ } else if (block.type === 'tool_result') {
768
+ const toolText = extractToolResultText(block);
769
+ if (toolText) {
770
+ result.messages.push(`[tool_result: ${block.tool_use_id}] ${toolText}`);
771
+ }
772
+ }
773
+ }
774
+ }
775
+ }
776
+ }
777
+ }
778
+
779
+ // 提取 tools:API 缓存顺序为 tools → system → messages,
780
+ // tools 作为 cache 前缀的一部分隐式被缓存(不需要自身有 cache_control 标记)。
781
+ // 只要 system 有缓存内容(说明 cache 前缀存在),tools 就应被展示。
782
+ // Keep in sync with server/lib/kv-cache-analyzer.js extractCachedContent
783
+ if (Array.isArray(body.tools) && body.tools.length > 0 && result.system.length > 0) {
784
+ for (const tool of body.tools) {
785
+ result.tools.push(formatToolAsXml(tool));
786
+ }
787
+ }
788
+
789
+ return result;
790
+ }
791
+
792
+ /**
793
+ * 把 extractCachedContent 产出的 tools XML 字符串数组拆分为内置工具和 MCP 工具两组。
794
+ *
795
+ * 输入形如 formatToolAsXml 的输出:每个字符串是完整的 <tool>...</tool> 块。
796
+ * 解析时只取出工具级别(最顶层)的 <name> 与 <description>——即字符串中第一次出现的那对。
797
+ *
798
+ * MCP 识别:name 匹配 /^mcp__(.+?)__(.+)$/,非贪心以支持 server 名含下划线
799
+ * (例如 mcp__some_server_name__do_thing → server: "some_server_name", tool: "do_thing")。
800
+ *
801
+ * 边界:
802
+ * - tools 非数组或空 → { builtin: [], mcpByServer: new Map() }
803
+ * - 项为空字符串 / 非字符串 → 跳过
804
+ * - 项不是 XML 块但含冒号 → 回退到 "name: description" 解析(向前兼容)
805
+ *
806
+ * @param {string[]} tools
807
+ * @returns {{ builtin: Array<{name:string, description:string}>, mcpByServer: Map<string, Array<{name:string, fullName:string, description:string}>> }}
808
+ */
809
+ export function parseCachedTools(tools) {
810
+ const builtin = [];
811
+ const mcpByServer = new Map();
812
+ if (!Array.isArray(tools)) return { builtin, mcpByServer };
813
+
814
+ for (const raw of tools) {
815
+ if (typeof raw !== 'string' || !raw) continue;
816
+
817
+ let name = '';
818
+ let description = '';
819
+
820
+ const nameMatch = raw.match(/<name>([\s\S]*?)<\/name>/);
821
+ if (nameMatch) {
822
+ name = nameMatch[1].trim();
823
+ const descMatch = raw.match(/<description>([\s\S]*?)<\/description>/);
824
+ description = descMatch ? descMatch[1].trim() : '';
825
+ } else {
826
+ // Back-compat: older "name: description" format
827
+ const colonIdx = raw.indexOf(':');
828
+ name = colonIdx >= 0 ? raw.slice(0, colonIdx).trim() : raw.trim();
829
+ description = colonIdx >= 0 ? raw.slice(colonIdx + 1).trim() : '';
830
+ }
831
+
832
+ if (!name) continue;
833
+
834
+ const mcpMatch = /^mcp__(.+?)__(.+)$/.exec(name);
835
+ if (mcpMatch) {
836
+ const server = mcpMatch[1];
837
+ const toolName = mcpMatch[2];
838
+ if (!mcpByServer.has(server)) mcpByServer.set(server, []);
839
+ mcpByServer.get(server).push({ name: toolName, fullName: name, description });
840
+ } else {
841
+ builtin.push({ name, description });
842
+ }
843
+ }
844
+
845
+ return { builtin, mcpByServer };
846
+ }