Przeglądaj źródła

Merge remote-tracking branch 'refs/remotes/origin/main'

Talegorithm 4 tygodni temu
rodzic
commit
438a6e3455

+ 107 - 0
frontend/htmlTemplate/api_data/goal_list.json

@@ -0,0 +1,107 @@
+{
+  "trace": {
+    "trace_id": "ffc7e6e8-6725-4bd3-8cd1-5c212856feff",
+    "mode": "agent",
+    "prompt_name": null,
+    "task": "[图片和特征描述已包含在 messages 中]",
+    "agent_type": "default",
+    "parent_trace_id": null,
+    "parent_goal_id": null,
+    "status": "completed",
+    "total_messages": 20,
+    "total_tokens": 145867,
+    "total_cost": 0.0,
+    "total_duration_ms": 0,
+    "last_sequence": 20,
+    "last_event_id": 25,
+    "uid": null,
+    "context": {
+      "model": "anthropic/claude-sonnet-4.5",
+      "temperature": 0.3
+    },
+    "current_goal_id": null,
+    "created_at": "2026-02-05T19:20:17.456366",
+    "completed_at": "2026-02-05T19:23:34.789135"
+  },
+  "goal_tree": {
+    "mission": "[图片和特征描述已包含在 messages 中]",
+    "goals": [
+      {
+        "id": "1",
+        "description": "调研构图特征的表示方案",
+        "reason": "需要系统性地调研、设计并输出构图特征表示方案",
+        "parent_id": null,
+        "type": "normal",
+        "status": "completed",
+        "summary": "基于计算机视觉领域的标准实践,构图特征应采用结构化+数值化的混合表示方式",
+        "sub_trace_ids": null,
+        "agent_call_mode": null,
+        "self_stats": {
+          "message_count": 6,
+          "total_tokens": 19265,
+          "total_cost": 0.0,
+          "preview": null
+        },
+        "cumulative_stats": {
+          "message_count": 6,
+          "total_tokens": 19265,
+          "total_cost": 0.0,
+          "preview": null
+        },
+        "created_at": "2026-02-05T19:20:26.177225"
+      },
+      {
+        "id": "2",
+        "description": "设计精简的特征表示结构",
+        "reason": "需要分步完成以确保质量",
+        "parent_id": null,
+        "type": "normal",
+        "status": "completed",
+        "summary": "已设计完整的特征表示结构,包含JSON schema、转换工具和示例",
+        "sub_trace_ids": null,
+        "agent_call_mode": null,
+        "self_stats": {
+          "message_count": 10,
+          "total_tokens": 92011,
+          "total_cost": 0.0,
+          "preview": null
+        },
+        "cumulative_stats": {
+          "message_count": 10,
+          "total_tokens": 92011,
+          "total_cost": 0.0,
+          "preview": null
+        },
+        "created_at": "2026-02-05T19:20:26.177233"
+      },
+      {
+        "id": "3",
+        "description": "输出最终方案文档",
+        "reason": "",
+        "parent_id": null,
+        "type": "normal",
+        "status": "pending",
+        "summary": null,
+        "sub_trace_ids": null,
+        "agent_call_mode": null,
+        "self_stats": {
+          "message_count": 2,
+          "total_tokens": 28740,
+          "total_cost": 0.0,
+          "preview": null
+        },
+        "cumulative_stats": {
+          "message_count": 2,
+          "total_tokens": 28740,
+          "total_cost": 0.0,
+          "preview": null
+        },
+        "created_at": "2026-02-05T19:20:26.177238"
+      }
+    ],
+    "current_id": null,
+    "_next_id": 1,
+    "created_at": "2026-02-05T19:20:17.456826"
+  },
+  "sub_traces": {}
+}

+ 28 - 0
frontend/htmlTemplate/api_data/trace_list.json

@@ -0,0 +1,28 @@
+{
+  "traces": [
+    {
+      "trace_id": "ffc7e6e8-6725-4bd3-8cd1-5c212856feff",
+      "mode": "agent",
+      "prompt_name": null,
+      "task": "[图片和特征描述已包含在 messages 中]",
+      "agent_type": "default",
+      "parent_trace_id": null,
+      "parent_goal_id": null,
+      "status": "completed",
+      "total_messages": 20,
+      "total_tokens": 145867,
+      "total_cost": 0.0,
+      "total_duration_ms": 0,
+      "last_sequence": 20,
+      "last_event_id": 25,
+      "uid": null,
+      "context": {
+        "model": "anthropic/claude-sonnet-4.5",
+        "temperature": 0.3
+      },
+      "current_goal_id": null,
+      "created_at": "2026-02-05T19:20:17.456366",
+      "completed_at": "2026-02-05T19:23:34.789135"
+    }
+  ]
+}

+ 194 - 286
frontend/htmlTemplate/templateData.py

@@ -3,287 +3,67 @@
 """
 templateData.py - 生成 Trace 可视化的 Mock 数据
 """
-
+import os
+import asyncio
 import json
 from datetime import datetime
-from typing import Dict, List, Any
+from typing import Dict, List, Any, Optional, Tuple
 
+import httpx
+import websockets
+from templateHtml import generate_trace_visualization_html
 
