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