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

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude AI
2026-03-19 00:39:56 +00:00
parent d36d4f6a54
commit ce9d4de58a
2 changed files with 142 additions and 6 deletions

View File

@@ -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) => {

View File

@@ -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);