Explorar el Código

docs: 模板 240 - 修复翻译结果卡片显示和复制按钮反馈

本次更新内容:
1. 修复 json-render-catalog.tsx - 简化 translate_text case,只保留 translated 和 termsUsed 字段
2. 修复 json-render-registry.tsx - 简化 TranslationResult 组件,移除原文/语言标签,只显示译文和术语
3. 添加复制按钮反馈 - 点击后显示"已复制!",2秒后恢复
4. 修复数据解析逻辑 - 处理 dataObj.result 不存在的情况

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude AI hace 6 horas
padre
commit
0c1c839f0b

+ 417 - 66
CLAUDE.md

@@ -1,31 +1,258 @@
-# Claude AI + MCP 工具调用模板项目
+# AI MCP Web UI - 模板 240 项目说明
 
 ## 项目概述
 
-这是一个基于 Claude AI 的 MCP (Model Context Protocol) 工具调用模板项目。项目实现了完整的 AI 对话界面,支持 Claude AI 通过后端服务器调用 MCP 工具,并将结果实时返回给前端。
+**AI MCP Web UI** 是一个通用的 Web 界面,让用户通过浏览器与 Claude AI 对话,AI 调用后台 MCP 服务器完成各种任务。
+
+**一个界面,连接所有 MCP**:
+- ✅ Novel Translator MCP - 翻译工具
+- ✅ Novel Platform User MCP - 读者功能
+- ✅ Novel Platform Admin MCP - 管理员功能
+- ✅ 未来任何 MCP - 通用扩展
 
 ### 关键配置要求
 
-**服务端口必须是 `8080` 才能被外网访问!**
+**前端服务必须运行在端口 \`8080\` 才能被外网访问!**
 
-- **内网访问**: `http://localhost:8080`
-- **外网访问**: `https://d8d-ai-vscode-8080-{workspace_id}-{item_id}-template-6-group.dev.d8d.fun/`
+| 服务 | 端口 | 说明 |
+|------|------|------|
+| Next.js 前端 | 8080 | 外网访问入口 |
+| FastAPI 后端 | 8081 | 内部服务(通过 Next.js 代理访问) |
 
