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

fix: json-render 系统提示统一化 + 错误日志功能

- 修复前端 componentsPrompt 与后端 availableComponents 字段名不匹配问题
- 清理 conversation_manager.py 中多余的工具指南 (NOVEL_TOOLS_GUIDE, NO_TOOLS_GUIDE)
- 前端统一使用 catalog.prompt({ mode: inline }) 生成组件系统提示
- 后端直接使用前端传来的 componentsPrompt,不再添加额外内容
- 添加 log_error 函数到 debug_logger.py
- 在 app_fastapi.py 所有 except 块中添加 log_error 调用
- 添加 quick-spec-json-render-system-prompt.md 规格文档

Co-Authored-By: Claude <noreply@anthropic.com>
Claude AI 1 день назад
Родитель
Сommit
25a07dcfa6

+ 23 - 12
backend/app_fastapi.py

@@ -16,7 +16,7 @@ from fastapi.staticfiles import StaticFiles
 import httpx
 from anthropic import Anthropic
 
-from debug_logger import log_debug
+from debug_logger import log_debug, log_error
 from config import MCP_SERVERS, ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL
 from conversation_manager import ConversationManager
 from tool_handler import ToolCallHandler
@@ -157,11 +157,11 @@ async def chat(request: Request):
             print(f"[DEBUG /api/chat]   {k}: {v[:30] if v else 'None'}...")
 
         # 获取前端发送的组件列表(实现动态组件注册)
-        available_components = data.get('availableComponents')
+        components_prompt = data.get('componentsPrompt')
         enabled_mcp_list = data.get('enabledMcpList')  # 前端发送的已启用 MCP 列表
 
-        if available_components:
-            print(f"[DEBUG /api/chat] Using dynamic components from frontend ({len(available_components)} chars)")
+        if components_prompt:
+            print(f"[DEBUG /api/chat] Using dynamic components from frontend ({len(components_prompt)} chars)")
 
         # DEBUG: 打印已启用的 MCP 列表
         if enabled_mcp_list is not None:
@@ -176,7 +176,7 @@ async def chat(request: Request):
             model=ANTHROPIC_MODEL,
             session_id=session_id,
             mcp_tokens=parsed_tokens,
-            components_prompt=available_components,  # 动态组件提示
+            components_prompt=components_prompt,  # 动态组件提示
             enabled_mcp_list=enabled_mcp_list  # 前端传递的已启用 MCP 列表
         )
 
@@ -205,6 +205,7 @@ async def chat(request: Request):
         raise
     except Exception as e:
         import traceback
