import{d as u,o as m,l,m as o,c as s,f as n,t as e,j as r,p as y,q as _}from"./index.js";import{r as j}from"./useMarkdownRenderer.js";import{_ as h}from"./_plugin-vue_export-helper.js";const g=`# 自定义工具开发指南(非常详细|三层架构版) 适用对象:**从零开始、第一次上手** 的管理员。 当前能力:**仅支持 Python**,且必须在容器内执行;自定义工具仅管理员可见/可用。 > 本项目默认不做严格校验(私有化客制化场景)。这让你更自由,但也更容易踩坑。本文用大量例子/反例把坑提前说明。 --- ## 0. 快速上手(5 分钟 checklist) 1) 创建工具文件夹:\`data/custom_tools//\` 2) 写 3 个文件: - \`definition.json\`(定义层) - \`execution.py\`(执行层) - \`return.json\`(返回层,可选但强烈建议先写一个简单截断) 3) 刷新管理端: - 打开 \`admin/policy\`:工具应该出现在 \`custom\` 分类的可选列表里 4) 用管理员账号在聊天中调用工具(让模型调用或你手动触发) 如果第 3 步看不到工具:先看本文的“常见问题/排查”章节。 --- ## 1. 三层架构:你在写什么? 每个自定义工具由三层组成,各层之间通过“工具运行时”串起来: ### 1.1 定义层:\`definition.json\` 你告诉系统: - 工具 ID(也是模型调用的函数名) - 工具用途描述(给模型看,决定它会不会用、怎么用) - 参数 schema(模型按它组织参数) - 分类/图标/超时等元信息 ### 1.2 执行层:\`execution.py\`(Python 模板) 你提供“低代码逻辑”: - 文件内容会先被做一次 **字符串格式化**(把 \`{参数名}\` 替换成实际入参) - 然后在容器内作为 Python 脚本运行 ### 1.3 返回层:\`return.json\`(后处理) 对执行结果二次处理,例如: - 限制返回长度(避免输出爆炸) - 用模板把结果包装成更友好的消息 --- ## 2. 目录结构规范(必须遵守) 每个工具一个独立目录:\`data/custom_tools//\` 标准文件名: \`\`\` definition.json # 定义层(必需) execution.py # 执行层(必需) return.json # 返回层(可选) meta.json # 备注/展示层(可选) \`\`\` 只要某个目录同时存在 \`definition.json\` + \`execution.py\`,系统就会把它加载为“自定义工具”。 > 文件编码建议统一 UTF-8。 --- ## 3. 运行时规则(你需要牢记的“真实行为”) ### 3.1 谁能看到/调用? - **只有管理员账号**能看到/调用自定义工具 - 普通用户看不到工具,也无法在聊天中调用 ### 3.2 工具如何出现在 \`admin/policy\`? - 后端会自动注入一个分类 \`custom\`(名称“自定义工具”) - 并把扫描到的自定义工具 ID 填入该分类的工具列表 - 所以 admin/policy 的“工具可选列表”会出现这些工具名,从而可分配到别的分类 ### 3.3 执行层为什么“像模板”? 执行层会先做一次 Python \`str.format\` 类似的格式化: - \`{a}\`、\`{op}\`、\`{b}\` 这样的占位符会被替换 - 这会导致一个大坑:**Python 字典/集合的花括号 \`{}\` 会被误认为占位符**(下文详解) --- ## 4. 定义层 \`definition.json\`:字段规范 + 大量示例 ### 4.1 推荐字段(强烈建议都写) \`\`\`json { "id": "calc", "description": "简单两数运算(加减乘除),仅管理员可见", "parameters": { "type": "object", "properties": { "a": { "type": "number", "description": "第一个数字" }, "op": { "type": "string", "enum": ["+", "-", "*", "/"], "description": "运算符" }, "b": { "type": "number", "description": "第二个数字" } }, "required": ["a", "op", "b"] }, "category": "custom", "icon": "brain.svg", "timeout": 15 } \`\`\` 字段说明: - \`id\`(必填):工具 ID,建议与文件夹名一致(例如文件夹 \`calc/\`,id 也叫 \`calc\`) - \`description\`(强烈建议):越清楚越好(模型会读它决定是否调用) - \`parameters\`(建议):JSON Schema 风格 - \`type\`:固定 \`"object"\` - \`properties\`:参数字典 - \`required\`:必填参数数组 - \`category\`(可选,默认 custom):用于管理端分类 - \`icon\`(可选):SVG 文件名(来自 \`static/icons/\`) - \`timeout\`(可选,默认 30):执行超时秒数 > 注意:当前实现里 \`required\` 以 \`parameters.required\` 为准;另外顶层 \`required\` 字段也会被识别,但建议统一写在 \`parameters.required\`。 --- ### 4.2 定义层示例 A:最小可用“回声工具” 文件:\`data/custom_tools/echo/definition.json\` \`\`\`json { "id": "echo", "description": "回声:原样返回 text", "parameters": { "type": "object", "properties": { "text": { "type": "string", "description": "要回显的文本" } }, "required": ["text"] }, "category": "custom", "timeout": 10 } \`\`\` --- ### 4.3 定义层示例 B:可选参数(optional)怎么写? \`\`\`json { "id": "greet", "description": "打招呼:name 可选", "parameters": { "type": "object", "properties": { "name": { "type": "string", "description": "名字(可选)" } }, "required": [] } } \`\`\` 执行层里要记得处理 name 缺失的情况(见执行层示例)。 --- ### 4.4 反例:参数 schema 写错导致模型不会传参 反例(\`type\` 写成 string): \`\`\`json { "id": "bad_schema", "parameters": { "type": "string", "properties": { "a": { "type": "number" } } } } \`\`\` 后果:模型端可能无法正确构造对象参数,调用失败或入参为空。 --- ## 5. 执行层 \`execution.py\`:核心坑点、写法规范、正反例 ### 5.1 执行层“模板替换”规则(最关键) 执行层在运行前会做一次模板替换: - \`{a}\` → 入参 a 的值 - \`{op}\` → 入参 op 的值 因此: - **你写的不是纯 Python,而是 Python + 占位符** - 任何你写的 \`{...}\` 都可能被当成占位符解析 --- ### 5.2 大坑:字典/集合花括号必须双写 \`{{\` \`}}\` 你之前遇到的报错: \`\`\`json { "success": false, "error": "缺少必填参数: \\"'+'\\"" } \`\`\` 根因就是模板引擎把这段当成“需要一个叫 \`+'\` 的占位符”: \`\`\`py ops = {'+': operator.add, ...} \`\`\` **正确写法:把字典的 \`{}\` 变成 \`{{}}\`** 示例(正确): \`\`\`py ops = {{'+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv}} \`\`\` 反例(错误): \`\`\`py ops = {'+': operator.add} \`\`\` 同理:集合 \`{1,2,3}\`、字典推导式 \`{k:v for ...}\` 都需要转义,否则会被误当占位符。 --- ### 5.3 参数是字符串时,必须自己加引号(或安全编码) 如果参数是字符串: - \`{name}\` 替换后会变成裸文本,导致 Python 语法错误 正确(加引号): \`\`\`py name = '{name}' \`\`\` 反例(没引号,容易 SyntaxError): \`\`\`py name = {name} \`\`\` 更稳健的写法(推荐):用 JSON 序列化来安全注入字符串(避免引号/换行破坏代码)。 \`\`\`py import json name = json.loads({name_json}) \`\`\` 但这需要你在定义层把参数设计成 \`name_json\`,且调用时传入 JSON 字符串;新手不建议第一天就这么做。 --- ### 5.4 强烈建议:执行层先做“输入校验”,再计算 因为没有强校验,入参可能是空、类型不对、字符串里有奇怪字符。 示例:对 calc 的 op 做严格白名单检查: \`\`\`py a = {a} op = '{op}' b = {b} if op not in ['+','-','*','/']: raise ValueError('op must be one of + - * /') \`\`\` 反例(危险):用 eval 解析运算符/表达式 \`\`\`py print(eval(f\\"{a}{op}{b}\\")) # 不要这么写 \`\`\` 原因:op 或其他参数可被注入恶意表达式。 --- ### 5.5 执行层示例 1:echo(最简单) 文件:\`data/custom_tools/echo/execution.py\` \`\`\`py text = '{text}' print(text) \`\`\` --- ### 5.6 执行层示例 2:greet(可选参数) 文件:\`data/custom_tools/greet/execution.py\` \`\`\`py name = '{name}' name = (name or '').strip() if not name: print('你好!') else: print(f'你好,{name}!') \`\`\` 注意:这里用到了 f-string 的 \`{name}\`,**它是 Python 运行时的花括号,不会参与模板替换**,因为模板替换已经在执行前完成,且我们这里的 \`{name}\` 是在 Python 代码字符串里,不是模板语法(但仍要注意别在最外层写出新的 \`{}\` 占位符)。 --- ### 5.7 执行层示例 3:calc(你要的三参数计算器) 文件:\`data/custom_tools/calc/execution.py\` \`\`\`py a = {a} op = '{op}' b = {b} import operator ops = {{'+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv}} print(ops[op](a, b)) \`\`\` 要点回顾: - 数字:\`a = {a}\`、\`b = {b}\` - 字符串:\`op = '{op}'\` - 字典:\`ops = {{...}}\`(双花括号) --- ### 5.8 反例:输出过大导致工具失败/截断 反例: \`\`\`py print('A' * 1000000) \`\`\` 后果:输出有全局上限(默认 10k 字符左右),你会看到截断或失败提示。 建议:配合 return.json 的 \`truncate\`,或自行在执行层控制输出长度。 --- ### 5.9 反例:死循环导致超时 \`\`\`py while True: pass \`\`\` 后果:到 \`timeout\` 后被中断,返回 timeout。 --- ## 6. 返回层 \`return.json\`:怎么做二次处理(例子 + 反例) 返回层是可选的,但强烈建议至少写一个 \`truncate\`,避免工具输出污染上下文。 ### 6.1 支持字段(当前版本) \`\`\`json { "truncate": 2000, "template": "[tool_id={tool_id}] 输出: {output}" } \`\`\` - \`truncate\`:整数,输出超过该长度时截断到前 N 字符 - \`template\`:字符串模板,可用变量: - \`{output}\`:stdout+stderr 合并后的输出文本(已按 truncate 处理后的版本) - \`{stderr}\`:stderr(如果存在) - \`{return_code}\`:进程返回码 - \`{tool_id}\`:工具 id > \`template\` 只是影响 \`message\` 展示,不会改变 \`output\` 字段本身(除 truncate)。 --- ### 6.2 返回层示例:calc 结果更友好 文件:\`data/custom_tools/calc/return.json\` \`\`\`json { "truncate": 2000, "template": "[calc] 结果: {output}" } \`\`\` --- ### 6.3 反例:template 写错占位符导致忽略 \`\`\`json { "template": "结果={result}" } \`\`\` 因为 \`{result}\` 不是支持变量,格式化会失败,系统会回退到默认 message(你会看到输出但不按模板展示)。 --- ## 7. 备注/展示层 \`meta.json\`:备注与图标 ### 7.1 meta.json 适合放什么? 示例: \`\`\`json { "notes": "这是一个示例工具,用于演示参数与模板写法", "icon": "calculator.svg", "owner": "admin", "created_at": "2026-01-05" } \`\`\` ### 7.2 图标怎么选? 目前图标字段会被保存/返回(前端后续可用它显示工具卡片 SVG)。 建议写成 \`static/icons/\` 下的文件名,例如: - \`brain.svg\` - \`terminal.svg\` - \`bot.svg\` 你可以用命令查看有哪些图标: \`\`\`bash ls static/icons \`\`\` > 当前 UI 可能还没把 icon 真正渲染出来,但字段已经为未来对接准备好。 --- ## 8. 如何创建一个新工具:从 0 到可用(完整示例) 下面示例创建一个工具 \`text_len\`:计算字符串长度,并在返回层截断。 ### 8.1 创建目录 \`\`\` data/custom_tools/text_len/ \`\`\` ### 8.2 definition.json \`\`\`json { "id": "text_len", "description": "计算文本长度(字符数)", "parameters": { "type": "object", "properties": { "text": { "type": "string", "description": "输入文本" } }, "required": ["text"] }, "category": "custom", "timeout": 10 } \`\`\` ### 8.3 execution.py \`\`\`py text = '{text}' print(len(text)) \`\`\` ### 8.4 return.json(可选) \`\`\`json { "truncate": 200, "template": "[text_len] 字符数: {output}" } \`\`\` ### 8.5 验证 1) 管理端 \`admin/policy\`:应看到 \`text_len\` 2) 管理员聊天:让模型调用 \`text_len\`,或提示它“用 text_len 计算这段话长度” --- ## 9. 用 API 管理工具(可选) 如果你不想手工改文件,也可以调用后端接口(管理员权限): ### 9.1 列表 \`GET /api/admin/custom-tools\` ### 9.2 新增/更新 \`POST /api/admin/custom-tools\` 请求 JSON 推荐结构(字段越全越好): \`\`\`json { "id": "calc", "description": "简单两数运算", "parameters": { "type": "object", "properties": {}, "required": [] }, "required": [], "category": "custom", "icon": "brain.svg", "timeout": 15, "execution_code": "print('hello')\\n", "return": { "truncate": 2000, "template": "输出: {output}" }, "meta": { "notes": "通过 API 创建" } } \`\`\` ### 9.3 删除 \`DELETE /api/admin/custom-tools?id=calc\` --- ## 10. 常见问题与排查(非常实用) ### 10.1 admin/policy 看不到工具 逐条检查: 1) 你是否用 **管理员账号** 登录? 2) 工具目录是否存在:\`data/custom_tools//\` 3) 是否同时存在两个文件: - \`data/custom_tools//definition.json\` - \`data/custom_tools//execution.py\` 4) \`definition.json\` 是否是合法 JSON(多一个逗号都会解析失败) 5) 刷新页面(强刷),必要时重启后端服务 ### 10.2 调用时报 “缺少必填参数: \\"'+'\\"” 或类似奇怪 key 几乎可以确定是:**执行层里有未转义的字典/集合花括号**。 把 \`{\` 改成 \`{{\`,把 \`}\` 改成 \`}}\`(只针对“字典/集合字面量部分”)。 ### 10.3 调用时报 SyntaxError(语法错误) 常见原因: - 字符串参数没加引号:\`name = {name}\`(错误) - 参数中包含换行/引号导致代码被截断(建议先做简单输入,或改用更稳健的注入方式) ### 10.4 调用成功但输出很乱/太长 解决: - 在 \`return.json\` 里加 \`truncate\` - 或执行层自行控制输出 ### 10.5 工具能执行但模型不愿意用/总用错 解决: - 优化 \`description\`:写清楚输入输出、限制、例子 - \`parameters.properties.*.description\` 写清楚含义 - \`enum\` 尽量给明确可选值(例如运算符) --- ## 11. 规范建议(团队协作必看) ### 11.1 命名规范 - 工具 id:小写 + 下划线(例如 \`calc\`, \`text_len\`, \`db_query\`) - 目录名与 \`definition.json.id\` 保持一致 ### 11.2 输出规范 建议输出尽量“短而确定”: - 优先输出一个结果值或一段简短摘要 - 需要结构化时输出 JSON(但注意长度) ### 11.3 安全与边界 虽然是私有化环境,也强烈建议: - 不要在执行层写破坏性命令/删除文件 - 不要 \`eval\` 用户输入 - 给工具合理的 \`timeout\` - 给返回层加 \`truncate\` --- ## 12. 当前版本限制(请先接受这些现实) - 仅支持 Python(没有 NodeJS 执行) - 不提供强校验与沙箱策略自定义(依赖现有容器执行与全局限制) - 图标字段已预留(\`icon\`),但前端是否渲染取决于后续 UI 接入 `,b={class:"guide-page"},f={class:"guide-header"},x={class:"title-block"},v={class:"desc"},T={class:"actions"},k={class:"ghost",href:"/admin/custom-tools",target:"_blank",rel:"noopener"},P={class:"panel"},S={key:0,class:"status error"},q={key:1,class:"status"},I=["innerHTML"],N=u({__name:"CustomToolsGuideApp",setup(E){const i=r(""),p=r(!0),a=r("");return m(()=>{try{i.value=j(g)}catch(t){a.value=t.message||l("adminCustomTools.renderFailed")}finally{p.value=!1}}),(t,c)=>(o(),s("div",b,[n("header",f,[n("div",x,[c[0]||(c[0]=n("p",{class:"eyebrow"},"Custom Tools",-1)),n("h1",null,e(t.$t("adminCustomTools.guide.title")),1),n("p",v,e(t.$t("adminCustomTools.guide.subtitle")),1)]),n("div",T,[n("a",k,e(t.$t("adminCustomTools.backToList")),1)])]),n("section",P,[a.value?(o(),s("div",S,e(a.value),1)):p.value?(o(),s("div",q,e(t.$t("common.loading")),1)):(o(),s("article",{key:2,class:"markdown",innerHTML:i.value},null,8,I))])]))}}),C=h(N,[["__scopeId","data-v-a78d893f"]]),d=y(C);_(d);d.mount("#custom-tools-guide");