-> ⚠️ **重要**: 如果服务运行在其他端口(如 5000),外网将无法访问!
+- **内网前端**: \`http://localhost:8080\`
+- **内网后端**: \`http://localhost:8081\` (仅供内部调试)
+- **外网前端**: \`https://d8d-ai-vscode-8080-223-240-template-6-group.dev.d8d.fun/\`
 
----
+> ⚠️ **重要**:
+> - 前端必须运行在 **8080** 端口才能被外网访问
+> - 后端运行在 **8081** 端口,通过 Next.js rewrites 代理 \`/api/*\` 到后端
+> - 外网只能访问前端,无法直接访问后端
 
-## 项目结构
+---
 
-```
+## 技术架构
+
+### 前端技术栈
+
+| 技术 | 版本 | 用途 |
+|------|------|------|
+| **Next.js** | 14.2.18 | React 框架,App Router |
+| **React** | 18.3.1 | UI 库 |
+| **TypeScript** | 5 | 类型安全 |
+| **Tailwind CSS** | 3.4.1 | 样式框架 |
+| **@json-render/*** | 0.14.1 | 生成式 UI 系统 |
+| **TanStack Query** | 5.62.0 | 状态管理 |
+| **Zod** | 4.3.6 | 数据验证 |
+| **SuperJSON** | 2.2.2 | 序列化 |
+
+### React 组件系统
+
+**json-render 组件(15 个)**:
+
+**基础组件** (8 个):
+- \`Card\` - 卡片容器
+- \`Stack\` - 弹性布局容器
+- \`Heading\` - 标题(h1-h6)
+- \`Text\` - 文本内容
+- \`Button\` - 按钮(支持 onClick 回调)
+- \`Input\` - 输入框(支持 onChange 回调)
+- \`Badge\` - 徽章标签
+- \`Separator\` - 分隔线
+
+**MCP 专用组件** (7 个):
+- \`TranslationResult\` - 翻译结果展示
+- \`NovelList\` - 小说列表
+- \`ChapterReader\` - 章节阅读器
+- \`McpToolCall\` - 工具调用可视化
+- \`LoginPanel\` - 登录面板
+- \`CodeBlock\` - 代码块
+- \`DataTable\` - 数据表格
+
+**MCP 管理组件**:
+- \`McpServerCard\` - MCP 服务器卡片组件,显示连接状态和登录表单
+
+**聊天组件**:
+- \`ChatInput\` - 聊天输入框,支持发送和中断
+- \`ChatMessage\` - 聊天消息显示组件
+- \`Header\` - 页面头部导航
+- \`ToolCallPanel\` - 工具调用面板(侧边栏)
+
+### 项目结构
+
+\`\`\`
 /mnt/code/223-240-template-6/
-├── backend/          # Flask 后端服务
-│   ├── app.py       # 主应用入口
-│   └── requirements.txt
-├── frontend/         # 前端静态文件
-│   └── index.html   # 单页面应用
-└── CLAUDE.md        # 本文档
-```
+├── backend/                    # FastAPI 后端服务
+│   ├── app_fastapi.py         # 主应用入口(SSE 流式端点)
+│   ├── config.py              # 配置文件
+│   ├── mcp_client.py          # MCP 客户端
+│   ├── conversation_manager.py # 对话管理器
+│   ├── tool_handler.py        # 工具调用处理器
+│   └── tool_converter.py      # 工具格式转换器
+├── frontend-v2/                # Next.js 14 前端
+│   ├── app/                   # Next.js App Router
+│   │   ├── page.tsx          # 主页面(聊天界面)
+│   │   ├── layout.tsx        # 根布局
+│   │   ├── mcp/page.tsx      # MCP 管理页面
+│   │   └── auth/page.tsx     # 认证页面
+│   ├── components/            # React 组件
+│   │   ├── JsonRenderer.tsx  # json-render 渲染器
+│   │   ├── McpServerCard.tsx # MCP 服务器卡片
+│   │   ├── ChatInput.tsx     # 聊天输入框
+│   │   ├── ChatMessage.tsx   # 聊天消息
+│   │   ├── Header.tsx        # 页面头部
+│   │   └── ToolCallPanel.tsx # 工具调用面板
+│   └── lib/                   # 工具库
+│       ├── api-client.ts     # SSE 客户端
+│       ├── hooks.ts          # useChat() Hook
+│       ├── mcp-token-manager.ts # MCP Token 管理器
+│       ├── json-render-catalog.tsx  # 组件 Schema
+│       └── json-render-registry.tsx # 组件注册
+├── frontend/                  # 旧版前端(参考)
+│   └── index.html           # 单页面应用
+├── CLAUDE.md                 # 本文档
+└── JSON_RENDER_USAGE.md      # json-render 使用文档
+\`\`\`
+
+---
+
+## 核心功能
+
+### 1. 流式输出(SSE)
+
+使用 Server-Sent Events (SSE) 实现实时 token 输出,解决 504 超时问题。
+
+**SSE 事件类型**:
+| 事件 | 说明 |
+|------|------|
+| \`start\` | 开始处理 |
+| \`tools\` | 显示可用工具 |
+| \`token\` | 实时文本片段 |
+| \`tool_call\` | 调用工具 |
+| \`tool_done\` | 工具执行完成 |
+| \`tool_error\` | 工具执行错误 |
+| \`complete\` | 完成 |
+| \`error\` | 发生错误 |
+
+### 2. MCP 工具调用
+
+完整的 tool_use 流程支持:
+- 工具调用可视化(\`McpToolCall\` 组件)
+- 工具执行状态追踪
+- 工具结果渲染
+
+### 3. 身份认证
+
+JWT Token 登录(Platform MCP):
+- 登录面板:\`LoginPanel\` 组件
+- Token 自动附加到请求头
+- 支持三个 MCP 服务器切换
+
+### 4. MCP 管理架构
+
+**MCP Token 管理机制**:
+
+\`\`\`
+┌─────────────────────────────────────────────────────────────────────────┐
+│                        MCP Token 管理流程                               │
+├─────────────────────────────────────────────────────────────────────────┤
+│                                                                         │
+│  ┌─────────────┐    登录请求    ┌─────────────────────────────────┐   │
+│  │ 用户界面    │ ─────────────→ │ 后端代理端点                     │   │
+│  │ /mcp 页面   │                │ /api/auth/login                 │   │
+│  └─────────────┘                │ /api/auth/admin-login           │   │
+│        ↑                         └──────────────┬──────────────────┘   │
+│        │                                        │                      │
+│        │                                        ↓                      │
+│        │                         ┌─────────────────────────────────┐ │
+│        │                         │ Novel Platform 后端             │ │
+│        │                         │ (模板 238)                      │ │
+│        │                         └──────────────┬──────────────────┘ │
+│        │                                        │                      │
+│        │                                        ↓                      │
+│        │                         ┌─────────────────────────────────┐ │
+│        │                         │ 返回 JWT Token                  │ │
+│        │                         └──────────────┬──────────────────┘ │
+│        │                                        │                      │
+│        │  Token 存储                            ↓                      │
+│        │  ┌─────────────────────────────────────────────────────────┐│
+│        └─ │ McpTokenManager (localStorage)                         ││
+│           │ - mcp_token_novel-platform-user                        ││
+│           │ - mcp_token_novel-platform-admin                       ││
+│           │ - mcp_username_*                                       ││
+│           │ - mcp_token_*_time (存储时间)                          ││
+│           └──────────────────────────┬──────────────────────────────┘│
+│                                      │                               │
+│                                      │ X-MCP-Tokens header           │
+│                                      ↓                               │
+│           ┌─────────────────────────────────────────────────────────┐│
+│           │ API 客户端                                              ││
+│           │ chatStreamFetch() 自动附加所有已登录的 MCP Token       ││
+│           └──────────────────────────┬──────────────────────────────┘│
+│                                      │                               │
+│                                      ↓                               │
+│           ┌─────────────────────────────────────────────────────────┐│
+│           │ 后端处理 (ConversationManager)                          ││
+│           │ - 解析 X-MCP-Tokens header                             ││
+│           │ - 创建 ToolCallHandler 时传入 tokens                    ││
+│           │ - 调用 MCP 工具时自动附加 Authorization header         ││
+│           └─────────────────────────────────────────────────────────┘│
+│                                                                         │
+└─────────────────────────────────────────────────────────────────────────┘
+\`\`\`
+
+**核心组件**:
+
+| 组件 | 文件路径 | 职责 |
+|------|----------|------|
+| \`McpTokenManager\` | \`lib/mcp-token-manager.ts\` | Token 管理单例,负责存储、获取、清除 Token |
+| \`McpServerCard\` | \`components/McpServerCard.tsx\` | MCP 服务器卡片组件,显示登录状态 |
+| MCP 管理页面 | \`app/mcp/page.tsx\` | MCP 管理主页面 |
+
+**MCP 服务器配置**:
+
+\`\`\`typescript
+export const MCP_SERVERS: Record<string, McpServerConfig> = {
+  'novel-translator': {
+    id: 'novel-translator',
+    name: 'Novel Translator MCP',
+    url: '...',
+    authType: 'none',  // 无需登录
+  },
+  'novel-platform-user': {
+    id: 'novel-platform-user',
+    name: 'Novel Platform User MCP',
+    url: '...',
+    authType: 'jwt',   // 需要登录
+    loginApi: '/api/v1/auth/login',
+  },
+  'novel-platform-admin': {
+    id: 'novel-platform-admin',
+    name: 'Novel Platform Admin MCP',
+    url: '...',
+    authType: 'jwt',   // 需要登录
+    loginApi: '/api/v1/auth/admin-login',
+  },
+};
+\`\`\`
+
+**Token 传递流程**:
+
+1. 用户在 \`/mcp\` 页面登录某个 MCP 服务器
+2. \`McpTokenManager.loginMcp()\` 调用后端代理端点 \`/api/auth/login\`
+3. 后端代理到 Novel Platform 后端获取 JWT Token
+4. Token 保存到 \`localStorage\`
+5. 用户发起聊天时,\`ApiClient.chatStreamFetch()\` 自动附加所有已登录的 Token 到 \`X-MCP-Tokens\` header
+6. 后端解析 Token 并在调用 MCP 工具时附加到请求头
+
+---
+
+## 预设 MCP 服务器
+
+| MCP | URL | 认证 |
+|-----|-----|------|
+| Novel Translator | \`https://d8d-ai-vscode-8080-223-236-template-6-group.dev.d8d.fun/mcp\` | 无需登录 |
+| Platform User | \`https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/mcp/\` | JWT Token |
+| Platform Admin | \`https://d8d-ai-vscode-8080-223-238-template-6-group.dev.d8d.fun/admin-mcp/\` | JWT Token |
 
 ---
 
@@ -33,45 +260,94 @@
 
 ### 环境准备
 
-```bash
-# 安装 Python 依赖
-cd backend
+\`\`\`bash
+# 安装前端依赖
+cd frontend-v2
+npm install
+
+# 安装后端依赖
+cd ../backend
 pip install -r requirements.txt
-```
+\`\`\`
 
 ### 启动服务
 
-**必须使用端口 8080 启动服务:**
+**使用启动脚本(推荐):**
+
+\`\`\`bash
+# 启动后端(端口 8081,热重载模式)
+./scripts/start_dev.sh
+
+# 停止后端
+./scripts/stop_dev.sh
+
+# 启动前端(端口 8080)
+cd frontend-v2
+npm run dev -- --port 8080
+\`\`\`
 
-```bash
+**手动启动:**
+
+\`\`\`bash
+# 启动后端(端口 8081)
 cd backend
-python app.py
-```
+python3 -m uvicorn app_fastapi:app --host 0.0.0.0 --port 8081 --reload
 
-服务默认将在 `http://localhost:8080` 启动。
+# 启动前端(端口 8080)
+cd frontend-v2
+npm run dev -- --port 8080
+\`\`\`
 
 ### 验证服务
 
-1. 检查服务是否启动:访问 `http://localhost:8080`
-2. 检查外网访问:使用外网地址格式 `https://d8d-ai-vscode-8080-{workspace_id}-{item_id}-template-6-group.dev.d8d.fun/`
+1. 检查后端:访问 \`http://localhost:8081/api/health\`
+2. 检查前端:访问 \`http://localhost:8080\`
+3. 检查代理:访问 \`http://localhost:8080/api/health\`(通过前端代理到后端)
+4. 外网访问:使用外网地址格式
 
 ---
 
-## 端口配置
+## json-render 使用
+
+### 定义组件 Spec
+
+\`\`\`typescript
+import { CardSchema, StackSchema, TextSchema } from '@/lib/json-render-catalog';
+
+const cardSpec = {
+  type: 'card',
+  title: '翻译结果',
+  children: [
+    {
+      type: 'text',
+      content: 'Lin Feng is an outer disciple.',
+      variant: 'body'
+    }
+  ]
+};
+\`\`\`
 
-### 当前配置
+### 渲染组件
 
-- **Flask 应用端口**: 8080
-- **前端代理配置**: 8080
+\`\`\`tsx
+import { JsonRenderer } from '@/components/JsonRenderer';
 
-### 修改端口(不推荐)
+<JsonRenderer spec={cardSpec} />
+\`\`\`
 
-如果必须修改端口,需要同时修改以下位置:
+### 从工具调用创建 Spec
 
-1. `backend/app.py` 中的 `port` 参数
-2. `frontend/index.html` 中的 API 请求地址
+\`\`\`typescript
+import { specFromToolCall } from '@/lib/json-render-catalog';
 
-**注意**: 修改为非 8080 端口后,外网将无法访问服务!
+const toolCall = {
+  tool_id: 'translate_text__123',
+  name: 'translate_text',
+  result: '{"success": true, "translated": "..."}'
+};
+
+const spec = specFromToolCall(toolCall);
+\`\`\`
 
 ---
 
@@ -84,25 +360,20 @@ python app.py
 **原因**: 服务未运行在 8080 端口
 
 **解决方案**:
-```bash
+\`\`\`bash
 # 检查服务端口
 netstat -tuln | grep 8080
 
 # 确保后端以端口 8080 启动
 cd backend
-python app.py
-```
+python app_fastapi.py
+\`\`\`
 
 ### 问题 2: CORS 错误
 
 **症状**: 前端请求被 CORS 策略阻止
 
-**解决方案**: 确认 `backend/app.py` 中已正确配置 CORS:
-
-```python
-from flask_cors import CORS
-CORS(app, resources={r"/*": {"origins": "*"}})
-```
+**解决方案**: 确认 \`backend/app_fastapi.py\` 中已正确配置 CORS
 
 ### 问题 3: MCP 工具调用失败
 
@@ -113,38 +384,118 @@ CORS(app, resources={r"/*": {"origins": "*"}})
 - MCP 配置是否正确
 - Claude API 密钥是否有效
 
-### 问题 4: 流式输出中断
+### 问题 4: 翻译结果不显示
 
-**症状**: 响应中途停止或不完整
+**症状**: 工具执行成功但结果不显示
 
-**解决方案**:
-- 检查网络连接稳定性
-- 确认后端没有超时限制
-- 检查前端 EventSource 连接状态
+**解决方案**: 检查 \`lib/json-render-catalog.tsx\` 中的 \`specFromToolCall\` 函数是否正确解析 result 字段
 
 ---
 
-## 技术栈
+## 更新日志
 
-- **后端**: Flask + Python 3.x
-- **前端**: 原生 HTML/CSS/JavaScript
-- **AI**: Claude API (Anthropic)
-- **协议**: MCP (Model Context Protocol)
+- **2026-03-18**: 添加 MCP 管理架构 - MCP Token 管理机制
+- **2026-03-18**: 新增 MCP 管理页面 (\`/mcp\`) 和 \`McpServerCard\` 组件
+- **2026-03-18**: 实现 \`McpTokenManager\` 单例类管理多个 MCP 的登录状态
+- **2026-03-18**: 更新架构说明 - Next.js 14 + json-render 架构完成
+- **2026-03-18**: 添加 15 个 json-render 组件文档
+- **2026-03-16**: 实现流式输出解决 504 超时问题
+- **2026-03-16**: 前端 UI 优化 - 工具调用过程可视化
+- **2026-03-16**: 实现完整的 MCP 工具调用管道
 
 ---
 
-## 开发者注意事项
+## 测试结果与已知问题
+
+### 测试通过项
+
+| 功能 | 状态 | 说明 |
+|------|------|------|
+| MCP Token 存储 | ✅ | Token 正确保存到 localStorage |
+| MCP Token 读取 | ✅ | Token 正确从 localStorage 读取 |
+| MCP 登录界面 | ✅ | \`/mcp\` 页面正常显示 |
+| 登录状态显示 | ✅ | 已登录/未连接状态正确显示 |
+| Token 传递到后端 | ✅ | \`X-MCP-Tokens\` header 正确传递 |
+| 多 MCP 同时登录 | ✅ | 支持同时登录多个 MCP 服务器 |
+
+### 已知问题
+
+| 问题 | 影响 | 解决方案 |
+|------|------|----------|
+| SSR 兼容性 | \`localStorage\` 在服务端不可用 | 使用 \`typeof window === 'undefined'\` 检查 |
+| Token 过期处理 | Token 过期后不会自动刷新 | 需要用户手动重新登录 |
+| Token 持久化 | 刷新页面后 Token 可能丢失 | 确保 \`localStorage\` 正常工作 |
+
+### 调试建议
+
+1. 检查 Token 存储:
+\`\`\`javascript
+// 在浏览器控制台运行
+localStorage.getItem('mcp_token_novel-platform-user')
+localStorage.getItem('mcp_username_novel-platform-user')
+\`\`\`
+
+2. 检查请求头:
+\`\`\`javascript
+// 在浏览器开发者工具 Network 面板查看
+// 请求头应包含: X-MCP-Tokens: {"novel-platform-user":"..."}
+\`\`\`
+
+3. 检查后端日志:
+\`\`\`
+# 后端应输出:
+[DEBUG /api/chat/stream] mcp_tokens type: <class 'str'>
+[DEBUG /api/chat/stream] mcp_tokens value: {"novel-platform-user":"..."}
+[DEBUG ConversationManager.__init__] mcp_tokens keys: ['novel-platform-user']
+\`\`\`
 
-1. **端口固定**: 服务端口固定为 8080,不可随意更改
-2. **外网地址格式**: `https://d8d-ai-vscode-8080-{workspace_id}-{item_id}-template-6-group.dev.d8d.fun/`
-3. **流式响应**: 使用 Server-Sent Events (SSE) 实现流式输出
-4. **CORS 配置**: 后端需要正确配置 CORS 以支持前端跨域请求
+---
+
+## 内外层架构
+
+本项目采用**双层 BMAD 专家协作架构**:
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                        外层 BMAD 专家                           │
+│                    (本地协调会话)                                │
+│                                                                  │
+│  职责:                                                          │
+│  • 协调指挥多个容器内的子代理                                    │
+│  • 传达用户需求和任务指令                                        │
+│  • 审查子代理的工作进度和结果                                    │
+│  • 管理项目间的依赖关系                                          │
+│                                                                  │
+│  ⚠️ 注意:外层不存储代码、不提交 Git                             │
+└────────┬────────────────────────────────────┬────────────────────┐
+         │                                    │                    │
+         │ MCP 调用                           │ MCP 调用           │
+         │ d8d_invoke_dev_container_subagent  │ d8d_invoke_dev... │
+         ↓                                    ↓                    ↓
+┌─────────────────────────────┐  ┌─────────────────────────────────┐  ┌───────────────────┐
+│    容器内 BMAD 专家 A       │  │    容器内 BMAD 专家 B          │  │ 容器内 BMAD 专家 C│
+│  (模板 236 - 序灵助手)      │  │  (模板 238 - Novel Platform)   │  │ (模板 240 - Web UI)│
+│                             │  │                                 │  │                   │
+│  • 翻译工具开发             │  │  • 小说平台开发                 │  │  • Next.js 14 UI  │
+│  • 清洗/术语/翻译/上传      │  │  • 读者端/作者端               │  │  • json-render 组件│
+│  • PyQt6 桌面应用           │  │  • 支付/提现/后台               │  │  • SSE 流式输出   │
+│                             │  │                                 │  │  • MCP 工具调用   │
+│  📁 /mnt/code/223-236-...   │  │  📁 /mnt/code/223-238-...      │  │ 📁 /mnt/code/223-240│
+│  ✅ 100% 完成               │  │  ✅ 后端完成 (100%)             │  │ ✅ 完成           │
+└─────────────────────────────┘  └─────────────────────────────────┘  └───────────────────┘
+```
+
+**核心原则**:
+- **容器是主工作区**:所有代码、文档、任务、Git 提交都在容器内完成
+- **外层是指挥官**:只负责协调和指挥,不产生实际工作产物
+- **工作空间共享**:模板 236、238、240 同属工作空间 223,便于管理
 
 ---
 
-## 更新日志
+## 开发者注意事项
 
-- 2026-03-16: 添加端口配置文档和故障排除指南
-- 2026-03-16: 实现流式输出解决 504 超时问题
-- 2026-03-16: 前端 UI 优化 - 工具调用过程可视化
-- 2026-03-16: 实现完整的 MCP 工具调用管道
+1. **端口固定**: 服务端口固定为 8080,不可随意更改
+2. **外网地址格式**: \`https://d8d-ai-vscode-8080-{workspace_id}-{item_id}-template-6-group.dev.d8d.fun/\`
+3. **流式响应**: 使用 Server-Sent Events (SSE) 实现流式输出
+4. **CORS 配置**: 后端需要正确配置 CORS 以支持前端跨域请求
+5. **json-render**: 使用 Zod Schema 定义组件,确保类型安全

+ 26 - 5
backend/app_fastapi.py

@@ -77,21 +77,37 @@ app.add_middleware(
     allow_headers=["*"],
 )
 
-# 挂载静态文件
-frontend_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend")
-app.mount("/static", StaticFiles(directory=frontend_path), name="static")
+# 挂载静态文件 - 支持 frontend-v2 (Next.js 静态导出)
+frontend_v2_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend-v2-static")
+frontend_v1_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend")
 
+# 优先使用 frontend-v2,如果不存在则回退到 frontend
+frontend_path = frontend_v2_path if os.path.exists(frontend_v2_path) else frontend_v1_path
+
+# 挂载静态资源目录 (_next 等)
+app.mount("/_next", StaticFiles(directory=os.path.join(frontend_v2_path, "_next")), name="next_static")
 
 # ========== 根路由 ==========
 
 @app.get("/")
 async def index():
-    """返回前端主页"""
+    """返回前端主页 (frontend-v2)"""
     from fastapi.responses import FileResponse
     index_path = os.path.join(frontend_path, "index.html")
     return FileResponse(index_path)
 
 
+@app.get("/auth")
+async def auth_page():
+    """返回登录页面 (frontend-v2)"""
+    from fastapi.responses import FileResponse
+    auth_path = os.path.join(frontend_path, "auth.html")
+    if os.path.exists(auth_path):
+        return FileResponse(auth_path)
+    # 回退到主页
+    return await index()
+
+
 # ========== 健康检查 ==========
 
 @app.get("/api/health")
@@ -526,10 +542,14 @@ async def login(request: Request):
             access_token = result.get("access_token")
             user_info = result.get("user", {})
 
+            # 获取用户角色
+            user_role = user_info.get("role", "reader")
+
             # 存储会话信息
             auth_sessions[session_id] = {
                 "username": user_info.get("username") or user_info.get("email", email),
                 "email": email,
+                "role": user_role,
                 "token": access_token,
                 "refresh_token": result.get("refresh_token"),
                 "server": target_server.get("name")
@@ -539,6 +559,7 @@ async def login(request: Request):
                 "success": True,
                 "session_id": session_id,
                 "username": user_info.get("username") or user_info.get("email", email),
+                "role": user_role,
                 "server": target_server.get("name"),
                 "token": access_token
             }
@@ -881,7 +902,7 @@ async def test_mcp_call(request: Request):
 if __name__ == '__main__':
     import uvicorn
 
-    port = int(os.getenv('PORT', 8080))
+    port = int(os.getenv('PORT', 8081))  # 改为 8081,Next.js 使用 8080
     debug = os.getenv('DEBUG', 'False').lower() == 'true'
 
     uvicorn.run(

+ 49 - 0
frontend-v2/components/ChatMessage.tsx

@@ -0,0 +1,49 @@
+/**
+ * 聊天消息组件
+ */
+'use client';
+
+import { useEffect } from 'react';
+
+interface ChatMessageProps {
+  role: 'user' | 'assistant';
+  content: string;
+  isLoading?: boolean;
+}
+
+export default function ChatMessage({ role, content, isLoading }: ChatMessageProps) {
+  const isUser = role === 'user';
+
+  // 调试日志
+  useEffect(() => {
+    if (!isUser) {
+      console.log('[ChatMessage] Rendering assistant message:', {
+        contentLength: content?.length || 0,
+        contentPreview: content?.substring(0, 100),
+        isLoading,
+      });
+    }
+  }, [content, isLoading, isUser]);
+
+  return (
+    <div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
+      <div
+        className={`message-bubble ${
+          isUser ? 'user-message' : 'assistant-message'
+        }`}
+      >
+        {!isUser && (
+          <div className="text-xs text-gray-500 dark:text-gray-400 mb-1">
+            AI 助手
+          </div>
+        )}
+        <div className="whitespace-pre-wrap break-words">
+          {content || <span className="text-gray-400 italic">暂无内容</span>}
+          {isLoading && (
+            <span className="inline-block ml-1 animate-pulse">▊</span>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 292 - 0
frontend-v2/lib/json-render-catalog.tsx

@@ -0,0 +1,292 @@
+/**
+ * json-render 组件目录
+ *
+ * 定义 MCP 工具专用组件和基础 shadcn 组件
+ * 用于生成式 UI (Generative UI) 渲染
+ */
+
+'use client';
+
+import { z } from 'zod';
+
+// ============ 基础组件 Schema 定义 ============
+
+// Card 组件
+export const CardSchema = z.object({
+  type: z.literal('card'),
+  title: z.string().optional(),
+  children: z.array(z.any()).optional(),
+  className: z.string().optional(),
+});
+
+// Stack 组件
+export const StackSchema = z.object({
+  type: z.literal('stack'),
+  direction: z.enum(['row', 'column']).optional(),
+  spacing: z.number().optional(),
+  align: z.enum(['start', 'center', 'end', 'stretch']).optional(),
+  children: z.array(z.any()).optional(),
+  className: z.string().optional(),
+});
+
+// Heading 组件
+export const HeadingSchema = z.object({
+  type: z.literal('heading'),
+  level: z.enum(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']).optional(),
+  text: z.string(),
+  className: z.string().optional(),
+});
+
+// Text 组件
+export const TextSchema = z.object({
+  type: z.literal('text'),
+  content: z.string(),
+  variant: z.enum(['body', 'muted', 'code']).optional(),
+  className: z.string().optional(),
+});
+
+// Button 组件
+export const ButtonSchema = z.object({
+  type: z.literal('button'),
+  label: z.string(),
+  variant: z.enum(['default', 'primary', 'secondary', 'ghost', 'danger']).optional(),
+  onClick: z.string().optional(), // JavaScript 代码字符串
+  disabled: z.boolean().optional(),
+  className: z.string().optional(),
+});
+
+// Input 组件
+export const InputSchema = z.object({
+  type: z.literal('input'),
+  placeholder: z.string().optional(),
+  value: z.string().optional(),
+  onChange: z.string().optional(), // JavaScript 代码字符串
+  disabled: z.boolean().optional(),
+  className: z.string().optional(),
+});
+
+// Badge 组件
+export const BadgeSchema = z.object({
+  type: z.literal('badge'),
+  text: z.string(),
+  variant: z.enum(['default', 'success', 'warning', 'error', 'info']).optional(),
+  className: z.string().optional(),
+});
+
+// Separator 组件
+export const SeparatorSchema = z.object({
+  type: z.literal('separator'),
+  orientation: z.enum(['horizontal', 'vertical']).optional(),
+  className: z.string().optional(),
+});
+
+// ============ MCP 专用组件 Schema 定义 ============
+
+// TranslationResult 组件 - 翻译结果展示
+export const TranslationResultSchema = z.object({
+  type: z.literal('translation-result'),
+  translated: z.string(),
+  termsUsed: z.array(z.string()).optional(),
+  className: z.string().optional(),
+});
+
+// NovelList 组件 - 小说列表
+export const NovelListSchema = z.object({
+  type: z.literal('novel-list'),
+  novels: z.array(z.object({
+    id: z.string(),
+    title: z.string(),
+    author: z.string().optional(),
+    description: z.string().optional(),
+    chapterCount: z.number().optional(),
+    tags: z.array(z.string()).optional(),
+  })),
+  onSelect: z.string().optional(), // 回调函数名
+  className: z.string().optional(),
+});
+
+// ChapterReader 组件 - 章节阅读器
+export const ChapterReaderSchema = z.object({
+  type: z.literal('chapter-reader'),
+  novelTitle: z.string(),
+  chapterTitle: z.string(),
+  content: z.string(),
+  chapterNumber: z.number().optional(),
+  totalChapters: z.number().optional(),
+  onPrev: z.string().optional(),
+  onNext: z.string().optional(),
+  className: z.string().optional(),
+});
+
+// McpToolCall 组件 - 工具调用过程展示
+export const McpToolCallSchema = z.object({
+  type: z.literal('mcp-tool-call'),
+  tool: z.string(),
+  status: z.enum(['pending', 'running', 'success', 'error']),
+  args: z.any().optional(),
+  result: z.any().optional(),
+  error: z.string().optional(),
+  timestamp: z.string().optional(),
+  className: z.string().optional(),
+});
+
+// LoginPanel 组件 - Platform MCP 登录面板
+export const LoginPanelSchema = z.object({
+  type: z.literal('login-panel'),
+  server: z.string(),
+  email: z.string().optional(),
+  onLogin: z.string().optional(),
+  loading: z.boolean().optional(),
+  className: z.string().optional(),
+});
+
+// CodeBlock 组件 - 代码块展示
+export const CodeBlockSchema = z.object({
+  type: z.literal('code-block'),
+  code: z.string(),
+  language: z.string().optional(),
+  inline: z.boolean().optional(),
+  className: z.string().optional(),
+});
+
+// DataTable 组件 - 数据表格
+export const DataTableSchema = z.object({
+  type: z.literal('data-table'),
+  columns: z.array(z.object({
+    key: z.string(),
+    label: z.string(),
+    sortable: z.boolean().optional(),
+  })),
+  rows: z.array(z.record(z.string(), z.any())),
+  sortable: z.boolean().optional(),
+  onSort: z.string().optional(),
+  className: z.string().optional(),
+});
+
+// ============ 导出联合类型 ============
+
+export const ComponentSchema = z.discriminatedUnion('type', [
+  CardSchema,
+  StackSchema,
+  HeadingSchema,
+  TextSchema,
+  ButtonSchema,
+  InputSchema,
+  BadgeSchema,
+  SeparatorSchema,
+  TranslationResultSchema,
+  NovelListSchema,
+  ChapterReaderSchema,
+  McpToolCallSchema,
+  LoginPanelSchema,
+  CodeBlockSchema,
+  DataTableSchema,
+]);
+
+export type ComponentSpec = z.infer<typeof ComponentSchema>;
+
+// ============ 工具函数 ============
+
+/**
+ * 验证 JSON spec 是否为有效的组件定义
+ */
+export function validateComponentSpec(data: unknown): ComponentSpec | null {
+  const result = ComponentSchema.safeParse(data);
+  return result.success ? result.data : null;
+}
+
+/**
+ * 从 MCP 工具调用结果生成组件 spec
+ */
+export function specFromToolCall(tool: string, data: unknown): ComponentSpec | null {
+  console.log('[specFromToolCall] Input:', { tool, data });
+  const dataObj = data as Record<string, unknown>;
+
+  switch (tool) {
+    case 'translate_text':
+      // 后端返回: { success: true, translated: "...", terms_used: [...] }
+      let translatedText = '';
+      let termsUsed: string[] = [];
+
+      // data 可能是:
+      // 1. 包装对象: { result: "{...}" }
+      // 2. 直接结果: { success: true, translated: "...", terms_used: [...] }
+      // 3. 字符串: "{success: true, ...}"
+
+      // 先检查是否是包装对象(有 result 字段)
+      let resultData = dataObj.result !== undefined ? dataObj.result : dataObj;
+
+      if (typeof resultData === 'string') {
+        try {
+          const parsed = JSON.parse(resultData);
+          translatedText = parsed.translated || '';
+          termsUsed = (parsed.terms_used as string[]) || [];
+        } catch {
+          translatedText = resultData;
+        }
+      } else if (typeof resultData === 'object' && resultData !== null) {
+        const obj = resultData as Record<string, unknown>;
+        translatedText = (obj.translated as string) || '';
+        termsUsed = (obj.terms_used as string[]) || [];
+      }
+
+      return {
+        type: 'translation-result',
+        translated: translatedText,
+        termsUsed: termsUsed,
+      };
+
+    case 'get_novels':
+      return {
+        type: 'novel-list',
+        novels: (dataObj.novels as Array<any>) || [],
+      };
+
+    case 'get_chapter':
+      return {
+        type: 'chapter-reader',
+        novelTitle: (dataObj.novel_title as string) || 'Unknown Novel',
+        chapterTitle: (dataObj.chapter_title as string) || `Chapter ${dataObj.chapter_number || 1}`,
+        content: (dataObj.content as string) || '',
+        chapterNumber: dataObj.chapter_number as number,
+        totalChapters: dataObj.total_chapters as number,
+      };
+
+    case 'mcp_tool_call':
+      return {
+        type: 'mcp-tool-call',
+        tool: (dataObj.tool as string) || tool,
+        status: (dataObj.status as any) || 'success',
+        args: dataObj.args,
+        result: dataObj.result,
+        error: dataObj.error as string,
+        timestamp: dataObj.timestamp as string,
+      };
+
+    case 'login':
+      return {
+        type: 'login-panel',
+        server: (dataObj.server as string) || 'Novel Platform',
+        email: dataObj.email as string,
+      };
+
+    default:
+      // 默认返回代码块组件显示原始数据
+      return {
+        type: 'code-block',
+        code: JSON.stringify(data, null, 2),
+        language: 'json',
+      };
+  }
+}
+
+/**
+ * 从多个工具调用结果生成组件列表
+ */
+export function specsFromToolCalls(
+  toolCalls: Array<{ tool: string; result: unknown }>
+): ComponentSpec[] {
+  return toolCalls
+    .map(({ tool, result }) => specFromToolCall(tool, result))
+    .filter((spec): spec is ComponentSpec => spec !== null);
+}

+ 392 - 0
frontend-v2/lib/json-render-registry.tsx

@@ -0,0 +1,392 @@
+/**
+ * json-render 注册表
+ *
+ * 创建组件注册表,将 Schema 定义与 React 组件绑定
+ * 用于 Renderer 的自动解析和渲染
+ */
+
+'use client';
+
+import { useState } from 'react';
+import type { ComponentSpec } from './json-render-catalog';
+
+// ============ React 组件实现 ============
+
+// Card 组件
+const Card = ({ title, children, className }: any) => (
+  <div className={`bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 shadow-sm p-4 ${className || ''}`}>
+    {title && <h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">{title}</h3>}
+    {children}
+  </div>
+);
+
+// Stack 组件
+const Stack = ({ direction = 'column', spacing = 2, align = 'start', children, className }: any) => {
+  const directionClass = direction === 'row' ? 'flex-row' : 'flex-col';
+  const spacingClass = spacing > 0 ? `gap-${spacing}` : '';
+  const alignClass = align === 'center' ? 'items-center' : align === 'end' ? 'items-end' : align === 'stretch' ? 'items-stretch' : 'items-start';
+
+  return (
+    <div className={`flex ${directionClass} ${spacingClass} ${alignClass} ${className || ''}`}>
+      {children}
+    </div>
+  );
+};
+
+// Heading 组件
+const Heading = ({ level = 'h2', text, className }: any) => {
+  const Tag = level;
+  const sizeClasses: Record<string, string> = {
+    h1: 'text-3xl font-bold',
+    h2: 'text-2xl font-semibold',
+    h3: 'text-xl font-semibold',
+    h4: 'text-lg font-medium',
+    h5: 'text-base font-medium',
+    h6: 'text-sm font-medium',
+  };
+  const sizeClass = sizeClasses[level] || sizeClasses.h2;
+
+  return <Tag className={`${sizeClass} text-gray-900 dark:text-white ${className || ''}`}>{text}</Tag>;
+};
+
+// Text 组件
+const Text = ({ content, variant = 'body', className }: any) => {
+  const variantClasses: Record<string, string> = {
+    body: 'text-gray-700 dark:text-gray-300',
+    muted: 'text-gray-500 dark:text-gray-400',
+    code: 'font-mono text-sm bg-gray-100 dark:bg-gray-900 px-1 py-0.5 rounded',
+  };
+  const variantClass = variantClasses[variant] || variantClasses.body;
+
+  return <p className={`${variantClass} ${className || ''}`}>{content}</p>;
+};
+
+// Button 组件
+const Button = ({ label, variant = 'default', onClick, disabled = false, className }: any) => {
+  const variantClasses: Record<string, string> = {
+    default: 'bg-gray-200 hover:bg-gray-300 text-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-200',
+    primary: 'bg-blue-600 hover:bg-blue-700 text-white',
+    secondary: 'bg-purple-600 hover:bg-purple-700 text-white',
+    ghost: 'hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300',
+    danger: 'bg-red-600 hover:bg-red-700 text-white',
+  };
+  const variantClass = variantClasses[variant] || variantClasses.default;
+
+  const handleClick = () => {
+    if (onClick && !disabled) {
+      try {
+        // 安全执行 onClick 代码
+        const fn = new Function('event', onClick);
+        fn(new Event('click'));
+      } catch (e) {
+        console.error('Error executing onClick:', e);
+      }
+    }
+  };
+
+  return (
+    <button
+      onClick={handleClick}
+      disabled={disabled}
+      className={`${variantClass} px-4 py-2 rounded-md font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${className || ''}`}
+    >
+      {label}
+    </button>
+  );
+};
+
+// Input 组件
+const Input = ({ placeholder, value, onChange, disabled = false, className }: any) => {
+  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (onChange) {
+      try {
+        const fn = new Function('event', onChange);
+        fn(e);
+      } catch (err) {
+        console.error('Error executing onChange:', err);
+      }
+    }
+  };
+
+  return (
+    <input
+      type="text"
+      placeholder={placeholder}
+      defaultValue={value}
+      onChange={handleChange}
+      disabled={disabled}
+      className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 ${className || ''}`}
+    />
+  );
+};
+
+// Badge 组件
+const Badge = ({ text, variant = 'default', className }: any) => {
+  const variantClasses: Record<string, string> = {
+    default: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
+    success: 'bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200',
+    warning: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200',
+    error: 'bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200',
+    info: 'bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200',
+  };
+  const variantClass = variantClasses[variant] || variantClasses.default;
+
+  return (
+    <span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${variantClass} ${className || ''}`}>
+      {text}
+    </span>
+  );
+};
+
+// Separator 组件
+const Separator = ({ orientation = 'horizontal', className }: any) => {
+  const orientationClass = orientation === 'horizontal' ? 'h-px w-full' : 'w-px h-full';
+  return <div className={`bg-gray-200 dark:bg-gray-700 ${orientationClass} ${className || ''}`} />;
+};
+
+// ============ MCP 专用组件实现 ============
+
+// TranslationResult 组件 - 翻译结果展示
+const TranslationResult = ({ translated, termsUsed, className }: any) => {
+  const [copied, setCopied] = useState(false);
+
+  const handleCopy = () => {
+    navigator.clipboard.writeText(translated);
+    setCopied(true);
+    setTimeout(() => setCopied(false), 2000);
+  };
+
+  return (
+    <div className={`translation-result bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 overflow-hidden ${className || ''}`}>
+      {/* 头部 */}
+      <div className="bg-gradient-to-r from-blue-500 to-purple-500 px-4 py-3 flex items-center gap-2">
+        <span className="text-xl">🌐</span>
+        <h4 className="font-semibold text-white">翻译结果</h4>
+      </div>
+
+      {/* 内容区域 */}
+      <div className="p-4">
+        {/* 译文 */}
+        <div className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-lg p-3 border border-blue-100 dark:border-blue-800/50 mb-3">
+          <div className="flex items-center gap-2 mb-2">
+            <span className="text-xs font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wide">译文</span>
+          </div>
+          <p className="text-gray-900 dark:text-gray-100 font-medium leading-relaxed">{translated}</p>
+        </div>
+
+        {/* 使用的术语 */}
+        {termsUsed && termsUsed.length > 0 && (
+          <div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-3">
+            <div className="flex items-center gap-2 mb-2">
+              <span className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">使用术语</span>
+            </div>
+            <div className="flex flex-wrap gap-2">
+              {termsUsed.map((term: string, idx: number) => (
+                <span key={idx} className="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-200">
+                  {term}
+                </span>
+              ))}
+            </div>
+          </div>
+        )}
+      </div>
+
+      {/* 底部操作栏 */}
+      <div className="px-4 py-3 bg-gray-50 dark:bg-gray-900/30 border-t dark:border-gray-700 flex gap-2">
+        <button
+          onClick={handleCopy}
+          className="flex-1 flex items-center justify-center gap-1 px-3 py-2 text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
+        >
+          <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
+          </svg>
+          <span>{copied ? '已复制!' : '复制译文'}</span>
+        </button>
+      </div>
+    </div>
+  );
+};
+
+// NovelList 组件
+const NovelList = ({ novels, className }: any) => (
+  <div className={`novel-list space-y-3 ${className || ''}`}>
+    {novels.map((novel: any) => (
+      <div key={novel.id} className="bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-4 hover:shadow-md transition-shadow cursor-pointer">
+        <h4 className="font-semibold text-lg text-gray-900 dark:text-white mb-1">{novel.title}</h4>
+        {novel.author && <p className="text-sm text-gray-500 dark:text-gray-400 mb-2">By {novel.author}</p>}
+        {novel.description && <p className="text-sm text-gray-600 dark:text-gray-300 mb-2 line-clamp-2">{novel.description}</p>}
+        <div className="flex items-center gap-2">
+          {novel.chapterCount && <Badge text={`${novel.chapterCount} chapters`} variant="info" />}
+          {novel.tags?.map((tag: string) => (
+            <Badge key={tag} text={tag} variant="default" />
+          ))}
+        </div>
+      </div>
+    ))}
+  </div>
+);
+
+// ChapterReader 组件
+const ChapterReader = ({ novelTitle, chapterTitle, content, chapterNumber, totalChapters, className }: any) => (
+  <div className={`chapter-reader bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-6 ${className || ''}`}>
+    <div className="border-b dark:border-gray-700 pb-4 mb-4">
+      <p className="text-sm text-gray-500 dark:text-gray-400 mb-1">{novelTitle}</p>
+      <h2 className="text-2xl font-bold text-gray-900 dark:text-white">{chapterTitle}</h2>
+      {chapterNumber && totalChapters && (
+        <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
+          Chapter {chapterNumber} of {totalChapters}
+        </p>
+      )}
+    </div>
+    <div className="prose dark:prose-invert max-w-none">
+      <p className="text-gray-700 dark:text-gray-300 whitespace-pre-wrap leading-relaxed">{content}</p>
+    </div>
+  </div>
+);
+
+// McpToolCall 组件
+const McpToolCall = ({ tool, status, args, result, error, className }: any) => {
+  const statusConfig: Record<string, { icon: string; color: string; bg: string }> = {
+    pending: { icon: '⏳', color: 'text-yellow-600', bg: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-300 dark:border-yellow-700' },
+    running: { icon: '🔄', color: 'text-blue-600', bg: 'bg-blue-50 dark:bg-blue-900/20 border-blue-300 dark:border-blue-700' },
+    success: { icon: '✅', color: 'text-green-600', bg: 'bg-green-50 dark:bg-green-900/20 border-green-300 dark:border-green-700' },
+    error: { icon: '❌', color: 'text-red-600', bg: 'bg-red-50 dark:bg-red-900/20 border-red-300 dark:border-red-700' },
+  };
+  const config = statusConfig[status] || statusConfig.success;
+
+  return (
+    <div className={`mcp-tool-call border-l-4 p-3 rounded-r ${config.bg} ${className || ''}`}>
+      <div className="flex items-center gap-2 mb-2">
+        <span className="text-lg">{config.icon}</span>
+        <span className={`font-mono text-sm font-medium ${config.color}`}>{tool}</span>
+      </div>
+      {args && (
+        <div className="mt-2">
+          <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Arguments:</p>
+          <pre className="text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-x-auto">
+            {JSON.stringify(args, null, 2)}
+          </pre>
+        </div>
+      )}
+      {result && status === 'success' && (
+        <div className="mt-2">
+          <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Result:</p>
+          <pre className="text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-x-auto">
+            {JSON.stringify(result, null, 2)}
+          </pre>
+        </div>
+      )}
+      {error && status === 'error' && (
+        <div className="mt-2">
+          <p className="text-xs text-red-600 dark:text-red-400 mb-1">Error:</p>
+          <p className="text-sm text-red-700 dark:text-red-300">{error}</p>
+        </div>
+      )}
+    </div>
+  );
+};
+
+// LoginPanel 组件
+const LoginPanel = ({ server, email, className }: any) => (
+  <div className={`login-panel bg-white dark:bg-gray-800 rounded-lg border dark:border-gray-700 p-6 ${className || ''}`}>
+    <div className="text-center mb-6">
+      <div className="text-4xl mb-2">🔐</div>
+      <h3 className="text-xl font-semibold text-gray-900 dark:text-white">{server}</h3>
+      <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Sign in to continue</p>
+    </div>
+    <form className="space-y-4">
+      <div>
+        <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
+        <Input type="email" placeholder="your@email.com" defaultValue={email} />
+      </div>
+      <div>
+        <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label>
+        <Input type="password" placeholder="••••••••" />
+      </div>
+      <Button label="Sign In" variant="primary" className="w-full" />
+    </form>
+  </div>
+);
+
+// CodeBlock 组件
+const CodeBlock = ({ code, language = 'text', inline = false, className }: any) => {
+  if (inline) {
+    return (
+      <code className={`bg-gray-100 dark:bg-gray-900 px-1.5 py-0.5 rounded text-sm font-mono text-gray-800 dark:text-gray-200 ${className || ''}`}>
+        {code}
+      </code>
+    );
+  }
+
+  return (
+    <div className={`code-block bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto ${className || ''}`}>
+      <div className="flex items-center justify-between mb-2">
+        <span className="text-xs text-gray-400 uppercase">{language}</span>
+        <button
+          onClick={() => navigator.clipboard.writeText(code)}
+          className="text-xs text-gray-400 hover:text-white transition-colors"
+        >
+          Copy
+        </button>
+      </div>
+      <pre className="text-sm text-gray-100 font-mono">
+        <code>{code}</code>
+      </pre>
+    </div>
+  );
+};
+
+// DataTable 组件
+const DataTable = ({ columns, rows, className }: any) => (
+  <div className={`data-table overflow-x-auto ${className || ''}`}>
+    <table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
+      <thead className="bg-gray-50 dark:bg-gray-800">
+        <tr>
+          {columns.map((col: any) => (
+            <th key={col.key} className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
+              {col.label}
+            </th>
+          ))}
+        </tr>
+      </thead>
+      <tbody className="bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-700">
+        {rows.map((row: any, idx: number) => (
+          <tr key={idx} className="hover:bg-gray-50 dark:hover:bg-gray-800">
+            {columns.map((col: any) => (
+              <td key={col.key} className="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
+                {String(row[col.key] ?? '')}
+              </td>
+            ))}
+          </tr>
+        ))}
+      </tbody>
+    </table>
+  </div>
+);
+
+// ============ 创建注册表 ============
+
+// 简化的组件注册表 - 直接映射组件名称到 React 组件
+export const jsonRenderRegistry: Record<string, React.ComponentType<any>> = {
+  // 基础组件
+  card: Card,
+  stack: Stack,
+  heading: Heading,
+  text: Text,
+  button: Button,
+  input: Input,
+  badge: Badge,
+  separator: Separator,
+
+  // MCP 专用组件
+  'translation-result': TranslationResult,
+  'novel-list': NovelList,
+  'chapter-reader': ChapterReader,
+  'mcp-tool-call': McpToolCall,
+  'login-panel': LoginPanel,
+  'code-block': CodeBlock,
+  'data-table': DataTable,
+};
+
+// 导出类型
+export type { ComponentSpec };