From 4540bb9f41be584df01596ef12b44352537a3e06 Mon Sep 17 00:00:00 2001 From: ViperEkura <3081035982@qq.com> Date: Tue, 24 Mar 2026 22:34:14 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E8=BF=9B=E8=A1=8C=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=92=8C=E6=96=87=E6=A1=A3=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/models.py | 10 ++-- backend/tools/services.py | 5 +- docs/Design.md | 71 +++++++++++++++-------- frontend/src/components/ChatView.vue | 10 ++++ frontend/src/components/MessageBubble.vue | 2 +- frontend/src/components/ProcessBlock.vue | 4 ++ pyproject.toml | 5 ++ 7 files changed, 73 insertions(+), 34 deletions(-) diff --git a/backend/models.py b/backend/models.py index 179f45f..0454439 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from . import db @@ -26,8 +26,8 @@ class Conversation(db.Model): temperature = db.Column(db.Float, nullable=False, default=1.0) max_tokens = db.Column(db.Integer, nullable=False, default=65536) thinking_enabled = db.Column(db.Boolean, default=False) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) messages = db.relationship("Message", backref="conversation", lazy="dynamic", cascade="all, delete-orphan", @@ -49,7 +49,7 @@ class Message(db.Model): tool_call_id = db.Column(db.String(64)) # Tool call ID (tool messages) name = db.Column(db.String(64)) # Tool name (tool messages) - created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) class TokenUsage(db.Model): @@ -62,7 +62,7 @@ class TokenUsage(db.Model): prompt_tokens = db.Column(db.Integer, default=0) completion_tokens = db.Column(db.Integer, default=0) total_tokens = db.Column(db.Integer, default=0) - created_at = db.Column(db.DateTime, default=datetime.utcnow) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) __table_args__ = ( db.UniqueConstraint("user_id", "date", "model", name="uq_user_date_model"), diff --git a/backend/tools/services.py b/backend/tools/services.py index 2122061..6b7d7af 100644 --- a/backend/tools/services.py +++ b/backend/tools/services.py @@ -1,5 +1,6 @@ """Tool helper services""" from typing import List, Dict, Optional, Any +from ddgs import DDGS import re @@ -38,10 +39,6 @@ class SearchService: region: str ) -> List[dict]: """DuckDuckGo search""" - try: - from duckduckgo_search import DDGS - except ImportError: - return [{"error": "Please install duckduckgo-search: pip install duckduckgo-search"}] with DDGS() as ddgs: results = list(ddgs.text( diff --git a/docs/Design.md b/docs/Design.md index ff6dac7..605d4ea 100644 --- a/docs/Design.md +++ b/docs/Design.md @@ -197,6 +197,7 @@ GET /api/conversations/:id/messages?cursor=msg_001&limit=50 "content": "你好", "token_count": 2, "thinking_content": null, + "tool_calls": null, "created_at": "2026-03-24T10:00:00Z" }, { @@ -206,6 +207,7 @@ GET /api/conversations/:id/messages?cursor=msg_001&limit=50 "content": "你好!有什么可以帮你的?", "token_count": 15, "thinking_content": null, + "tool_calls": null, "created_at": "2026-03-24T10:00:01Z" } ], @@ -226,10 +228,17 @@ POST /api/conversations/:id/messages ```json { "content": "介绍一下你的能力", - "stream": true + "stream": true, + "tools_enabled": true } ``` +| 参数 | 类型 | 说明 | +| --------------- | -------- | ---------------------- | +| `content` | string | 用户消息内容 | +| `stream` | boolean | 是否流式响应,默认 `true` | +| `tools_enabled` | boolean | 是否启用工具调用,默认 `true`(可选) | + **流式响应 (stream=true):** **普通回复示例:** @@ -298,6 +307,7 @@ data: {"message_id": "msg_003", "token_count": 150} "content": "我是智谱AI开发的大语言模型...", "token_count": 200, "thinking_content": "用户想了解我的能力...", + "tool_calls": null, "created_at": "2026-03-24T10:01:00Z" }, "usage": { @@ -534,13 +544,11 @@ User 1 ── * Conversation 1 ── * Message | ------------------ | ------------- | ------------------------------- | | `id` | string (UUID) | 消息 ID | | `conversation_id` | string | 所属会话 ID | -| `role` | enum | `user` / `assistant` / `system` / `tool` | +| `role` | enum | `user` / `assistant` / `system` | | `content` | string | 消息内容 | | `token_count` | integer | token 消耗数 | | `thinking_content` | string | 思维链内容(启用时) | -| `tool_calls` | string (JSON) | 工具调用请求(assistant 消息) | -| `tool_call_id` | string | 工具调用 ID(tool 消息) | -| `name` | string | 工具名称(tool 消息) | +| `tool_calls` | array (JSON) | 工具调用信息(含结果),仅 assistant 消息 | | `created_at` | datetime | 创建时间 | #### 消息类型说明 @@ -563,16 +571,22 @@ User 1 ── * Conversation 1 ── * Message "content": "北京今天天气晴朗...", "token_count": 50, "thinking_content": "用户想了解天气...", + "tool_calls": null, "created_at": "2026-03-24T10:00:01Z" } ``` -**3. 助手消息 - 工具调用 (role=assistant, with tool_calls)** +**3. 助手消息 - 含工具调用 (role=assistant, with tool_calls)** + +工具调用结果直接合并到 `tool_calls` 数组中,每个调用包含 `result` 字段: + ```json { "id": "msg_003", "role": "assistant", - "content": "", + "content": "北京今天天气晴朗,温度25°C,湿度60%", + "token_count": 80, + "thinking_content": "用户想知道北京天气,需要调用工具获取...", "tool_calls": [ { "id": "call_abc123", @@ -580,21 +594,10 @@ User 1 ── * Conversation 1 ── * Message "function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" - } + }, + "result": "{\"temperature\": 25, \"humidity\": 60, \"description\": \"晴天\"}" } ], - "created_at": "2026-03-24T10:00:01Z" -} -``` - -**4. 工具返回消息 (role=tool)** -```json -{ - "id": "msg_004", - "role": "tool", - "content": "{\"temperature\": 25, \"humidity\": 60, \"description\": \"晴天\"}", - "tool_call_id": "call_abc123", - "name": "get_weather", "created_at": "2026-03-24T10:00:02Z" } ``` @@ -606,9 +609,29 @@ User 1 ── * Conversation 1 ── * Message ↓ [msg_001] role=user, content="北京今天天气怎么样?" ↓ -[msg_002] role=assistant, tool_calls=[{get_weather, args:{"city":"北京"}}] +[AI 调用工具 get_weather] ↓ -[msg_003] role=tool, name=get_weather, content="{...weather data...}" - ↓ -[msg_004] role=assistant, content="北京今天天气晴朗,温度25°C..." +[msg_002] role=assistant, tool_calls=[{get_weather, args:{"city":"北京"}, result="{...}"}] + content="北京今天天气晴朗,温度25°C..." ``` + +**说明:** +- 工具调用结果直接存储在 `tool_calls[].result` 字段中 +- 不再创建独立的 `role=tool` 消息 +- 前端可通过 `tool_calls` 数组展示完整的工具调用过程 + +--- + +## 前端特性 + +### 工具调用控制 + +- 工具调用开关位于输入框右侧(扳手图标) +- 状态存储在浏览器 localStorage(`tools_enabled`) +- 默认开启,可通过请求参数 `tools_enabled` 控制每次请求 + +### 消息渲染 + +- 助手消息的 `tool_calls` 通过可折叠面板展示 +- 面板显示:思考过程 → 工具调用 → 工具结果 +- 每个子项可独立展开/折叠 diff --git a/frontend/src/components/ChatView.vue b/frontend/src/components/ChatView.vue index c00e7b7..02b63ae 100644 --- a/frontend/src/components/ChatView.vue +++ b/frontend/src/components/ChatView.vue @@ -139,6 +139,8 @@ defineExpose({ scrollToBottom }) height: 100vh; background: var(--bg-secondary); min-width: 0; + max-width: 100%; + overflow: hidden; transition: background 0.2s; } @@ -247,6 +249,7 @@ defineExpose({ scrollToBottom }) flex: 1; overflow-y: auto; padding: 0 24px; + width: 100%; } .messages-container::-webkit-scrollbar { @@ -305,6 +308,12 @@ defineExpose({ scrollToBottom }) color: white; } +.message-bubble .message-body { + flex: 1; + min-width: 0; + overflow: hidden; +} + .streaming-content { font-size: 15px; line-height: 1.7; @@ -327,6 +336,7 @@ defineExpose({ scrollToBottom }) padding: 16px; overflow-x: auto; margin: 8px 0; + max-width: 100%; } .streaming-content :deep(pre code) { diff --git a/frontend/src/components/MessageBubble.vue b/frontend/src/components/MessageBubble.vue index a235a79..5775b27 100644 --- a/frontend/src/components/MessageBubble.vue +++ b/frontend/src/components/MessageBubble.vue @@ -106,7 +106,7 @@ function copyContent() { } .message-body { - max-width: 720px; + flex: 1; min-width: 0; } diff --git a/frontend/src/components/ProcessBlock.vue b/frontend/src/components/ProcessBlock.vue index 86fae4b..57fc170 100644 --- a/frontend/src/components/ProcessBlock.vue +++ b/frontend/src/components/ProcessBlock.vue @@ -180,6 +180,7 @@ watch(() => props.streaming, (streaming) => { border: 1px solid var(--border-light); overflow: hidden; background: var(--bg-secondary); + max-width: 100%; } .process-toggle { @@ -309,6 +310,7 @@ watch(() => props.streaming, (streaming) => { padding: 12px; background: var(--bg-primary); border-top: 1px solid var(--border-light); + overflow: hidden; } .thinking-text { @@ -357,6 +359,8 @@ watch(() => props.streaming, (streaming) => { color: var(--text-secondary); overflow-x: auto; border: 1px solid var(--border-light); + white-space: pre-wrap; + word-break: break-word; } .result-label { diff --git a/pyproject.toml b/pyproject.toml index 55f71d3..d75aef2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,11 @@ dependencies = [ "pymysql>=1.1", "requests>=2.31", "pyyaml>=6.0", + "duckduckgo-search>=8.0", + "ddgs>=8.0", + "beautifulsoup4>=4.12", + "httpx>=0.25", + "lxml>=5.0", ] [build-system]