diff --git a/docs/superpowers/specs/2026-08-05-agent-tool-framework-design.md b/docs/superpowers/specs/2026-08-05-agent-tool-framework-design.md new file mode 100644 index 0000000..48a3cad --- /dev/null +++ b/docs/superpowers/specs/2026-08-05-agent-tool-framework-design.md @@ -0,0 +1,422 @@ +# Agent 工具调用框架设计(fetch 为首个工具) + +## 概述 + +构建一个通用工具调用框架,作为 agent 系统的前置基础设施。fetch 工具是第一个内置工具,后续可扩展 search、code-exec 等。框架支持 LLM tool-calling 自主调用,也支持独立执行 API 用于调试。 + +## 架构 + +``` +┌─────────────────────────────────────────────────┐ +│ 前端 │ +│ ┌──────────────┐ ┌───────────────────────┐ │ +│ │ 工具管理页面 │ │ Chat 页面(现有) │ │ +│ │ admin/ │ │ + 工具调用状态展示 │ │ +│ │ agent-tools │ │ │ │ +│ └──────┬───────┘ └───────────┬───────────┘ │ +└─────────┼────────────────────────┼──────────────┘ + │ │ +┌─────────┼────────────────────────┼──────────────┐ +│ 服务端 │ │ │ +│ ┌──────▼────────┐ ┌──────────▼───────────┐ │ +│ │ tools API │ │ llm/chat API(扩展) │ │ +│ │ CRUD + execute│ │ tool-calling loop │ │ +│ └──────┬────────┘ └──────────┬───────────┘ │ +│ │ │ │ +│ ┌──────▼────────────────────────▼───────────┐ │ +│ │ Tool Registry(工具注册中心) │ │ +│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ +│ │ │ fetch │ │ (future)│ │ (future)│ │ │ +│ │ │ executor│ │ executor│ │ executor│ │ │ +│ │ └─────────┘ └─────────┘ └─────────┘ │ │ +│ └───────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────▼────────┐ ┌──────────────────────┐ │ +│ │ agent_tools │ │ agent_tool_logs │ │ +│ │ (DB) │ │ (DB) │ │ +│ └───────────────┘ └──────────────────────┘ │ +└──────────────────────────────────────────────────┘ +``` + +核心分层: +- **Tool Registry**:type → executor 的映射,硬编码执行逻辑 + 根据 config 收窄 inputSchema +- **agent_tools 表**:存工具实例(全局共享),含 type/config/enabled +- **agent_tool_logs 表**:执行日志 +- **tools API**:CRUD + `POST /tools/:id/execute`(独立执行,调试用) +- **llm/chat 扩展**:注入 enabled 工具到 ai-sdk `streamText`,自动 tool loop + +## 数据库 Schema + +### agent_tools 表 + +```typescript +export const agentTools = sqliteTable("agent_tools", { + id: text("id").primaryKey(), // UUID + name: text("name", { length: 50 }).notNull(), // 展示名(给 admin 看) + slug: text("slug", { length: 50 }).notNull().unique(), // 唯一标识 + description: text("description").notNull(), // 给 LLM 看的工具描述 + type: text("type", { length: 30 }).notNull(), // "fetch" | 未来扩展 + config: text("config").notNull(), // JSON string,按 type 解析 + enabled: integer("enabled").default(1).notNull(), // 0/1 + sortOrder: integer("sort_order").default(0).notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }).defaultNow().$onUpdate(() => new Date()).notNull(), +}); +``` + +### agent_tool_logs 表 + +```typescript +export const agentToolLogs = sqliteTable("agent_tool_logs", { + id: integer("id").primaryKey({ autoIncrement: true }), + toolId: text("tool_id").notNull(), + toolSlug: text("tool_slug", { length: 50 }).notNull(), // 冗余存 slug,防 tool 删除后日志丢失上下文 + userId: integer("user_id"), // null = 系统调用 + input: text("input").notNull(), // JSON string + output: text("output"), // JSON string(可能被截断) + status: text("status", { length: 20 }).notNull(), // "success" | "error" | "timeout" + errorMessage: text("error_message"), + durationMs: integer("duration_ms").notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(), +}); +``` + +### fetch 工具的 config 结构 + +```typescript +interface FetchToolConfig { + defaultMethod: "GET" | "POST"; // 默认 GET + defaultHeaders: Record; + timeout: number; // ms,默认 10000 + maxResponseSize: number; // bytes,默认 102400 + allowedDomains: string[]; // ["*"] | ["example.com", "*.api.com"] + blockedDomains: string[]; + parseMode: "raw" | "markdown" | "json"; // 默认 markdown +} +``` + +## Tool Registry + +### 核心接口 + +```typescript +// server/service/agent-tool/registry.ts + +interface ToolExecutor { + /** 根据 config 生成给 LLM 的 inputSchema(JSON Schema) */ + buildInputSchema(config: TConfig): JSONSchema7; + /** 根据 config 生成给 LLM 的 description */ + buildDescription(config: TConfig): string; + /** 执行工具 */ + execute(input: unknown, config: TConfig, ctx: ToolContext): Promise; +} + +interface ToolContext { + toolId: string; + toolSlug: string; + userId: number | null; +} + +interface ToolResult { + success: boolean; + data: unknown; // 成功时的返回内容 + error?: string; // 失败时的错误信息 + metadata?: { // 执行元数据 + statusCode?: number; + responseSize?: number; + durationMs: number; + }; +} +``` + +### 注册机制 + +```typescript +const registry = new Map>(); + +export function registerToolType(type: string, executor: ToolExecutor) { + registry.set(type, executor); +} + +export function getExecutor(type: string): ToolExecutor { + const executor = registry.get(type); + if (!executor) throw new Error(`Unknown tool type: ${type}`); + return executor; +} +``` + +### fetch executor + +```typescript +// server/service/agent-tool/executors/fetch.ts + +export const fetchExecutor: ToolExecutor = { + buildInputSchema(config) { + return { + type: "object", + properties: { + url: { type: "string", description: "要抓取的 URL(http/https)" }, + method: { type: "string", enum: ["GET", "POST"], default: config.defaultMethod }, + headers: { type: "object", description: "请求头" }, + body: { type: "string", description: "POST 请求体" }, + }, + required: ["url"], + }; + }, + + buildDescription(config) { + const domains = config.allowedDomains.includes("*") + ? "任意域名" + : `仅限: ${config.allowedDomains.join(", ")}`; + return `抓取网页或 API 内容。支持 ${config.parseMode} 模式。域名限制: ${domains}。超时 ${config.timeout}ms。`; + }, + + async execute(input, config, ctx) { + // 1. zod 校验 input + // 2. SSRF 检查(解析 IP,拒绝内网) + // 3. 域名白/黑名单检查 + // 4. 发起 fetch(timeout via AbortController) + // 5. 大小限制检查 + // 6. 按 parseMode 处理响应 + // 7. 写日志 + // 8. 返回 ToolResult + }, +}; +``` + +### SSRF 防护 + +```typescript +// server/service/agent-tool/executors/fetch/security.ts + +async function assertSafeUrl(url: string): Promise { + const parsed = new URL(url); + if (!["http:", "https:"].includes(parsed.protocol)) { + throw new Error("仅支持 http/https 协议"); + } + // 解析主机名 → IP + const addresses = await dns.lookup(parsed.hostname, { all: true }); + for (const addr of addresses) { + if (isPrivateIp(addr.address)) { + throw new Error(`禁止访问内网地址: ${addr.address}`); + } + } +} + +function isPrivateIp(ip: string): boolean { + // 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, + // 169.254.0.0/16, ::1, fc00::/7, fe80::/10 +} +``` + +### parseMode 处理 + +- `raw`:直接返回原始响应体文本 +- `markdown`:用 `marked`(项目已有依赖)将 HTML 转为 markdown +- `json`:`JSON.parse` 后返回结构化对象;解析失败则降级为 raw + 错误提示 + +## API 设计 + +### 工具管理 CRUD + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/agent-tools` | 列出所有工具(按 sortOrder 排序) | +| POST | `/api/agent-tools` | 创建工具 | +| GET | `/api/agent-tools/:id` | 获取工具详情 | +| PUT | `/api/agent-tools/:id` | 更新工具 | +| DELETE | `/api/agent-tools/:id` | 删除工具 | +| POST | `/api/agent-tools/:id/execute` | 独立执行工具(调试用) | + +### 独立执行接口 + +``` +POST /api/agent-tools/:id/execute +Body: { input: { url: "https://...", method: "GET" } } +Response: { success: true, data: "...", metadata: { statusCode, responseSize, durationMs } } +``` + +- 需要 admin 权限(复用现有 `requireUser` + admin 校验) +- 执行前从 DB 加载 tool 配置,交给 registry 执行 +- 自动写 `agent_tool_logs` + +### chat 接口扩展 + +```typescript +// server/api/llm/chat/index.post.ts +// 现有 body 新增可选字段 +const { modelId, messages, enableThinking, enableTools } = body as { + // ... + enableTools?: boolean; // 默认 false,显式开启 +}; + +// 当 enableTools = true 时: +// 1. 查询所有 enabled = 1 的 agent_tools +// 2. 对每个 tool,用 registry.buildInputSchema + buildDescription 生成 ai-sdk tool 定义 +// 3. 调用 streamText({ model, messages, tools, maxSteps: 5 }) +// - ai-sdk 自动处理 tool-calling loop(LLM 决定调用 → 执行 → 结果回填 → 继续) +// - maxSteps = 5 防死循环 +// 4. 工具执行时自动写日志 +``` + +### ai-sdk tool 定义生成 + +```typescript +import { tool } from "ai"; +import { z } from "zod"; + +function buildAiTool(agentTool: AgentToolRow) { + const executor = getExecutor(agentTool.type); + const config = JSON.parse(agentTool.config); + const inputSchema = executor.buildInputSchema(config); + + // JSON Schema → zod schema(用于 ai-sdk 校验) + // 使用 zod 的 z.object 手动构建,或引入 json-schema-to-zod 库 + const zodSchema = jsonSchemaToZod(inputSchema); + + return tool({ + description: executor.buildDescription(config), + parameters: zodSchema, + execute: async (input) => { + const result = await executor.execute(input, config, ctx); + return result.success ? result.data : { error: result.error }; + }, + }); +} +``` + +### 关键决策 + +- `enableTools` 默认 false,现有 chat 行为不变,向后兼容 +- `maxSteps: 5` 硬编码,后续可配置化 +- 工具执行失败不中断对话,错误信息作为 tool result 喂回 LLM,让 LLM 自行处理 + +## Service 分层 + +``` +server/service/agent-tool/ +├── index.ts # 对外导出(listTools, getTool, createTool, executeTool 等) +├── registry.ts # ToolExecutor 接口 + 注册机制 +├── log.ts # 执行日志写入 +└── executors/ + ├── fetch.ts # fetch 执行器 + └── fetch/ + ├── security.ts # SSRF 防护 + 域名检查 + ├── parse.ts # parseMode 处理(raw/markdown/json) + └── config.ts # FetchToolConfig 类型 + 默认值 + zod 校验 +``` + +### index.ts 对外 API + +```typescript +// CRUD(与现有 service/tool 模式一致) +export async function listAgentTools(): Promise +export async function getAgentToolById(id: string): Promise +export async function createAgentTool(input: CreateAgentToolInput): Promise +export async function updateAgentTool(id: string, input: UpdateAgentToolInput): Promise +export async function deleteAgentTool(id: string): Promise + +// 执行 +export async function executeAgentTool( + id: string, + input: unknown, + userId: number | null +): Promise + +// 给 chat 集成用 +export async function getEnabledToolsForLlm(): Promise> +``` + +### log.ts + +```typescript +export async function writeToolLog(entry: { + toolId: string; + toolSlug: string; + userId: number | null; + input: unknown; + result: ToolResult; +}): Promise +``` + +### 与现有代码的关系 + +- 现有 `server/service/tool/` 和 `tools` 表**完全不动**,它们服务于 cards/articles 的分类标签 +- 新增 `server/service/agent-tool/` 独立模块,职责清晰 +- `server/api/agent-tools/` 新增 API 目录,与 `server/api/tools/` 并行 + +## 前端 + +### 工具管理页面 + +`app/pages/admin/agent-tools.vue`: +- 表格列出所有工具:name、slug、type、enabled(开关)、sortOrder、操作(编辑/删除/执行测试) +- 新建/编辑表单:name、slug、description、type(下拉,目前只有 fetch)、enabled、sortOrder、config(JSON 编辑器,按 type 显示对应 schema 提示) +- 执行测试面板:选择工具 → 填入 input JSON → 点"执行" → 显示返回结果和耗时 + +### chat 页面改动 + +- 对话输入框旁加一个"工具"开关(默认关),开启后 `enableTools: true` 发给后端 +- 流式回复中,工具调用过程以轻量提示展示(如"正在调用 fetch..."),不展开详情 + +### 组件复用 + +- 表格、表单、开关等基础组件优先从 `bolt-ui` 复用 +- JSON 编辑器:评估 `bolt-ui` 是否有,没有则用简单 `