2
0
Просмотр исходного кода

feat(json-render): 修复 markdown JSON 提取和渲染时序问题

- 添加 extractJsonFromMarkdown 函数从 response 提取 JSON
- 修复时序问题:specs 更新后自动更新最后一条消息
- 修复循环依赖:移除不必要依赖项
- 小说列表卡片正确渲染(10个卡片)

测试验证:移动端模式下,"获取小说列表"成功渲染 10 个小说卡片

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude AI 15 часов назад
Родитель
Сommit
ce9d4de58a
2 измененных файлов с 142 добавлено и 6 удалено
  1. 20 1
      frontend-v2/app/page.tsx
  2. 122 5
      frontend-v2/lib/hooks.ts

+ 20 - 1
frontend-v2/app/page.tsx

@@ -41,8 +41,9 @@ export default function Home() {
       toolCallsCount: toolCalls?.length || 0,
       hasError: !!error,
     });
+
+    // 情况 1: 添加新消息(加载完成且有待处理消息)
     if (!isLoading && pendingMessageRef.current) {
-      // 即使没有响应内容,也添加消息(可能只有工具调用结果)
       const assistantMessage = {
         role: 'assistant' as const,
         content: response || (error ? `⚠️ ${error}` : ''),
@@ -53,6 +54,24 @@ export default function Home() {
       setHistory((prev) => [...prev, assistantMessage]);
       pendingMessageRef.current = null;
     }
+    // 情况 2: 更新最后一条消息(specs 从 response 中提取后更新)
+    // 这解决了 specs 在消息添加后才更新的时序问题
+    else if (!isLoading && specs && specs.length > 0) {
+      setHistory((prev) => {
+        const lastMessage = prev[prev.length - 1];
+        // 如果最后一条是 assistant 消息且没有 specs,更新它
+        if (lastMessage && lastMessage.role === 'assistant' && !lastMessage.specs) {
+          console.log('[useEffect] Updating last message with specs:', specs);
+          const updated = [...prev];
+          updated[prev.length - 1] = {
+            ...lastMessage,
+            specs: specs,
+          };
+          return updated;
+        }
+        return prev;
+      });
+    }
   }, [isLoading, response, toolCalls, specs, error]);
 
   const handleSend = async (message: string) => {

+ 122 - 5
frontend-v2/lib/hooks.ts

@@ -39,30 +39,147 @@ export function useChat() {
     console.log('[useEffect] toolCalls updated:', toolCalls);
   }, [toolCalls]);
 