-def generate_mock_trace_list() -> Dict[str, Any]:
-    """生成 Trace 列表的 Mock 数据"""
-    return {
-        "traces": [
-            {
-                "trace_id": "trace_001",
-                "mode": "agent",
-                "task": "实现用户登录功能",
-                "status": "completed",
-                "total_messages": 15,
-                "total_tokens": 3500,
-                "total_cost": 0.05,
-                "current_goal_id": "goal_005",
-                "created_at": "2024-01-15T10:30:00Z"
-            },
-            {
-                "trace_id": "trace_002",
-                "mode": "agent",
-                "task": "优化数据库查询性能",
-                "status": "running",
-                "total_messages": 8,
-                "total_tokens": 2100,
-                "total_cost": 0.03,
-                "current_goal_id": "goal_003",
-                "created_at": "2024-01-15T11:00:00Z"
-            },
-            {
-                "trace_id": "trace_003",
-                "mode": "call",
-                "task": "修复支付接口bug",
-                "status": "failed",
-                "total_messages": 5,
-                "total_tokens": 1200,
-                "total_cost": 0.02,
-                "current_goal_id": "goal_002",
-                "created_at": "2024-01-15T09:45:00Z"
-            }
-        ],
-        "total": 3
-    }
+goalList: List[Dict[str, Any]] = []
+msgList: List[Dict[str, Any]] = []
+msgGroups: Dict[str, List[Dict[str, Any]]] = {}
 
 
-def generate_mock_trace_detail(trace_id: str = "trace_001") -> Dict[str, Any]:
-    """生成 Trace 详情的 Mock 数据(包含 GoalTree)"""
-    return {
-        "trace_id": trace_id,
-        "mode": "agent",
-        "task": "实现用户登录功能",
-        "status": "completed",
-        "total_messages": 15,
-        "total_tokens": 3500,
-        "total_cost": 0.05,
-        "created_at": "2024-01-15T10:30:00Z",
-        "completed_at": "2024-01-15T11:45:00Z",
-        "goal_tree": {
-            "mission": "实现用户登录功能",
-            "current_id": "goal_005",
-            "goals": [
-                {
-                    "id": "goal_001",
-                    "parent_id": None,
-                    "branch_id": None,
-                    "type": "normal",
-                    "description": "分析登录功能需求",
-                    "reason": "需要先理解需求才能开始实现",
-                    "status": "completed",
-                    "summary": "已完成需求分析,确定需要实现用户名密码登录和第三方登录",
-                    "self_stats": {
-                        "message_count": 3,
-                        "total_tokens": 800,
-                        "total_cost": 0.01,
-                        "preview": "read × 2 → analyze"
-                    },
-                    "cumulative_stats": {
-                        "message_count": 15,
-                        "total_tokens": 3500,
-                        "total_cost": 0.05,
-                        "preview": "read × 5 → write × 3 → test × 2"
-                    }
-                },
-                {
-                    "id": "goal_002",
-                    "parent_id": "goal_001",
-                    "branch_id": None,
-                    "type": "normal",
-                    "description": "设计数据库表结构",
-                    "reason": "需要存储用户信息和登录凭证",
-                    "status": "completed",
-                    "summary": "已创建 users 表和 auth_tokens 表",
-                    "self_stats": {
-                        "message_count": 2,
-                        "total_tokens": 500,
-                        "total_cost": 0.008,
-                        "preview": "design → create"
-                    },
-                    "cumulative_stats": {
-                        "message_count": 12,
-                        "total_tokens": 2700,
-                        "total_cost": 0.04,
-                        "preview": "design → write × 3 → test × 2"
-                    }
-                },
-                {
-                    "id": "goal_003",
-                    "parent_id": "goal_002",
-                    "branch_id": None,
-                    "type": "explore_start",
-                    "description": "探索不同的认证方案",
-                    "reason": "需要选择最合适的认证方式",
-                    "status": "completed",
-                    "summary": "决定使用 JWT 作为认证方案",
-                    "self_stats": {
-                        "message_count": 4,
-                        "total_tokens": 900,
-                        "total_cost": 0.012,
-                        "preview": "research × 3 → compare"
-                    },
-                    "cumulative_stats": {
-                        "message_count": 10,
-                        "total_tokens": 2200,
-                        "total_cost": 0.032,
-                        "preview": "research × 3 → write × 2 → test"
-                    },
-                    "branch_ids": ["branch_001", "branch_002"]
-                },
-                {
-                    "id": "goal_004",
-                    "parent_id": "goal_003",
-                    "branch_id": None,
-                    "type": "explore_merge",
-                    "description": "整合认证方案",
-                    "reason": "需要将选定的方案整合到系统中",
-                    "status": "completed",
-                    "summary": "已完成 JWT 认证的集成",
-                    "self_stats": {
-                        "message_count": 3,
-                        "total_tokens": 700,
-                        "total_cost": 0.01,
-                        "preview": "integrate × 2 → test"
-                    },
-                    "cumulative_stats": {
-                        "message_count": 6,
-                        "total_tokens": 1300,
-                        "total_cost": 0.02,
-                        "preview": "integrate × 2 → test × 2"
-                    },
-                    "explore_start_id": "goal_003",
-                    "merge_summary": "JWT 方案性能最优,安全性高",
-                    "selected_branch": "branch_001"
-                },
-                {
-                    "id": "goal_005",
-                    "parent_id": "goal_004",
-                    "branch_id": None,
-                    "type": "normal",
-                    "description": "实现登录API接口",
-                    "reason": "需要提供登录的HTTP接口",
-                    "status": "completed",
-                    "summary": "已完成 /api/login 接口的实现和测试",
-                    "self_stats": {
-                        "message_count": 3,
-                        "total_tokens": 600,
-                        "total_cost": 0.009,
-                        "preview": "write × 2 → test"
-                    },
-                    "cumulative_stats": {
-                        "message_count": 3,
-                        "total_tokens": 600,
-                        "total_cost": 0.009,
-                        "preview": "write × 2 → test"
-                    }
-                }
-            ]
-        },
-        "branches": {
-            "branch_001": {
-                "id": "branch_001",
-                "explore_start_id": "goal_003",
-                "description": "JWT 认证方案",
-                "status": "completed",
-                "summary": "JWT 方案实现完成,性能测试通过",
-                "cumulative_stats": {
-                    "message_count": 5,
-                    "total_tokens": 1100,
-                    "total_cost": 0.015,
-                    "preview": "implement × 3 → test × 2"
-                },
-                "goal_count": 3,
-                "last_message": {
-                    "message_id": "msg_015",
-                    "role": "assistant",
-                    "content": "JWT 认证方案实现完成"
-                }
-            },
-            "branch_002": {
-                "id": "branch_002",
-                "explore_start_id": "goal_003",
-                "description": "Session 认证方案",
-                "status": "abandoned",
-                "summary": "Session 方案在分布式环境下存在问题,已放弃",
-                "cumulative_stats": {
-                    "message_count": 3,
-                    "total_tokens": 600,
-                    "total_cost": 0.008,
-                    "preview": "implement × 2 → test"
-                },
-                "goal_count": 2,
-                "last_message": {
-                    "message_id": "msg_010",
-                    "role": "assistant",
-                    "content": "Session 方案不适合当前架构"
-                }
-            }
-        }
-    }
+def generate_trace_list(
+    base_url: str = "http://localhost:8000",
+    status: Optional[str] = None,
+    mode: Optional[str] = None,
+    limit: int = 20,
+) -> Dict[str, Any]:
+    params: Dict[str, Any] = {"limit": limit}
+    if status:
+        params["status"] = status
+    if mode:
+        params["mode"] = mode
+    url = f"{base_url.rstrip('/')}/api/traces"
+    response = httpx.get(url, params=params, timeout=10.0)
+    response.raise_for_status()
+    return response.json()
 
 
-def generate_mock_messages(trace_id: str = "trace_001", goal_id: str = "goal_001") -> Dict[str, Any]:
-    """生成 Messages 的 Mock 数据"""
-    return {
-        "messages": [
-            {
-                "message_id": "msg_001",
-                "role": "assistant",
-                "sequence": 1,
-                "goal_id": goal_id,
-                "branch_id": None,
-                "description": "开始分析需求文档",
-                "content": {
-                    "text": "我将开始分析登录功能的需求文档",
-                    "tool_calls": [
-                        {
-                            "id": "call_001",
-                            "name": "read_file",
-                            "arguments": {
-                                "path": "docs/requirements.md"
-                            }
-                        }
-                    ]
-                },
-                "tokens": 150,
-                "cost": 0.002,
-                "created_at": "2024-01-15T10:30:05Z"
-            },
-            {
-                "message_id": "msg_002",
-                "role": "tool",
-                "sequence": 2,
-                "goal_id": goal_id,
-                "branch_id": None,
-                "tool_call_id": "call_001",
-                "tokens": 200,
-                "cost": 0.003,
-                "created_at": "2024-01-15T10:30:06Z"
-            },
-            {
-                "message_id": "msg_003",
-                "role": "assistant",
-                "sequence": 3,
-                "goal_id": goal_id,
-                "branch_id": None,
-                "description": "分析需求并总结",
-                "content": {
-                    "text": "根据需求文档,需要实现以下功能:\n1. 用户名密码登录\n2. 第三方登录(微信、支付宝)\n3. 记住登录状态\n4. 登录失败重试限制",
-                    "tool_calls": []
-                },
-                "tokens": 180,
-                "cost": 0.0025,
-                "created_at": "2024-01-15T10:30:10Z"
-            }
-        ],
-        "total": 3
-    }
+def generate_goal_list(
+    trace_id: str = "trace_001", base_url: str = "http://localhost:8000"
+) -> Dict[str, Any]:
+    url = f"{base_url.rstrip('/')}/api/traces/{trace_id}"
+    response = httpx.get(url, timeout=10.0)
+    response.raise_for_status()
+    return response.json()
+
+
+def generate_subgoal_list(
+    sub_trace_id: str, base_url: str = "http://localhost:8000"
+) -> Dict[str, Any]:
+    url = f"{base_url.rstrip('/')}/api/traces/{sub_trace_id}"
+    response = httpx.get(url, timeout=10.0)
+    response.raise_for_status()
+    return response.json()
 
 
+def generate_messages_list(
+    trace_id: str, goal_id: Optional[str] = None, base_url: str = "http://localhost:8000"
+) -> Dict[str, Any]:
+    url = f"{base_url.rstrip('/')}/api/traces/{trace_id}/messages"
+    params = {}
+    if goal_id:
+        params["goal_id"] = goal_id
+    response = httpx.get(url, params=params, timeout=10.0)
+    response.raise_for_status()
+    return response.json()
+
 def generate_mock_branch_detail(trace_id: str = "trace_001", branch_id: str = "branch_001") -> Dict[str, Any]:
     """生成分支详情的 Mock 数据"""
     return {
@@ -373,29 +153,157 @@ def generate_mock_branch_detail(trace_id: str = "trace_001", branch_id: str = "b
     }
 
 
-def save_mock_data_to_file():
-    """将 Mock 数据保存到文件"""
-    import os
+async def _fetch_ws_connected_event(trace_id: str, since_event_id: int = 0, ws_url: Optional[str] = None) -> Dict[str, Any]:
+    url = ws_url or f"ws://localhost:8000/api/traces/{trace_id}/watch?since_event_id={since_event_id}"
+    async with websockets.connect(url) as ws:
+        while True:
+            raw_message = await ws.recv()
+            data = json.loads(raw_message)
+            if data.get("event") == "connected":
+                return data
+
+
+def _get_goals_container(trace_detail: Dict[str, Any]) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
+    goal_tree = trace_detail.get("goal_tree")
+    if isinstance(goal_tree, dict):
+        goals = goal_tree.get("goals")
+        if isinstance(goals, list):
+            return goal_tree, goals
+    goals = trace_detail.get("goals")
+    if isinstance(goals, list):
+        return trace_detail, goals
+    trace_detail["goal_tree"] = {"goals": []}
+    return trace_detail["goal_tree"], trace_detail["goal_tree"]["goals"]
+
+
+def _message_sort_key(message: Dict[str, Any]) -> int:
+    message_id = message.get("message_id")
+    if not isinstance(message_id, str):
+        return 0
+    if "-" not in message_id:
+        return 0
+    suffix = message_id.rsplit("-", 1)[-1]
+    return int(suffix) if suffix.isdigit() else 0
+
+
+def _update_message_groups(message: Dict[str, Any]):
+    group_key = message.get("goal_id") or "START"
+    group_list = msgGroups.setdefault(group_key, [])
+    group_list.append(message)
+    group_list.sort(key=_message_sort_key)
+
+
+def _apply_event(data: Dict[str, Any]):
+    event = data.get("event")
+    if event == "connected":
+        goal_tree = data.get("goal_tree") or (data.get("trace") or {}).get("goal_tree") or {}
+        goals = goal_tree.get("goals") if isinstance(goal_tree, dict) else []
+        if isinstance(goals, list):
+            goalList.clear()
+            goalList.extend(goals)
+    if event == "goal_added":
+        goal = data.get("goal")
+        if isinstance(goal, dict):
+            for idx, existing in enumerate(goalList):
+                if existing.get("id") == goal.get("id"):
+                    goalList[idx] = {**existing, **goal}
+                    break
+            else:
+                goalList.append(goal)
+    elif event == "goal_updated":
+        goal_id = data.get("goal_id")
+        updates = data.get("updates") or {}
+        for g in goalList:
+            if g.get("id") == goal_id:
+                if "status" in updates:
+                    g["status"] = updates.get("status")
+                if "summary" in updates:
+                    g["summary"] = updates.get("summary")
+                break
+    elif event == "message_added":
+        message = data.get("message")
+        if isinstance(message, dict):
+            msgList.append(message)
+            _update_message_groups(message)
+
+
+def _append_event_jsonl(event_data: Dict[str, Any], mock_dir: str):
+    event_path = os.path.join(mock_dir, "event.jsonl")
+    with open(event_path, "a", encoding="utf-8") as f:
+        f.write(json.dumps(event_data, ensure_ascii=False) + "\n")
 
-    # 创建 mock_data 目录
-    mock_dir = os.path.join(os.path.dirname(__file__), "mock_data")
+
+async def _watch_ws_events(trace_id: str, since_event_id: int = 0, ws_url: Optional[str] = None):
+    url = ws_url or f"ws://localhost:8000/api/traces/{trace_id}/watch?since_event_id={since_event_id}"
+    mock_dir = os.path.join(os.path.dirname(__file__), "ws_data")
+    os.makedirs(mock_dir, exist_ok=True)
+    while True:
+        try:
+            print(f"开始监听 WebSocket: {url}")
+            async with websockets.connect(url) as ws:
+                async for raw_message in ws:
+                    data = json.loads(raw_message)
+                    _apply_event(data)
+                    _append_event_jsonl(data, mock_dir)
+                    generate_trace_visualization_html(goalList, msgGroups)
+                    event = data.get("event")
+                    if event:
+                        print(f"收到事件: {event}")
+        except Exception:
+            print("WebSocket 连接断开,1 秒后重连")
+            await asyncio.sleep(1)
+
+
+
+
+def save_ws_data_to_file(trace_list_data: Dict[str, Any], goal_list: List[Dict[str, Any]]):
+
+    mock_dir = os.path.join(os.path.dirname(__file__), "api_data")
     os.makedirs(mock_dir, exist_ok=True)
 
-    # 保存各类 Mock 数据
     with open(os.path.join(mock_dir, "trace_list.json"), "w", encoding="utf-8") as f:
-        json.dump(generate_mock_trace_list(), f, ensure_ascii=False, indent=2)
+        json.dump(trace_list_data, f, ensure_ascii=False, indent=2)
 
-    with open(os.path.join(mock_dir, "trace_detail.json"), "w", encoding="utf-8") as f:
-        json.dump(generate_mock_trace_detail(), f, ensure_ascii=False, indent=2)
+    with open(os.path.join(mock_dir, "goal_list.json"), "w", encoding="utf-8") as f:
+        json.dump(goal_list, f, ensure_ascii=False, indent=2)
 
-    with open(os.path.join(mock_dir, "messages.json"), "w", encoding="utf-8") as f:
-        json.dump(generate_mock_messages(), f, ensure_ascii=False, indent=2)
 
-    with open(os.path.join(mock_dir, "branch_detail.json"), "w", encoding="utf-8") as f:
-        json.dump(generate_mock_branch_detail(), f, ensure_ascii=False, indent=2)
 
-    print(f"Mock 数据已保存到: {mock_dir}")
+    print(f"Trace 数据已保存到: {mock_dir}")
 
 
 if __name__ == "__main__":
-    save_mock_data_to_file()
+    import argparse
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--trace-id", dest="trace_id")
+    parser.add_argument("--since-event-id", dest="since_event_id", type=int, default=0)
+    parser.add_argument("--ws-url", dest="ws_url")
+    parser.add_argument("--watch", action="store_true")
+    args = parser.parse_args()
+
+    if args.trace_id:
+        if args.watch:
+            print(f"使用 trace_id 监听: {args.trace_id}")
+            asyncio.run(_watch_ws_events(args.trace_id, args.since_event_id, args.ws_url))
+        else:
+            print(f"❌暂无 trace_id")
+            # save_ws_data_to_file(args.trace_id, args.since_event_id, args.ws_url)
+    else:
+        trace_list_data = generate_trace_list()
+        print(f"🐒trace_list_data: {trace_list_data}")
+
+        traces = trace_list_data.get("traces") or []
+        trace_id = traces[0].get("trace_id") if traces else None
+        if trace_id:
+            if args.watch:
+                print(f"✅使用 trace_id 监听: {trace_id}")
+                asyncio.run(_watch_ws_events(trace_id, args.since_event_id, args.ws_url))
+            else:
+                goal_list = generate_goal_list(trace_id)
+                print(f"✅使用 trace_id 生成 goal_list: {goal_list}")
+
+                save_ws_data_to_file(trace_list_data, goal_list)
+                # save_ws_data_to_file(trace_id, args.since_event_id, args.ws_url)
+        else:
+            raise Exception("trace_list.json 中没有 trace_id")

+ 41 - 12
frontend/htmlTemplate/templateHtml.py

@@ -10,21 +10,43 @@ import os
 from pathlib import Path
 
 
-def generate_trace_visualization_html(output_path: str = None):
+def generate_trace_visualization_html(goal_list: list, msg_groups: dict, output_path: str = None):
     """
     生成 Trace 可视化 HTML 文件
 
     Args:
+        goal_list: 节点列表
+        msg_groups: 节点消息分组
         output_path: 输出的 HTML 文件路径
     """
-    # 读取 mock 数据
-    mock_dir = Path(__file__).parent / "mock_data"
+    if goal_list is None:
+        goal_list = []
+    if msg_groups is None:
+        msg_groups = {}
 
-    with open(mock_dir / "trace_list.json", "r", encoding="utf-8") as f:
-        trace_list_data = json.load(f)
+    # 1.新建一个数组goal_list_new = goal_list
+    goal_list_new = list(goal_list)
 
-    with open(mock_dir / "trace_detail.json", "r", encoding="utf-8") as f:
-        trace_detail_data = json.load(f)
+    # 2.查找msg_groups如果里面包含START,那么需要在goal_list_new添加一个对象
+    if "START" in msg_groups:
+        start_node = {
+            "id": "START",
+            "description": "START",
+            "reason": None,
+            "parent_id": None,
+            "type": "start",
+            "status": "completed",
+            "summary": None,
+            "sub_trace_ids": None,
+            "agent_call_mode": None,
+            "self_stats": None,
+            "cumulative_stats": None,
+            "created_at": None,
+            "sub_trace_metadata": None
+        }
+        # 确保 START 节点在最前面或合适位置,这里直接 append
+        # 如果需要它作为根节点被正确渲染,它应该在列表里即可
+        goal_list_new.insert(0, start_node)
 
     # 读取 HTML 模板
     template_path = Path(__file__).parent / "trace_template.html"
@@ -32,11 +54,11 @@ def generate_trace_visualization_html(output_path: str = None):
         template_content = f.read()
 
     html_content = template_content.replace(
-        '"__TRACE_LIST_DATA__"',
-        json.dumps(trace_list_data, ensure_ascii=False)
+        '"__GOAL_LIST__"',
+        json.dumps(goal_list_new, ensure_ascii=False)
     ).replace(
-        '"__TRACE_DETAIL_DATA__"',
-        json.dumps(trace_detail_data, ensure_ascii=False)
+        '"__MSG_GROUPS__"',
+        json.dumps(msg_groups, ensure_ascii=False)
     )
 
     # 确定输出路径
@@ -45,6 +67,13 @@ def generate_trace_visualization_html(output_path: str = None):
     else:
         output_path = Path(output_path)
 
+
+    mock_dir = Path(__file__).parent / "mock_data"
+    mock_dir.mkdir(parents=True, exist_ok=True)
+    with open(mock_dir / "goal_list.json", "w", encoding="utf-8") as f:
+        json.dump(goal_list_new, f, ensure_ascii=False, indent=2)
+    with open(mock_dir / "msg_groups.json", "w", encoding="utf-8") as f:
+        json.dump(msg_groups, f, ensure_ascii=False, indent=2)
     # 写入 HTML 文件
     with open(output_path, 'w', encoding='utf-8') as f:
         f.write(html_content)
@@ -54,4 +83,4 @@ def generate_trace_visualization_html(output_path: str = None):
 
 
 if __name__ == "__main__":
-    generate_trace_visualization_html()
+    generate_trace_visualization_html([], {})

+ 290 - 127
frontend/htmlTemplate/trace_template.html

@@ -103,18 +103,22 @@
       .node rect {
         fill: transparent;
         stroke: none;
+        stroke-width: 0;
       }
+
       .node.selected rect {
-        stroke: #ff6b6b;
-        stroke-width: 2px;
-        stroke-dasharray: 5, 5;
+        stroke: none;
       }
+
       .node text {
-        font-size: 14px;
-        fill: #000;
+        font-size: 10px;
+        font-family: sans-serif;
         text-anchor: middle;
-        dominant-baseline: middle;
-        pointer-events: none;
+      }
+
+      .node.running rect {
+        stroke: none;
+        fill: transparent;
       }
       .node.trace_root text {
         font-size: 16px;
@@ -130,7 +134,6 @@
       }
       .link.highlighted {
         stroke: #ff6b6b;
-        stroke-width: 3px;
       }
       .link-text {
         font-size: 12px;
@@ -332,6 +335,13 @@
         font-weight: bold;
         color: #ff9800;
       }
+      .node.message rect {
+        stroke: none;
+        fill: transparent;
+      }
+      .node.message text {
+        fill: #666;
+      }
     </style>
   </head>
   <body>
@@ -350,12 +360,6 @@
                 <option value="failed">失败</option>
               </select>
             </div>
-            <div class="filter-item">
-              <label for="trace-select">Trace 选择:</label>
-              <select id="trace-select">
-                <!-- 动态填充 -->
-              </select>
-            </div>
             <div class="filter-item">
               <button id="refresh-btn">刷新</button>
             </div>
@@ -396,50 +400,117 @@
     </div>
 
     <script>
-      const rawTraceListData = "__TRACE_LIST_DATA__";
-      const traceListData =
-        typeof rawTraceListData === "string" && rawTraceListData === "__TRACE_LIST_DATA__"
-          ? { traces: [] }
-          : rawTraceListData;
-
-      const rawTraceDetailData = "__TRACE_DETAIL_DATA__";
-      const traceDetailData =
-        typeof rawTraceDetailData === "string" && rawTraceDetailData === "__TRACE_DETAIL_DATA__"
-          ? { trace_id: "mock", goal_tree: { goals: [] } }
-          : rawTraceDetailData;
-
-      let currentTraceId = traceDetailData.trace_id;
+      const rawGoalList = "__GOAL_LIST__";
+      const goalList = typeof rawGoalList === "string" && rawGoalList === "__GOAL_LIST__" ? [] : rawGoalList;
+      console.log("%c [ goalList ]-401", "font-size:13px; background:pink; color:#bf2c9f;", goalList);
+
+      const rawMsgGroups = "__MSG_GROUPS__";
+      const msgGroups = typeof rawMsgGroups === "string" && rawMsgGroups === "__MSG_GROUPS__" ? {} : rawMsgGroups;
+      console.log("%c [ msgGroups ]-404", "font-size:13px; background:pink; color:#bf2c9f;", msgGroups);
+
       let selectedNode = null;
+      let selectedLink = null;
       let svg, g, zoom;
+      let root;
+      // Set to store IDs of expanded nodes (both goals and messages)
+      const expandedState = new Set();
 
       // 初始化
       function init() {
-        // 更新标题
         updateTitle();
+        // Initialize: Ensure at least the first goal is in the chain if needed,
+        // but our logic handles it.
+        renderWrapper();
+        bindEvents();
+      }
 
-        // 渲染图表
+      function renderWrapper() {
+        root = setupData();
         renderGraph();
-
-        // 绑定事件
-        bindEvents();
       }
 
       // 更新标题
       function updateTitle() {
-        const traces = Array.isArray(traceListData)
-          ? traceListData
-          : Array.isArray(traceListData.traces)
-            ? traceListData.traces
-            : traceListData.trace_id
-              ? [traceListData]
-              : [];
-
-        const trace = traces.find((t) => t.trace_id === currentTraceId);
-        if (trace) {
-          document.getElementById("task-title").textContent = trace.task;
+        if (goalList.length > 0 && goalList[0].description) {
+          document.getElementById("task-title").textContent = goalList[0].description;
         }
       }
 
+      // 构建数据
+      function setupData() {
+        const goals = Array.isArray(goalList) ? goalList : [];
+        const nodeMap = new Map();
+
+        // Helper to create a node object
+        const createNode = (data, type) => ({
+          ...data,
+          type: type,
+          children: [],
+          _original: data,
+        });
+
+        // 1. Identify Top-Level Goals and Child Goals
+        const topLevelGoals = [];
+        const childGoalsMap = new Map(); // parent_id -> [goals]
+
+        goals.forEach((goal) => {
+          if (goal.parent_id === null) {
+            topLevelGoals.push(goal);
+          } else {
+            if (!childGoalsMap.has(goal.parent_id)) {
+              childGoalsMap.set(goal.parent_id, []);
+            }
+            childGoalsMap.get(goal.parent_id).push(goal);
+          }
+        });
+
+        // 2. Build the Tree
+        const virtualRoot = {
+          id: "VIRTUAL_ROOT",
+          children: [],
+        };
+
+        let currentParent = virtualRoot;
+
+        topLevelGoals.forEach((goal, index) => {
+          const goalNode = createNode(goal, "goal");
+          const nextGoal = index < topLevelGoals.length - 1 ? topLevelGoals[index + 1] : null;
+          goalNode._nextGoalId = nextGoal ? nextGoal.id : null;
+
+          if (childGoalsMap.has(goal.id)) {
+            childGoalsMap.get(goal.id).forEach((subGoal) => {
+              goalNode.children.push(createNode(subGoal, "goal"));
+            });
+          }
+
+          currentParent.children.push(goalNode);
+
+          if (msgGroups && msgGroups[goal.id] && expandedState.has(goal.id)) {
+            const msgs = msgGroups[goal.id];
+            let msgParent = goalNode;
+
+            for (let i = 0; i < msgs.length; i++) {
+              const msg = msgs[i];
+              const msgNode = createNode(msg, "message");
+              msgNode.id = `${goal.id}-msg-${i}`;
+              msgParent.children.push(msgNode);
+              msgParent = msgNode;
+            }
+          }
+
+          currentParent = goalNode;
+        });
+
+        return d3.hierarchy(virtualRoot);
+      }
+
+      function buildLayoutData(node) {
+        const children = Array.isArray(node.children)
+          ? node.children.filter((child) => child.type !== "message").map((child) => buildLayoutData(child))
+          : [];
+        return { ...node, children };
+      }
+
       // 渲染图表
       function renderGraph() {
         const chartDiv = document.getElementById("chart");
@@ -457,8 +528,10 @@
           .attr("preserveAspectRatio", "xMidYMid meet");
 
         // 定义箭头
-        svg
-          .append("defs")
+        const defs = svg.append("defs");
+
+        // 默认蓝色箭头
+        defs
           .append("marker")
           .attr("id", "arrowhead")
           .attr("viewBox", "0 -5 10 10")
@@ -471,104 +544,147 @@
           .attr("d", "M0,-5L10,0L0,5")
           .attr("fill", "#4e79a7");
 
+        // 选中红色箭头
+        defs
+          .append("marker")
+          .attr("id", "arrowhead-selected")
+          .attr("viewBox", "0 -5 10 10")
+          .attr("refX", 10)
+          .attr("refY", 0)
+          .attr("markerWidth", 6)
+          .attr("markerHeight", 6)
+          .attr("orient", "auto")
+          .append("path")
+          .attr("d", "M0,-5L10,0L0,5")
+          .attr("fill", "#ff6b6b");
+
         g = svg.append("g");
 
-        const goals =
-          traceDetailData.goal_tree && Array.isArray(traceDetailData.goal_tree.goals)
-            ? traceDetailData.goal_tree.goals
-            : Array.isArray(traceDetailData.goals)
-              ? traceDetailData.goals
-              : [];
-        const nodeMap = new Map();
+        const treeLayout = d3
+          .tree()
+          .size([height - 100, width - 200])
+          .separation((a, b) => (a.parent == b.parent ? 1.2 : 1.5));
 
-        // 1. 创建虚拟根节点
-        const virtualRoot = {
-          id: "VIRTUAL_ROOT",
-          children: [],
-        };
+        const layoutData = buildLayoutData(root.data);
+        const layoutRoot = d3.hierarchy(layoutData);
+        treeLayout(layoutRoot);
 
-        // 2. 构建节点映射
-        goals.forEach((goal) => {
-          nodeMap.set(goal.id, {
-            ...goal,
-            children: [],
-          });
+        const positionMap = new Map();
+        layoutRoot.descendants().forEach((d) => {
+          if (d.data && d.data.id !== undefined) {
+            positionMap.set(d.data.id, { x: d.x, y: d.y });
+          }
         });
 
-        // 3. 构建树结构 - 还原 parent_id 关系,多根节点挂载到虚拟根节点
-        goals.forEach((goal) => {
-          const node = nodeMap.get(goal.id);
-          if (goal.parent_id === null) {
-            // 根节点(parent_id 为 null),挂载到虚拟根节点
-            virtualRoot.children.push(node);
-          } else {
-            // 子节点,挂载到对应的父节点
-            const parent = nodeMap.get(goal.parent_id);
-            if (parent) {
-              parent.children.push(node);
-            } else {
-              // 如果找不到父节点,作为根节点处理(容错)
-              console.warn(`Parent node ${goal.parent_id} not found for goal ${goal.id}`);
-              virtualRoot.children.push(node);
-            }
+        root.descendants().forEach((d) => {
+          if (d.data && d.data.type !== "message" && positionMap.has(d.data.id)) {
+            const pos = positionMap.get(d.data.id);
+            d.x = pos.x;
+            d.y = pos.y;
           }
         });
 
-        // 创建层次结构 - 从虚拟根节点开始
-        const root = d3.hierarchy(virtualRoot);
+        const getMessageChain = (goalNode) => {
+          const chain = [];
+          if (!goalNode.children) return chain;
+          let current = goalNode.children.find((child) => child.data && child.data.type === "message");
+          while (current && current.data && current.data.type === "message") {
+            chain.push(current);
+            if (!current.children) break;
+            const next = current.children.find((child) => child.data && child.data.type === "message");
+            current = next || null;
+          }
+          return chain;
+        };
 
-        // 创建树布局
-        // 注意:由于隐藏了第一层(虚拟根节点),实际显示的节点从第二层开始
-        // 我们需要调整布局大小,或者在绘制时进行坐标偏移
-        const treeLayout = d3
-          .tree()
-          .size([height - 100, width - 200])
-          .separation((a, b) => (a.parent == b.parent ? 1.2 : 1.5)); // 增加节点间距
+        const goalNodes = root.descendants().filter((d) => d.data && d.data.type === "goal");
+        const goalNodeMap = new Map();
+        goalNodes.forEach((goalNode) => {
+          goalNodeMap.set(goalNode.data.id, goalNode);
+        });
 
-        treeLayout(root);
+        const msgBaseDown = 80;
+        const msgStepDown = 70;
+        const msgIndent = 30;
+
+        goalNodes.forEach((goalNode) => {
+          const messageNodes = getMessageChain(goalNode);
+          if (messageNodes.length === 0) return;
+          messageNodes.forEach((messageNode, idx) => {
+            messageNode.x = goalNode.x + msgBaseDown + idx * msgStepDown;
+            messageNode.y = goalNode.y + msgIndent;
+          });
+        });
 
-        // 过滤掉虚拟根节点及其连接的边
         const nodesData = root.descendants().filter((d) => d.depth > 0);
-        const linksData = root.links().filter((d) => d.source.depth > 0);
+        const baseLinksData = root.links().filter((d) => d.source.depth > 0);
+        const extraLinks = [];
+
+        goalNodes.forEach((goalNode) => {
+          if (!expandedState.has(goalNode.data.id)) return;
+          const messageNodes = getMessageChain(goalNode);
+          if (messageNodes.length === 0) return;
+          const lastMessage = messageNodes[messageNodes.length - 1];
+          const nextGoalId = goalNode.data._nextGoalId;
+          const nextGoalNode = nextGoalId ? goalNodeMap.get(nextGoalId) : null;
+          if (nextGoalNode) {
+            extraLinks.push({
+              source: lastMessage,
+              target: nextGoalNode,
+              _linkType: "message-to-next-goal",
+            });
+          }
+        });
+
+        const linksData = baseLinksData.concat(extraLinks);
 
         // 绘制连线
         const linkGroups = g.selectAll(".link-group").data(linksData).enter().append("g").attr("class", "link-group");
 
         linkGroups
           .append("path")
-          .attr("class", "link")
+          .attr("class", (d) => {
+            // 保持高亮状态
+            return d === selectedLink ? "link highlighted" : "link";
+          })
           .attr("d", (d) => {
-            // 调整坐标:减去虚拟根节点带来的层级偏移
-            // 假设每一层级大约占用的宽度,这里我们简单地减去一定偏移量让其靠左
-            // 但更好的方式是依赖 fitToView 自动调整
-            const sourceX = d.source.y + 100 + 80;
             const sourceY = d.source.x + 50;
-            const targetX = d.target.y + 100 - 80;
             const targetY = d.target.x + 50;
+            if (d._linkType === "message-to-next-goal") {
+              const sourceX = d.source.y + 100;
+              const targetX = d.target.y + 100 - 80;
+              const controlX = (sourceX + targetX) / 2;
+              const controlY = Math.max(sourceY, targetY) + 90;
+              return `M${sourceX},${sourceY} Q${controlX},${controlY} ${targetX},${targetY}`;
+            }
+            if (d.target.data.type === "message") {
+              const sourceX = d.source.y + 100;
+              const targetX = d.target.y + 100;
+              // Use smooth S-curve (cubic bezier) for parent->message and message->message
+              // This avoids the "looping" effect and provides a clean flow
+              return `M${sourceX},${sourceY} C${sourceX},${(sourceY + targetY) / 2} ${targetX},${(sourceY + targetY) / 2} ${targetX},${targetY}`;
+            }
+            const sourceX = d.source.y + 100 + 80;
+            const targetX = d.target.y + 100 - 80;
             return `M${sourceX},${sourceY} C${(sourceX + targetX) / 2},${sourceY} ${(sourceX + targetX) / 2},${targetY} ${targetX},${targetY}`;
           })
-          .attr("marker-end", "url(#arrowhead)")
+          .attr("marker-end", (d) => {
+            // 如果选中,使用红色箭头
+            if (d === selectedLink) return "url(#arrowhead-selected)";
+            return "url(#arrowhead)";
+          })
+          .attr("stroke-dasharray", (d) => {
+            if (d._linkType === "message-to-next-goal") return "5,5";
+            if (d.target.data.type === "message") return "5,5";
+            return null;
+          })
           .on("click", function (event, d) {
             event.stopPropagation();
+            selectedLink = d;
+            renderWrapper(); // 重绘以更新样式
             showEdgeDetail(d);
           });
 
-        // 添加连线文字
-        linkGroups
-          .append("text")
-          .attr("class", "link-text")
-          .attr("x", (d) => {
-            const sourceX = d.source.y + 100 + 80;
-            const targetX = d.target.y + 100 - 80;
-            return (sourceX + targetX) / 2;
-          })
-          .attr("y", (d) => {
-            const sourceY = d.source.x + 50;
-            const targetY = d.target.x + 50;
-            return (sourceY + targetY) / 2 - 5;
-          })
-          .text((d) => d.target.data.edgeLabel || "");
-
         // 绘制节点
         const nodes = g
           .selectAll(".node")
@@ -579,17 +695,23 @@
           .attr("transform", (d) => `translate(${d.y + 100},${d.x + 50})`)
           .on("click", function (event, d) {
             event.stopPropagation();
-            selectNode(d);
+            handleNodeClick(d);
           });
 
-        nodes.append("rect").attr("x", -80).attr("y", -30).attr("width", 160).attr("height", 60);
-
-        nodes
+        // 节点文本
+        const textNode = nodes
           .append("text")
           .attr("dy", 5)
-          .text((d) => d.data.description.substring(0, 10) + (d.data.description.length > 10 ? "..." : ""));
+          .text((d) => {
+            const text = d.data.description || "";
+            const limit = d.data.type === "message" ? 6 : 15; // Message limited to 6 chars
+            return text.length > limit ? text.substring(0, limit) + "..." : text;
+          });
 
-        // 添加缩放功能
+        // Add tooltip for full description
+        textNode.append("title").text((d) => d.data.description || "");
+
+        // 缩放
         zoom = d3
           .zoom()
           .scaleExtent([0.1, 5])
@@ -599,10 +721,27 @@
 
         svg.call(zoom);
 
-        // 自动缩放以适应屏幕
-        setTimeout(() => {
-          fitToView();
-        }, 0);
+        // 初始自适应(仅在第一次或重置时?)
+        // 为简单起见,这里不每次重置视图,除非是第一次
+        // 或者保留当前的 transform
+      }
+
+      function handleNodeClick(d) {
+        // Toggle expanded state
+        if (expandedState.has(d.data.id)) {
+          expandedState.delete(d.data.id);
+        } else {
+          expandedState.add(d.data.id);
+        }
+
+        // 2. 选中逻辑
+        selectedNode = d;
+
+        // 3. 重绘
+        renderWrapper();
+
+        // 4. 显示详情
+        selectNode(d);
       }
 
       // 自动缩放以适应屏幕
@@ -650,7 +789,7 @@
       // 高亮路径
       function highlightPath(d) {
         // 移除之前的高亮
-        d3.selectAll(".link").classed("highlighted", false);
+        d3.selectAll(".link").classed("highlighted", false).attr("marker-end", "url(#arrowhead)");
 
         // 找到从根节点到当前节点的路径
         const path = d.ancestors().reverse();
@@ -660,7 +799,7 @@
           const sourceInPath = path.includes(linkData.source);
           const targetInPath = path.includes(linkData.target);
           if (sourceInPath && targetInPath) {
-            d3.select(this).classed("highlighted", true);
+            d3.select(this).classed("highlighted", true).attr("marker-end", "url(#arrowhead-selected)");
           }
         });
       }
@@ -751,6 +890,31 @@
                 `;
         }
 
+        const messages = msgGroups[goal.id] || [];
+        if (Array.isArray(messages) && messages.length > 0) {
+          const messageHtml = messages
+            .map((message) => {
+              const role = message.role || message.sender || "unknown";
+              const content = message.content || message.text || "";
+              return `
+                        <div class="message-item ${role}">
+                            <div class="message-header">
+                                <span class="message-role">${role}</span>
+                                <span class="message-time">${message.created_at || ""}</span>
+                            </div>
+                            <div class="message-content">${content}</div>
+                        </div>
+                    `;
+            })
+            .join("");
+          html += `
+                    <div class="detail-item">
+                        <label>节点内容:</label>
+                        <div class="message-list">${messageHtml}</div>
+                    </div>
+                `;
+        }
+
         detailContent.innerHTML = html;
       }
 
@@ -776,7 +940,6 @@
       function bindEvents() {
         // Trace 选择器
         document.getElementById("trace-select").addEventListener("change", function () {
-          currentTraceId = this.value;
           updateTitle();
           renderGraph();
         });

Plik diff jest za duży
+ 18 - 20
frontend/htmlTemplate/trace_visualization.html


Plik diff jest za duży
+ 13 - 0
frontend/htmlTemplate/ws_data/event.jsonl


+ 28 - 0
frontend/htmlTemplate/ws_data/trace_list.json

@@ -0,0 +1,28 @@
+{
+  "traces": [
+    {
+      "trace_id": "ffc7e6e8-6725-4bd3-8cd1-5c212856feff",
+      "mode": "agent",
+      "prompt_name": null,
+      "task": "[图片和特征描述已包含在 messages 中]",
+      "agent_type": "default",
+      "parent_trace_id": null,
+      "parent_goal_id": null,
+      "status": "completed",
+      "total_messages": 20,
+      "total_tokens": 145867,
+      "total_cost": 0.0,
+      "total_duration_ms": 0,
+      "last_sequence": 20,
+      "last_event_id": 25,
+      "uid": null,
+      "context": {
+        "model": "anthropic/claude-sonnet-4.5",
+        "temperature": 0.3
+      },
+      "current_goal_id": null,
+      "created_at": "2026-02-05T19:20:17.456366",
+      "completed_at": "2026-02-05T19:23:34.789135"
+    }
+  ]
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików