Explorar el Código

compression test

guantao hace 7 horas
padre
commit
14859cecf0

+ 1 - 1
agent/cli/interactive.py

@@ -291,7 +291,7 @@ class InteractiveController:
         print("\n正在加载消息列表...")
 
         # 1. 获取所有消息
-        messages = await self.store.get_messages(trace_id)
+        messages = await self.store.get_trace_messages(trace_id)
         if not messages:
             print("❌ 没有找到任何消息")
             return

+ 6 - 4
agent/tools/builtin/search.py

@@ -101,11 +101,11 @@ async def _build_collage(posts: List[Dict[str, Any]]) -> Optional[str]:
     # 尝试加载字体
     try:
         font_title = ImageFont.truetype("msyh.ttc", 16)
-        font_index = ImageFont.truetype("msyh.ttc", 22)
+        font_index = ImageFont.truetype("msyh.ttc", 32)
     except Exception:
         try:
             font_title = ImageFont.truetype("arial.ttf", 16)
-            font_index = ImageFont.truetype("arial.ttf", 22)
+            font_index = ImageFont.truetype("arial.ttf", 32)
         except Exception:
             font_title = ImageFont.load_default()
             font_index = font_title
@@ -125,8 +125,10 @@ async def _build_collage(posts: List[Dict[str, Any]]) -> Optional[str]:
         index_text = f" {idx} "
         bbox = draw.textbbox((0, 0), index_text, font=font_index)
         tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
-        draw.rectangle([x, y, x + tw + 4, y + th + 4], fill=INDEX_COLOR)
-        draw.text((x + 2, y + 2), index_text, fill=(255, 255, 255), font=font_index)
+        # 增加背景块的 padding,确保完全覆盖数字
+        pad_x, pad_y = 8, 6
+        draw.rectangle([x, y, x + tw + pad_x * 2, y + th + pad_y * 2], fill=INDEX_COLOR)
+        draw.text((x + pad_x, y + pad_y), index_text, fill=(255, 255, 255), font=font_index)
 
         # 写标题
         title_text = _truncate_text(item["title"], max_len=16)

+ 52 - 3
agent/trace/goal_models.py

@@ -200,6 +200,46 @@ class GoalTree:
             except ValueError:
                 return f"{parent_display}.?"
 
+    def _find_next_pending_goal(self, completed_goal_id: str) -> Optional[Goal]:
+        """
+        完成 goal 后,自动查找下一个应该执行的 pending goal
+
+        查找顺序:
+        1. 同级的下一个 pending goal
+        2. 父级的下一个 pending goal
+        3. 任意顶层 pending goal
+
+        Args:
+            completed_goal_id: 刚完成的 goal ID
+
+        Returns:
+            下一个 pending goal,如果没有则返回 None
+        """
+        completed_goal = self.find(completed_goal_id)
+        if not completed_goal:
+            return None
+
+        # 1. 查找同级的下一个 pending goal
+        siblings = self.get_children(completed_goal.parent_id)
+        found_current = False
+        for sibling in siblings:
+            if sibling.id == completed_goal_id:
+                found_current = True
+                continue
+            if found_current and sibling.status == "pending":
+                return sibling
+
+        # 2. 如果有父级,查找父级的下一个 pending goal
+        if completed_goal.parent_id:
+            return self._find_next_pending_goal(completed_goal.parent_id)
+
+        # 3. 查找任意顶层 pending goal
+        for goal in self.goals:
+            if goal.parent_id is None and goal.status == "pending":
+                return goal
+
+        return None
+
     def add_goals(
         self,
         descriptions: List[str],
@@ -300,7 +340,12 @@ class GoalTree:
 
         # 如果完成的是当前焦点,根据参数决定是否清除焦点
         if clear_focus and self.current_id == goal_id:
-            self.current_id = None
+            # 不直接清空,尝试自动切换到下一个 pending goal
+            next_goal = self._find_next_pending_goal(goal_id)
+            if next_goal:
+                self.current_id = next_goal.id
+            else:
+                self.current_id = None
 
         # 检查是否所有兄弟都完成了,如果是则自动完成父节点
         if goal.parent_id:
@@ -325,9 +370,13 @@ class GoalTree:
         goal.status = "abandoned"
         goal.summary = reason
 
-        # 如果放弃的是当前焦点,清除焦点
+        # 如果放弃的是当前焦点,尝试自动切换到下一个 pending goal
         if self.current_id == goal_id:
-            self.current_id = None
+            next_goal = self._find_next_pending_goal(goal_id)
+            if next_goal:
+                self.current_id = next_goal.id
+            else:
+                self.current_id = None
 
         return goal
 

+ 57 - 165
frontend/react-template/src/components/DetailPanel/DetailPanel.tsx

@@ -13,7 +13,12 @@ interface DetailPanelProps {
   onClose: () => void;
 }
 
-export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelProps) => {
+export const DetailPanel = ({
+  node,
+  edge,
+  messages = [],
+  onClose,
+}: DetailPanelProps) => {
   const [previewImage, setPreviewImage] = useState<string | null>(null);
 
   console.log("DetailPanel - node:", node);
@@ -38,42 +43,12 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
     );
   };
 