-  // 监控 response 状态变化
+  // 监控 response 状态变化,并在完成时提取 JSON spec
   useEffect(() => {
     console.log('[useEffect] response updated:', {
       length: response.length,
       preview: response.substring(0, 100),
     });
-  }, [response]);
+
+    // 只在加载完成且有响应内容时尝试提取 spec
+    // 这确保在流式输出完成后,从 response 中提取 JSON
+    if (!isLoading && response && response.trim()) {
+      console.log('[useEffect] Loading complete, extracting JSON from response...');
+      const extractedData = extractJsonFromMarkdown(response);
+      console.log('[useEffect] Extracted data:', extractedData);
+
+      if (extractedData) {
+        const newSpecs: any[] = [];
+
+        // 检查是否为有效的组件 spec(有 type 字段)
+        if (extractedData.type && typeof extractedData.type === 'string') {
+          newSpecs.push(extractedData);
+          console.log('[useEffect] Added spec from response:', extractedData);
+        }
+        // 如果是数组,检查每个元素
+        else if (Array.isArray(extractedData)) {
+          for (const item of extractedData) {
+            if (item && item.type && typeof item.type === 'string') {
+              newSpecs.push(item);
+              console.log('[useEffect] Added spec from response array:', item);
+            }
+          }
+        }
+
+        if (newSpecs.length > 0) {
+          console.log('[useEffect] Setting specs from response:', newSpecs);
+          setSpecs(newSpecs);
+        }
+      }
+    }
+  }, [response, isLoading]); // extractJsonFromMarkdown 依赖为空数组,不需要在依赖项中
+
+  /**
+   * 从 markdown 代码块中提取 JSON
+   * 支持格式:```json ... ``` 或 ``` ... ```
+   */
+  const extractJsonFromMarkdown = useCallback((text: string): any | null => {
+    if (typeof text !== 'string') {
+      return null;
+    }
+
+    // 1. 匹配 ```json ... ``` 代码块(先提取整个代码块内容,不限制格式)
+    const jsonCodeBlockMatch = text.match(/```json\s*([\s\S]*?)\s*```/);
+    if (jsonCodeBlockMatch) {
+      try {
+        const jsonStr = jsonCodeBlockMatch[1].trim();
+        console.log('[extractJsonFromMarkdown] Found json code block, parsing:', jsonStr.substring(0, 100));
+        const parsed = JSON.parse(jsonStr);
+        console.log('[extractJsonFromMarkdown] Parsed successfully:', parsed);
+        return parsed;
+      } catch (e) {
+        console.error('[extractJsonFromMarkdown] Failed to parse JSON code block:', e);
+        // 继续尝试其他方式
+      }
+    }
+
+    // 2. 匹配 ``` ... ``` 代码块(无语言标识)
+    const codeBlockMatch = text.match(/```\s*([\s\S]*?)\s*```/);
+    if (codeBlockMatch) {
+      try {
+        const jsonStr = codeBlockMatch[1].trim();
+        console.log('[extractJsonFromMarkdown] Found code block, trying to parse:', jsonStr.substring(0, 100));
+        const parsed = JSON.parse(jsonStr);
+        console.log('[extractJsonFromMarkdown] Parsed successfully:', parsed);
+        return parsed;
+      } catch (e) {
+        console.log('[extractJsonFromMarkdown] Code block is not JSON, continuing...');
+        // 不是 JSON,继续尝试其他方式
+      }
+    }
+
+    // 3. 尝试直接解析整个文本
+    try {
+      console.log('[extractJsonFromMarkdown] Trying to parse entire text as JSON');
+      const parsed = JSON.parse(text);
+      console.log('[extractJsonFromMarkdown] Parsed successfully:', parsed);
+      return parsed;
+    } catch {
+      console.log('[extractJsonFromMarkdown] Failed to parse as JSON');
+      return null;
+    }
+  }, []);
 
   // 生成 specs 的通用函数
   const generateSpecs = useCallback(() => {
     const currentToolCalls = toolCallsRef.current;
     console.log('[generateSpecs] Current toolCalls:', currentToolCalls);
+    console.log('[generateSpecs] Current response:', response?.substring(0, 200));
+
+    const newSpecs: any[] = [];
+
+    // 1. 从 toolCalls 生成 specs(原有逻辑)
     if (currentToolCalls.length > 0) {
       const { specFromToolCall } = require('@/lib/json-render-catalog');
-      const newSpecs = currentToolCalls.map((call) => specFromToolCall(call.tool, call.result)).filter(Boolean);
-      console.log('[generateSpecs] Generated specs:', newSpecs);
+      const toolSpecs = currentToolCalls.map((call) => specFromToolCall(call.tool, call.result)).filter(Boolean);
+      newSpecs.push(...toolSpecs);
+      console.log('[generateSpecs] Generated specs from toolCalls:', toolSpecs);
+    }
+
+    // 2. 从 response 中提取 JSON 并生成 specs(新增逻辑)
+    if (response && response.trim()) {
+      const extractedData = extractJsonFromMarkdown(response);
+      console.log('[generateSpecs] Extracted JSON from response:', extractedData);
+
+      if (extractedData) {
+        // 检查是否为有效的组件 spec(有 type 字段)
+        if (extractedData.type && typeof extractedData.type === 'string') {
+          newSpecs.push(extractedData);
+          console.log('[generateSpecs] Added spec from response:', extractedData);
+        }
+        // 如果是数组,检查每个元素
+        else if (Array.isArray(extractedData)) {
+          for (const item of extractedData) {
+            if (item && item.type && typeof item.type === 'string') {
+              newSpecs.push(item);
+              console.log('[generateSpecs] Added spec from response array:', item);
+            }
+          }
+        }
+      }
+    }
+
+    if (newSpecs.length > 0) {
+      console.log('[generateSpecs] Setting specs:', newSpecs);
       setSpecs(newSpecs);
     }
+
     // 使用 setTimeout 确保 specs 状态更新先被处理
     setTimeout(() => {
       setIsLoading(false);
       console.log('[generateSpecs] Set isLoading to false');
     }, 0);
-  }, []);
+  }, [response]); // extractJsonFromMarkdown 依赖为空数组,不需要在依赖项中
 
   const sendMessage = useCallback(async (message: string, history: ChatMessage[] = []) => {
     setIsLoading(true);