From d66aa64436325b7824229a4df38b4e35330d4c31 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Sun, 15 Feb 2026 21:11:22 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat(model):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=A8=A1=E5=9E=8B=E4=BB=A5=E5=8F=8A=E5=80=99?= =?UTF-8?q?=E9=80=89=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/static/models.py | 39 +++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/config/static/models.py b/src/config/static/models.py index d1b83b8d..75d6856e 100644 --- a/src/config/static/models.py +++ b/src/config/static/models.py @@ -49,9 +49,9 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = { name="OpenAI", url="https://platform.openai.com/docs/models", base_url="https://api.openai.com/v1", - default="gpt-4o-mini", + default="gpt-5-mini", env="OPENAI_API_KEY", - models=["gpt-4", "gpt-4o", "gpt-4o-mini"], + models=["gpt-5.2", "gpt-5-mini", "gpt-5.2-pro"], ), "deepseek": ChatModelProvider( name="DeepSeek", @@ -65,22 +65,21 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = { name="智谱AI (Zhipu)", url="https://open.bigmodel.cn/dev/api", base_url="https://open.bigmodel.cn/api/paas/v4/", - default="glm-4.5-flash", + default="glm-4.7-flash", env="ZHIPUAI_API_KEY", - models=["glm-4.6", "glm-4.5-air", "glm-4.5-flash"], + models=["glm-5", "glm-4.5-air", "glm-4.7-flash"], ), "siliconflow": ChatModelProvider( name="SiliconFlow", url="https://cloud.siliconflow.cn/models", base_url="https://api.siliconflow.cn/v1", - default="deepseek-ai/DeepSeek-V3.2", + default="Pro/deepseek-ai/DeepSeek-V3.2", env="SILICONFLOW_API_KEY", models=[ - "deepseek-ai/DeepSeek-V3.2", - "Qwen/Qwen3-235B-A22B-Thinking-2507", - "Qwen/Qwen3-235B-A22B-Instruct-2507", - "moonshotai/Kimi-K2-Instruct-0905", - "zai-org/GLM-4.6", + "Pro/deepseek-ai/DeepSeek-V3.2", + "Pro/MiniMaxAI/MiniMax-M2.5", + "Pro/zai-org/GLM-5", + "Pro/moonshotai/Kimi-K2.5", ], ), # "together": ChatModelProvider( @@ -101,33 +100,31 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = { "qwen-max-latest", "qwen-plus-latest", "qwen-turbo-latest", - "qwen3-235b-a22b-thinking-2507", - "qwen3-235b-a22b-instruct-2507", ], ), "ark": ChatModelProvider( name="豆包(Ark)", url="https://console.volcengine.com/ark/region:ark+cn-beijing/model", base_url="https://ark.cn-beijing.volces.com/api/v3", - default="doubao-seed-1-6-250615", + default="doubao-seed-2-0-lite-260215", env="ARK_API_KEY", models=[ - "doubao-seed-1-6-250615", - "doubao-seed-1-6-thinking-250715", - "doubao-seed-1-6-flash-250715", + "doubao-seed-2-0-pro-260215", + "doubao-seed-2-0-lite-260215", + "doubao-seed-2-0-mini-260215", ], ), "openrouter": ChatModelProvider( name="OpenRouter", url="https://openrouter.ai/models", base_url="https://openrouter.ai/api/v1", - default="openai/gpt-4o", + default="x-ai/grok-4.1-fast", env="OPENROUTER_API_KEY", models=[ - "openai/gpt-4o", + "anthropic/claude-opus-4.6", + "anthropic/claude-sonnet-4.5", + "x-ai/grok-4.1-fast", "x-ai/grok-4", - "google/gemini-2.5-pro", - "anthropic/claude-sonnet-4", ], ), # "moonshot": ChatModelProvider( @@ -148,7 +145,7 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = { base_url="https://api-inference.modelscope.cn/v1/", default="deepseek-ai/DeepSeek-V3.2", env="MODELSCOPE_ACCESS_TOKEN", - models=["Qwen/Qwen3-32B", "deepseek-ai/DeepSeek-V3.2"], + models=["ZhipuAI/GLM-5", "ZhipuAI/GLM-4.7-Flash", "MiniMax/MiniMax-M2.5", "moonshotai/Kimi-K2.5", ""], ), } From ed849a2e5c8031b57028404832ff31bd2df4ab05 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Sun, 15 Feb 2026 21:57:41 +0800 Subject: [PATCH 02/12] =?UTF-8?q?feat(tool):=20=E4=BC=98=E5=8C=96=20MySQL?= =?UTF-8?q?=20=E5=B7=A5=E5=85=B7=E5=91=BD=E5=90=8D=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=89=8D=E7=AB=AF=E6=B8=B2=E6=9F=93=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将工具 name_or_callable 从中文改为英文 (mysql_query, mysql_describe_table, mysql_list_tables) - 添加 MySQL 工具结果的前端渲染组件,提升用户体验 --- src/agents/common/toolkits/mysql/tools.py | 12 +- .../ToolCallingResult/ToolCallRenderer.vue | 24 ++++ web/src/components/ToolCallingResult/index.js | 3 + .../tools/MysqlDescribeTableTool.vue | 83 ++++++++++++ .../tools/MysqlListTablesTool.vue | 67 ++++++++++ .../tools/MysqlQueryTool.vue | 119 ++++++++++++++++++ 6 files changed, 302 insertions(+), 6 deletions(-) create mode 100644 web/src/components/ToolCallingResult/tools/MysqlDescribeTableTool.vue create mode 100644 web/src/components/ToolCallingResult/tools/MysqlListTablesTool.vue create mode 100644 web/src/components/ToolCallingResult/tools/MysqlQueryTool.vue diff --git a/src/agents/common/toolkits/mysql/tools.py b/src/agents/common/toolkits/mysql/tools.py index 17a681d0..f163b24d 100644 --- a/src/agents/common/toolkits/mysql/tools.py +++ b/src/agents/common/toolkits/mysql/tools.py @@ -52,9 +52,9 @@ class TableListModel(BaseModel): pass -@tool(name_or_callable="查询表名及说明", args_schema=TableListModel) +@tool(name_or_callable="mysql_list_tables", args_schema=TableListModel) def mysql_list_tables() -> str: - """获取数据库中的所有表名 + """【查询表名及说明】获取数据库中的所有表名 这个工具用来列出当前数据库中所有的表名,帮助你了解数据库的结构。 """ @@ -107,9 +107,9 @@ class TableDescribeModel(BaseModel): table_name: str = Field(description="要查询的表名", example="users") -@tool(name_or_callable="描述表", args_schema=TableDescribeModel) +@tool(name_or_callable="mysql_describe_table", args_schema=TableDescribeModel) def mysql_describe_table(table_name: Annotated[str, "要查询结构的表名"]) -> str: - """获取指定表的详细结构信息 + """【描述表】获取指定表的详细结构信息 这个工具用来查看表的字段信息、数据类型、是否允许NULL、默认值、键类型等。 帮助你了解表的结构,以便编写正确的SQL查询。 @@ -203,12 +203,12 @@ class QueryModel(BaseModel): timeout: int | None = Field(default=60, description="查询超时时间(秒),默认60秒,最大600秒", ge=1, le=600) -@tool(name_or_callable="执行 SQL 查询", args_schema=QueryModel) +@tool(name_or_callable="mysql_query", args_schema=QueryModel) def mysql_query( sql: Annotated[str, "要执行的SQL查询语句(只能是SELECT语句)"], timeout: Annotated[int | None, "查询超时时间(秒),默认60秒,最大600秒"] = 60, ) -> str: - """执行只读的SQL查询语句 + """【执行 SQL 查询】执行只读的SQL查询语句 这个工具用来执行SQL查询并返回结果。支持复杂的SELECT查询,包括JOIN、GROUP BY等。 注意:只能执行查询操作,不能修改数据。 diff --git a/web/src/components/ToolCallingResult/ToolCallRenderer.vue b/web/src/components/ToolCallingResult/ToolCallRenderer.vue index 0b9c5626..6310c79b 100644 --- a/web/src/components/ToolCallingResult/ToolCallRenderer.vue +++ b/web/src/components/ToolCallingResult/ToolCallRenderer.vue @@ -38,6 +38,15 @@ + + + + + + + + + @@ -61,6 +70,9 @@ import ListDirectoryTool from './tools/ListDirectoryTool.vue' import SearchFileContentTool from './tools/SearchFileContentTool.vue' import GlobTool from './tools/GlobTool.vue' import EditFileTool from './tools/EditFileTool.vue' +import MysqlQueryTool from './tools/MysqlQueryTool.vue' +import MysqlDescribeTableTool from './tools/MysqlDescribeTableTool.vue' +import MysqlListTablesTool from './tools/MysqlListTablesTool.vue' const props = defineProps({ toolCall: { @@ -170,6 +182,18 @@ const isEditFileResult = computed(() => { return toolName.value === 'edit_file' || toolName.value === 'replace' }) +const isMysqlQueryResult = computed(() => { + return toolName.value === 'mysql_query' +}) + +const isMysqlDescribeTableResult = computed(() => { + return toolName.value === 'mysql_describe_table' +}) + +const isMysqlListTablesResult = computed(() => { + return toolName.value === 'mysql_list_tables' +}) + // 处理知识图谱刷新 const graphToolCallRef = ref(null) const refreshGraph = () => { diff --git a/web/src/components/ToolCallingResult/index.js b/web/src/components/ToolCallingResult/index.js index 15a8c361..577d0bfe 100644 --- a/web/src/components/ToolCallingResult/index.js +++ b/web/src/components/ToolCallingResult/index.js @@ -15,3 +15,6 @@ export { default as ListDirectoryTool } from './tools/ListDirectoryTool.vue' export { default as SearchFileContentTool } from './tools/SearchFileContentTool.vue' export { default as GlobTool } from './tools/GlobTool.vue' export { default as EditFileTool } from './tools/EditFileTool.vue' +export { default as MysqlQueryTool } from './tools/MysqlQueryTool.vue' +export { default as MysqlDescribeTableTool } from './tools/MysqlDescribeTableTool.vue' +export { default as MysqlListTablesTool } from './tools/MysqlListTablesTool.vue' diff --git a/web/src/components/ToolCallingResult/tools/MysqlDescribeTableTool.vue b/web/src/components/ToolCallingResult/tools/MysqlDescribeTableTool.vue new file mode 100644 index 00000000..b7ae5afe --- /dev/null +++ b/web/src/components/ToolCallingResult/tools/MysqlDescribeTableTool.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/web/src/components/ToolCallingResult/tools/MysqlListTablesTool.vue b/web/src/components/ToolCallingResult/tools/MysqlListTablesTool.vue new file mode 100644 index 00000000..104e3f49 --- /dev/null +++ b/web/src/components/ToolCallingResult/tools/MysqlListTablesTool.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/web/src/components/ToolCallingResult/tools/MysqlQueryTool.vue b/web/src/components/ToolCallingResult/tools/MysqlQueryTool.vue new file mode 100644 index 00000000..4c04e1fd --- /dev/null +++ b/web/src/components/ToolCallingResult/tools/MysqlQueryTool.vue @@ -0,0 +1,119 @@ + + + + + From e6ad5dfa1d192d10ff5f3400b5af9c03027ce6e5 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Sun, 15 Feb 2026 21:58:24 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat(agent):=20=E4=B8=BA=20SqlReporterAge?= =?UTF-8?q?nt=20=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=92=8C=E9=99=84=E4=BB=B6=E4=BF=9D=E5=AD=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 FilesystemMiddleware 和 StateBackend 支持文件存储 - 添加 save_attachments_to_fs 中间件保存附件到文件系统 - 配置 agent 的 capabilities 支持 file_upload 和 files --- src/agents/reporter/graph.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/agents/reporter/graph.py b/src/agents/reporter/graph.py index 0aadf343..ff49bde1 100644 --- a/src/agents/reporter/graph.py +++ b/src/agents/reporter/graph.py @@ -1,16 +1,24 @@ from dataclasses import dataclass, field from typing import Annotated +from deepagents.backends import StateBackend +from deepagents.middleware.filesystem import FilesystemMiddleware from langchain.agents import create_agent from src.agents.common import BaseAgent, BaseContext, load_chat_model from src.agents.common.middlewares import ( RuntimeConfigMiddleware, + save_attachments_to_fs, ) from src.agents.common.toolkits.mysql import get_mysql_tools from src.services.mcp_service import get_mcp_server_names, get_tools_from_all_servers from src.utils import logger +def _create_fs_backend(rt): + """创建文件存储后端""" + return StateBackend(rt) + + PROMPT = """你的任务是根据用户的指令,使用数据库工具和图表绘制工具,构建 SQL 查询报告。 你需要根据用户的指令,生成相应的 SQL 查询,并将查询结果以报表的形式返回给用户。 在生成报表时,你可以调用工具生成图表,以更直观地展示数据。 @@ -52,6 +60,10 @@ class SqlReporterAgent(BaseAgent): "MySQL 工具默认启用,无法选择,mcp 默认启用 Charts MCPs。" ) context_schema = ReporterContext + capabilities = [ + "file_upload", + "files", + ] def __init__(self, **kwargs): super().__init__(**kwargs) @@ -66,7 +78,9 @@ class SqlReporterAgent(BaseAgent): system_prompt=context.system_prompt, tools=get_mysql_tools(), # MySQL 工具默认启用,这里添加的 tools,不会在工具选择框中出现 middleware=[ + FilesystemMiddleware(backend=_create_fs_backend), # 文件系统后端 RuntimeConfigMiddleware(extra_tools=all_mcp_tools), + save_attachments_to_fs, # 附件保存到文件系统 ], checkpointer=await self._get_checkpointer(), ) From 426b3ddc5bd51b554866155d2ec8c25e36d4491f Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Sun, 15 Feb 2026 21:59:28 +0800 Subject: [PATCH 04/12] =?UTF-8?q?fix(web):=20=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=AF=A6=E6=83=85=E5=BC=B9=E7=AA=97=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=88=87=E6=8D=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 改为检查 hasChunks(有实际分块数据)而非 hasIndexed(索引状态) - 当文件没有 chunks 时正确重置为 markdown 视图 --- web/src/components/FileDetailModal.vue | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/web/src/components/FileDetailModal.vue b/web/src/components/FileDetailModal.vue index 0c529af1..d2da897b 100644 --- a/web/src/components/FileDetailModal.vue +++ b/web/src/components/FileDetailModal.vue @@ -25,7 +25,7 @@ -
+
@@ -141,21 +141,22 @@ const hasIndexed = computed(() => ['done', 'indexed'].includes(file.value?.statu const hasContent = computed( () => (file.value?.lines && file.value?.lines.length > 0) || file.value?.content ) +// 是否有实际的分块数据 +const hasChunks = computed(() => mappedChunks.value && mappedChunks.value.length > 0) const viewModeOptions = computed(() => { const options = [{ label: 'Markdown', value: 'markdown' }] - if (hasIndexed.value) { + // 只有当有实际的分块数据时才显示 Chunks 选项 + if (hasChunks.value) { options.push({ label: 'Chunks', value: 'chunks' }) } return options }) -// 监听文件状态变化,重置视图模式 +// 监听文件变化,如果没有 chunks 则重置为 markdown watch(file, (newFile) => { - if (newFile) { - if (!hasIndexed.value) { - viewMode.value = 'markdown' - } + if (newFile && !hasChunks.value) { + viewMode.value = 'markdown' } }) From d532e2bb4687ca4970ada5f68aae3dd208160f3e Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Sun, 15 Feb 2026 21:59:59 +0800 Subject: [PATCH 05/12] =?UTF-8?q?style(web):=20=E7=A7=BB=E9=99=A4=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E4=BF=A1=E6=81=AF=E9=A1=B5=E7=AD=BE=E7=9A=84?= =?UTF-8?q?=E5=BA=95=E9=83=A8=E8=BE=B9=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/views/DataBaseInfoView.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/views/DataBaseInfoView.vue b/web/src/views/DataBaseInfoView.vue index 3f1e2c41..6ebcf06e 100644 --- a/web/src/views/DataBaseInfoView.vue +++ b/web/src/views/DataBaseInfoView.vue @@ -653,7 +653,6 @@ const handleMouseUp = () => { :deep(.ant-tabs-nav) { margin-bottom: 0; // background-color: var(--gray-0); - border-bottom: 1px solid var(--gray-200); } :deep(.ant-tabs-extra-content) { From 80cf6937edf9d77fe46cb630f4ed3cf364ceed8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=82=96=E6=B3=BD=E6=B6=9B?= Date: Fri, 20 Feb 2026 23:41:44 +0800 Subject: [PATCH 06/12] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=AF=B9=E8=AF=9D=E6=97=B6=E5=86=85=E5=AE=B9=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E4=BD=9C=E4=B8=BA=E6=A0=87=E9=A2=98=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E8=B6=85=E5=87=BA=E8=A1=A8=E5=AD=97=E6=AE=B5=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E9=99=90=E5=88=B6=E9=97=AE=E9=A2=98=EF=BC=8C=20titile=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E9=95=BF=E5=BA=A6=E9=99=90=E5=88=B6=E4=B8=BA?= =?UTF-8?q?255?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 MAX_CONVERSATION_TITLE_LENGTH 常量 (255 字符) - 新增 _normalize_title() 方法用于截断和校验标题 - 防止创建和更新对话时数据库字段溢出 - 前端 AgentChatComponent 同步添加标题标准化处理 - 添加标题标准化的单元测试 --- src/repositories/conversation_repository.py | 24 ++++++++++++++++++--- test/test_conversation_repository.py | 22 +++++++++++++++++++ web/src/components/AgentChatComponent.vue | 12 ++++++++--- 3 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 test/test_conversation_repository.py diff --git a/src/repositories/conversation_repository.py b/src/repositories/conversation_repository.py index 4b0cd3b7..018fb180 100644 --- a/src/repositories/conversation_repository.py +++ b/src/repositories/conversation_repository.py @@ -12,11 +12,26 @@ from src.storage.postgres.models_business import Conversation, ConversationStats from src.utils import logger from src.utils.datetime_utils import utc_now_naive +MAX_CONVERSATION_TITLE_LENGTH = 255 + class ConversationRepository: def __init__(self, db_session: AsyncSession): self.db = db_session + def _normalize_title(self, title: str | None) -> str | None: + if title is None: + return None + normalized = str(title).strip() + if not normalized: + return "" + if len(normalized) > MAX_CONVERSATION_TITLE_LENGTH: + logger.warning( + f"Conversation title too long ({len(normalized)}), truncate to {MAX_CONVERSATION_TITLE_LENGTH}" + ) + return normalized[:MAX_CONVERSATION_TITLE_LENGTH] + return normalized + async def create_conversation( self, user_id: str, @@ -31,11 +46,13 @@ class ConversationRepository: metadata = (metadata or {}).copy() metadata.setdefault("attachments", []) + normalized_title = self._normalize_title(title) + conversation = Conversation( thread_id=thread_id, user_id=str(user_id), agent_id=agent_id, - title=title or "New Conversation", + title=normalized_title or "New Conversation", status="active", extra_metadata=metadata, ) @@ -203,8 +220,9 @@ class ConversationRepository: if not conversation: return None - if title is not None: - conversation.title = title + normalized_title = self._normalize_title(title) + if normalized_title is not None: + conversation.title = normalized_title if status is not None: conversation.status = status diff --git a/test/test_conversation_repository.py b/test/test_conversation_repository.py new file mode 100644 index 00000000..8729223f --- /dev/null +++ b/test/test_conversation_repository.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from src.repositories.conversation_repository import ConversationRepository, MAX_CONVERSATION_TITLE_LENGTH + + +def test_normalize_title_truncates_when_too_long(): + repo = ConversationRepository(None) # type: ignore[arg-type] + raw = "a" * (MAX_CONVERSATION_TITLE_LENGTH + 50) + + normalized = repo._normalize_title(raw) + + assert normalized is not None + assert len(normalized) == MAX_CONVERSATION_TITLE_LENGTH + assert normalized == "a" * MAX_CONVERSATION_TITLE_LENGTH + + +def test_normalize_title_trims_spaces(): + repo = ConversationRepository(None) # type: ignore[arg-type] + + normalized = repo._normalize_title(" hello world ") + + assert normalized == "hello world" diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index d777dca2..b2468efa 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -626,12 +626,15 @@ const deleteThread = async (threadId) => { const updateThread = async (threadId, title) => { if (!threadId || !title) return + const normalizedTitle = String(title).replace(/\s+/g, ' ').trim().slice(0, 255) + if (!normalizedTitle) return + chatState.isRenamingThread = true try { - await threadApi.updateThread(threadId, title) + await threadApi.updateThread(threadId, normalizedTitle) const thread = threads.value.find((t) => t.id === threadId) if (thread) { - thread.title = title + thread.title = normalizedTitle } } catch (error) { console.error('Failed to update thread:', error) @@ -750,7 +753,10 @@ const sendMessage = async ({ // 如果是新对话,用消息内容作为标题 if ((threadMessages.value[threadId] || []).length === 0) { - updateThread(threadId, text) + const autoTitle = text.replace(/\s+/g, ' ').trim().slice(0, 255) + if (autoTitle) { + void updateThread(threadId, autoTitle).catch(() => {}) + } } const requestData = { From 7bfde7cc2c6c9ad0a4848e67aee37bc38512b776 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Wed, 25 Feb 2026 11:07:28 +0800 Subject: [PATCH 07/12] =?UTF-8?q?style(format):=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/agents/reporter/graph.py | 1 + .../ToolCallingResult/tools/MysqlDescribeTableTool.vue | 4 +++- web/src/components/ToolCallingResult/tools/MysqlQueryTool.vue | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/agents/reporter/graph.py b/src/agents/reporter/graph.py index ff49bde1..5d1731b7 100644 --- a/src/agents/reporter/graph.py +++ b/src/agents/reporter/graph.py @@ -14,6 +14,7 @@ from src.agents.common.toolkits.mysql import get_mysql_tools from src.services.mcp_service import get_mcp_server_names, get_tools_from_all_servers from src.utils import logger + def _create_fs_backend(rt): """创建文件存储后端""" return StateBackend(rt) diff --git a/web/src/components/ToolCallingResult/tools/MysqlDescribeTableTool.vue b/web/src/components/ToolCallingResult/tools/MysqlDescribeTableTool.vue index b7ae5afe..24a17ed2 100644 --- a/web/src/components/ToolCallingResult/tools/MysqlDescribeTableTool.vue +++ b/web/src/components/ToolCallingResult/tools/MysqlDescribeTableTool.vue @@ -3,7 +3,9 @@ diff --git a/web/src/components/ToolCallingResult/tools/MysqlQueryTool.vue b/web/src/components/ToolCallingResult/tools/MysqlQueryTool.vue index 4c04e1fd..11389523 100644 --- a/web/src/components/ToolCallingResult/tools/MysqlQueryTool.vue +++ b/web/src/components/ToolCallingResult/tools/MysqlQueryTool.vue @@ -3,7 +3,9 @@ From 80f97cc2d941cc39c416a87ee4e7441eafbb997b Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Wed, 25 Feb 2026 11:42:31 +0800 Subject: [PATCH 08/12] =?UTF-8?q?fix(attachments):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=99=84=E4=BB=B6=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E4=BF=AE=E5=A4=8D=20files=20=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/chat_router.py | 5 +++++ src/services/conversation_service.py | 21 +++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/server/routers/chat_router.py b/server/routers/chat_router.py index eb9343d5..95b41a01 100644 --- a/server/routers/chat_router.py +++ b/server/routers/chat_router.py @@ -658,6 +658,11 @@ async def update_thread( ) + +# ================================ +# > === 附件管理分组 === +# ================================ + @chat.post("/thread/{thread_id}/attachments", response_model=AttachmentResponse) async def upload_thread_attachment( thread_id: str, diff --git a/src/services/conversation_service.py b/src/services/conversation_service.py index 3f44d101..bf41d805 100644 --- a/src/services/conversation_service.py +++ b/src/services/conversation_service.py @@ -2,6 +2,7 @@ import uuid from datetime import UTC, datetime from fastapi import HTTPException, UploadFile +from langgraph.types import Command from sqlalchemy.ext.asyncio import AsyncSession from src.agents import agent_manager @@ -78,26 +79,34 @@ async def _sync_thread_attachment_state( graph = await agent.get_graph() config = {"configurable": {"thread_id": thread_id, "user_id": str(user_id)}} + + # 先获取现有 state,保留非附件文件 state = await graph.aget_state(config) state_values = getattr(state, "values", {}) if state else {} existing_files = state_values.get("files", {}) if isinstance(state_values, dict) else {} if not isinstance(existing_files, dict): existing_files = {} - attachment_files = _build_state_files(attachments) + # 保留非 /attachments/ 开头的现有文件(如 Agent 创建的 a.md) merged_files = { path: file_data for path, file_data in existing_files.items() if isinstance(path, str) and not path.startswith("/attachments/") } + + # 添加附件文件 + attachment_files = _build_state_files(attachments) merged_files.update(attachment_files) + # 使用 Command 确保 reducer 被正确应用 await graph.aupdate_state( config=config, - values={ - "attachments": attachments, - "files": merged_files, - }, + values=Command( + update={ + "attachments": attachments, + "files": merged_files, + } + ), ) except Exception as e: logger.warning(f"Failed to sync attachment state for thread {thread_id}: {e}") @@ -260,7 +269,7 @@ async def upload_thread_attachment_view( "uploaded_at": utc_isoformat(), "truncated": conversion.truncated, "file_path": file_path, # 用于 StateBackend,前端不返回此字段 - "minio_url": minio_url, + "minio_url": minio_url, # 暂未使用 } await conv_repo.add_attachment(conversation.id, attachment_record) all_attachments = await conv_repo.get_attachments(conversation.id) From 5fa89ddc85426296c87a6e387c59e73cfd2db9f6 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Wed, 25 Feb 2026 12:10:50 +0800 Subject: [PATCH 09/12] =?UTF-8?q?style(web):=20=E4=BC=98=E5=8C=96=E9=83=A8?= =?UTF-8?q?=E5=88=86=E7=BB=86=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/AgentChatComponent.vue | 2 +- web/src/components/AgentInputArea.vue | 21 +++++++++++++++++++-- web/src/components/AgentPanel.vue | 7 +++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index b2468efa..bfe2e111 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -1349,7 +1349,7 @@ watch( background: var(--gray-0); border-radius: 12px; box-shadow: 0 4px 20px var(--shadow-1); - border: 1px solid var(--gray-200); + border: 1px solid var(--gray-150); min-width: 0; will-change: flex-basis; } diff --git a/web/src/components/AgentInputArea.vue b/web/src/components/AgentInputArea.vue index a7e91b56..4fea3789 100644 --- a/web/src/components/AgentInputArea.vue +++ b/web/src/components/AgentInputArea.vue @@ -1,6 +1,7 @@