From 85619c0d977cccaf3a4e954f0054140eed0dbb15 Mon Sep 17 00:00:00 2001 From: ViperEkura <3081035982@qq.com> Date: Mon, 13 Apr 2026 20:46:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E6=8E=A5=E5=8F=A3=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- asserts/ARCHITECTURE.md | 76 ++++++ config.yaml | 2 +- dashboard/src/components/ProcessBlock.vue | 9 +- dashboard/src/utils/api.js | 60 ++++- .../src/views/ConversationDetailView.vue | 23 +- dashboard/src/views/ConversationsView.vue | 24 +- luxx/__init__.py | 1 + luxx/routes/messages.py | 4 +- luxx/routes/tools.py | 15 +- luxx/services/chat.py | 57 +++-- luxx/tools/builtin/code.py | 88 ++++--- luxx/tools/builtin/crawler.py | 74 ++++-- luxx/tools/builtin/data.py | 217 +++++++++--------- luxx/tools/factory.py | 103 +++++++-- luxx/tools/services.py | 56 ++--- 15 files changed, 539 insertions(+), 270 deletions(-) diff --git a/asserts/ARCHITECTURE.md b/asserts/ARCHITECTURE.md index eeffd90..f5f2579 100644 --- a/asserts/ARCHITECTURE.md +++ b/asserts/ARCHITECTURE.md @@ -216,6 +216,82 @@ classDiagram | `get_weather` | 天气查询 | 支持城市名查询 | | `process_data` | 数据处理 | JSON 转换、格式化等 | +#### 工具开发规范 + +所有工具必须遵循统一的开发规范,确保错误处理和返回格式一致。 + +**核心原则:装饰器自动处理一切,工具函数只写业务逻辑** + +```python +from luxx.tools.factory import tool + +@tool( + name="my_tool", + description="工具描述", + parameters={...}, + required_params=["arg1"], # 自动验证 + category="my_category" +) +def my_tool(arguments: dict): + # 业务逻辑 - 只管返回数据 + data = fetch_data(arguments["arg1"]) + return {"items": data, "count": len(data)} + + # 或者直接抛出异常(装饰器自动捕获并转换) + if invalid: + raise ValueError("Invalid input") +``` + +**装饰器自动处理:** +1. 必需参数验证(`required_params`) +2. 所有异常捕获和转换 +3. 结果格式统一包装 + +**返回格式转换** + +| 工具函数返回/抛出 | 装饰器转换为 | +|-------------------|-------------| +| `return {"result": "ok"}` | `{"success": true, "data": {...}, "error": null}` | +| `raise ValueError("msg")` | `{"success": false, "data": null, "error": "ValueError: msg"}` | +| `raise Exception()` | `{"success": false, "data": null, "error": "..."}` | + +**工具调用流程** + +``` +LLM 请求 + ↓ +ToolRegistry.execute(name, args) + ↓ +@tool 装饰器 + ├─ 验证 required_params + ├─ 执行工具函数 (try-except 包裹) + ├─ 捕获异常 → 转换为 error + ├─ 包装返回格式 + └─ 返回 ToolResult + ↓ +前端 ProcessBlock 显示 +``` + +| 工具函数返回 | 装饰器转换为 | +|-------------|-------------| +| `{"result": "ok"}` | `{"success": true, "data": {...}, "error": null}` | +| `{"error": "msg"}` | `{"success": false, "data": null, "error": "msg"}` | +| `raise Exception()` | `{"success": false, "data": null, "error": "..."}` | + +**工具调用流程** + +``` +LLM 请求 → ToolRegistry.execute() → @tool 装饰器包装 + ↓ +工具函数执行 + ↓ +返回 ToolResult 或 dict + ↓ +自动转换为标准格式 {"success": bool, "data": any, "error": str} + ↓ +前端 ProcessBlock 显示结果 +``` + ### 6. 服务层 #### ChatService (`services/chat.py`) diff --git a/config.yaml b/config.yaml index b83a005..27d6136 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ # 配置文件 app: secret_key: ${APP_SECRET_KEY} - debug: false + debug: true host: 0.0.0.0 port: 8000 diff --git a/dashboard/src/components/ProcessBlock.vue b/dashboard/src/components/ProcessBlock.vue index 5303d6e..810a8ca 100644 --- a/dashboard/src/components/ProcessBlock.vue +++ b/dashboard/src/components/ProcessBlock.vue @@ -113,13 +113,20 @@ const allItems = computed(() => { if (match) { let resultContent = step.content || '' let displayContent = resultContent + let hasError = false // 尝试解析 JSON 并格式化显示 try { const parsed = JSON.parse(resultContent) + // 检查标准 ToolResult 格式 if (parsed.error) { displayContent = `错误: ${parsed.error}` + hasError = true + } else if (parsed.success !== undefined && parsed.data !== undefined) { + // 标准 ToolResult 格式: 只显示 data 部分 + displayContent = JSON.stringify(parsed.data, null, 2) } else { + // 旧格式或其他 JSON displayContent = JSON.stringify(parsed, null, 2) } } catch (e) { @@ -129,7 +136,7 @@ const allItems = computed(() => { match.resultSummary = displayContent.slice(0, 200) match.fullResult = displayContent match.displayResult = displayContent.length > 2048 ? displayContent.slice(0, 2048) + '...' : displayContent - match.isSuccess = step.success !== false + match.isSuccess = !hasError && step.success !== false match.loading = false } else { // 如果没有找到对应的 tool_call,创建一个占位符 diff --git a/dashboard/src/utils/api.js b/dashboard/src/utils/api.js index 7046059..387f4af 100644 --- a/dashboard/src/utils/api.js +++ b/dashboard/src/utils/api.js @@ -71,9 +71,41 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) { while (true) { const { done, value } = await reader.read() - if (done) break + + // 处理数据 + if (value) { + buffer += decoder.decode(value, { stream: true }) + } + + // 流结束时,先处理 buffer 中的剩余数据,再 break + if (done) { + // 处理 buffer 中剩余的数据 + const lines = buffer.split('\n') + buffer = '' + + let currentEvent = '' + for (const line of lines) { + if (line.startsWith('event: ')) { + currentEvent = line.slice(7).trim() + } else if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.slice(6)) + if (currentEvent === 'process_step' && onProcessStep) { + onProcessStep(data.step) + } else if (currentEvent === 'done' && onDone) { + completed = true + onDone(data) + } else if (currentEvent === 'error' && onError) { + onError(data.content) + } + } catch (e) { + // 忽略解析错误 + } + } + } + break + } - buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' @@ -82,19 +114,24 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) { if (line.startsWith('event: ')) { currentEvent = line.slice(7).trim() } else if (line.startsWith('data: ')) { - const data = JSON.parse(line.slice(6)) - if (currentEvent === 'process_step' && onProcessStep) { - onProcessStep(data.step) - } else if (currentEvent === 'done' && onDone) { - completed = true - onDone(data) - } else if (currentEvent === 'error' && onError) { - onError(data.content) + try { + const data = JSON.parse(line.slice(6)) + if (currentEvent === 'process_step' && onProcessStep) { + onProcessStep(data.step) + } else if (currentEvent === 'done' && onDone) { + completed = true + onDone(data) + } else if (currentEvent === 'error' && onError) { + onError(data.content) + } + } catch (e) { + // 忽略解析错误 } } } } + // 流结束但没有收到 done 事件,才报错 if (!completed && onError) { onError('stream ended unexpectedly') } @@ -139,7 +176,8 @@ export const messagesAPI = { return createSSEStream('/messages/stream', { conversation_id: data.conversation_id, content: data.content, - thinking_enabled: data.thinking_enabled || false + thinking_enabled: data.thinking_enabled || false, + enabled_tools: data.enabled_tools || [] }, callbacks) }, diff --git a/dashboard/src/views/ConversationDetailView.vue b/dashboard/src/views/ConversationDetailView.vue index e962a9a..e012994 100644 --- a/dashboard/src/views/ConversationDetailView.vue +++ b/dashboard/src/views/ConversationDetailView.vue @@ -165,18 +165,8 @@ const loadEnabledTools = async () => { try { const res = await toolsAPI.list() if (res.success) { - const data = res.data?.categorized || {} - const enabled = [] - Object.values(data).forEach(arr => { - if (Array.isArray(arr)) { - arr.forEach(t => { - if (t.enabled !== false) { - enabled.push(t.function?.name || t.name) - } - }) - } - }) - enabledTools.value = enabled + const tools = res.data?.tools || [] + enabledTools.value = tools.map(t => t.function?.name || t.name) } } catch (e) { console.error('Failed to load tools:', e) @@ -317,6 +307,15 @@ const formatTime = (time) => { return new Date(time).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) } +// 监听对话 ID 变化,重新加载消息 +watch(() => route.params.id, (newId) => { + if (newId) { + conversationId.value = newId + loadMessages() + loadEnabledTools() + } +}) + onMounted(() => { loadMessages() loadEnabledTools() diff --git a/dashboard/src/views/ConversationsView.vue b/dashboard/src/views/ConversationsView.vue index f7e9325..aa0b89a 100644 --- a/dashboard/src/views/ConversationsView.vue +++ b/dashboard/src/views/ConversationsView.vue @@ -139,7 +139,7 @@