-  const renderResultImages = (imageUrls: string[]) => {
-    if (!imageUrls || imageUrls.length === 0) return null;
-    const normalized = imageUrls
-      .map((raw) => String(raw))
-      .map((raw) => raw.replace(/^[\s`'"]+|[\s`'"]+$/g, ""))
-      .filter((url) => url.startsWith("http") || url.startsWith("data:"));
-    if (normalized.length === 0) return null;
-    return (
-      <div className="grid grid-cols-4 gap-2 mt-2">
-        {normalized.map((url, idx) => (
-          <img
-            key={`${url}-${idx}`}
-            src={url}
-            alt={`Image ${idx + 1}`}
-            className="w-full h-16 object-cover rounded border border-gray-200 cursor-pointer hover:opacity-80 transition-opacity bg-gray-50"
-            onClick={() => setPreviewImage(url)}
-            onError={(e) => {
-              (e.target as HTMLImageElement).style.display = "none";
-            }}
-          />
-        ))}
-      </div>
-    );
-  };
-
-  const normalizeImageUrl = (raw: unknown) => String(raw).replace(/^[\s`'"]+|[\s`'"]+$/g, "");
-
-  const getValidImageUrls = (values: unknown[]): string[] => {
-    return values.map((v) => normalizeImageUrl(v)).filter((url) => url.startsWith("http") || url.startsWith("data:"));
-  };
-
   const title = node ? "节点详情" : edge ? "连线详情" : "详情";
 
   const renderMessageContent = (content: Message["content"]) => {
     if (!content) return "";
-    if (typeof content === "string") return <ReactMarkdown>{content}</ReactMarkdown>;
+    if (typeof content === "string")
+      return <ReactMarkdown>{content}</ReactMarkdown>;
 
     const hasText = !!content.text;
     const hasToolCalls = content.tool_calls && content.tool_calls.length > 0;
@@ -87,11 +62,14 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
             <div className={styles.toolCalls}>
               {content.tool_calls!.map((call, idx) => {
                 const anyCall = call as unknown as Record<string, unknown>;
-                const fn = anyCall.function as Record<string, unknown> | undefined;
+                const fn = anyCall.function as
+                  | Record<string, unknown>
+                  | undefined;
                 const name =
                   (fn && (fn.name as string)) ||
                   (anyCall.name as string) ||
-                  ((content as unknown as Record<string, unknown>).tool_name as string) ||
+                  ((content as unknown as Record<string, unknown>)
+                    .tool_name as string) ||
                   `tool_${idx}`;
                 let args: unknown =
                   (fn && fn.arguments) ||
@@ -106,12 +84,11 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
                 }
                 const key = (anyCall.id as string) || `${name}-${idx}`;
                 return (
-                  <div
-                    key={key}
-                    className={styles.toolCall}
-                  >
+                  <div key={key} className={styles.toolCall}>
                     <div className={styles.toolName}>工具调用: {name}</div>
-                    <pre className={styles.toolArgs}>{JSON.stringify(args, null, 2)}</pre>
+                    <pre className={styles.toolArgs}>
+                      {JSON.stringify(args, null, 2)}
+                    </pre>
                   </div>
                 );
               })}
@@ -121,100 +98,11 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
             <div className={styles.toolResult}>
               <div className={styles.toolName}>工具: {content.tool_name}</div>
               <div className={styles.resultContent}>
-                {(() => {
-                  let resultData: unknown = content.result;
-
-                  const tryParseEmbeddedJson = (text: string): unknown | null => {
-                    const firstArray = text.indexOf("[");
-                    const lastArray = text.lastIndexOf("]");
-                    if (firstArray !== -1 && lastArray !== -1 && lastArray > firstArray) {
-                      const sliced = text.slice(firstArray, lastArray + 1);
-                      try {
-                        return JSON.parse(sliced);
-                      } catch {
-                        return null;
-                      }
-                    }
-                    const firstObj = text.indexOf("{");
-                    const lastObj = text.lastIndexOf("}");
-                    if (firstObj !== -1 && lastObj !== -1 && lastObj > firstObj) {
-                      const sliced = text.slice(firstObj, lastObj + 1);
-                      try {
-                        return JSON.parse(sliced);
-                      } catch {
-                        return null;
-                      }
-                    }
-                    return null;
-                  };
-
-                  if (typeof resultData === "string") {
-                    const raw = resultData;
-                    try {
-                      resultData = JSON.parse(raw);
-                    } catch {
-                      const embedded = tryParseEmbeddedJson(raw);
-                      if (embedded !== null) {
-                        resultData = embedded;
-                      } else {
-                        return <ReactMarkdown>{raw}</ReactMarkdown>;
-                      }
-                    }
-                  }
-
-                  const renderStructuredResult = (value: unknown, field?: string): JSX.Element => {
-                    if (Array.isArray(value)) {
-                      if (field === "images") {
-                        const urls = getValidImageUrls(value);
-                        if (urls.length > 0) {
-                          return <div>{renderResultImages(urls)}</div>;
-                        }
-                      }
-                      return (
-                        <div className="space-y-2">
-                          {value.map((item, idx) => (
-                            <div
-                              key={`${field || "arr"}-${idx}`}
-                              className="pl-2 border-l border-slate-200"
-                            >
-                              <div className="text-xs text-slate-500 mb-1">[{idx}]</div>
-                              {renderStructuredResult(item)}
-                            </div>
-                          ))}
-                        </div>
-                      );
-                    }
-
-                    if (value && typeof value === "object") {
-                      return (
-                        <div className="space-y-2">
-                          {Object.entries(value as Record<string, unknown>).map(([k, v]) => (
-                            <div key={k}>
-                              <div className="text-xs text-slate-500">{k}</div>
-                              <div className="pl-2">{renderStructuredResult(v, k)}</div>
-                            </div>
-                          ))}
-                        </div>
-                      );
-                    }
-
-                    if (typeof value === "string") {
-                      return <span className="whitespace-pre-wrap break-all">{value}</span>;
-                    }
-
-                    if (typeof value === "number" || typeof value === "boolean") {
-                      return <span>{String(value)}</span>;
-                    }
-
-                    if (value === null) {
-                      return <span>null</span>;
-                    }
-
-                    return <span>{String(value)}</span>;
-                  };
-
-                  return renderStructuredResult(resultData);
-                })()}
+                {typeof content.result === "string" ? (
+                  <ReactMarkdown>{content.result}</ReactMarkdown>
+                ) : (
+                  <pre>{JSON.stringify(content.result, null, 2)}</pre>
+                )}
               </div>
             </div>
           )}
@@ -232,7 +120,9 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
   };
 
   const isMessageNode = (node: Goal | Message): node is Message =>
-    "message_id" in node || "role" in node || "sequence" in node;
+    "message_id" in node ||
+    "role" in node ||
+    "sequence" in node;
 
   const renderKnowledge = (knowledge: Goal["knowledge"]) => {
     if (!knowledge || knowledge.length === 0) return null;
@@ -240,32 +130,34 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
     return (
       <div className={styles.knowledgeList}>
         {knowledge.map((item) => (
-          <div
-            key={item.id}
-            className={styles.knowledgeItem}
-          >
+          <div key={item.id} className={styles.knowledgeItem}>
             <div className={styles.knowledgeHeader}>
               <span className={styles.knowledgeId}>{item.id}</span>
               <div className={styles.knowledgeMetrics}>
-                {item.score !== undefined && <span className={styles.metricScore}>⭐ {item.score}</span>}
+                {item.score !== undefined && (
+                  <span className={styles.metricScore}>⭐ {item.score}</span>
+                )}
                 {item.quality_score !== undefined && (
-                  <span className={styles.metricQuality}>✨ {item.quality_score.toFixed(1)}</span>
+                  <span className={styles.metricQuality}>
+                    ✨ {item.quality_score.toFixed(1)}
+                  </span>
                 )}
                 {item.metrics?.helpful !== undefined && (
-                  <span className={styles.metricHelpful}>👍 {item.metrics.helpful}</span>
+                  <span className={styles.metricHelpful}>
+                    👍 {item.metrics.helpful}
+                  </span>
                 )}
                 {item.metrics?.harmful !== undefined && (
-                  <span className={styles.metricHarmful}>👎 {item.metrics.harmful}</span>
+                  <span className={styles.metricHarmful}>
+                    👎 {item.metrics.harmful}
+                  </span>
                 )}
               </div>
             </div>
             {item.tags?.type && item.tags.type.length > 0 && (
               <div className={styles.knowledgeTags}>
                 {item.tags.type.map((tag) => (
-                  <span
-                    key={tag}
-                    className={styles.tag}
-                  >
+                  <span key={tag} className={styles.tag}>
                     {tag}
                   </span>
                 ))}
@@ -287,11 +179,7 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
     <aside className={styles.panel}>
       <div className={styles.header}>
         <div className={styles.title}>{title}</div>
-        <button
-          className={styles.close}
-          onClick={onClose}
-          aria-label="关闭"
-        >
+        <button className={styles.close} onClick={onClose} aria-label="关闭">
           ×
         </button>
       </div>
@@ -301,14 +189,17 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
             <div className={styles.sectionTitle}>节点</div>
             <div className={styles.section}>
               <div className={styles.label}>ID</div>
-              <div className={styles.value}>{isMessageNode(node) ? node.message_id || node.id : node.id}</div>
-            </div>
-            {isMessageNode(node) && extractImagesFromMessage(node).length > 0 && (
-              <div className={styles.section}>
-                <div className={styles.label}>图片</div>
-                {renderImages(node)}
+              <div className={styles.value}>
+                {isMessageNode(node) ? node.message_id || node.id : node.id}
               </div>
-            )}
+            </div>
+            {isMessageNode(node) &&
+              extractImagesFromMessage(node).length > 0 && (
+                <div className={styles.section}>
+                  <div className={styles.label}>图片</div>
+                  {renderImages(node)}
+                </div>
+              )}
 
             {isGoal(node) ? (
               <>
@@ -355,7 +246,9 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
                 )}
                 <div className={styles.section}>
                   <div className={styles.label}>内容</div>
-                  <div className={styles.value}>{node.content && renderMessageContent(node.content)}</div>
+                  <div className={styles.value}>
+                    {node.content && renderMessageContent(node.content)}
+                  </div>
                 </div>
                 {node.goal_id && (
                   <div className={styles.section}>
@@ -377,17 +270,16 @@ export const DetailPanel = ({ node, edge, messages = [], onClose }: DetailPanelP
           <div className={styles.messages}>
             <div className={styles.sectionTitle}>边</div>
             {messages.map((msg, idx) => (
-              <div
-                key={msg.id || idx}
-                className={styles.messageItem}
-              >
+              <div key={msg.id || idx} className={styles.messageItem}>
                 <div className={styles.section}>
                   <div className={styles.label}>描述</div>
                   <div className={styles.value}>{msg.description || "-"}</div>
                 </div>
                 <div className={styles.section}>
                   <div className={styles.label}>内容</div>
-                  <div className={styles.value}>{msg.content && renderMessageContent(msg.content)}</div>
+                  <div className={styles.value}>
+                    {msg.content && renderMessageContent(msg.content)}
+                  </div>
                 </div>
               </div>
             ))}