Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/03-核心模块详解.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 插入间隔、压缩阈值 |
Expand Down
3 changes: 3 additions & 0 deletions docs/04-工具系统.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 工具:`<服务器名>_<工具名>`(见下节)。
Expand All @@ -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`。
Expand Down
2 changes: 1 addition & 1 deletion docs/08-相关后端项目.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。

## 部署与一键管理

Expand Down
2 changes: 2 additions & 0 deletions src/config/configs/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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, "音乐服务配置")
}
Expand Down
2 changes: 1 addition & 1 deletion src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions src/tool/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
48 changes: 45 additions & 3 deletions src/tool/tools/image/tool_image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null> {
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",
Expand Down Expand Up @@ -61,14 +91,18 @@ 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']
}
}
});
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) {
Expand All @@ -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()}`;
Expand All @@ -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);

Expand Down
4 changes: 3 additions & 1 deletion src/tool/tools/image/tool_render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 });
Expand All @@ -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 });
Expand Down
3 changes: 2 additions & 1 deletion src/tool/tools/web/tool_web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 格式添加`;

Expand Down
Loading