+        log_error("app_fastapi.chat_stream_error", e, {"endpoint": "/api/chat/stream"})
         return JSONResponse(
             status_code=500,
             content={
@@ -219,7 +220,7 @@ async def generate_chat_stream(
     conversation_history: List[Dict[str, Any]],
     session_id: Optional[str],
     mcp_tokens: Optional[Dict[str, str]] = None,
-    available_components: Optional[str] = None,  # 新增:前端发送的组件列表
+    components_prompt: Optional[str] = None,  # 新增:前端发送的组件列表
     enabled_mcp_list: Optional[List[str]] = None  # 新增:前端发送的已启用 MCP 列表
 ):
     """生成 SSE 流式响应的异步生成器"""
@@ -244,8 +245,8 @@ async def generate_chat_stream(
             print(f"[DEBUG generate_chat_stream]   {k}: {v[:30] if v else 'None'}...")
 
         # DEBUG: 打印组件提示
-        if available_components:
-            print(f"[DEBUG generate_chat_stream] Using dynamic components from frontend ({len(available_components)} chars)")
+        if components_prompt:
+            print(f"[DEBUG generate_chat_stream] Using dynamic components from frontend ({len(components_prompt)} chars)")
         else:
             print(f"[DEBUG generate_chat_stream] Using default components")
 
@@ -262,7 +263,7 @@ async def generate_chat_stream(
             model=ANTHROPIC_MODEL,
             session_id=session_id,
             mcp_tokens=parsed_tokens,
-            components_prompt=available_components,  # 动态组件提示
+            components_prompt=components_prompt,  # 动态组件提示
             enabled_mcp_list=enabled_mcp_list  # 前端传递的已启用 MCP 列表
         )
 
@@ -428,6 +429,7 @@ async def generate_chat_stream(
 
     except Exception as e:
         import traceback
+        log_error("app_fastapi.chat_error", e, {"endpoint": "/api/chat"})
         yield f"event: error\ndata: {json_module.dumps({'error': str(e), 'traceback': traceback.format_exc()})}\n\n"
 
 
@@ -449,7 +451,7 @@ async def chat_stream(request: Request):
         conversation_history = data.get('history', [])
         session_id = request.headers.get('X-Session-ID')
         mcp_tokens = request.headers.get('X-MCP-Tokens')  # MCP tokens (JSON string)
-        available_components = data.get('availableComponents')  # 前端发送的组件列表
+        components_prompt = data.get('componentsPrompt')  # 前端发送的组件列表
         enabled_mcp_list = data.get('enabledMcpList')  # 前端发送的已启用 MCP 列表
 
         # 记录到 JSONL 文件
@@ -462,14 +464,14 @@ async def chat_stream(request: Request):
         # DEBUG: 打印收到的 token
         print(f"[DEBUG /api/chat/stream] mcp_tokens type: {type(mcp_tokens)}")
         print(f"[DEBUG /api/chat/stream] mcp_tokens value: {mcp_tokens[:150] if mcp_tokens else 'None'}...")
-        print(f"[DEBUG /api/chat/stream] available_components: {len(available_components) if available_components else 0} chars")
+        print(f"[DEBUG /api/chat/stream] components_prompt: {len(components_prompt) if components_prompt else 0} chars")
         print(f"[DEBUG /api/chat/stream] enabled_mcp_list: {enabled_mcp_list}")
 
         if not message:
             raise HTTPException(status_code=400, detail="Message is required")
 
         return StreamingResponse(
-            generate_chat_stream(message, conversation_history, session_id, mcp_tokens, available_components, enabled_mcp_list),
+            generate_chat_stream(message, conversation_history, session_id, mcp_tokens, components_prompt, enabled_mcp_list),
             media_type="text/event-stream",
             headers={
                 'Cache-Control': 'no-cache',
@@ -481,6 +483,7 @@ async def chat_stream(request: Request):
         raise
     except Exception as e:
         import traceback
+        log_error("app_fastapi.chat_stream_error", e, {"endpoint": "/api/chat/stream"})
         return JSONResponse(
             status_code=500,
             content={
@@ -535,6 +538,7 @@ async def list_mcp_tools(
         }
     except Exception as e:
         import traceback
+        log_error("app_fastapi.list_mcp_tools_error", e, {"endpoint": "/api/mcp/tools"})
         return JSONResponse(
             status_code=500,
             content={
@@ -624,6 +628,7 @@ async def check_mcp_health(mcp_type: str):
 
     except Exception as e:
         import traceback
+        log_error("app_fastapi.check_mcp_health_error", e, {"endpoint": f"/api/mcp/health/{mcp_type}", "mcp_type": mcp_type})
         return JSONResponse(
             status_code=500,
             content={
@@ -708,6 +713,7 @@ async def login(request: Request):
     except HTTPException:
         raise
     except Exception as e:
+        log_error("app_fastapi.login_error", e, {"endpoint": "/api/auth/login"})
         raise HTTPException(status_code=500, detail=str(e))
 
 
@@ -777,6 +783,7 @@ async def admin_login(request: Request):
     except HTTPException:
         raise
     except Exception as e:
+        log_error("app_fastapi.admin_login_error", e, {"endpoint": "/api/auth/admin-login"})
         raise HTTPException(status_code=500, detail=str(e))
 
 
@@ -849,6 +856,7 @@ async def register(request: Request):
     except HTTPException:
         raise
     except Exception as e:
+        log_error("app_fastapi.register_error", e, {"endpoint": "/api/auth/register"})
         raise HTTPException(status_code=500, detail=str(e))
 
 
@@ -864,6 +872,7 @@ async def logout(request: Request):
 
         return {"success": True}
     except Exception as e:
+        log_error("app_fastapi.logout_error", e, {"endpoint": "/api/auth/logout"})
         raise HTTPException(status_code=500, detail=str(e))
 
 
@@ -938,6 +947,7 @@ async def test_mcp_get(
 
     except Exception as e:
         import traceback
+        log_error("app_fastapi.test_mcp_get_error", e, {"endpoint": "/api/test-mcp", "method": "GET", "tool_name": tool_name, "server_id": server_id})
         print(f"[TEST-MCP GET] 异常: {e}")
         traceback.print_exc()
         return JSONResponse(
@@ -1021,6 +1031,7 @@ async def test_mcp_call(request: Request):
         raise
     except Exception as e:
         import traceback
+        log_error("app_fastapi.test_mcp_post_error", e, {"endpoint": "/api/test-mcp", "method": "POST", "tool_name": tool_name, "server_id": server_id})
         print(f"[TEST-MCP] 异常: {e}")
         traceback.print_exc()
         return JSONResponse(

+ 10 - 59
backend/conversation_manager.py

@@ -18,45 +18,9 @@ BASE_SYSTEM_PROMPT = """你是一个 AI 助手,可以通过调用 MCP 工具
 ## 当前状态
 
 {MCP_STATUS}
-
-{MCP_TOOLS_GUIDE}
-"""
-
-# 小说相关操作说明(只有启用相关 MCP 时才显示)
-NOVEL_TOOLS_GUIDE = """### 小说相关操作(重要)
-
-当用户说"查看小说:xxx"、"小说详情:xxx"或点击小说卡片时:
-1. 从消息中提取小说标题或 ID
-2. 使用 `get_novel_detail` 工具获取小说数据(需要提供 novel_id 参数)
-3. 使用 `novel-detail` 组件展示结果,格式如下:
-```json
-{
-  "type": "novel-detail",
-  "novel": {
-    "id": "小说ID",
-    "title": "小说标题",
-    "author": "作者",
-    "category": "分类",
-    "description": "简介",
-    "status": "状态",
-    "chapterCount": 章节数,
-    "wordCount": 字数,
-    "viewCount": 阅读量,
-    "isVip": 是否VIP
-  }
-}
-```
 """
 
-# 无 MCP 工具时的提示
-NO_TOOLS_GUIDE = """### 注意
 
-当前没有启用任何 MCP 服务器,因此你没有可用的工具。你只能:
-- 进行普通对话
-- 返回 json-render 组件来展示信息
-
-如果用户询问关于翻译、小说等功能,请告知用户需要先在 MCP 管理页面启用相关服务器。
-"""
 
 
 def create_anthropic_client(api_key: str, base_url: str) -> Anthropic:
@@ -114,40 +78,27 @@ class ConversationManager:
         self.client = create_anthropic_client(api_key, base_url)
 
     def _build_system_prompt(self) -> str:
-        """构建完整的系统提示词(基础提示 + MCP 状态 + 工具指南 + 组件列表)"""
-        # 根据 enabled_mcp_list 构建 MCP 状态提示和工具指南
+        """构建完整的 system prompt"""
+        # 构建 MCP 状态部分
         if self.enabled_mcp_list is not None and len(self.enabled_mcp_list) == 0:
-            # 所有 MCP 都被禁用
-            mcp_status = """**重要:当前没有启用任何 MCP 服务器**
-
-你没有可用的 MCP 工具。如果用户询问关于翻译、小说、组件等功能,请告诉用户:
-- 当前没有启用任何 MCP 服务器
-- 请前往 MCP 管理页面启用需要的服务器
-- 启用后刷新页面即可使用相关功能
-
-你仍然可以使用基础的对话能力和返回 json-render 组件来帮助用户。"""
-            mcp_tools_guide = NO_TOOLS_GUIDE
-        elif self.enabled_mcp_list is not None and len(self.enabled_mcp_list) > 0:
+            # 用户明确禁用了所有 MCP
+            mcp_status = """当前没有启用任何 MCP 服务器。你只能进行普通对话和返回 json-render 组件。"""
+        elif self.enabled_mcp_list:
             # 部分或全部 MCP 已启用
             enabled_names = ", ".join(self.enabled_mcp_list)
             mcp_status = f"""**已启用的 MCP 服务器**: {enabled_names}
 
 你可以调用这些 MCP 服务器的工具来帮助用户完成任务。"""
-            # 检查是否有小说相关的 MCP 启用
-            has_novel_mcp = any('novel-platform' in mcp or 'novel' in mcp for mcp in self.enabled_mcp_list)
-            if has_novel_mcp:
-                mcp_tools_guide = NOVEL_TOOLS_GUIDE
-            else:
-                mcp_tools_guide = ""
         else:
-            # enabled_mcp_list 是 None,使用配置文件的默认状态
+            # enabled_mcp_list 是 None,使用默认状态
             mcp_status = """你可以通过调用 MCP 工具来帮助用户完成任务。"""
-            mcp_tools_guide = NOVEL_TOOLS_GUIDE  # 默认显示小说工具指南
 
         # 替换占位符并添加组件列表
         prompt = BASE_SYSTEM_PROMPT.replace("{MCP_STATUS}", mcp_status)
-        prompt = prompt.replace("{MCP_TOOLS_GUIDE}", mcp_tools_guide)
-        return prompt + "\n\n" + self.components_prompt
+        if self.components_prompt:
+            return prompt + "\n\n" + self.components_prompt
+        else:
+            return prompt
 
     async def get_available_tools(self) -> List[Dict[str, Any]]:
         """获取可用的 Claude 格式工具列表(带缓存)"""

+ 28 - 1
backend/debug_logger.py

@@ -4,11 +4,12 @@
 日志文件存放在 /tmp 目录,容器重启时自动清理
 """
 import json
-import os
+import traceback
 from datetime import datetime
 
 # 日志文件存放在 /tmp 目录,容器重启时自动清理
 LOG_FILE = "/tmp/debug.log.jsonl"
+ERROR_LOG_FILE = "/tmp/error.log.jsonl"
 
 
 def log_debug(event_name: str, data: dict = None):
@@ -30,3 +31,29 @@ def log_debug(event_name: str, data: dict = None):
             f.write(json.dumps(entry) + "\n")
     except Exception as e:
         print(f"[debug_logger] Failed to write log: {e}")
+
+
+def log_error(event_name: str, error: Exception = None, data: dict = None):
+    """
+    记录错误日志到 JSONL 文件
+
+    Args:
+        event_name: 事件名称
+        error: 异常对象
+        data: 额外数据
+    """
+    entry = {
+        "timestamp": datetime.now().isoformat(),
+        "event": event_name,
+        "error_type": type(error).__name__ if error else None,
+        "error_message": str(error) if error else None,
+        "traceback": traceback.format_exc() if error else None,
+        "data": data or {}
+    }
+
+    try:
+        with open(ERROR_LOG_FILE, "a") as f:
+            f.write(json.dumps(entry) + "\n")
+        print(f"[log_error] {event_name}: {error}")
+    except Exception as e:
+        print(f"[debug_logger] Failed to write error log: {e}")

+ 4 - 4
frontend-v2/lib/api-client.ts

@@ -5,7 +5,7 @@
 
 import { serialize, deserialize } from './superjson';
 import { mcpTokenManager } from './mcp-token-manager';
-import { generateComponentsPrompt } from './component-registry';
+import catalog from './catalog/catalog';
 
 // 配置 - 使用 Next.js rewrites 反向代理到 FastAPI
 const BACKEND_URL = '/api';
@@ -183,7 +183,7 @@ export class ApiClient {
       message,
       history,
       // 发送可用组件列表给后端,实现动态组件注册
-      availableComponents: generateComponentsPrompt()
+      componentsPrompt: catalog.prompt({ mode: 'inline' })
     };
 
     return this.request<ChatResponse>('/chat', {
@@ -210,7 +210,7 @@ export class ApiClient {
       message,
       history,
       // 发送可用组件列表给后端,实现动态组件注册
-      availableComponents: generateComponentsPrompt()
+      componentsPrompt: catalog.prompt({ mode: 'inline' })
     };
 
     // 使用 fetch 创建流式连接
@@ -240,7 +240,7 @@ export class ApiClient {
         message,
         history,
         // 发送可用组件列表给后端,实现动态组件注册
-        availableComponents: generateComponentsPrompt(),
+        componentsPrompt: catalog.prompt({ mode: 'inline' }),
         // 发送已启用的 MCP 列表,后端只获取这些 MCP 的工具
         enabledMcpList: mcpTokenManager.getEnabledMcpList()
       };

+ 131 - 0
planning-artifacts/quick-spec-json-render-system-prompt.md

@@ -0,0 +1,131 @@
+---
+title: 'json-render 系统提示统一化'
+slug: 'json-render-system-prompt-unification'
+created: '2026-03-20'
+status: 'completed'
+stepsCompleted: ['cleanup-backend', 'verify-frontend-prompt', 'verify-transmission']
+tech_stack: ['TypeScript', 'Python']
+files_modified:
+  - 'frontend-v2/lib/catalog/catalog.ts'
+  - 'backend/conversation_manager.py'
+---
+
+# Tech-Spec: json-render 系统提示统一化
+
+**Created:** 2026-03-20
+**Completed:** 2026-03-20
+
+## 最终架构
+
+````
+┌─────────────────────────────────────────────────────────┐
+│ 前端                                                     │
+│   catalog.prompt({ mode: 'inline' })                    │
+│   → json-render 组件完整说明 (12547 chars)              │
+│   → 通过 componentsPrompt 字段发送                      │
+└─────────────────────────────────────────────────────────┘
+                          ↓
+┌─────────────────────────────────────────────────────────┐
+│ 后端                                                     │
+│   BASE_SYSTEM_PROMPT (角色 + MCP 状态)                  │
+│   + components_prompt (前端传来的组件说明)              │
+│   = 完整 system prompt                                  │
+└─────────────────────────────────────────────────────────┘
+````
+
+## 验证结果 (2026-03-20)
+
+### 后端日志确认
+
+````
+[DEBUG /api/chat/stream] components_prompt: 12547 chars
+[DEBUG /api/chat/stream] enabled_mcp_list: ['novel-translator', 'novel-platform-user', 'novel-platform-admin', 'template-241-mcp-app']
+````
+
+### 前端传来的 componentsPrompt 内容预览
+
+````
+You are a UI generator that outputs JSON.
+
+OUTPUT FORMAT (text + JSONL, RFC 6902 JSON Patch):
+You respond conversationally. When generating UI, first write a brief explanation (1-3 sentences), then output JSONL patch lines wrapped in a \`\`\`spec code fence.
+The JSONL lines use RFC 6902 JSON Patch operations to build a UI tree...
+````
+
+## 实施记录
+
+### 已删除的后端冗余代码
+
+| 删除项 | 原因 |
+|--------|------|
+| \`NOVEL_TOOLS_GUIDE\` | 引用了不存在的 \`novel-detail\` 组件,与 catalog 不一致 |
+| \`NO_TOOLS_GUIDE\` | 冗余,AI 可以自己探索工具 |
+| \`_build_system_prompt()\` 中的 \`mcp_tools_guide\` 逻辑 | 简化后端,让 AI 自己决定 |
+
+### 简化后的 \`_build_system_prompt()\` 方法
+
+```python
+def _build_system_prompt(self) -> str:
+    """构建完整的 system prompt"""
+    # 构建 MCP 状态部分
+    if self.enabled_mcp_list is not None and len(self.enabled_mcp_list) == 0:
+        mcp_status = """当前没有启用任何 MCP 服务器。你只能进行普通对话和返回 json-render 组件。"""
+    elif self.enabled_mcp_list:
+        enabled_names = ", ".join(self.enabled_mcp_list)
+        mcp_status = f"""**已启用的 MCP 服务器**: {enabled_names}
+
+你可以调用这些 MCP 服务器的工具来帮助用户完成任务。"""
+    else:
+        mcp_status = """你可以通过调用 MCP 工具来帮助用户完成任务。"""
+
+    # 替换占位符并添加组件列表
+    prompt = BASE_SYSTEM_PROMPT.replace("{MCP_STATUS}", mcp_status)
+    if self.components_prompt:
+        return prompt + "\n\n" + self.components_prompt
+    else:
+        return prompt
+```
+
+### 简化后的 \`BASE_SYSTEM_PROMPT\`
+
+```python
+BASE_SYSTEM_PROMPT = """你是一个 AI 助手,可以通过调用 MCP 工具来帮助用户完成任务。
+
+## 当前状态
+
+{MCP_STATUS}
+"""
+```
+
+## 核心原则
+
+1. **单一来源**: 组件格式由前端 \`catalog.prompt({ mode: 'inline' })\` 统一定义
+2. **后端精简**: 后端只提供基础角色定义 + MCP 状态
+3. **AI 自主**: AI 自己决定用什么工具、返回什么组件
+
+## Catalog 组件列表
+
+前端 \`catalog.ts\` 注册的组件(17 个):
+
+| 组件 | 用途 |
+|------|------|
+| \`card\` | 卡片容器组件
+| \`stack\` | 布局容器组件
+| \`heading\` | 标题组件
+| \`text\` | 文本组件
+| \`button\` | 按钮组件
+| \`badge\` | 徽章组件
+| \`separator\` | 分隔线组件
+| \`input\` | 输入框组件
+| \`text-area\` | 多行文本输入组件
+| \`data-table\` | 数据表格组件
+| \`translation-result\` | 翻译结果展示组件
+| \`novel-list\` | 小说列表组件
+| \`chapter-reader\` | 章节阅读器组件
+| \`code-block\` | 代码块组件
+| \`tool-call\` | MCP 工具调用状态组件
+| \`login-panel\` | MCP 登录面板组件
+| \`mcp-status\` | MCP 服务器状态组件
+| \`suggestion-buttons\` | 建议按钮组组件
+
+**注意**: 没有 \`novel-detail\` 组件!后端旧代码引用的是不存在的组件。