From d282cab00cd581e6cf77ab643aba345d683cbaab Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Mon, 30 Mar 2026 16:39:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=AF=B4=E6=98=8E=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yuxi/agents/toolkits/buildin/tools.py | 16 ++++++- .../yuxi/agents/toolkits/mysql/tools.py | 19 ++++++++ .../package/yuxi/agents/toolkits/registry.py | 3 ++ backend/package/yuxi/services/tool_service.py | 2 + .../integration/api/test_system_router.py | 11 +++++ .../test/unit/services/test_tool_service.py | 48 +++++++++++++++++++ docs/develop-guides/roadmap.md | 1 + web/src/components/ToolsManagerComponent.vue | 17 ++++++- 8 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 backend/test/unit/services/test_tool_service.py diff --git a/backend/package/yuxi/agents/toolkits/buildin/tools.py b/backend/package/yuxi/agents/toolkits/buildin/tools.py index daff551e..7b9a984d 100644 --- a/backend/package/yuxi/agents/toolkits/buildin/tools.py +++ b/backend/package/yuxi/agents/toolkits/buildin/tools.py @@ -20,6 +20,15 @@ from yuxi.utils.question_utils import normalize_questions # Lazy initialization for TavilySearch (only when API key is available) _tavily_search_instance = None +QWEN_IMAGE_CONFIG_GUIDE = """ +使用前需要先配置硅基流动的图片生成访问凭证。 + +请在后端运行环境中配置环境变量: +- `SILICONFLOW_API_KEY`:用于调用 SiliconFlow 的图片生成接口 + +配置完成后即可使用该工具生成图片。 +""".strip() + def _create_tavily_search(): """Create and register TavilySearch tool with metadata.""" @@ -280,7 +289,12 @@ def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge return f"知识图谱查询失败: {str(e)}" -@tool(category="buildin", tags=["图片", "生成"], display_name="Qwen-Image") +@tool( + category="buildin", + tags=["图片", "生成"], + display_name="Qwen-Image", + config_guide=QWEN_IMAGE_CONFIG_GUIDE, +) async def text_to_img_qwen_image( prompt: Annotated[str, "用于生成图片的文本描述"], negative_prompt: Annotated[str, "负面提示词,用于指定不想出现在图片中的元素"] = "", diff --git a/backend/package/yuxi/agents/toolkits/mysql/tools.py b/backend/package/yuxi/agents/toolkits/mysql/tools.py index 16174dcd..cdb59679 100644 --- a/backend/package/yuxi/agents/toolkits/mysql/tools.py +++ b/backend/package/yuxi/agents/toolkits/mysql/tools.py @@ -17,6 +17,22 @@ from .security import MySQLSecurityChecker # 全局连接管理器实例 _connection_manager: MySQLConnectionManager | None = None +MYSQL_CONFIG_GUIDE = """ +使用前需要先配置 MySQL 连接相关环境变量。 + +必填环境变量: +- `MYSQL_HOST` +- `MYSQL_PORT` +- `MYSQL_USER` +- `MYSQL_PASSWORD` +- `MYSQL_DATABASE` + +可选环境变量: +- `MYSQL_DATABASE_DESCRIPTION`:数据库说明,会追加到工具描述中,帮助模型理解库表语义 + +请在后端运行环境中完成以上配置后再使用这些 MySQL 工具。 +""".strip() + def get_connection_manager() -> MySQLConnectionManager: """获取全局连接管理器""" @@ -50,6 +66,7 @@ def get_connection_manager() -> MySQLConnectionManager: category="mysql", tags=["数据库", "查询"], display_name="列出MySQL表", + config_guide=MYSQL_CONFIG_GUIDE, name_or_callable="mysql_list_tables", ) def mysql_list_tables() -> str: @@ -110,6 +127,7 @@ class TableDescribeModel(BaseModel): category="mysql", tags=["数据库", "结构"], display_name="描述MySQL表结构", + config_guide=MYSQL_CONFIG_GUIDE, name_or_callable="mysql_describe_table", args_schema=TableDescribeModel, ) @@ -212,6 +230,7 @@ class QueryModel(BaseModel): category="mysql", tags=["数据库", "SQL"], display_name="执行MySQL查询", + config_guide=MYSQL_CONFIG_GUIDE, name_or_callable="mysql_query", args_schema=QueryModel, ) diff --git a/backend/package/yuxi/agents/toolkits/registry.py b/backend/package/yuxi/agents/toolkits/registry.py index 9cfe8a5b..a8628da4 100644 --- a/backend/package/yuxi/agents/toolkits/registry.py +++ b/backend/package/yuxi/agents/toolkits/registry.py @@ -10,6 +10,7 @@ class ToolExtraMetadata: tags: list[str] = field(default_factory=list) display_name: str = "" # 显示名称(给人看的名字) icon: str = "" + config_guide: str = "" # 配置说明(给人看的使用前配置提示) # 全局注册表: tool_name -> ToolExtraMetadata @@ -40,6 +41,7 @@ def tool( tags: list[str] = None, display_name: str = "", icon: str = "", + config_guide: str = "", name_or_callable: str | Callable | None = None, description: str | None = None, args_schema: type | None = None, @@ -83,6 +85,7 @@ def tool( tags=tags or [], display_name=display_name, icon=icon, + config_guide=config_guide, ) # 自动收集工具实例 diff --git a/backend/package/yuxi/services/tool_service.py b/backend/package/yuxi/services/tool_service.py index 8bdb76dc..05f3ba70 100644 --- a/backend/package/yuxi/services/tool_service.py +++ b/backend/package/yuxi/services/tool_service.py @@ -55,6 +55,7 @@ def _ensure_metadata_loaded(): extra = extra_meta[tool_name] runtime_info["category"] = extra.category runtime_info["tags"] = extra.tags + runtime_info["config_guide"] = extra.config_guide # display_name 优先级高于 tool.name if extra.display_name: runtime_info["name"] = extra.display_name @@ -62,6 +63,7 @@ def _ensure_metadata_loaded(): # 未注册,设为默认分类 runtime_info["category"] = "buildin" runtime_info["tags"] = [] + runtime_info["config_guide"] = "" _metadata_cache.append(runtime_info) diff --git a/backend/test/integration/api/test_system_router.py b/backend/test/integration/api/test_system_router.py index 65843d23..b816fc94 100644 --- a/backend/test/integration/api/test_system_router.py +++ b/backend/test/integration/api/test_system_router.py @@ -38,3 +38,14 @@ async def test_admin_can_fetch_config_and_reload_info(test_client, admin_headers reload_payload = reload_response.json() assert reload_payload["success"] is True assert "data" in reload_payload + + +async def test_admin_can_fetch_tools_with_config_guide_field(test_client, admin_headers): + response = await test_client.get("/api/system/tools", headers=admin_headers) + assert response.status_code == 200, response.text + + payload = response.json() + assert payload["success"] is True + assert isinstance(payload["data"], list) + assert payload["data"] + assert "config_guide" in payload["data"][0] diff --git a/backend/test/unit/services/test_tool_service.py b/backend/test/unit/services/test_tool_service.py new file mode 100644 index 00000000..8fe40cc7 --- /dev/null +++ b/backend/test/unit/services/test_tool_service.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from types import SimpleNamespace + +from yuxi.services import tool_service + + +def test_get_tool_metadata_includes_config_guide(monkeypatch): + tool_service._metadata_cache.clear() + + fake_tool = SimpleNamespace( + name="demo_tool", + description="demo description", + metadata={}, + args_schema=None, + ) + fake_extra = SimpleNamespace( + category="buildin", + tags=["demo"], + display_name="演示工具", + config_guide="请先配置 DEMO_API_KEY", + ) + + monkeypatch.setattr( + "yuxi.agents.toolkits.registry.get_all_tool_instances", + lambda: [fake_tool], + ) + monkeypatch.setattr( + "yuxi.agents.toolkits.registry.get_all_extra_metadata", + lambda: {"demo_tool": fake_extra}, + ) + + result = tool_service.get_tool_metadata() + + assert result == [ + { + "id": "demo_tool", + "name": "演示工具", + "description": "demo description", + "metadata": {}, + "args": [], + "category": "buildin", + "tags": ["demo"], + "config_guide": "请先配置 DEMO_API_KEY", + } + ] + + tool_service._metadata_cache.clear() diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index ae23faa4..1d1c1857 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -48,6 +48,7 @@ - 重构内置 Skills/MCP/Subagents 安装/添加/移除机制:内置 skill 支持按需安装、基于 `version + content_hash` 的更新提示与覆盖确认,不再使用服务器级开关切换 - 新增知识库 PDF、图片的预览功能 - 重构后端测试目录结构:按 `unit / integration / e2e` 分层迁移现有测试,拆分全局 `conftest.py`,统一测试入口为 `uv run --group test pytest`,并新增独立测试规范文档 `docs/vibe/testing-guidelines.md` +- 新增工具元数据 `config_guide` 字段:后端工具列表接口现在可返回“给人看的配置说明”,前端工具详情页会展示该说明,用于提示工具使用前需要配置的环境变量或入口;首批为 MySQL 工具和 `Qwen-Image` 补充了配置指引 diff --git a/web/src/components/ToolsManagerComponent.vue b/web/src/components/ToolsManagerComponent.vue index b9e73da9..24db394e 100644 --- a/web/src/components/ToolsManagerComponent.vue +++ b/web/src/components/ToolsManagerComponent.vue @@ -101,6 +101,16 @@ +
+
+ + 配置说明 +
+
+ {{ currentTool.config_guide }} +
+
+
@@ -190,7 +200,8 @@ const filteredTools = computed(() => { (t) => t.name.toLowerCase().includes(q) || t.id.toLowerCase().includes(q) || - t.description?.toLowerCase().includes(q) + t.description?.toLowerCase().includes(q) || + t.config_guide?.toLowerCase().includes(q) ) } return result @@ -261,4 +272,8 @@ defineExpose({ font-size: 12px; } } + +.config-guide { + white-space: pre-line; +}