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""" + +{html_title} + +{html_body}""" + + # 尝试 weasyprint + try: + from weasyprint import HTML + HTML(string=html_content).write_pdf(full_path) + return json.dumps({"file": full_path, "format": "pdf", "engine": "weasyprint"}, ensure_ascii=False) + except ImportError: + pass + + # 降级:保存为 HTML + html_path = full_path.rsplit(".", 1)[0] + ".html" + with open(html_path, "w", encoding="utf-8") as f: + f.write(html_content) + return json.dumps({ + "file": html_path, + "format": "html", + "engine": "builtin", + "note": "weasyprint 未安装,已生成 HTML 文件。安装 weasyprint 后可直接生成 PDF。", + }, ensure_ascii=False) + + +def _simple_md_to_html(md: str) -> str: + """极简 Markdown→HTML:支持标题/列表/代码块/粗体/斜体/链接/表格。""" + lines = md.split("\n") + out = [] + in_code_block = False + in_table = False + table_rows = [] + + for line in lines: + # 代码块 + if line.strip().startswith("```"): + if in_code_block: + out.append("") + in_code_block = False + else: + out.append('
')
+                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"" for c in table_rows[0]) + "")
+                for row in table_rows[1:]:
+                    out.append("" + "".join(f"" for c in row) + "")
+            out.append("
{c}
{c}
") + 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'\1', 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\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") +``` ---