diff --git "a/docs/03-\346\240\270\345\277\203\346\250\241\345\235\227\350\257\246\350\247\243.md" "b/docs/03-\346\240\270\345\277\203\346\250\241\345\235\227\350\257\246\350\247\243.md" index 478fa0d..f1b352e 100644 --- "a/docs/03-\346\240\270\345\277\203\346\250\241\345\235\227\350\257\246\350\247\243.md" +++ "b/docs/03-\346\240\270\345\277\203\346\250\241\345\235\227\350\257\246\350\247\243.md" @@ -49,7 +49,7 @@ | `received.ts` | 消息接收 | 接收图片/指令/骰子消息、忽略私聊、忽略豹语条件、忽略正则 | | `trigger.ts` | 消息触发 | 默认计数器/计时器/概率/活跃时间、向量相似度、触发正则、触发条件、令牌桶 | | `image.ts` | 图片 | 图片识别条件、URL 转 base64、识别 prompt | -| `tool.ts` | 工具 | 函数调用开关、提示词工程、连续调用次数(0=不限制)、禁止/默认关闭函数、牌堆、MCP(仅 JSON)、技能(仅标准 SKILL.md)、音色 | +| `tool.ts` | 工具 | 函数调用开关、提示词工程、连续调用次数(0=不限制)、禁止/默认关闭函数、牌堆、MCP 总开关与服务器配置、技能(仅标准 SKILL.md)、音色 | | `memory.ts` | 记忆 | 长期/总结/知识库开关、总结上限/间隔/参与轮数、短期记忆上限、知识库注入阈值、知识库(Markdown 模板) | | `reply.ts` | 回复 | 引用(含戳戳跳过)、最大字数、去空白、禁止复读与相似度、过滤正则与模板 | | `message.ts` | 对话 | 角色扮演设定(每条第一行为名称,以 `.` 开头隐藏)、示例对话、轮数、token 上限、system 插入间隔、压缩阈值 | diff --git "a/docs/04-\345\267\245\345\205\267\347\263\273\347\273\237.md" "b/docs/04-\345\267\245\345\205\267\347\263\273\347\273\237.md" index edc5735..f948648 100644 --- "a/docs/04-\345\267\245\345\205\267\347\263\273\347\273\237.md" +++ "b/docs/04-\345\267\245\345\205\267\347\263\273\347\273\237.md" @@ -74,6 +74,8 @@ > 注:`draw_deck` 支持 `send` 参数——`true` 时直接把抽牌结果发送给用户;`false` 或省略(默认)时仅把结果返回给 AI,由 AI 转述,便于结合剧情再加工。 +> 注:`text_to_image` 支持可选 `image` 参数进行以图生图。参数可传图片 ID、`user_avatar:...`、`group_avatar:...`、图片 URL、data URL 或 base64;tti 模型配置需要在请求体中使用 `{image}`、`{image_url}` 或 `{image_base64}` 占位符。 + 外部注册: - MCP 工具:`<服务器名>_<工具名>`(见下节)。 @@ -91,6 +93,7 @@ ## MCP 集成(src/tool/mcp.ts) +- 配置项 `是否启用MCP`(工具分组,默认关闭)是 MCP 总开关;开启后才会解析 `MCP服务器配置` 并连接/注册 MCP 工具。 - 配置项 `MCP服务器配置`(工具分组)只支持 JSON 格式的标准 `mcpServers` 块(Claude Desktop / Cursor `.mcp.json`):`{"mcpServers":{"名称":{"type":"http","url":"...","headers":{...}}}}`,支持任意自定义 headers 与 `token` 简写;`command`(stdio)与 `sse` 传输会明确提示后跳过。默认配置包含三个服务器:`mcp-files-exec`(文件执行)、`web-read`(网页读取)、`md-html-render`(Markdown/HTML 渲染),其中后两者是内置后端服务,由 `web_read` / `render_markdown` / `render_html` 包装工具调用,不会重复注册为 AI 工具;改 URL/Token 时服务器会话自动重建(热加载) - 协议:Streamable HTTP 上的 JSON-RPC(`initialize` → `notifications/initialized` → `tools/list` → `tools/call`),带 `Mcp-Session-Id` 会话头,兼容 SSE 响应。 - 工具以 `<服务器名>_<工具名>` 注册为敏感工具,参数透传 `inputSchema`。 diff --git "a/docs/08-\347\233\270\345\205\263\345\220\216\347\253\257\351\241\271\347\233\256.md" "b/docs/08-\347\233\270\345\205\263\345\220\216\347\253\257\351\241\271\347\233\256.md" index 66ff45a..57b6f8b 100644 --- "a/docs/08-\347\233\270\345\205\263\345\220\216\347\253\257\351\241\271\347\233\256.md" +++ "b/docs/08-\347\233\270\345\205\263\345\220\216\347\253\257\351\241\271\347\233\256.md" @@ -13,7 +13,7 @@ | `md-html-render` | Markdown/HTML 渲染为图片 | 37632 | 工具 → MCP服务器配置(md-html-render) | | `mcp-files-exec` | 本地文件读写/执行 | 3910 | 工具 → MCP服务器配置(mcp-files-exec) | -`web-read`、`md-html-render` 与 `mcp-files-exec` 均为 **MCP(Streamable HTTP)** 服务:插件通过「工具 → MCP服务器配置」中的 `mcpServers` 块按标准格式接入(默认 `http://127.0.0.1:46799/mcp`、`http://127.0.0.1:37632/mcp`、`http://127.0.0.1:3910/mcp`)。其中 `web-read` / `md-html-render` 由内置包装工具(`web_read` / `render_markdown` / `render_html`)调用,不重复注册为 AI 工具;`mcp-files-exec` 的工具(文件读写/执行)直接注册为 AI 工具。这些后端的 REST 接口已移除,仅提供 MCP。 +`web-read`、`md-html-render` 与 `mcp-files-exec` 均为 **MCP(Streamable HTTP)** 服务:插件需先在「工具 → 是否启用MCP」打开总开关,再通过「工具 → MCP服务器配置」中的 `mcpServers` 块按标准格式接入(默认 `http://127.0.0.1:46799/mcp`、`http://127.0.0.1:37632/mcp`、`http://127.0.0.1:3910/mcp`)。其中 `web-read` / `md-html-render` 由内置包装工具(`web_read` / `render_markdown` / `render_html`)调用,不重复注册为 AI 工具;`mcp-files-exec` 的工具(文件读写/执行)直接注册为 AI 工具。这些后端的 REST 接口已移除,仅提供 MCP。 ## 部署与一键管理 diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index 0b7d248..9f4523c 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -14,6 +14,7 @@ export default class ToolConfig { seal.ext.registerBoolConfig(ext, "是否允许调用所有指令", false, "开启后忽略白名单,允许调用所有可解析的指令;默认关闭,建议保持关闭以限制权限", "工具"); seal.ext.registerTemplateConfig(ext, "内置扩展列表", ['fun', 'story', 'coc7', 'deck', 'dnd5e', 'exp', 'log', 'reply'], "每行一个 SealDice 核心内置扩展名,用于「允许所有指令」模式下枚举其指令;可自行增删。修改后自动生效", "工具"); seal.ext.registerTemplateConfig(ext, "提供给AI的牌堆名称", [''], "每行一个牌堆名,示例:克苏鲁的呼唤;没有的话建议把 draw_deck 加入不允许调用", "工具"); + seal.ext.registerBoolConfig(ext, "是否启用MCP", false, "MCP 功能总开关;默认关闭,避免未安装 MCP 后端时启动或对话报错。开启后才会解析下方「MCP服务器配置」并连接/注册 MCP 工具", "工具"); seal.ext.registerTemplateConfig(ext, "MCP服务器配置", [ `{ "mcpServers": { @@ -119,6 +120,7 @@ description: 对指定玩家进行 san check(sc) ALLOW_ALL_CMDS: seal.ext.getBoolConfig(ext, "是否允许调用所有指令"), BUILTIN_EXT_NAMES: seal.ext.getTemplateConfig(ext, "内置扩展列表"), DECKS: seal.ext.getTemplateConfig(ext, "提供给AI的牌堆名称"), + MCP_ENABLED: seal.ext.getBoolConfig(ext, "是否启用MCP"), TTS_CHARACTER: seal.ext.getOptionConfig(ext, "ai语音使用的音色"), MUSIC: seal.ext.getTemplateConfig(ext, "音乐服务配置") } diff --git a/src/global.d.ts b/src/global.d.ts index b0483ac..5d596a5 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -18,7 +18,7 @@ interface AiDrawingApi { interface TtiApi { readonly name: string; readonly version: string; - generate(request: { text: string; negativeText?: string; model?: string }): Promise<{ success: boolean; type: 'image'; data: string; error?: string }>; + generate(request: { text: string; negativeText?: string; model?: string; image?: string }): Promise<{ success: boolean; type: 'image'; data: string; error?: string }>; } interface TtsApi { diff --git a/src/tool/mcp.ts b/src/tool/mcp.ts index 2e1695c..9d068e7 100644 --- a/src/tool/mcp.ts +++ b/src/tool/mcp.ts @@ -40,6 +40,10 @@ function sameServerConfig(a: MCPServer, b: MCPServer): boolean { } /** 按名称取最新配置:始终实时解析当前配置(热加载后立即生效),不保留已移除服务器的旧会话 */ +export function isMCPEnabled(): boolean { + return seal.ext.getBoolConfig(ext, "是否启用MCP"); +} + export function getMCPServerByName(name: string): MCPServer | null { // 只返回仍存在于配置中的服务器:服务器被移除后,即使工具列表尚未清理,调用也立即失败而非继续使用旧地址 return getMCPServers().find(s => s.name === name) || null; @@ -85,6 +89,8 @@ function normalizeMCPServer(name: string, cfg: any): MCPServer | null { } function getMCPServers(): MCPServer[] { + if (!isMCPEnabled()) return []; + const servers: MCPServer[] = []; for (const line of seal.ext.getTemplateConfig(ext, "MCP服务器配置").map(l => (l || '').trim()).filter(Boolean)) { try { diff --git a/src/tool/tools/image/tool_image.ts b/src/tool/tools/image/tool_image.ts index dae0c79..3ddf9bf 100644 --- a/src/tool/tools/image/tool_image.ts +++ b/src/tool/tools/image/tool_image.ts @@ -4,6 +4,36 @@ import Image from "../../../resource/image"; import { generateId } from "../../../utils/utils"; import Tool from "../../tool"; +async function resolveTtiImage(ctx: seal.MsgContext, session: any, raw: string): Promise { + let input = String(raw || '').trim(); + if (!input) return null; + + const tagMatch = input.match(/^[[[]img[::]\s?([^\]]]+)[\]]]$/i); + if (tagMatch) input = tagMatch[1].trim(); + + if (/^data:/i.test(input) || /^https?:\/\//i.test(input)) return input; + + // 直接传入 base64 时原样返回;长度阈值用于避免把普通图片 ID 误判为 base64。 + const compact = input.replace(/\s+/g, ''); + if (compact.length >= 64 && /^[A-Za-z0-9+/=]+$/.test(compact)) return input; + + const image = await session.context.findImage(ctx, input) + || (input.includes(':') ? await session.context.findImage(ctx, input.split(':')[0]) : null); + if (!image) return null; + if (image.type === 'local') return null; + + if (image.type === 'url') { + try { + await image.urlToBase64(); + } catch (e) { + logger.warning(`参考图URL转base64失败: ${e}`); + } + return image.base64 ? image.base64Url : image.src; + } + + return image.base64Url; +} + export function registerImage() { const toolITT = new Tool({ type: "function", @@ -61,6 +91,10 @@ export function registerImage() { name: { type: "string", description: "如果保存图片,图片的名称" + }, + image: { + type: "string", + description: "可选参考图,用于以图生图。支持图片id、user_avatar:用户名称或纯数字QQ号、group_avatar:群聊名称或纯数字群号、http(s)图片URL、data:image/...;base64或base64数据" } }, required: ['prompt', 'save', 'name'] @@ -68,7 +102,7 @@ export function registerImage() { } }); toolTTI.solve = async (ctx, msg, session, args) => { - const { prompt, negative_prompt, save, name } = args; + const { prompt, negative_prompt, save, name, image } = args; const ext = seal.ext.find('tti'); if (!ext) { @@ -84,7 +118,15 @@ export function registerImage() { try { // tti 统一 API if (globalThis.tti && typeof globalThis.tti.generate === 'function') { - const result = await globalThis.tti.generate({ text: prompt, negativeText: negative_prompt }); + const request: { text: string; negativeText?: string; image?: string } = { text: prompt }; + if (negative_prompt) request.negativeText = negative_prompt; + if (image) { + const reference = await resolveTtiImage(ctx, session, image); + if (!reference) return `未找到参考图${image}`; + request.image = reference; + } + + const result = await globalThis.tti.generate(request); if (!result.success) throw new Error(result.error || '图像生成失败'); const img = new Image(); img.imageId = `${name}_${generateId()}`; @@ -103,7 +145,7 @@ export function registerImage() { } img.format = img.format || 'unknown'; - img.description = `AI绘图[img:${img.imageId}]\n${prompt ? `描述: ${prompt}` : ''}\n${negative_prompt ? `不希望出现: ${negative_prompt}` : ''}`; + img.description = `AI绘图[img:${img.imageId}]\n${prompt ? `描述: ${prompt}` : ''}\n${negative_prompt ? `不希望出现: ${negative_prompt}` : ''}\n${image ? `参考图: 已提供` : ''}`; if (save) session.memory.addMemory(ctx, session, [], [], kws, [img], img.description); diff --git a/src/tool/tools/image/tool_render.ts b/src/tool/tools/image/tool_render.ts index e2d54a9..ee1d124 100644 --- a/src/tool/tools/image/tool_render.ts +++ b/src/tool/tools/image/tool_render.ts @@ -4,7 +4,7 @@ import Image from "../../../resource/image"; import { Session } from "../../../session/session"; import { parseSpecialTokens } from "../../../utils/string"; import { generateId } from "../../../utils/utils"; -import { callServerTool, getMCPServerByName } from "../../mcp"; +import { callServerTool, getMCPServerByName, isMCPEnabled } from "../../mcp"; import Tool from "../../tool"; interface RenderResponse { @@ -83,6 +83,7 @@ async function transformContentToUrlText(ctx: seal.MsgContext, session: Session, // Markdown 渲染 async function renderMarkdown(markdown: string, theme: 'light' | 'dark' | 'gradient' = 'light', width = 1200, hasImages = false) { + if (!isMCPEnabled()) throw new Error(`MCP 功能未启用:请在「工具 → 是否启用MCP」中开启,并确认已配置 MCP 服务器 md-html-render`); const mcpServer = getMCPServerByName('md-html-render'); if (!mcpServer) throw new Error(`未配置 MCP 服务器 md-html-render:请在「工具 → MCP服务器配置」中按标准 mcpServers 格式添加`); const base64 = await callServerTool(mcpServer, 'render_markdown', { markdown, theme, width, quality: 90, hasImages }); @@ -92,6 +93,7 @@ async function renderMarkdown(markdown: string, theme: 'light' | 'dark' | 'gradi // HTML 渲染 async function renderHtml(html: string, width = 1200, hasImages = false) { + if (!isMCPEnabled()) throw new Error(`MCP 功能未启用:请在「工具 → 是否启用MCP」中开启,并确认已配置 MCP 服务器 md-html-render`); const mcpServer = getMCPServerByName('md-html-render'); if (!mcpServer) throw new Error(`未配置 MCP 服务器 md-html-render:请在「工具 → MCP服务器配置」中按标准 mcpServers 格式添加`); const base64 = await callServerTool(mcpServer, 'render_html', { html, theme: 'light', width, quality: 90, hasImages }); diff --git a/src/tool/tools/web/tool_web.ts b/src/tool/tools/web/tool_web.ts index 978571c..f11ddc4 100644 --- a/src/tool/tools/web/tool_web.ts +++ b/src/tool/tools/web/tool_web.ts @@ -3,7 +3,7 @@ import Config from "../../../config/config"; import { logger } from "../../../logger"; import Image from "../../../resource/image"; import { generateId } from "../../../utils/utils"; -import { callServerTool, getMCPServerByName } from "../../mcp"; +import { callServerTool, getMCPServerByName, isMCPEnabled } from "../../mcp"; import Tool from "../../tool"; export function registerWeb() { @@ -126,6 +126,7 @@ export function registerWeb() { }); tool.solve = async (_, __, ___, args) => { const { url, screenshot = false, width, height, fullPage = false, delay } = args; + if (!isMCPEnabled()) return `MCP 功能未启用:请在「工具 → 是否启用MCP」中开启,并确认已配置 MCP 服务器 web-read`; const mcpServer = getMCPServerByName('web-read'); if (!mcpServer) return `未配置 MCP 服务器 web-read:请在「工具 → MCP服务器配置」中按标准 mcpServers 格式添加`;