| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
- // MCP Gateway 的内置工具(两个引擎共用):文档转换、公式重算、网页抓取、资料库列表
- import path from 'node:path'
- import { z } from 'zod'
- import type { McpBuiltinToolDto } from '@agent-server/shared'
- import { CONVERT_FORMATS, convertDocument, recalcWorkbook } from '../services/office'
- import { describeKnowledge, linkOrigins } from '../knowledge/describe'
- import { FetchBlockedError, webFetch } from '../services/web-fetch'
- import type { GatewayRunContext } from './tokens'
- export type ToolContent = { type: string; [key: string]: unknown }
- export type ToolResult = { content: ToolContent[]; isError?: boolean }
- export const ok = (text: string): ToolResult => ({ content: [{ type: 'text', text }] })
- export const fail = (text: string): ToolResult => ({ content: [{ type: 'text', text }], isError: true })
- export interface BuiltinTool {
- name: string
- description: string
- schema: z.ZodObject
- readOnly: boolean
- handler(args: Record<string, unknown>, ctx: GatewayRunContext): Promise<ToolResult>
- }
- const readonlyBlock = (ctx: GatewayRunContext) => (ctx.permissionMode === 'readonly' ? fail('当前为只读模式,不允许生成或修改文件') : null)
- const convertSchema = z.object({
- input: z.string().describe('输入文件路径(相对工作区或绝对路径)'),
- format: z.enum(CONVERT_FORMATS).describe('目标格式'),
- out_dir: z.string().optional().describe('输出目录,默认 outputs'),
- })
- const recalcSchema = z.object({ input: z.string().describe('xlsx/xlsm 文件路径(相对工作区或绝对路径)') })
- const fetchSchema = z.object({ url: z.string().describe('http/https 网址') })
- export const BUILTIN_TOOLS: BuiltinTool[] = [
- {
- name: 'convert_document',
- description: '用 LibreOffice 转换文档格式(例如 docx→pdf、xlsx→pdf、pptx→pdf、doc→docx、html→pdf)。'
- + '输入和输出都必须在工作区内;输出文件名与输入相同、扩展名换成目标格式。不要在命令行里调用 soffice,它在沙箱中无法运行。',
- schema: convertSchema,
- readOnly: false,
- async handler(raw, ctx) {
- const blocked = readonlyBlock(ctx)
- if (blocked) return blocked
- const args = raw as z.infer<typeof convertSchema>
- const out = await convertDocument({ workspaceDir: ctx.workspaceDir, input: args.input, format: args.format, outDir: args.out_dir })
- return ok(`已生成:${path.relative(ctx.workspaceDir, out)}`)
- },
- },
- {
- name: 'recalc_workbook',
- description: '用 LibreOffice 重新计算 Excel 文件中的所有公式并写回原文件,然后报告公式数量和错误单元格(#REF!、#DIV/0! 等)。'
- + '用 openpyxl 写入公式后必须调用它,否则文件里没有计算结果。不要在命令行里调用 soffice 或 recalc.py,它们在沙箱中无法运行。',
- schema: recalcSchema,
- readOnly: false,
- async handler(raw, ctx) {
- const blocked = readonlyBlock(ctx)
- if (blocked) return blocked
- const args = raw as z.infer<typeof recalcSchema>
- const r = await recalcWorkbook({ workspaceDir: ctx.workspaceDir, input: args.input, pythonRuntime: ctx.settings.paths.pythonRuntime })
- const lines = [`已重算:${path.relative(ctx.workspaceDir, r.path)},公式 ${r.formulas} 个,错误 ${r.errorCount} 个`]
- for (const e of r.errors) lines.push(`- ${e.sheet}!${e.cell}: ${e.error}`)
- return r.errorCount > 0 ? fail(lines.join('\n')) : ok(lines.join('\n'))
- },
- },
- {
- name: 'web_fetch',
- description: '抓取网页并转换为 Markdown(HTML),或原样返回文本/JSON;PDF、图片、表格等二进制文件会下载到工作区并返回路径。'
- + '内容过长时只返回前一部分,完整内容保存在工作区。不能访问内网地址(网络链接资料库所在站点除外)。',
- schema: fetchSchema,
- readOnly: true,
- async handler(raw, ctx) {
- const args = raw as z.infer<typeof fetchSchema>
- try {
- const r = await webFetch({ url: args.url, workspaceDir: ctx.workspaceDir, blockPrivate: ctx.settings.network.blockPrivateFetch, allowOrigins: linkOrigins(ctx.knowledgeRoots) })
- const head = `URL: ${r.url}\n状态: ${r.status}\n类型: ${r.contentType}${r.savedPath ? `\n${r.truncated ? '完整内容' : '文件'}已保存到: ${r.savedPath}` : ''}${r.truncated ? '\n(内容过长,以下为前一部分)' : ''}\n\n`
- return r.status >= 400 ? fail(head + r.text) : ok(head + r.text)
- } catch (err) {
- if (err instanceof FetchBlockedError) ctx.audit('network_denied', err.message, { url: args.url, engine: ctx.provider })
- throw err
- }
- },
- },
- {
- name: 'kb_list_roots',
- description: '列出当前可用的资料库(名称、类型、路径或链接、说明、读写属性、是否可用)。查找资料前先调用它。',
- schema: z.object({}),
- readOnly: true,
- async handler(_args, ctx) {
- if (!ctx.knowledgeRoots.length) return ok('当前没有可用的资料库。')
- return ok(describeKnowledge(ctx.knowledgeRoots))
- },
- },
- ]
- export const BUILTIN_TOOL_INFO: McpBuiltinToolDto[] = BUILTIN_TOOLS.map(t => ({ name: t.name, description: t.description, readOnly: t.readOnly }))
|