visualize_trace.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 生成JSON跟踪文件的可视化HTML页面
  5. """
  6. import json
  7. import os
  8. from pathlib import Path
  9. def load_json_data(json_path):
  10. """加载JSON数据"""
  11. with open(json_path, 'r', encoding='utf-8') as f:
  12. return json.load(f)
  13. def calculate_grid_layout(nodes, node_width=200, node_height=120, horizontal_spacing=250, vertical_spacing=150, margin=50, screen_width=1600):
  14. """
  15. 计算网格布局的节点位置
  16. 从左到右,到右边后向下再向左,呈蛇形排列,所有节点对齐到网格
  17. """
  18. # 按sequence排序
  19. sorted_nodes = sorted(nodes, key=lambda x: x.get('sequence', 0))
  20. positions = {}
  21. grid_positions = {} # 存储每个节点在网格中的行列位置
  22. col = 0
  23. row = 0
  24. direction = 1 # 1表示向右,-1表示向左
  25. # 计算每行可以放置的节点数
  26. available_width = screen_width - 2 * margin
  27. nodes_per_row = max(1, int(available_width / (node_width + horizontal_spacing)))
  28. for node in sorted_nodes:
  29. seq = node.get('sequence', 0)
  30. # 检查是否需要换行
  31. if direction == 1 and col >= nodes_per_row:
  32. # 向右超出,向下移动,开始向左
  33. row += 1
  34. col = nodes_per_row - 1
  35. direction = -1
  36. elif direction == -1 and col < 0:
  37. # 向左超出,向下移动,开始向右
  38. row += 1
  39. col = 0
  40. direction = 1
  41. # 记录网格位置
  42. grid_positions[seq] = {'row': row, 'col': col}
  43. # 计算实际像素位置(对齐到网格)
  44. x = margin + col * (node_width + horizontal_spacing)
  45. y = margin + row * (node_height + vertical_spacing)
  46. positions[seq] = {
  47. 'x': x,
  48. 'y': y,
  49. 'width': node_width,
  50. 'height': node_height,
  51. 'row': row,
  52. 'col': col
  53. }
  54. # 移动到下一个网格位置
  55. col += direction
  56. # 计算最大尺寸
  57. max_col = max([pos['col'] for pos in grid_positions.values()]) if grid_positions else 0
  58. max_row = max([pos['row'] for pos in grid_positions.values()]) if grid_positions else 0
  59. max_x = margin + (max_col + 1) * (node_width + horizontal_spacing)
  60. max_y = margin + (max_row + 1) * (node_height + vertical_spacing)
  61. return positions, grid_positions, max_x, max_y
  62. def generate_html(json_data, output_path):
  63. """生成HTML可视化页面"""
  64. # 提取所有节点
  65. nodes = []
  66. node_map = {}
  67. for item in json_data:
  68. seq = item.get('sequence')
  69. if seq is not None:
  70. nodes.append(item)
  71. node_map[seq] = item
  72. # 计算网格布局
  73. positions, grid_positions, max_width, max_height = calculate_grid_layout(nodes)
  74. # 计算连线信息
  75. def calculate_connection(from_seq, to_seq, from_pos, to_pos):
  76. """计算两个节点之间的连线方向和起止点"""
  77. from_row, from_col = from_pos.get('row', 0), from_pos.get('col', 0)
  78. to_row, to_col = to_pos.get('row', 0), to_pos.get('col', 0)
  79. # 判断方向
  80. if to_col > from_col:
  81. direction = 'right' # 下一个节点在右侧
  82. elif to_row > from_row:
  83. direction = 'down' # 下一个节点在下侧
  84. elif to_col < from_col:
  85. direction = 'left' # 下一个节点在左侧
  86. else:
  87. direction = 'down' # 默认向下
  88. # 计算起止点(节点的最近边)
  89. from_x = from_pos['x']
  90. from_y = from_pos['y']
  91. from_w = from_pos['width']
  92. from_h = from_pos['height']
  93. to_x = to_pos['x']
  94. to_y = to_pos['y']
  95. to_w = to_pos['width']
  96. to_h = to_pos['height']
  97. if direction == 'right':
  98. # 从右侧边中点连接到左侧边中点
  99. start_x = from_x + from_w
  100. start_y = from_y + from_h / 2
  101. end_x = to_x
  102. end_y = to_y + to_h / 2
  103. elif direction == 'down':
  104. # 从下侧边中点连接到上侧边中点
  105. start_x = from_x + from_w / 2
  106. start_y = from_y + from_h
  107. end_x = to_x + to_w / 2
  108. end_y = to_y
  109. elif direction == 'left':
  110. # 从左侧边中点连接到右侧边中点
  111. start_x = from_x
  112. start_y = from_y + from_h / 2
  113. end_x = to_x + to_w
  114. end_y = to_y + to_h / 2
  115. return {
  116. 'direction': direction,
  117. 'start_x': start_x,
  118. 'start_y': start_y,
  119. 'end_x': end_x,
  120. 'end_y': end_y
  121. }
  122. # 生成连线数据(按照sequence顺序连接相邻节点)
  123. connections = []
  124. sorted_sequences = sorted([node.get('sequence') for node in nodes if node.get('sequence') is not None])
  125. for i in range(len(sorted_sequences) - 1):
  126. from_seq = sorted_sequences[i]
  127. to_seq = sorted_sequences[i + 1]
  128. if from_seq in positions and to_seq in positions:
  129. conn = calculate_connection(from_seq, to_seq, positions[from_seq], positions[to_seq])
  130. conn['from'] = from_seq
  131. conn['to'] = to_seq
  132. connections.append(conn)
  133. # 准备传递给JavaScript的数据(简化节点数据,避免循环引用)
  134. nodes_js = []
  135. for node in nodes:
  136. node_js = {
  137. 'sequence': node.get('sequence'),
  138. 'role': node.get('role', 'unknown'),
  139. 'parent_sequence': node.get('parent_sequence'),
  140. 'status': node.get('status', 'unknown'),
  141. 'title': node.get('title', '无标题'),
  142. 'text': node.get('text', ''),
  143. 'tokens': node.get('tokens', 0)
  144. }
  145. # 处理content字段
  146. content = node.get('content')
  147. if content:
  148. if isinstance(content, str):
  149. node_js['content'] = content
  150. else:
  151. node_js['content'] = json.dumps(content, ensure_ascii=False, indent=2)
  152. # 处理children字段
  153. children = node.get('children')
  154. if children:
  155. node_js['children'] = json.dumps(children, ensure_ascii=False, indent=2)
  156. nodes_js.append(node_js)
  157. # 生成HTML
  158. html_content = f"""<!DOCTYPE html>
  159. <html lang="zh-CN">
  160. <head>
  161. <meta charset="UTF-8">
  162. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  163. <title>执行跟踪可视化</title>
  164. <style>
  165. * {{
  166. margin: 0;
  167. padding: 0;
  168. box-sizing: border-box;
  169. }}
  170. body {{
  171. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
  172. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  173. min-height: 100vh;
  174. padding: 20px;
  175. overflow: auto;
  176. }}
  177. .container {{
  178. position: relative;
  179. width: 100%;
  180. min-width: {max_width}px;
  181. min-height: {max_height}px;
  182. background: rgba(255, 255, 255, 0.95);
  183. border-radius: 12px;
  184. box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  185. padding: 20px;
  186. }}
  187. .node {{
  188. position: absolute;
  189. width: 200px;
  190. height: 120px;
  191. background: linear-gradient(135deg, #6366f1 0%, #7c3aed 100%);
  192. border-radius: 8px;
  193. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
  194. cursor: pointer;
  195. transition: all 0.3s ease;
  196. display: flex;
  197. flex-direction: column;
  198. justify-content: center;
  199. align-items: center;
  200. padding: 12px;
  201. border: 2px solid rgba(255, 255, 255, 0.3);
  202. }}
  203. .node:hover {{
  204. transform: translateY(-5px) scale(1.05);
  205. box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
  206. z-index: 100;
  207. }}
  208. .node.system {{
  209. background: linear-gradient(135deg, #a855f7 0%, #be185d 100%);
  210. }}
  211. .node.user {{
  212. background: linear-gradient(135deg, #3b82f6 0%, #0284c7 100%);
  213. }}
  214. .node.assistant {{
  215. background: linear-gradient(135deg, #10b981 0%, #059669 100%);
  216. }}
  217. .node-title {{
  218. color: white;
  219. font-size: 13px;
  220. font-weight: 600;
  221. text-align: center;
  222. line-height: 1.4;
  223. overflow: hidden;
  224. text-overflow: ellipsis;
  225. display: -webkit-box;
  226. -webkit-line-clamp: 4;
  227. -webkit-box-orient: vertical;
  228. word-break: break-word;
  229. }}
  230. .node-sequence {{
  231. position: absolute;
  232. top: 5px;
  233. left: 8px;
  234. color: rgba(255, 255, 255, 0.8);
  235. font-size: 11px;
  236. font-weight: bold;
  237. }}
  238. .arrow {{
  239. position: absolute;
  240. stroke: #667eea;
  241. stroke-width: 2;
  242. fill: none;
  243. marker-end: url(#arrowhead);
  244. opacity: 0.6;
  245. transition: opacity 0.3s ease;
  246. }}
  247. .arrow:hover {{
  248. opacity: 1;
  249. stroke-width: 3;
  250. }}
  251. .tooltip {{
  252. position: fixed;
  253. background: rgba(0, 0, 0, 0.9);
  254. color: white;
  255. padding: 12px 16px;
  256. border-radius: 6px;
  257. font-size: 13px;
  258. max-width: 400px;
  259. z-index: 1000;
  260. pointer-events: none;
  261. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
  262. display: none;
  263. line-height: 1.6;
  264. word-break: break-word;
  265. }}
  266. .modal {{
  267. display: none;
  268. position: fixed;
  269. top: 0;
  270. left: 0;
  271. width: 100%;
  272. height: 100%;
  273. background: rgba(0, 0, 0, 0.7);
  274. z-index: 2000;
  275. justify-content: center;
  276. align-items: center;
  277. }}
  278. .modal-content {{
  279. background: white;
  280. border-radius: 12px;
  281. padding: 30px;
  282. width: 75vw;
  283. max-width: 75vw;
  284. max-height: 80vh;
  285. overflow-y: auto;
  286. box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  287. position: relative;
  288. }}
  289. .modal-close {{
  290. position: absolute;
  291. top: 15px;
  292. right: 20px;
  293. font-size: 28px;
  294. cursor: pointer;
  295. color: #999;
  296. transition: color 0.3s ease;
  297. }}
  298. .modal-close:hover {{
  299. color: #333;
  300. }}
  301. .modal-header {{
  302. margin-bottom: 20px;
  303. padding-bottom: 15px;
  304. border-bottom: 2px solid #eee;
  305. }}
  306. .modal-title {{
  307. font-size: 20px;
  308. font-weight: bold;
  309. color: #333;
  310. margin-bottom: 10px;
  311. }}
  312. .modal-info {{
  313. font-size: 13px;
  314. color: #666;
  315. }}
  316. .modal-body {{
  317. font-size: 14px;
  318. line-height: 2.2;
  319. color: #444;
  320. }}
  321. .modal-section {{
  322. margin-bottom: 28px;
  323. }}
  324. .modal-section-title {{
  325. font-weight: bold;
  326. color: #667eea;
  327. margin-bottom: 14px;
  328. font-size: 16px;
  329. }}
  330. .modal-section-content {{
  331. background: #f8f9fa;
  332. padding: 18px;
  333. border-radius: 6px;
  334. white-space: pre-wrap;
  335. word-break: break-word;
  336. line-height: 1.9;
  337. }}
  338. .stats {{
  339. position: fixed;
  340. top: 20px;
  341. right: 20px;
  342. background: rgba(255, 255, 255, 0.95);
  343. padding: 15px 20px;
  344. border-radius: 8px;
  345. box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
  346. font-size: 14px;
  347. z-index: 100;
  348. }}
  349. .stats-item {{
  350. margin: 5px 0;
  351. color: #333;
  352. }}
  353. </style>
  354. </head>
  355. <body>
  356. <div class="stats">
  357. <div class="stats-item"><strong>总节点数:</strong> {len(nodes)}</div>
  358. <div class="stats-item"><strong>系统节点:</strong> <span id="system-count">0</span></div>
  359. <div class="stats-item"><strong>用户节点:</strong> <span id="user-count">0</span></div>
  360. <div class="stats-item"><strong>助手节点:</strong> <span id="assistant-count">0</span></div>
  361. </div>
  362. <div class="container" id="container">
  363. <svg style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
  364. <defs>
  365. <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
  366. <polygon points="0 0, 10 3, 0 6" fill="#667eea" />
  367. </marker>
  368. </defs>
  369. </svg>
  370. </div>
  371. <div class="tooltip" id="tooltip"></div>
  372. <div class="modal" id="modal">
  373. <div class="modal-content">
  374. <span class="modal-close" onclick="closeModal()">&times;</span>
  375. <div id="modal-body"></div>
  376. </div>
  377. </div>
  378. <script type="application/json" id="nodes-data">{json.dumps(nodes_js, ensure_ascii=False)}</script>
  379. <script type="application/json" id="positions-data">{json.dumps(positions, ensure_ascii=False)}</script>
  380. <script type="application/json" id="connections-data">{json.dumps(connections, ensure_ascii=False)}</script>
  381. <script>
  382. const nodes = JSON.parse(document.getElementById('nodes-data').textContent);
  383. const positions = JSON.parse(document.getElementById('positions-data').textContent);
  384. const connections = JSON.parse(document.getElementById('connections-data').textContent);
  385. // 统计节点类型
  386. let systemCount = 0, userCount = 0, assistantCount = 0;
  387. // 创建节点
  388. const container = document.getElementById('container');
  389. const svg = container.querySelector('svg');
  390. nodes.forEach(node => {{
  391. const seq = node.sequence;
  392. const pos = positions[seq];
  393. if (!pos) return;
  394. const role = node.role || 'unknown';
  395. if (role === 'system') systemCount++;
  396. else if (role === 'user') userCount++;
  397. else if (role === 'assistant') assistantCount++;
  398. // 创建节点元素
  399. const nodeEl = document.createElement('div');
  400. nodeEl.className = `node ${{role}}`;
  401. nodeEl.style.left = pos.x + 'px';
  402. nodeEl.style.top = pos.y + 'px';
  403. nodeEl.setAttribute('data-sequence', seq);
  404. const sequenceEl = document.createElement('div');
  405. sequenceEl.className = 'node-sequence';
  406. sequenceEl.textContent = `#${{seq}}`;
  407. const titleEl = document.createElement('div');
  408. titleEl.className = 'node-title';
  409. titleEl.textContent = node.title || '无标题';
  410. nodeEl.appendChild(sequenceEl);
  411. nodeEl.appendChild(titleEl);
  412. container.appendChild(nodeEl);
  413. // 添加事件监听
  414. nodeEl.addEventListener('mouseenter', (e) => {{
  415. showTooltip(e, node.text || node.title || '无内容');
  416. }});
  417. nodeEl.addEventListener('mouseleave', () => {{
  418. hideTooltip();
  419. }});
  420. nodeEl.addEventListener('click', () => {{
  421. showModal(node);
  422. }});
  423. }});
  424. // 创建连线(在节点创建完成后)
  425. connections.forEach(conn => {{
  426. const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
  427. arrow.setAttribute('x1', conn.start_x);
  428. arrow.setAttribute('y1', conn.start_y);
  429. arrow.setAttribute('x2', conn.end_x);
  430. arrow.setAttribute('y2', conn.end_y);
  431. arrow.setAttribute('class', 'arrow');
  432. arrow.setAttribute('data-direction', conn.direction);
  433. svg.appendChild(arrow);
  434. }});
  435. // 更新统计
  436. document.getElementById('system-count').textContent = systemCount;
  437. document.getElementById('user-count').textContent = userCount;
  438. document.getElementById('assistant-count').textContent = assistantCount;
  439. // 工具提示
  440. const tooltip = document.getElementById('tooltip');
  441. function showTooltip(event, text) {{
  442. if (!text || text.trim() === '') return;
  443. tooltip.textContent = text;
  444. tooltip.style.display = 'block';
  445. updateTooltipPosition(event);
  446. }}
  447. function hideTooltip() {{
  448. tooltip.style.display = 'none';
  449. }}
  450. function updateTooltipPosition(event) {{
  451. const x = event.clientX + 10;
  452. const y = event.clientY + 10;
  453. tooltip.style.left = x + 'px';
  454. tooltip.style.top = y + 'px';
  455. }}
  456. document.addEventListener('mousemove', (e) => {{
  457. if (tooltip.style.display === 'block') {{
  458. updateTooltipPosition(e);
  459. }}
  460. }});
  461. // 模态框
  462. const modal = document.getElementById('modal');
  463. const modalBody = document.getElementById('modal-body');
  464. function formatText(text) {{
  465. if (!text) return '';
  466. // 将转义字符转换为实际字符,并处理换行
  467. return String(text)
  468. .replace(/\\\\n/g, '\\n')
  469. .replace(/\\\\t/g, '\\t')
  470. .replace(/\\\\"/g, '"')
  471. .replace(/\\\\'/g, "'")
  472. .replace(/\\\\\\\\/g, '\\\\');
  473. }}
  474. function showModal(node) {{
  475. let html = `
  476. <div class="modal-header">
  477. <div class="modal-title">节点 #${{node.sequence}}</div>
  478. </div>
  479. `;
  480. if (node.content) {{
  481. let contentStr = '';
  482. try {{
  483. // 尝试解析JSON字符串
  484. const contentObj = JSON.parse(node.content);
  485. // 优先显示text内容
  486. if (contentObj.text) {{
  487. contentStr = contentObj.text;
  488. }}
  489. // 然后显示其他内容
  490. if (contentObj.tool_calls && Array.isArray(contentObj.tool_calls)) {{
  491. if (contentStr) contentStr += '\\n\\n---\\n\\n';
  492. contentObj.tool_calls.forEach((call, idx) => {{
  493. if (idx > 0) contentStr += '\\n\\n';
  494. contentStr += '工具 ' + (idx + 1) + ': ' + (call.function?.name || '未知工具');
  495. if (call.function?.arguments) {{
  496. try {{
  497. const args = JSON.parse(call.function.arguments);
  498. contentStr += '\\n参数:\\n' + JSON.stringify(args, null, 2);
  499. }} catch (e) {{
  500. contentStr += '\\n参数: ' + call.function.arguments;
  501. }}
  502. }}
  503. }});
  504. }} else {{
  505. // 如果不是tool_calls格式,显示其他字段(text已优先显示)
  506. const otherFields = {{}};
  507. Object.keys(contentObj).forEach(key => {{
  508. if (key !== 'text') {{
  509. otherFields[key] = contentObj[key];
  510. }}
  511. }});
  512. if (Object.keys(otherFields).length > 0) {{
  513. if (contentStr) contentStr += '\\n\\n---\\n\\n';
  514. contentStr += JSON.stringify(otherFields, null, 2);
  515. }}
  516. }}
  517. }} catch (e) {{
  518. // 如果不是JSON,直接显示字符串
  519. contentStr = node.content;
  520. }}
  521. if (contentStr) {{
  522. html += `
  523. <div class="modal-section">
  524. <div class="modal-section-title">完整内容</div>
  525. <div class="modal-section-content">${{escapeHtml(formatText(contentStr))}}</div>
  526. </div>
  527. `;
  528. }}
  529. }}
  530. if (node.children) {{
  531. let childrenStr = '';
  532. try {{
  533. const children = JSON.parse(node.children);
  534. // 处理数组格式
  535. if (Array.isArray(children) && children.length > 0) {{
  536. children.forEach((child, idx) => {{
  537. childrenStr += '\\n[' + (idx + 1) + '] ';
  538. // 动态显示所有字段
  539. const fields = [];
  540. // 常见字段按顺序显示(移除 type 和 id)
  541. if (child.tool_name !== undefined) {{
  542. fields.push('工具名称: ' + child.tool_name);
  543. }}
  544. if (child.name !== undefined) {{
  545. fields.push('名称: ' + child.name);
  546. }}
  547. // 参数相关字段
  548. if (child.arguments !== undefined) {{
  549. if (typeof child.arguments === 'object' && child.arguments !== null) {{
  550. fields.push('参数: ' + JSON.stringify(child.arguments, null, 2));
  551. }} else {{
  552. fields.push('参数: ' + child.arguments);
  553. }}
  554. }}
  555. if (child.raw_arguments !== undefined) {{
  556. fields.push('原始参数: ' + child.raw_arguments);
  557. }}
  558. // 结果相关字段
  559. if (child.result !== undefined) {{
  560. if (typeof child.result === 'object' && child.result !== null) {{
  561. fields.push('结果: ' + JSON.stringify(child.result, null, 2));
  562. }} else {{
  563. fields.push('结果: ' + child.result);
  564. }}
  565. }}
  566. if (child.response !== undefined) {{
  567. if (typeof child.response === 'object' && child.response !== null) {{
  568. fields.push('响应: ' + JSON.stringify(child.response, null, 2));
  569. }} else {{
  570. fields.push('响应: ' + child.response);
  571. }}
  572. }}
  573. // 状态相关字段
  574. if (child.status !== undefined) {{
  575. fields.push('状态: ' + child.status);
  576. }}
  577. if (child.sequence !== undefined) {{
  578. fields.push('序列号: ' + child.sequence);
  579. }}
  580. // 显示所有字段
  581. childrenStr += fields.join('\\n');
  582. // 如果有其他未处理的字段,也显示出来(排除不需要的字段)
  583. const knownFields = ['type', 'tool_name', 'tool_call_id', 'name', 'id',
  584. 'arguments', 'raw_arguments', 'result', 'response',
  585. 'status', 'sequence', 'tokens', 'prompt_tokens',
  586. 'completion_tokens', 'cost'];
  587. const otherFields = Object.keys(child).filter(key => !knownFields.includes(key));
  588. if (otherFields.length > 0) {{
  589. childrenStr += '\\n其他字段:';
  590. otherFields.forEach(key => {{
  591. const value = child[key];
  592. if (typeof value === 'object' && value !== null) {{
  593. childrenStr += '\\n ' + key + ': ' + JSON.stringify(value, null, 2);
  594. }} else {{
  595. childrenStr += '\\n ' + key + ': ' + value;
  596. }}
  597. }});
  598. }}
  599. childrenStr += '\\n\\n---\\n';
  600. }});
  601. }}
  602. // 处理对象格式(单个child)
  603. else if (typeof children === 'object' && children !== null && !Array.isArray(children)) {{
  604. // 过滤掉不需要的字段
  605. const filtered = {{}};
  606. Object.keys(children).forEach(key => {{
  607. if (!['type', 'id', 'tool_call_id', 'tokens', 'prompt_tokens', 'completion_tokens', 'cost'].includes(key)) {{
  608. filtered[key] = children[key];
  609. }}
  610. }});
  611. childrenStr = JSON.stringify(filtered, null, 2);
  612. }}
  613. // 处理其他格式
  614. else {{
  615. childrenStr = JSON.stringify(children, null, 2);
  616. }}
  617. }} catch (e) {{
  618. // 如果解析失败,直接显示原始字符串
  619. childrenStr = node.children;
  620. }}
  621. html += `
  622. <div class="modal-section">
  623. <div class="modal-section-title">子节点 (Children)</div>
  624. <div class="modal-section-content">${{escapeHtml(formatText(childrenStr))}}</div>
  625. </div>
  626. `;
  627. }}
  628. modalBody.innerHTML = html;
  629. modal.style.display = 'flex';
  630. }}
  631. function closeModal() {{
  632. modal.style.display = 'none';
  633. }}
  634. function escapeHtml(text) {{
  635. const div = document.createElement('div');
  636. div.textContent = text;
  637. return div.innerHTML;
  638. }}
  639. // 点击模态框外部关闭
  640. modal.addEventListener('click', (e) => {{
  641. if (e.target === modal) {{
  642. closeModal();
  643. }}
  644. }});
  645. // ESC键关闭模态框
  646. document.addEventListener('keydown', (e) => {{
  647. if (e.key === 'Escape') {{
  648. closeModal();
  649. }}
  650. }});
  651. </script>
  652. </body>
  653. </html>"""
  654. # 写入文件
  655. with open(output_path, 'w', encoding='utf-8') as f:
  656. f.write(html_content)
  657. print(f"✅ 可视化页面已生成: {output_path}")
  658. def main():
  659. """主函数"""
  660. # 获取脚本所在目录
  661. script_dir = Path(__file__).parent
  662. # JSON文件路径
  663. json_path = script_dir / '.trace' / 'bf1263a7-49d3-48b5-81c5-15cf98f143a1' / 'output.json'
  664. # 输出HTML文件路径
  665. output_path = script_dir / 'trace_visualization.html'
  666. if not json_path.exists():
  667. print(f"❌ 错误: 找不到JSON文件: {json_path}")
  668. return
  669. print(f"📖 正在读取JSON文件: {json_path}")
  670. json_data = load_json_data(json_path)
  671. print(f"📊 找到 {len(json_data)} 个节点")
  672. print(f"🎨 正在生成可视化页面...")
  673. generate_html(json_data, output_path)
  674. print(f"\n✨ 完成! 请在浏览器中打开: {output_path}")
  675. if __name__ == '__main__':
  676. main()