diff --git a/backend/app/core/tools_bootstrap.py b/backend/app/core/tools_bootstrap.py index f4a6de1..481a5cd 100644 --- a/backend/app/core/tools_bootstrap.py +++ b/backend/app/core/tools_bootstrap.py @@ -8,11 +8,11 @@ logger = logging.getLogger(__name__) _registered = False -_EXPECTED_BUILTIN = 19 +_EXPECTED_BUILTIN = 31 def ensure_builtin_tools_registered() -> None: - """幂等:注册 file_write / system_info 等内置工具,供工作流 LLM 节点使用。""" + """幂等:注册所有内置工具,供工作流 LLM 节点使用。""" global _registered if _registered: return @@ -37,6 +37,18 @@ def ensure_builtin_tools_registered() -> None: url_parse_tool, regex_test_tool, agent_call_tool, + code_execute_tool, + git_operation_tool, + web_search_tool, + pdf_generate_tool, + project_scaffold_tool, + task_plan_tool, + excel_process_tool, + browser_use_tool, + docker_manage_tool, + deploy_push_tool, + agent_create_tool, + tool_register_tool, HTTP_REQUEST_SCHEMA, FILE_READ_SCHEMA, FILE_WRITE_SCHEMA, @@ -56,6 +68,18 @@ def ensure_builtin_tools_registered() -> None: URL_PARSE_SCHEMA, REGEX_TEST_SCHEMA, AGENT_CALL_SCHEMA, + CODE_EXECUTE_SCHEMA, + GIT_OPERATION_SCHEMA, + WEB_SEARCH_SCHEMA, + PDF_GENERATE_SCHEMA, + PROJECT_SCAFFOLD_SCHEMA, + TASK_PLAN_SCHEMA, + EXCEL_PROCESS_SCHEMA, + BROWSER_USE_SCHEMA, + DOCKER_MANAGE_SCHEMA, + DEPLOY_PUSH_SCHEMA, + AGENT_CREATE_SCHEMA, + TOOL_REGISTER_SCHEMA, ) tool_registry.register_builtin_tool("http_request", http_request_tool, HTTP_REQUEST_SCHEMA) @@ -77,6 +101,18 @@ def ensure_builtin_tools_registered() -> None: tool_registry.register_builtin_tool("url_parse", url_parse_tool, URL_PARSE_SCHEMA) tool_registry.register_builtin_tool("regex_test", regex_test_tool, REGEX_TEST_SCHEMA) tool_registry.register_builtin_tool("agent_call", agent_call_tool, AGENT_CALL_SCHEMA) + tool_registry.register_builtin_tool("code_execute", code_execute_tool, CODE_EXECUTE_SCHEMA) + tool_registry.register_builtin_tool("git_operation", git_operation_tool, GIT_OPERATION_SCHEMA) + tool_registry.register_builtin_tool("web_search", web_search_tool, WEB_SEARCH_SCHEMA) + tool_registry.register_builtin_tool("pdf_generate", pdf_generate_tool, PDF_GENERATE_SCHEMA) + tool_registry.register_builtin_tool("project_scaffold", project_scaffold_tool, PROJECT_SCAFFOLD_SCHEMA) + tool_registry.register_builtin_tool("task_plan", task_plan_tool, TASK_PLAN_SCHEMA) + tool_registry.register_builtin_tool("excel_process", excel_process_tool, EXCEL_PROCESS_SCHEMA) + tool_registry.register_builtin_tool("browser_use", browser_use_tool, BROWSER_USE_SCHEMA) + tool_registry.register_builtin_tool("docker_manage", docker_manage_tool, DOCKER_MANAGE_SCHEMA) + tool_registry.register_builtin_tool("deploy_push", deploy_push_tool, DEPLOY_PUSH_SCHEMA) + tool_registry.register_builtin_tool("agent_create", agent_create_tool, AGENT_CREATE_SCHEMA) + tool_registry.register_builtin_tool("tool_register", tool_register_tool, TOOL_REGISTER_SCHEMA) _registered = True n = tool_registry.builtin_tool_count() diff --git a/backend/app/services/builtin_tools.py b/backend/app/services/builtin_tools.py index d4e504b..1a86501 100644 --- a/backend/app/services/builtin_tools.py +++ b/backend/app/services/builtin_tools.py @@ -2315,3 +2315,1327 @@ AGENT_CALL_SCHEMA = { }, }, } + + +# ── code_execute ───────────────────────────────────────────── + +CODE_EXECUTE_TIMEOUT = 30 +CODE_EXECUTE_MAX_OUTPUT = 8000 + +_CODE_SAFE_BUILTINS = { + "abs": abs, "all": all, "any": any, "ascii": ascii, + "bin": bin, "bool": bool, "bytearray": bytearray, "bytes": bytes, + "chr": chr, "complex": complex, "dict": dict, "divmod": divmod, + "enumerate": enumerate, "filter": filter, "float": float, "format": format, + "frozenset": frozenset, "getattr": getattr, "hasattr": hasattr, + "hash": hash, "hex": hex, "id": id, "int": int, "isinstance": isinstance, + "issubclass": issubclass, "iter": iter, "len": len, "list": list, + "map": map, "max": max, "min": min, "next": next, "object": object, + "oct": oct, "ord": ord, "pow": pow, "print": print, "range": range, + "repr": repr, "reversed": reversed, "round": round, "set": set, + "slice": slice, "sorted": sorted, "str": str, "sum": sum, "tuple": tuple, + "type": type, "zip": zip, +} + + +async def code_execute_tool( + code: str, + language: str = "python", + timeout: int = CODE_EXECUTE_TIMEOUT, +) -> str: + """沙箱执行 Python/JS 代码并返回 stdout/stderr。""" + import asyncio as _asyncio + import tempfile + + lang = language.lower().strip() + if lang not in ("python", "py", "javascript", "js"): + return json.dumps({"error": f"不支持的语言: {language},仅支持 python / javascript"}, ensure_ascii=False) + + if lang in ("javascript", "js"): + # JS 用 node -e 执行 + try: + proc = await _asyncio.create_subprocess_exec( + "node", "-e", code, + stdout=_asyncio.subprocess.PIPE, + stderr=_asyncio.subprocess.PIPE, + ) + stdout, stderr = await _asyncio.wait_for( + proc.communicate(), timeout=timeout, + ) + return json.dumps({ + "stdout": stdout.decode("utf-8", errors="replace")[:CODE_EXECUTE_MAX_OUTPUT], + "stderr": stderr.decode("utf-8", errors="replace")[:CODE_EXECUTE_MAX_OUTPUT], + "returncode": proc.returncode, + }, ensure_ascii=False) + except FileNotFoundError: + return json.dumps({"error": "Node.js 未安装,无法执行 JavaScript"}, ensure_ascii=False) + except _asyncio.TimeoutError: + return json.dumps({"error": f"执行超时 ({timeout}s)"}, ensure_ascii=False) + + # Python: 写入临时文件再 exec,保证安全 + with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as f: + f.write(code) + tmp_path = f.name + + try: + proc = await _asyncio.create_subprocess_exec( + sys.executable, tmp_path, + stdout=_asyncio.subprocess.PIPE, + stderr=_asyncio.subprocess.PIPE, + ) + stdout, stderr = await _asyncio.wait_for( + proc.communicate(), timeout=timeout, + ) + return json.dumps({ + "stdout": stdout.decode("utf-8", errors="replace")[:CODE_EXECUTE_MAX_OUTPUT], + "stderr": stderr.decode("utf-8", errors="replace")[:CODE_EXECUTE_MAX_OUTPUT], + "returncode": proc.returncode, + }, ensure_ascii=False) + except _asyncio.TimeoutError: + return json.dumps({"error": f"执行超时 ({timeout}s)"}, ensure_ascii=False) + finally: + try: + os.unlink(tmp_path) + except OSError: + pass + + +CODE_EXECUTE_SCHEMA = { + "type": "function", + "function": { + "name": "code_execute", + "description": "在沙箱中执行 Python 或 JavaScript 代码,返回 stdout/stderr。适合编写脚本验证逻辑、数据处理、自动化任务。超时默认 30s。", + "parameters": { + "type": "object", + "properties": { + "code": {"type": "string", "description": "要执行的代码"}, + "language": {"type": "string", "enum": ["python", "javascript"], "description": "语言(默认 python)", "default": "python"}, + "timeout": {"type": "integer", "description": "超时秒数(默认 30)", "default": 30}, + }, + "required": ["code"], + }, + }, +} + + +# ── git_operation ──────────────────────────────────────────── + +_GIT_ALLOWED_COMMANDS = { + "log": ["git", "log", "--oneline", "--max-count=50"], + "diff": ["git", "diff"], + "diff_staged": ["git", "diff", "--staged"], + "status": ["git", "status", "--short"], + "branch": ["git", "branch", "-a"], + "blame": ["git", "blame", "--date=short"], + "show": ["git", "show", "--stat"], + "tag": ["git", "tag", "-l"], + "remote": ["git", "remote", "-v"], + "rev_parse": ["git", "rev-parse", "--abbrev-ref", "HEAD"], +} + + +async def git_operation_tool(operation: str, file_path: str = "", revision: str = "HEAD") -> str: + """执行 Git 只读操作(log/diff/status/blame/show/branch/tag/remote)。""" + import asyncio as _asyncio + + op = operation.lower().strip() + if op not in _GIT_ALLOWED_COMMANDS: + return json.dumps({ + "error": f"不支持的 git 操作: {operation}", + "allowed": sorted(_GIT_ALLOWED_COMMANDS.keys()), + }, ensure_ascii=False) + + base_cmd = list(_GIT_ALLOWED_COMMANDS[op]) + + # 需要文件路径的操作 + if op in ("diff", "blame", "log") and file_path: + if op == "blame": + base_cmd.append(file_path) + elif op == "diff": + base_cmd.extend(["--", file_path]) + elif op == "log": + base_cmd.extend(["--", file_path]) + if op in ("show",) and revision: + base_cmd[-1] = revision + + try: + proc = await _asyncio.create_subprocess_exec( + *base_cmd, + stdout=_asyncio.subprocess.PIPE, + stderr=_asyncio.subprocess.PIPE, + ) + stdout, stderr = await _asyncio.wait_for(proc.communicate(), timeout=15) + return json.dumps({ + "stdout": stdout.decode("utf-8", errors="replace")[:8000], + "stderr": stderr.decode("utf-8", errors="replace")[:2000], + "returncode": proc.returncode, + }, ensure_ascii=False) + except FileNotFoundError: + return json.dumps({"error": "Git 未安装或不在 PATH 中"}, ensure_ascii=False) + except _asyncio.TimeoutError: + return json.dumps({"error": "Git 操作超时"}, ensure_ascii=False) + + +GIT_OPERATION_SCHEMA = { + "type": "function", + "function": { + "name": "git_operation", + "description": "执行 Git 只读操作:log(提交历史)、diff(差异)、status(状态)、blame(谁改了哪行)、branch(分支列表)、show(提交详情)、tag(标签)、remote(远程仓库)。所有操作均为只读,不会修改仓库。", + "parameters": { + "type": "object", + "properties": { + "operation": { + "type": "string", + "enum": sorted(_GIT_ALLOWED_COMMANDS.keys()), + "description": "Git 操作名称", + }, + "file_path": {"type": "string", "description": "文件路径(diff/blame/log 时可选)"}, + "revision": {"type": "string", "description": "提交哈希或分支名(show 时使用)", "default": "HEAD"}, + }, + "required": ["operation"], + }, + }, +} + + +# ── web_search ─────────────────────────────────────────────── + +async def web_search_tool(query: str, max_results: int = 8) -> str: + """搜索网页(使用 DuckDuckGo HTML 搜索,无需 API Key)。""" + import asyncio as _asyncio + + try: + url = f"https://html.duckduckgo.com/html/?q={httpx.URL(query).raw_path.decode() if hasattr(httpx.URL(query), 'raw_path') else query}" + # 简单 URL 编码 + import urllib.parse + safe_q = urllib.parse.quote(query, safe="") + url = f"https://html.duckduckgo.com/html/?q={safe_q}" + + async with httpx.AsyncClient(timeout=15.0, follow_redirects=True) as client: + resp = await client.get(url, headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + }) + if resp.status_code != 200: + return json.dumps({"error": f"搜索失败 HTTP {resp.status_code}"}, ensure_ascii=False) + + html = resp.text + + # 简单解析搜索结果 + results = [] + # 匹配每个结果块:标题、摘要、链接 + import re as _re + # DuckDuckGo HTML 结果模式 + blocks = _re.findall( + r']*class="result__a"[^>]*href="([^"]*)"[^>]*>(.*?).*?]*class="result__snippet"[^>]*>(.*?)', + html, _re.DOTALL | _re.IGNORECASE, + ) + if not blocks: + # 备选模式 + blocks = _re.findall( + r']*rel="nofollow"[^>]*class="result__url"[^>]*href="([^"]*)"[^>]*>.*?]*class="result__a"[^>]*>(.*?).*?]*class="result__snippet"[^>]*>(.*?)', + html, _re.DOTALL | _re.IGNORECASE, + ) + + for i, (link, title, snippet) in enumerate(blocks[:max_results]): + title_clean = _re.sub(r"<.*?>", "", title).strip() + snippet_clean = _re.sub(r"<.*?>", "", snippet).strip() + results.append({ + "index": i + 1, + "title": title_clean, + "url": link, + "snippet": snippet_clean[:300], + }) + + if not results: + return json.dumps({"message": "未找到搜索结果,请尝试更具体的关键词", "results": []}, ensure_ascii=False) + + return json.dumps({"results": results, "count": len(results)}, ensure_ascii=False) + + except Exception as e: + logger.error(f"web_search 失败: {e}") + return json.dumps({"error": f"搜索失败: {e}"}, ensure_ascii=False) + + +WEB_SEARCH_SCHEMA = { + "type": "function", + "function": { + "name": "web_search", + "description": "搜索互联网获取网页信息,返回标题、摘要和 URL 列表。适合查找最新文档、技术方案、新闻资讯。", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "搜索关键词"}, + "max_results": {"type": "integer", "description": "最大结果数(默认 8)", "default": 8}, + }, + "required": ["query"], + }, + }, +} + + +# ── pdf_generate ───────────────────────────────────────────── + +async def pdf_generate_tool(markdown: str, output_path: str = "", title: str = "") -> str: + """将 Markdown 内容生成 PDF 文件。优先用 weasyprint,否则生成 HTML。""" + import tempfile + import asyncio as _asyncio + + full_path = output_path or os.path.join( + _local_file_workspace_root(), f"report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" + ) + if not os.path.isabs(full_path): + full_path = os.path.join(_local_file_workspace_root(), full_path) + + # 简单的 Markdown → HTML 转换(不使用外部库) + html_title = title or "报告" + html_body = _simple_md_to_html(markdown) + html_content = f""" + +
')
+ in_code_block = True
+ continue
+ if in_code_block:
+ out.append(line)
+ continue
+
+ # 表格
+ if "|" in line and line.strip().startswith("|"):
+ cells = [c.strip() for c in line.strip().strip("|").split("|")]
+ if not in_table:
+ in_table = True
+ # 判断是否分隔行
+ if all(set(c) <= {"-", ":", " "} for c in cells):
+ continue
+ out.append("")
+ table_rows.append(cells)
+ continue
+ elif in_table:
+ # 结束表格
+ if table_rows:
+ out.append("" + "".join(f"{c} " for c in table_rows[0]) + " ")
+ for row in table_rows[1:]:
+ out.append("" + "".join(f"{c} " for c in row) + " ")
+ out.append("
")
+ in_table = False
+ table_rows = []
+
+ # 标题
+ if line.startswith("# "):
+ out.append(f"{line[2:]}
")
+ elif line.startswith("## "):
+ out.append(f"{line[3:]}
")
+ elif line.startswith("### "):
+ out.append(f"{line[4:]}
")
+ elif line.startswith("#### "):
+ out.append(f"{line[5:]}
")
+ # 无序列表
+ elif line.strip().startswith("- "):
+ out.append(f"{_inline_md(line.strip()[2:])} ")
+ elif line.strip().startswith("* "):
+ out.append(f"{_inline_md(line.strip()[2:])} ")
+ # 引用
+ elif line.startswith("> "):
+ out.append(f"{_inline_md(line[2:])}
")
+ # 分隔线
+ elif line.strip() in ("---", "***"):
+ out.append("
")
+ # 空行
+ elif not line.strip():
+ out.append("
")
+ # 普通段落
+ else:
+ out.append(f"{_inline_md(line)}
")
+
+ if in_table:
+ if table_rows:
+ out.append("" + "".join(f"{c} " for c in table_rows[0]) + " ")
+ for row in table_rows[1:]:
+ out.append("" + "".join(f"{c} " for c in row) + " ")
+ out.append("")
+
+ return "\n".join(out)
+
+
+def _inline_md(text: str) -> str:
+ """处理行内 Markdown:粗体/斜体/代码/链接/图片。"""
+ import re as _re
+ text = _re.sub(r"\*\*(.+?)\*\*", r"\1", text)
+ text = _re.sub(r"\*(.+?)\*", r"\1", text)
+ text = _re.sub(r"`(.+?)`", r"\1", text)
+ text = _re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", r'
', text)
+ text = _re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'\1', text)
+ return text
+
+
+PDF_GENERATE_SCHEMA = {
+ "type": "function",
+ "function": {
+ "name": "pdf_generate",
+ "description": "将 Markdown 内容生成 PDF 报告(需 weasyprint)或降级为 HTML 文件。适合生成项目文档、分析报告、周报等。",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "markdown": {"type": "string", "description": "Markdown 格式的报告内容"},
+ "output_path": {"type": "string", "description": "输出文件路径(可选,默认自动生成)"},
+ "title": {"type": "string", "description": "报告标题(可选)"},
+ },
+ "required": ["markdown"],
+ },
+ },
+}
+
+
+# ── project_scaffold ─────────────────────────────────────────
+
+_PROJECT_TEMPLATES = {
+ "fastapi": {
+ "description": "FastAPI 后端项目模板",
+ "files": {
+ "main.py": "from fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get('/')\ndef root():\n return {'message': 'Hello'}\n",
+ "requirements.txt": "fastapi\nuvicorn\n",
+ "Dockerfile": "FROM python:3.12-slim\nWORKDIR /app\nCOPY . .\nRUN pip install -r requirements.txt\nCMD [\"uvicorn\", \"main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n",
+ },
+ },
+ "vue": {
+ "description": "Vue 3 + Vite 前端项目",
+ "files": {
+ "src/App.vue": "\n \n {{ title }}
\n \n\n\n\n",
+ "src/main.js": "import { createApp } from 'vue'\nimport App from './App.vue'\n\ncreateApp(App).mount('#app')\n",
+ "index.html": "\n\nVue App \n\n \n \n\n\n",
+ "package.json": '{"name":"my-app","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"vue":"^3.4"},"devDependencies":{"vite":"^5","@vitejs/plugin-vue":"^5"}}\n',
+ "vite.config.js": "import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\n\nexport default defineConfig({\n plugins: [vue()],\n})\n",
+ },
+ },
+ "react": {
+ "description": "React + Vite 项目模板",
+ "files": {
+ "src/App.jsx": "export default function App() {\n return React App
\n}\n",
+ "src/main.jsx": "import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App'\n\nReactDOM.createRoot(document.getElementById('root')).render( )\n",
+ "index.html": "\n\nReact App \n\n \n \n\n\n",
+ "package.json": '{"name":"my-app","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^18","react-dom":"^18"},"devDependencies":{"vite":"^5","@vitejs/plugin-react":"^4"}}\n',
+ "vite.config.js": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\nexport default defineConfig({\n plugins: [react()],\n})\n",
+ },
+ },
+ "python_cli": {
+ "description": "Python CLI 项目模板",
+ "files": {
+ "main.py": "#!/usr/bin/env python3\n\"\"\"CLI 工具描述\"\"\"\nimport argparse\n\ndef main():\n parser = argparse.ArgumentParser()\n parser.add_argument('input', help='输入文件')\n args = parser.parse_args()\n print(f'Processing: {args.input}')\n\nif __name__ == '__main__':\n main()\n",
+ "requirements.txt": "",
+ },
+ },
+ "shell": {
+ "description": "Shell 脚本项目模板",
+ "files": {
+ "run.sh": "#!/bin/bash\nset -euo pipefail\n\necho \"Script started\"\n",
+ },
+ },
+}
+
+
+async def project_scaffold_tool(template: str, project_name: str, target_dir: str = "") -> str:
+ """生成项目脚手架。"""
+ tmpl = template.lower().strip()
+ if tmpl not in _PROJECT_TEMPLATES:
+ return json.dumps({
+ "error": f"未知模板: {template}",
+ "available": sorted(_PROJECT_TEMPLATES.keys()),
+ "description": {k: v["description"] for k, v in _PROJECT_TEMPLATES.items()},
+ }, ensure_ascii=False)
+
+ root = _local_file_workspace_root()
+ base = os.path.join(target_dir, project_name) if target_dir else os.path.join(root, project_name)
+ base = os.path.abspath(base)
+
+ if os.path.exists(base):
+ return json.dumps({"error": f"目录已存在: {base}"}, ensure_ascii=False)
+
+ tpl = _PROJECT_TEMPLATES[tmpl]
+ created = []
+ for rel_path, content in tpl["files"].items():
+ full = os.path.join(base, rel_path)
+ os.makedirs(os.path.dirname(full), exist_ok=True)
+ with open(full, "w", encoding="utf-8") as f:
+ f.write(content)
+ created.append(rel_path)
+
+ return json.dumps({
+ "project_dir": base,
+ "template": tmpl,
+ "description": tpl["description"],
+ "files_created": created,
+ "count": len(created),
+ }, ensure_ascii=False)
+
+
+PROJECT_SCAFFOLD_SCHEMA = {
+ "type": "function",
+ "function": {
+ "name": "project_scaffold",
+ "description": "按模板生成项目目录结构。支持 fastapi、vue、react、python_cli、shell。适合快速搭项目骨架。",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "template": {
+ "type": "string",
+ "enum": sorted(_PROJECT_TEMPLATES.keys()),
+ "description": "项目模板名称",
+ },
+ "project_name": {"type": "string", "description": "项目名称(将作为目录名)"},
+ "target_dir": {"type": "string", "description": "目标父目录(可选,默认工作区根目录)"},
+ },
+ "required": ["template", "project_name"],
+ },
+ },
+}
+
+
+# ── task_plan ────────────────────────────────────────────────
+
+_TASK_PLAN_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "data", "task_plans")
+
+
+async def task_plan_tool(action: str, plan_id: str = "", title: str = "", steps_json: str = "", step_index: int = -1, status: str = "") -> str:
+ """任务规划:创建/查看/更新/完成/删除 任务计划。"""
+ os.makedirs(_TASK_PLAN_DIR, exist_ok=True)
+
+ action = action.lower().strip()
+
+ if action == "list":
+ plans = []
+ for fname in sorted(os.listdir(_TASK_PLAN_DIR)):
+ if fname.endswith(".json"):
+ fpath = os.path.join(_TASK_PLAN_DIR, fname)
+ try:
+ with open(fpath, "r", encoding="utf-8") as f:
+ p = json.load(f)
+ plans.append({
+ "id": p.get("id", fname.replace(".json", "")),
+ "title": p.get("title", ""),
+ "total_steps": len(p.get("steps", [])),
+ "completed": sum(1 for s in p.get("steps", []) if s.get("status") == "done"),
+ "created_at": p.get("created_at", ""),
+ })
+ except Exception:
+ pass
+ return json.dumps({"plans": plans, "count": len(plans)}, ensure_ascii=False)
+
+ if action == "create":
+ if not title or not steps_json:
+ return json.dumps({"error": "create 操作需要 title 和 steps_json 参数"}, ensure_ascii=False)
+ try:
+ steps = json.loads(steps_json)
+ except json.JSONDecodeError:
+ return json.dumps({"error": "steps_json 不是有效 JSON"}, ensure_ascii=False)
+ plan_id = plan_id or f"plan_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+ plan = {
+ "id": plan_id,
+ "title": title,
+ "steps": [{"index": i, "title": s, "status": "pending", "note": ""} for i, s in enumerate(steps)],
+ "created_at": datetime.now().isoformat(),
+ "updated_at": datetime.now().isoformat(),
+ }
+ fpath = os.path.join(_TASK_PLAN_DIR, f"{plan_id}.json")
+ with open(fpath, "w", encoding="utf-8") as f:
+ json.dump(plan, f, ensure_ascii=False, indent=2)
+ return json.dumps({"plan": plan, "file": fpath}, ensure_ascii=False)
+
+ if not plan_id:
+ return json.dumps({"error": "需要 plan_id 参数"}, ensure_ascii=False)
+
+ fpath = os.path.join(_TASK_PLAN_DIR, f"{plan_id}.json")
+ if not os.path.exists(fpath):
+ return json.dumps({"error": f"任务计划不存在: {plan_id}"}, ensure_ascii=False)
+
+ with open(fpath, "r", encoding="utf-8") as f:
+ plan = json.load(f)
+
+ if action == "view":
+ return json.dumps(plan, ensure_ascii=False)
+
+ if action == "update_step":
+ if step_index < 0 or status not in ("pending", "in_progress", "done", "blocked"):
+ return json.dumps({"error": "需要有效的 step_index 和 status (pending/in_progress/done/blocked)"}, ensure_ascii=False)
+ for s in plan["steps"]:
+ if s["index"] == step_index:
+ s["status"] = status
+ plan["updated_at"] = datetime.now().isoformat()
+ break
+ with open(fpath, "w", encoding="utf-8") as f:
+ json.dump(plan, f, ensure_ascii=False, indent=2)
+ return json.dumps({"plan": plan, "updated_step": step_index, "new_status": status}, ensure_ascii=False)
+
+ if action == "delete":
+ os.remove(fpath)
+ return json.dumps({"deleted": plan_id}, ensure_ascii=False)
+
+ return json.dumps({"error": f"不支持的操作: {action},支持 create/list/view/update_step/delete"}, ensure_ascii=False)
+
+
+TASK_PLAN_SCHEMA = {
+ "type": "function",
+ "function": {
+ "name": "task_plan",
+ "description": "管理复杂任务的分解和进度跟踪。可创建计划、查看进度、更新步骤状态。适合多步骤项目(如「开发用户系统」拆成 7 步逐一完成)。",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "enum": ["create", "list", "view", "update_step", "delete"],
+ "description": "操作:create=创建计划, list=列出所有, view=查看详情, update_step=更新步骤状态, delete=删除",
+ },
+ "plan_id": {"type": "string", "description": "计划 ID(create 可选,其他必填)"},
+ "title": {"type": "string", "description": "计划标题(create 时必填)"},
+ "steps_json": {"type": "string", "description": "步骤列表 JSON 字符串,如 [\"分析需求\",\"设计架构\",\"编码实现\",\"测试验证\"](create 时必填)"},
+ "step_index": {"type": "integer", "description": "步骤序号(update_step 时必填,从 0 开始)"},
+ "status": {"type": "string", "enum": ["pending", "in_progress", "done", "blocked"], "description": "步骤新状态"},
+ },
+ "required": ["action"],
+ },
+ },
+}
+
+
+# ── excel_process ─────────────────────────────────────────────
+
+async def excel_process_tool(
+ file_path: str,
+ action: str = "read",
+ sheet_name: str = "",
+ data_json: str = "",
+ chart_config_json: str = "",
+) -> str:
+ """高级 Excel 处理:读写、创建图表、数据透视。"""
+ if not file_path:
+ return json.dumps({"error": "缺少 file_path"}, ensure_ascii=False)
+
+ if not os.path.isabs(file_path):
+ file_path = os.path.join(_local_file_workspace_root(), file_path)
+
+ try:
+ import openpyxl
+ from openpyxl.chart import BarChart, LineChart, PieChart, Reference
+ from openpyxl.utils import get_column_letter
+ except ImportError:
+ return json.dumps({"error": "openpyxl 未安装,请执行 pip install openpyxl"}, ensure_ascii=False)
+
+ action = action.lower().strip()
+
+ if action == "read":
+ try:
+ wb = openpyxl.load_workbook(file_path, data_only=True)
+ except FileNotFoundError:
+ return json.dumps({"error": f"文件不存在: {file_path}"}, ensure_ascii=False)
+
+ result = {"sheets": {}}
+ sheets = [sheet_name] if sheet_name else wb.sheetnames
+ for sn in sheets:
+ ws = wb[sn]
+ rows = []
+ for row in ws.iter_rows(min_row=1, max_row=min(ws.max_row, 200), max_col=min(ws.max_column, 50), values_only=True):
+ rows.append(list(row))
+ result["sheets"][sn] = {
+ "rows": rows,
+ "row_count": len(rows),
+ "col_count": len(rows[0]) if rows else 0,
+ }
+ wb.close()
+ return json.dumps(result, ensure_ascii=False, default=str)
+
+ if action == "write":
+ try:
+ data = json.loads(data_json)
+ except json.JSONDecodeError:
+ return json.dumps({"error": "data_json 不是有效 JSON"}, ensure_ascii=False)
+
+ wb = openpyxl.Workbook()
+ for sn, rows in data.items() if isinstance(data, dict) else {"Sheet": data}.items():
+ if sn != list((data if isinstance(data, dict) else {"Sheet": data}).keys())[0]:
+ wb.create_sheet(sn)
+ ws = wb[sn]
+ for row_idx, row in enumerate(rows, 1):
+ for col_idx, val in enumerate(row, 1):
+ ws.cell(row=row_idx, column=col_idx, value=val)
+
+ wb.save(file_path)
+ wb.close()
+ return json.dumps({"file": file_path, "message": "写入成功"}, ensure_ascii=False)
+
+ if action == "chart":
+ try:
+ cfg = json.loads(chart_config_json) if chart_config_json else {}
+ except json.JSONDecodeError:
+ return json.dumps({"error": "chart_config_json 不是有效 JSON"}, ensure_ascii=False)
+
+ wb = openpyxl.load_workbook(file_path)
+ ws = wb[sheet_name] if sheet_name else wb.active
+
+ chart_type = cfg.get("type", "bar")
+ if chart_type == "bar":
+ chart = BarChart()
+ elif chart_type == "line":
+ chart = LineChart()
+ elif chart_type == "pie":
+ chart = PieChart()
+ else:
+ wb.close()
+ return json.dumps({"error": f"不支持的图表类型: {chart_type}"}, ensure_ascii=False)
+
+ chart.title = cfg.get("title", "Chart")
+ data = Reference(ws, min_col=cfg.get("data_min_col", 1), min_row=cfg.get("data_min_row", 1),
+ max_col=cfg.get("data_max_col", 2), max_row=cfg.get("data_max_row", ws.max_row))
+ chart.add_data(data, titles_from_data=True)
+
+ if "categories_min_col" in cfg:
+ cats = Reference(ws, min_col=cfg["categories_min_col"], min_row=cfg.get("data_min_row", 2),
+ max_row=cfg.get("data_max_row", ws.max_row))
+ chart.set_categories(cats)
+
+ chart_row = cfg.get("chart_row", ws.max_row + 3)
+ ws.add_chart(chart, f"A{chart_row}")
+
+ wb.save(file_path)
+ wb.close()
+ return json.dumps({"file": file_path, "chart_type": chart_type, "position": f"A{chart_row}", "message": "图表已添加"}, ensure_ascii=False)
+
+ return json.dumps({"error": f"不支持的操作: {action},支持 read/write/chart"}, ensure_ascii=False)
+
+
+EXCEL_PROCESS_SCHEMA = {
+ "type": "function",
+ "function": {
+ "name": "excel_process",
+ "description": "高级 Excel 处理:读写数据、添加图表(柱状图/折线图/饼图)。需要 openpyxl。适合数据分析、报表生成。",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "file_path": {"type": "string", "description": "Excel 文件路径"},
+ "action": {
+ "type": "string",
+ "enum": ["read", "write", "chart"],
+ "description": "操作:read=读取, write=写入, chart=添加图表",
+ },
+ "sheet_name": {"type": "string", "description": "工作表名称"},
+ "data_json": {"type": "string", "description": "写入数据 JSON(write 时,格式 {\"Sheet1\": [[1,2],[3,4]]})"},
+ "chart_config_json": {"type": "string", "description": "图表配置 JSON(chart 时,如 {\"type\":\"bar\",\"title\":\"销售额\"})"},
+ },
+ "required": ["file_path", "action"],
+ },
+ },
+}
+
+
+# ── browser_use ──────────────────────────────────────────────
+
+async def browser_use_tool(
+ url: str,
+ action: str = "screenshot",
+ selector: str = "",
+ script: str = "",
+ wait_ms: int = 2000,
+) -> str:
+ """无头浏览器操控。需要 playwright(pip install playwright && playwright install chromium)。"""
+ import asyncio as _asyncio
+
+ try:
+ from playwright.async_api import async_playwright
+ except ImportError:
+ return json.dumps({
+ "error": "playwright 未安装",
+ "help": "安装方法: pip install playwright && playwright install chromium",
+ }, ensure_ascii=False)
+
+ action = action.lower().strip()
+ result = {}
+
+ async with async_playwright() as p:
+ try:
+ browser = await p.chromium.launch(headless=True)
+ except Exception:
+ return json.dumps({
+ "error": "无法启动 Chromium",
+ "help": "请运行: playwright install chromium",
+ }, ensure_ascii=False)
+
+ page = await browser.new_page()
+ try:
+ await page.goto(url, wait_until="networkidle", timeout=30000)
+ await _asyncio.sleep(wait_ms / 1000.0)
+
+ if action == "screenshot":
+ import tempfile
+ ss_path = os.path.join(
+ tempfile.gettempdir(),
+ f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png",
+ )
+ await page.screenshot(path=ss_path, full_page=True)
+ result["screenshot"] = ss_path
+
+ elif action == "content":
+ html = await page.content()
+ # 提取文本
+ text = await page.evaluate("() => document.body.innerText")
+ result["text"] = text[:8000]
+ result["text_length"] = len(text)
+
+ elif action == "click" and selector:
+ await page.click(selector)
+ result["clicked"] = selector
+
+ elif action == "fill" and selector and script:
+ await page.fill(selector, script)
+ result["filled"] = selector
+
+ elif action == "evaluate" and script:
+ val = await page.evaluate(script)
+ result["evaluate_result"] = str(val)[:4000]
+
+ else:
+ result["error"] = f"不支持的操作: {action},支持 screenshot/content/click/fill/evaluate"
+
+ except Exception as e:
+ result["error"] = str(e)
+ finally:
+ await browser.close()
+
+ return json.dumps(result, ensure_ascii=False)
+
+
+BROWSER_USE_SCHEMA = {
+ "type": "function",
+ "function": {
+ "name": "browser_use",
+ "description": "无头浏览器操控:截图、提取内容、点击、填表、执行 JS。需要 playwright。适合网页测试、动态内容抓取。",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "url": {"type": "string", "description": "目标 URL"},
+ "action": {
+ "type": "string",
+ "enum": ["screenshot", "content", "click", "fill", "evaluate"],
+ "description": "操作类型",
+ },
+ "selector": {"type": "string", "description": "CSS 选择器(click/fill 时必填)"},
+ "script": {"type": "string", "description": "要填入的文本或要执行的 JS 代码"},
+ "wait_ms": {"type": "integer", "description": "页面加载后等待毫秒数(默认 2000)", "default": 2000},
+ },
+ "required": ["url", "action"],
+ },
+ },
+}
+
+
+# ── docker_manage ────────────────────────────────────────────
+
+_DOCKER_ALLOWED_COMMANDS = [
+ "docker", "ps", "images", "logs", "stats", "inspect", "version", "info",
+ "compose", "ps", "logs", "config", "top",
+]
+
+
+async def docker_manage_tool(operation: str, resource: str = "", options: str = "") -> str:
+ """Docker 管理操作(只读为主)。"""
+ import asyncio as _asyncio
+
+ op_parts = operation.strip().split()
+ # 检查命令是否安全
+ if not op_parts:
+ return json.dumps({"error": "缺少操作参数"}, ensure_ascii=False)
+
+ base = op_parts[0]
+ allowed_prefixes = {
+ "ps": ["docker", "ps"],
+ "images": ["docker", "images"],
+ "logs": ["docker", "logs"],
+ "stats": ["docker", "stats"],
+ "inspect": ["docker", "inspect"],
+ "version": ["docker", "version"],
+ "info": ["docker", "info"],
+ "compose": ["docker", "compose"],
+ }
+
+ if base not in allowed_prefixes:
+ return json.dumps({
+ "error": f"不支持的操作: {base}",
+ "allowed": sorted(allowed_prefixes.keys()),
+ }, ensure_ascii=False)
+
+ cmd = list(allowed_prefixes[base])
+ if resource:
+ cmd.append(resource)
+ # 解析额外选项(从 options 字符串拆分)
+ if options:
+ import shlex
+ try:
+ cmd.extend(shlex.split(options))
+ except ValueError:
+ cmd.extend(options.split())
+
+ # compose 只读子命令限制
+ if base == "compose" and resource not in ("ps", "logs", "config", "top", ""):
+ return json.dumps({"error": f"docker compose {resource} 不被允许(仅限只读操作)"}, ensure_ascii=False)
+
+ try:
+ proc = await _asyncio.create_subprocess_exec(
+ *cmd,
+ stdout=_asyncio.subprocess.PIPE,
+ stderr=_asyncio.subprocess.PIPE,
+ )
+ stdout, stderr = await _asyncio.wait_for(proc.communicate(), timeout=30)
+ return json.dumps({
+ "stdout": stdout.decode("utf-8", errors="replace")[:8000],
+ "stderr": stderr.decode("utf-8", errors="replace")[:2000],
+ "returncode": proc.returncode,
+ }, ensure_ascii=False)
+ except FileNotFoundError:
+ return json.dumps({"error": "Docker 未安装或不在 PATH 中"}, ensure_ascii=False)
+ except _asyncio.TimeoutError:
+ return json.dumps({"error": "Docker 操作超时"}, ensure_ascii=False)
+
+
+DOCKER_MANAGE_SCHEMA = {
+ "type": "function",
+ "function": {
+ "name": "docker_manage",
+ "description": "Docker 管理(只读为主):查看容器列表、镜像、日志、资源使用、容器详情。需要 Docker CLI。",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "operation": {
+ "type": "string",
+ "enum": ["ps", "images", "logs", "stats", "inspect", "version", "info", "compose"],
+ "description": "操作类型",
+ },
+ "resource": {"type": "string", "description": "容器名/镜像名/服务名"},
+ "options": {"type": "string", "description": "额外命令行选项"},
+ },
+ "required": ["operation"],
+ },
+ },
+}
+
+
+# ── deploy_push ──────────────────────────────────────────────
+
+async def deploy_push_tool(
+ source_path: str,
+ target: str,
+ method: str = "copy",
+ exclude: str = ".git,__pycache__,node_modules,.venv,venv",
+) -> str:
+ """文件部署:本地复制、SCP 或 RSYNC。"""
+ import asyncio as _asyncio
+
+ if not os.path.isabs(source_path):
+ source_path = os.path.join(_local_file_workspace_root(), source_path)
+ source_path = os.path.abspath(source_path)
+
+ if not os.path.exists(source_path):
+ return json.dumps({"error": f"源路径不存在: {source_path}"}, ensure_ascii=False)
+
+ method = method.lower().strip()
+
+ if method == "copy":
+ import shutil
+ target = os.path.abspath(target) if target else os.path.join(_local_file_workspace_root(), "deploy")
+ try:
+ if os.path.isdir(source_path):
+ if os.path.exists(target):
+ shutil.rmtree(target)
+ # 构建 rsync-like exclude
+ exclude_set = set(e.strip() for e in exclude.split(",") if e.strip())
+ shutil.copytree(source_path, target, ignore=shutil.ignore_patterns(*exclude_set))
+ else:
+ os.makedirs(os.path.dirname(target) or ".", exist_ok=True)
+ shutil.copy2(source_path, target)
+ return json.dumps({"method": "copy", "source": source_path, "target": target, "status": "ok"}, ensure_ascii=False)
+ except Exception as e:
+ return json.dumps({"error": f"复制失败: {e}"}, ensure_ascii=False)
+
+ elif method == "rsync":
+ exclude_args = []
+ for e in exclude.split(","):
+ e = e.strip()
+ if e:
+ exclude_args.extend(["--exclude", e])
+ cmd = ["rsync", "-avz", "--progress"] + exclude_args + [source_path + "/" if os.path.isdir(source_path) else source_path, target]
+ try:
+ proc = await _asyncio.create_subprocess_exec(
+ *cmd,
+ stdout=_asyncio.subprocess.PIPE,
+ stderr=_asyncio.subprocess.PIPE,
+ )
+ stdout, stderr = await _asyncio.wait_for(proc.communicate(), timeout=120)
+ return json.dumps({
+ "method": "rsync",
+ "source": source_path,
+ "target": target,
+ "stdout": stdout.decode("utf-8", errors="replace")[:4000],
+ "stderr": stderr.decode("utf-8", errors="replace")[:2000],
+ "returncode": proc.returncode,
+ }, ensure_ascii=False)
+ except FileNotFoundError:
+ return json.dumps({"error": "rsync 未安装"}, ensure_ascii=False)
+ except _asyncio.TimeoutError:
+ return json.dumps({"error": "rsync 超时"}, ensure_ascii=False)
+
+ else:
+ return json.dumps({"error": f"不支持的部署方式: {method},支持 copy/rsync"}, ensure_ascii=False)
+
+
+DEPLOY_PUSH_SCHEMA = {
+ "type": "function",
+ "function": {
+ "name": "deploy_push",
+ "description": "部署文件到目标位置:本地复制(支持 exclude 规则)或 rsync 远程同步。适合将项目部署到服务器。",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "source_path": {"type": "string", "description": "源文件或目录路径"},
+ "target": {"type": "string", "description": "目标路径(本地目录或 rsync 远程地址如 user@host:/path)"},
+ "method": {
+ "type": "string",
+ "enum": ["copy", "rsync"],
+ "description": "部署方式:copy=本地复制, rsync=rsync 同步",
+ "default": "copy",
+ },
+ "exclude": {"type": "string", "description": "排除模式(逗号分隔,如 .git,node_modules)", "default": ".git,__pycache__,node_modules,.venv,venv"},
+ },
+ "required": ["source_path", "target"],
+ },
+ },
+}
+
+
+# ── agent_create ─────────────────────────────────────────────
+
+async def agent_create_tool(
+ name: str,
+ system_prompt: str,
+ description: str = "",
+ model: str = "deepseek-v4-flash",
+ provider: str = "deepseek",
+ temperature: float = 0.7,
+) -> str:
+ """
+ 动态创建专业子 Agent。
+
+ 当本 Agent 发现某个任务超出自身专业领域时,可创建一个专门的子 Agent 来处理,
+ 创建后通过 agent_call 委派任务。新 Agent 会持久化到数据库,后续可直接复用。
+
+ Args:
+ name: 新 Agent 名称(建议体现专业领域,如「SQL优化专家」)
+ system_prompt: 新 Agent 的系统提示词,定义其角色和专业能力
+ description: 简短描述
+ model: 使用的模型
+ provider: 模型提供商
+ temperature: 温度参数
+ """
+ import uuid
+
+ try:
+ from app.core.database import SessionLocal
+ from app.models.agent import Agent
+ from app.models.user import User
+
+ db = SessionLocal()
+ try:
+ # 检查重名
+ existing = db.query(Agent).filter(Agent.name == name).first()
+ if existing:
+ db.close()
+ return json.dumps({
+ "error": "name_conflict",
+ "message": f"Agent「{name}」已存在(id={existing.id}),可直接通过 agent_call 调用",
+ "existing_id": existing.id,
+ }, ensure_ascii=False)
+
+ # 取第一个用户作为所有者
+ owner = db.query(User).first()
+ if not owner:
+ db.close()
+ return json.dumps({"error": "no_user", "message": "数据库中无用户,无法创建 Agent"}, ensure_ascii=False)
+
+ agent_id = str(uuid.uuid4())
+ agent = Agent(
+ id=agent_id,
+ name=name,
+ description=description or f"由 Agent 自主创建的专业子 Agent:{name}",
+ workflow_config={
+ "nodes": [
+ {"id": "start-1", "type": "start", "position": {"x": 80, "y": 120}, "data": {}},
+ {
+ "id": "agent-1",
+ "type": "agent",
+ "position": {"x": 320, "y": 120},
+ "data": {
+ "label": name,
+ "system_prompt": system_prompt,
+ "model": model,
+ "provider": provider,
+ "temperature": temperature,
+ "max_iterations": 10,
+ "tools": [],
+ "memory": True,
+ },
+ },
+ {"id": "end-1", "type": "end", "position": {"x": 560, "y": 120}, "data": {}},
+ ],
+ "edges": [
+ {"id": "e_start_agent", "source": "start-1", "target": "agent-1", "sourceHandle": "right", "targetHandle": "left"},
+ {"id": "e_agent_end", "source": "agent-1", "target": "end-1", "sourceHandle": "right", "targetHandle": "left"},
+ ],
+ },
+ status="active",
+ user_id=owner.id,
+ )
+ db.add(agent)
+ db.commit()
+
+ result = {
+ "status": "created",
+ "agent": {
+ "id": agent_id,
+ "name": name,
+ "description": agent.description,
+ "model": model,
+ "provider": provider,
+ "temperature": temperature,
+ "tools": "ALL (31 builtin tools)",
+ "memory": "RAG enabled",
+ },
+ "hint": f"现在可以用 agent_call(agent_name=\"{name}\", query=\"...\") 来调用这个新 Agent",
+ }
+ return json.dumps(result, ensure_ascii=False)
+ finally:
+ db.close()
+ except Exception as e:
+ logger.error(f"agent_create 工具执行失败: {e}", exc_info=True)
+ return json.dumps({"error": "creation_failed", "message": f"创建 Agent 失败: {e}"}, ensure_ascii=False)
+
+
+AGENT_CREATE_SCHEMA = {
+ "type": "function",
+ "function": {
+ "name": "agent_create",
+ "description": (
+ "动态创建一个专业子 Agent。当本 Agent 发现某个任务超出自身专业领域时,"
+ "用此工具创建专门 Agent,然后用 agent_call 委派任务。"
+ "新 Agent 会持久化,后续可直接复用。"
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "新 Agent 名称(如「SQL优化专家」「日语翻译官」)"},
+ "system_prompt": {"type": "string", "description": "新 Agent 的系统提示词,定义其角色、专业能力和行为准则"},
+ "description": {"type": "string", "description": "简短描述(可选)"},
+ "model": {"type": "string", "description": "使用的模型", "default": "deepseek-v4-flash"},
+ "provider": {"type": "string", "description": "模型提供商", "default": "deepseek"},
+ "temperature": {"type": "number", "description": "温度参数", "default": 0.7},
+ },
+ "required": ["name", "system_prompt"],
+ },
+ },
+}
+
+
+# ── tool_register ────────────────────────────────────────────
+
+async def tool_register_tool(
+ name: str,
+ description: str,
+ method: str = "GET",
+ url: str = "",
+ headers_json: str = "{}",
+ body_template_json: str = "",
+) -> str:
+ """
+ 动态注册 HTTP 工具。
+
+ 当 Agent 发现实用的外部 API 时,将其注册为可复用工具。
+ 工具写入数据库后立即加载到 tool_registry,后续所有 Agent 均可直接调用。
+
+ Args:
+ name: 工具名称(英文,如 stock_price、weather_query)
+ description: 工具功能描述
+ method: HTTP 方法
+ url: 请求 URL 模板,参数用 {param_name} 占位,如 https://api.example.com/stock?symbol={symbol}
+ headers_json: 请求头 JSON 字符串
+ body_template_json: 请求体模板 JSON 字符串
+ """
+ import uuid
+ import re as _re
+
+ try:
+ from app.core.database import SessionLocal
+ from app.models.tool import Tool
+ from app.models.user import User
+ from app.services.tool_registry import tool_registry
+
+ # 解析 URL 模板参数 {param_name}
+ url_params = _re.findall(r"\{(\w+)\}", url)
+ body_params = []
+ if body_template_json:
+ try:
+ body_tpl = json.loads(body_template_json)
+ body_str = json.dumps(body_tpl)
+ body_params = _re.findall(r"\{(\w+)\}", body_str)
+ except json.JSONDecodeError:
+ return json.dumps({"error": "body_template_json 不是有效 JSON"}, ensure_ascii=False)
+
+ # 构建 OpenAI function schema
+ properties = {}
+ required = []
+ for p in sorted(set(url_params + body_params)):
+ properties[p] = {"type": "string", "description": f"参数 {p}"}
+ required.append(p)
+ # headers 参数
+ try:
+ hdrs = json.loads(headers_json)
+ for hk in hdrs:
+ if "{" in str(hdrs[hk]):
+ continue # 模板头跳过
+ except json.JSONDecodeError:
+ pass
+
+ function_schema = {
+ "type": "function",
+ "function": {
+ "name": name,
+ "description": description,
+ "parameters": {
+ "type": "object",
+ "properties": properties,
+ "required": required,
+ },
+ },
+ }
+
+ db = SessionLocal()
+ try:
+ # 检查重名
+ existing = db.query(Tool).filter(Tool.name == name).first()
+ if existing:
+ db.close()
+ return json.dumps({
+ "error": "name_conflict",
+ "message": f"工具「{name}」已存在(id={existing.id})",
+ "existing_id": existing.id,
+ }, ensure_ascii=False)
+
+ owner = db.query(User).first()
+ tool_id = str(uuid.uuid4())
+
+ tool = Tool(
+ id=tool_id,
+ name=name,
+ description=description,
+ category="agent_created",
+ function_schema=function_schema,
+ implementation_type="http",
+ implementation_config={
+ "url": url,
+ "method": method.upper(),
+ "headers": json.loads(headers_json) if headers_json else {},
+ "body_template": json.loads(body_template_json) if body_template_json else None,
+ },
+ is_public=False,
+ user_id=owner.id if owner else None,
+ )
+ db.add(tool)
+ db.commit()
+
+ # 立刻加载到 tool_registry
+ tool_registry.load_tools_from_db(db, tool_names=[name])
+
+ return json.dumps({
+ "status": "registered",
+ "tool": {
+ "id": tool_id,
+ "name": name,
+ "description": description,
+ "method": method.upper(),
+ "url_template": url,
+ "params": required,
+ },
+ "hint": f"工具已就绪,可直接调用 {name}({', '.join(p + '=...' for p in required) if required else ''})",
+ }, ensure_ascii=False)
+ finally:
+ db.close()
+ except Exception as e:
+ logger.error(f"tool_register 工具执行失败: {e}", exc_info=True)
+ return json.dumps({"error": "registration_failed", "message": f"注册工具失败: {e}"}, ensure_ascii=False)
+
+
+TOOL_REGISTER_SCHEMA = {
+ "type": "function",
+ "function": {
+ "name": "tool_register",
+ "description": (
+ "动态注册一个新 HTTP 工具。当 Agent 发现实用的外部 API 时,"
+ "将其注册为可复用工具。工具写入数据库后立即可用,后续所有 Agent 均可调用。"
+ "URL 中用 {param_name} 占位参数,如 https://api.example.com/weather?city={city}。"
+ ),
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "name": {"type": "string", "description": "工具名称(英文,如 stock_price、weather_query)"},
+ "description": {"type": "string", "description": "工具功能描述"},
+ "method": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE"], "description": "HTTP 方法", "default": "GET"},
+ "url": {"type": "string", "description": "请求 URL 模板,参数用 {param_name} 占位"},
+ "headers_json": {"type": "string", "description": "请求头 JSON(可选)", "default": "{}"},
+ "body_template_json": {"type": "string", "description": "请求体模板 JSON(POST/PUT 时可选)"},
+ },
+ "required": ["name", "description", "url"],
+ },
+ },
+}
diff --git a/frontend/src/utils/agentSkills.ts b/frontend/src/utils/agentSkills.ts
index c8b6aea..382045c 100644
--- a/frontend/src/utils/agentSkills.ts
+++ b/frontend/src/utils/agentSkills.ts
@@ -16,6 +16,18 @@ export const BUILTIN_SKILL_OPTIONS: { name: string; label: string }[] = [
{ name: 'database_query', label: '数据库查询' },
{ name: 'adb_log', label: 'ADB 日志' },
{ name: 'agent_call', label: '调用 Agent' },
+ { name: 'code_execute', label: '代码执行' },
+ { name: 'git_operation', label: 'Git 操作' },
+ { name: 'web_search', label: '网页搜索' },
+ { name: 'pdf_generate', label: 'PDF 生成' },
+ { name: 'project_scaffold', label: '项目脚手架' },
+ { name: 'task_plan', label: '任务规划' },
+ { name: 'excel_process', label: 'Excel 处理' },
+ { name: 'browser_use', label: '浏览器操控' },
+ { name: 'docker_manage', label: 'Docker 管理' },
+ { name: 'deploy_push', label: '部署推送' },
+ { name: 'agent_create', label: '创建 Agent' },
+ { name: 'tool_register', label: '注册工具' },
]
export const BUILTIN_SKILL_LABELS: Record = Object.fromEntries(
diff --git a/使用文档.md b/使用文档.md
new file mode 100644
index 0000000..1187f48
--- /dev/null
+++ b/使用文档.md
@@ -0,0 +1,484 @@
+# Agent 工具使用文档
+
+## 概述
+
+所有 Agent 默认拥有 **31 个内置工具**(`tools: []` = 全部可用)。工具涵盖文件操作、网络访问、代码执行、项目管理、部署等能力,Agent 可在 ReAct 循环中自主选择和调用。
+
+---
+
+## 工具清单(31个)
+
+### 文件与数据
+
+| # | 工具 | 参数 | 说明 |
+|---|------|------|------|
+| 1 | `file_read` | `file_path` | 读取文本/PDF/docx/xlsx/图片OCR |
+| 2 | `file_write` | `file_path`, `content`, `mode` | 写入文本文件(覆盖/追加) |
+| 3 | `json_process` | `json_data`, `operation`, `key` | JSON 结构化数据处理 |
+| 4 | `excel_process` | `file_path`, `action`, `sheet_name`, `data_json`, `chart_config_json` | Excel 读写 + 图表生成 |
+
+### 网络
+
+| # | 工具 | 参数 | 说明 |
+|---|------|------|------|
+| 5 | `http_request` | `url`, `method`, `headers`, `body` | HTTP 请求(GET/POST/PUT/DELETE) |
+| 6 | `url_parse` | `url` | URL 解析 |
+| 7 | `web_search` | `query`, `max_results` | DuckDuckGo 网页搜索 |
+
+### 代码与文本
+
+| # | 工具 | 参数 | 说明 |
+|---|------|------|------|
+| 8 | `code_execute` | `code`, `language`, `timeout` | Python/JS 沙箱执行 |
+| 9 | `text_analyze` | `text`, `operation` | 文本分析 |
+| 10 | `regex_test` | `pattern`, `text` | 正则表达式测试 |
+
+### 项目管理
+
+| # | 工具 | 参数 | 说明 |
+|---|------|------|------|
+| 11 | `git_operation` | `operation`, `file_path`, `revision` | Git 只读操作(log/diff/blame/status) |
+| 12 | `project_scaffold` | `template`, `project_name`, `target_dir` | 项目模板生成(fastapi/vue/react等) |
+| 13 | `task_plan` | `action`, `plan_id`, `title`, `steps_json`, `step_index`, `status` | 任务分解与进度跟踪 |
+| 14 | `deploy_push` | `source_path`, `target`, `method`, `exclude` | 文件部署(本地复制/rsync) |
+
+### 文档与报告
+
+| # | 工具 | 参数 | 说明 |
+|---|------|------|------|
+| 15 | `pdf_generate` | `markdown`, `output_path`, `title` | Markdown→PDF/HTML 报告生成 |
+
+### 计算与系统
+
+| # | 工具 | 参数 | 说明 |
+|---|------|------|------|
+| 16 | `math_calculate` | `expression` | 数学计算 |
+| 17 | `system_info` | | 系统环境信息 |
+| 18 | `datetime` | `operation`, `datetime_str`, `timezone` | 日期时间计算 |
+| 19 | `crypto_util` | `operation`, `text`, `key` | 加密/解密/哈希 |
+| 20 | `random_generate` | `type`, `count`, `min`, `max` | 随机数据生成 |
+
+### 自动化
+
+| # | 工具 | 参数 | 说明 |
+|---|------|------|------|
+| 21 | `schedule_create` | `agent_id`, `name`, `cron_expression`, `input_message` | 创建定时任务 |
+| 22 | `schedule_list` | `agent_id` | 查看定时任务 |
+| 23 | `schedule_delete` | `schedule_id` | 删除定时任务 |
+
+### 通信与协作
+
+| # | 工具 | 参数 | 说明 |
+|---|------|------|------|
+| 24 | `send_email` | `to`, `subject`, `body` | 发送邮件 |
+| 25 | `agent_call` | `agent_name`, `query`, `max_iterations` | 调用其他 Agent 委派任务 |
+| 26 | `agent_create` | `name`, `system_prompt`, `description` | 动态创建专业子 Agent |
+| 27 | `tool_register` | `name`, `description`, `method`, `url` | 动态注册 HTTP 工具 |
+
+### DevOps
+
+| # | 工具 | 参数 | 说明 |
+|---|------|------|------|
+| 26 | `database_query` | `query`, `params` | 数据库查询 |
+| 27 | `docker_manage` | `operation`, `resource`, `options` | Docker 容器管理(只读) |
+| 28 | `browser_use` | `url`, `action`, `selector`, `script` | 无头浏览器操控(截图/提取/填表) |
+| 31 | `adb_log` | `device`, `lines`, `tag` | Android ADB 日志 |
+
+---
+
+## 核心工具详解
+
+### 1. code_execute — 代码执行
+
+```
+code_execute(code="print(sum(range(1,101)))", language="python")
+→ {"stdout": "5050\n", "stderr": "", "returncode": 0}
+```
+
+**场景:**
+- 写脚本验证逻辑 → 看输出 → 修正 → 完成闭环
+- 数据处理:pandas 读取 → 清洗 → 统计
+- 批量文件重命名、格式转换
+
+**安全限制:** 超时 30s,Python/JS 沙箱,任意代码可执行
+
+---
+
+### 2. git_operation — Git 操作
+
+```
+git_operation(operation="log", file_path="src/main.py")
+→ 最近 50 条该文件的提交历史
+
+git_operation(operation="diff", file_path="src/utils.py")
+→ 当前未暂存的差异
+
+git_operation(operation="blame", file_path="src/app.py")
+→ 每行代码的作者和提交时间
+```
+
+**支持的 operation:** `log`, `diff`, `diff_staged`, `status`, `branch`, `blame`, `show`, `tag`, `remote`, `rev_parse`
+
+**场景:**
+- 理解项目演进历史
+- 定位 bug 引入的提交
+- 审查代码变更范围
+
+---
+
+### 3. web_search — 网页搜索
+
+```
+web_search(query="FastAPI middleware CORS setup 2025", max_results=5)
+→ {"results": [
+ {"index": 1, "title": "...", "url": "https://...", "snippet": "..."},
+ ...
+ ], "count": 5}
+```
+
+**场景:**
+- 查最新技术文档、API 用法
+- 搜索错误信息找到解决方案
+- 了解行业最佳实践
+
+---
+
+### 4. task_plan — 任务规划
+
+```
+# 创建计划
+task_plan(
+ action="create",
+ title="开发用户认证系统",
+ steps_json='["设计数据库模型","实现注册API","实现登录API","添加JWT中间件","编写测试","部署配置"]'
+)
+→ {"plan": {"id": "plan_20260503_120000", "steps": [...], ...}}
+
+# 标记步骤完成
+task_plan(
+ action="update_step",
+ plan_id="plan_20260503_120000",
+ step_index=0,
+ status="done"
+)
+
+# 查看进度
+task_plan(action="view", plan_id="plan_20260503_120000")
+→ 完整计划及每个步骤的状态
+
+# 列出所有计划
+task_plan(action="list")
+→ 所有活跃计划的摘要
+```
+
+**步骤状态:** `pending` → `in_progress` → `done` / `blocked`
+
+**场景:**
+- 复杂多步骤任务:"帮我搭建一个完整的博客系统"
+- Agent 自主拆解 → 逐步执行 → 汇报每步结果
+- 长期项目跟踪
+
+---
+
+### 5. project_scaffold — 项目脚手架
+
+```
+project_scaffold(template="fastapi", project_name="my-api")
+→ 在工作区创建标准 FastAPI 项目目录结构
+
+project_scaffold(template="vue", project_name="my-dashboard")
+→ Vue 3 + Vite 前端项目
+```
+
+**支持的模板:**
+
+| 模板 | 说明 |
+|------|------|
+| `fastapi` | FastAPI 后端 + Dockerfile |
+| `vue` | Vue 3 + Vite 前端 |
+| `react` | React 18 + Vite |
+| `python_cli` | Python CLI 工具 |
+| `shell` | Shell 脚本项目 |
+
+---
+
+### 6. pdf_generate — 报告生成
+
+```
+pdf_generate(
+ markdown="# 项目周报\n\n## 进展\n- 完成用户模块\n- 修复3个bug\n\n## 下周计划\n- 集成支付",
+ title="项目周报 2026-05-03"
+)
+→ {"file": "/path/to/report_20260503_120000.pdf", "format": "pdf"}
+```
+
+**依赖:** `pip install weasyprint` 生成 PDF;否则降级为 HTML
+
+---
+
+### 7. excel_process — 高级 Excel
+
+```
+# 读取
+excel_process(file_path="data.xlsx", action="read")
+→ 返回所有工作表的数据
+
+# 写入
+excel_process(
+ file_path="output.xlsx",
+ action="write",
+ data_json='{"Sheet1": [["姓名","分数"],["张三",95],["李四",87]]}'
+)
+
+# 添加图表
+excel_process(
+ file_path="output.xlsx",
+ action="chart",
+ chart_config_json='{"type":"bar","title":"成绩分布","data_min_col":2,"data_min_row":1,"data_max_col":2,"data_max_row":3}'
+)
+```
+
+**支持的图表:** `bar`(柱状图)、`line`(折线图)、`pie`(饼图)
+
+---
+
+### 8. browser_use — 浏览器操控
+
+```
+# 截图
+browser_use(url="https://example.com", action="screenshot")
+→ {"screenshot": "/tmp/screenshot_20260503_120000.png"}
+
+# 提取文本
+browser_use(url="https://docs.example.com", action="content")
+→ {"text": "...页面全部可读文本...", "text_length": 5432}
+
+# 执行 JS
+browser_use(url="https://example.com", action="evaluate", script="document.title")
+→ {"evaluate_result": "Example Domain"}
+```
+
+**依赖:** `pip install playwright && playwright install chromium`
+
+---
+
+### 9. docker_manage — Docker 管理
+
+```
+docker_manage(operation="ps")
+→ 正在运行的容器列表
+
+docker_manage(operation="logs", resource="my-container")
+→ 容器日志
+
+docker_manage(operation="compose", resource="ps")
+→ docker compose 服务状态
+```
+
+**只读操作:** `ps`, `images`, `logs`, `stats`, `inspect`, `version`, `info`, `compose ps/logs/config/top`
+
+---
+
+### 10. deploy_push — 部署推送
+
+```
+# 本地复制(自动排除 .git/node_modules 等)
+deploy_push(source_path="/app/dist", target="/var/www/html", method="copy")
+
+# rsync 到远程服务器
+deploy_push(
+ source_path="/app/dist",
+ target="user@server.com:/var/www/html",
+ method="rsync",
+ exclude=".git,node_modules,.venv"
+)
+```
+
+---
+
+### 11. agent_call — Agent 间协作
+
+```
+agent_call(agent_name="家庭医生助手", query="用户头痛3天,伴随失眠,该注意什么?")
+→ {
+ "agent": "家庭医生助手",
+ "status": "success",
+ "iterations": 3,
+ "tool_calls": 1,
+ "reply": "根据您描述的症状..."
+ }
+```
+
+详见下一节。
+
+---
+
+### 12. agent_create — 动态创建子 Agent
+
+```
+agent_create(
+ name="SQL优化专家",
+ system_prompt="你是MySQL性能优化专家,擅长分析慢查询日志...",
+ description="专门分析MySQL慢查询并提供优化方案"
+)
+→ {"status": "created", "agent": {"id": "uuid", "name": "SQL优化专家", ...}}
+```
+
+**场景:**
+- Agent 发现任务需要专业知识但当前没有对应子 Agent → 创建一个 → 委派
+- 新 Agent 写入 DB,拥有全部 31 工具 + RAG 记忆,持久化复用
+- 创建后立即通过 `agent_call` 调用来完成任务
+
+---
+
+### 13. tool_register — 动态注册工具
+
+```
+tool_register(
+ name="currency_exchange",
+ description="查询实时汇率",
+ method="GET",
+ url="https://api.exchangerate-api.com/v4/latest/{base_currency}"
+)
+→ {"status": "registered", "tool": {"id": "uuid", "name": "currency_exchange", ...}}
+```
+
+**场景:**
+- Agent 发现某个外部 API 很实用 → 注册为工具 → 立即调用
+- URL 中用 `{param}` 占位 → 自动解析为工具参数
+- 工具写入 DB → 加载到 tool_registry → 后续所有 Agent 均可复用
+
+---
+
+## Agent 自主能力扩展流程
+
+```
+全能助手收到: "监控线上MySQL慢查询并优化"
+ │
+ ├─ 检查自身 → 缺少SQL专业深度
+ ├─ web_search("MySQL慢查询分析方案")
+ │
+ ├─ agent_create(name="SQL优化专家", system_prompt="你是MySQL性能优化专家...")
+ │ └─ 新 Agent 写入 DB,拥有全部 31 工具
+ │
+ ├─ agent_call("SQL优化专家", "分析这份慢查询日志并给出优化建议")
+ │ └─ 子 Agent 独立 ReAct 循环执行
+ │
+ ├─ 发现 percona-toolkit 有 REST API
+ ├─ tool_register(name="pt_query_digest", url="http://internal-api/analyze?log={path}")
+ │ └─ 工具立即可用
+ │
+ └─ 整合结果: "慢查询优化方案如下..."
+```
+
+---
+
+## Agent 间协作模式
+
+### 路由模式(全能助手)
+
+```
+用户 → 全能助手 → 分析意图
+ ├─ 健康问题 → agent_call("家庭医生助手")
+ ├─ 代码问题 → agent_call("代码助手")
+ └─ 学习问题 → agent_call("学习助手")
+```
+
+### 串行协作
+
+```
+全能助手收到"分析服务器日志并给出健康建议"
+ → agent_call("日志分析师", "分析这段错误日志")
+ → agent_call("家庭医生助手", "这个错误模式是不是意味着系统压力太大,对运维人员的健康有什么建议")
+ → 整合两个结果回复用户
+```
+
+### 并行协作
+
+```
+全能助手收到"对比 React 和 Vue 的优缺点"
+ → 并行调用多个搜索/代码分析
+ → 综合汇总
+```
+
+---
+
+## 复杂项目示例
+
+### 示例 1:从零搭建 Web 应用
+
+```
+用户: "帮我搭一个用户管理后台"
+Agent 自主规划:
+
+1. task_plan(action="create", title="用户管理后台", steps_json=[...])
+2. project_scaffold(template="fastapi", project_name="user-admin")
+3. code_execute 写模型代码 → 验证 → 修正
+4. project_scaffold(template="vue", project_name="user-admin-ui")
+5. web_search("Vue 3 Element Plus table CRUD example")
+6. code_execute 写前端组件 → 验证
+7. docker_manage(operation="compose") 检查环境
+8. deploy_push(source_path="user-admin", target="/opt/app", method="copy")
+9. pdf_generate(markdown="# 部署文档...") 生成交付文档
+```
+
+### 示例 2:数据分析报告
+
+```
+用户: "分析 sales.xlsx 的销售趋势并生成报告"
+Agent 自主规划:
+
+1. excel_process(action="read", file_path="sales.xlsx")
+2. code_execute("用 Python 计算月度增长率/同比/环比")
+3. excel_process(action="chart", chart_config_json='{"type":"line",...}')
+4. web_search("2025 电商行业销售趋势对比") 补充行业背景
+5. pdf_generate(markdown="# 销售分析报告\n...")
+```
+
+### 示例 3:Bug 排查
+
+```
+用户: "线上 /api/orders 接口报 500,帮我排查"
+Agent 自主规划:
+
+1. docker_manage(operation="logs", resource="api-container")
+2. git_operation(operation="log", file_path="src/api/orders.py")
+3. git_operation(operation="diff") 查看最近改动
+4. database_query("SELECT * FROM orders WHERE created_at > '...'")
+5. web_search("FastAPI order API 500 error common causes")
+6. code_execute 复现 bug → 定位根因 → 输出修复方案
+```
+
+---
+
+## 依赖安装(可选工具)
+
+部分工具需要额外依赖才能使用。Agent 首次调用时会返回明确的安装指引。
+
+| 工具 | 依赖 | 安装命令 |
+|------|------|----------|
+| `pdf_generate`(PDF模式) | weasyprint | `pip install weasyprint` |
+| `browser_use` | playwright | `pip install playwright && playwright install chromium` |
+| `excel_process` | openpyxl | `pip install openpyxl` |
+| `docker_manage` | docker CLI | 安装 Docker Desktop |
+| `deploy_push`(rsync模式) | rsync | `apt install rsync` / `brew install rsync` |
+| `git_operation` | git CLI | 安装 Git |
+| `code_execute`(JS模式) | node | 安装 Node.js |
+
+---
+
+## 扩展工具
+
+如需添加新工具:
+
+1. 在 `backend/app/services/builtin_tools.py` 添加工具函数 + Schema
+2. 在 `backend/app/core/tools_bootstrap.py` 注册
+3. 在 `frontend/src/utils/agentSkills.ts` 添加条目
+4. 重启后端
+
+工具函数签名为:
+```python
+async def my_tool(param1: str, param2: int = 0) -> str:
+ # ... 实现 ...
+ return json.dumps({"result": "..."}, ensure_ascii=False)
+```
diff --git a/创建agent.md b/创建agent.md
index 914411b..6aaf0c2 100644
--- a/创建agent.md
+++ b/创建agent.md
@@ -2,9 +2,9 @@
## 概述
-本系统支持多种方式创建 Agent。**所有创建方式均默认赋予 Agent 全部 19 个内置工具能力**,除非明确限制。
+本系统支持多种方式创建 Agent。**所有创建方式均默认赋予 Agent 全部 31 个内置工具能力**,除非明确限制。
-## 内置工具清单(19个)
+## 内置工具清单(31个)
| 类别 | 工具 | 用途 |
|------|------|------|
@@ -27,6 +27,58 @@
| 工具 | `random_generate` | 随机数据生成 |
| 调试 | `adb_log` | Android 设备日志(ADB) |
| 协作 | `agent_call` | 调用其他 Agent 委派任务(Agent 间协作) |
+| 协作 | `agent_create` | 动态创建专业子 Agent(自我扩展能力) |
+| 协作 | `tool_register` | 动态注册新 HTTP 工具(自我扩展工具) |
+
+---
+
+## Agent 自主能力扩展
+
+Agent 可以通过 `agent_create` + `tool_register` 在运行时自我扩展能力和工具。
+
+### 能力不足时的自主决策流程
+
+```
+Agent 收到复杂任务
+ │
+ ├─ 评估自身能力是否足够
+ ├─ 能力足够 → 直接处理
+ │
+ ├─ 领域知识不足 → agent_create 创建专业子 Agent → agent_call 委派
+ │
+ ├─ 缺少实用工具 → web_search 找到外部 API → tool_register 注册工具 → 直接调用
+ │
+ └─ 整合所有结果返回用户
+```
+
+### agent_create — 动态创建子 Agent
+
+```
+agent_create(
+ name="SQL优化专家",
+ system_prompt="你是MySQL性能优化专家,擅长分析慢查询日志并给出优化建议...",
+ description="专门优化MySQL慢查询"
+)
+→ {"status": "created", "agent": {"id": "uuid", ...}}
+
+# 创建后立即委派
+agent_call(agent_name="SQL优化专家", query="分析这份慢查询日志")
+```
+
+### tool_register — 动态注册工具
+
+```
+tool_register(
+ name="currency_exchange",
+ description="查询实时汇率",
+ method="GET",
+ url="https://api.exchangerate-api.com/v4/latest/{base_currency}"
+)
+→ {"status": "registered", "tool": {"id": "uuid", ...}}
+
+# 注册后立即可用
+currency_exchange(base_currency="USD")
+```
---