From d6508793eb7ec2208f0af3099ced3661f7c2ddc3 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Mon, 17 Aug 2026 22:18:09 +0800 Subject: [PATCH 01/20] feat: add OB11 core command bridge tools --- README.md | 6 +- ...45\345\205\267\347\263\273\347\273\237.md" | 15 +- ...70\350\247\201\351\227\256\351\242\230.md" | 3 +- src/config/configs/backend.ts | 6 +- src/config/configs/tool.ts | 22 +- src/config/configs/trigger.ts | 8 +- src/integration/core_bridge/client.ts | 135 ++++++++++ src/integration/core_bridge/types.ts | 41 +++ src/pipeline.ts | 4 +- src/session/session.ts | 58 ++++- src/tool/command_catalog.ts | 116 +++++++++ src/tool/tool.ts | 46 ++-- src/tool/tools/core/tool_cmd.ts | 243 +++++++----------- src/tool/types.ts | 6 +- src/update.ts | 11 +- 15 files changed, 506 insertions(+), 214 deletions(-) create mode 100644 src/integration/core_bridge/client.ts create mode 100644 src/integration/core_bridge/types.ts create mode 100644 src/tool/command_catalog.ts diff --git a/README.md b/README.md index 58c6124..15d525e 100644 --- a/README.md +++ b/README.md @@ -410,7 +410,7 @@ max_tokens = 2048 | 消息 | `send_msg`、`get_msg`、`delete_msg`、`send_forward_msg`、`get_context` | | 定时 | `set_timer`、`show_timer_list`、`cancel_timer` | | 触发 | `set_trigger_condition` | -| 指令 | `run_command`(列出/调用海豹指令)、`get_cmd_help`(查看指令帮助) | +| 指令 | `run_ext_command`(调用扩展指令)、`run_core_command`(调用核心指令) | | 工具调度 | `search_tools`(按需搜索工具)、`call_tool`(统一执行任意工具) | | 语音 | `record`、`text_to_sound` | | 网页 | `web_search`、`web_read` | @@ -425,11 +425,11 @@ max_tokens = 2048 | 论坛 | `forum_get_posts`、`forum_get_post_detail`、`forum_search`、`forum_create_post`、`forum_manage_comment`、`forum_get_activity`、`forum_manage_post` | | MCP / 技能 | `<服务器名>_<工具名>`(MCP 工具)、`use_skill`(技能) | -> 指令类技能(今日人品、COC 模组抽取/搜索、属性展示、属性检定、san 检定等)通过 `use_skill` 按需获取内容,内部统一使用 `run_command` 调用海豹指令,对应指令需加入「可调用指令白名单」。 +> 指令类技能(今日人品、COC 模组抽取/搜索、属性展示、属性检定、san 检定等)通过 `use_skill` 按需获取内容,内部统一使用 `run_ext_command` / `run_core_command` 调用海豹指令,对应指令需加入「可调用指令白名单」。 > 依赖说明:ob11 相关工具需要安装 [ob11 网络连接依赖](https://raw.githubusercontent.com/error2913/sealdice-plugin-ob11-net-connection/refs/heads/main/dist/ob11%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%E4%BE%9D%E8%B5%96.js) 或 [http 依赖插件](https://github.com/error2913/sealdice-js/blob/main/HTTP%E4%BE%9D%E8%B5%96.js);`text_to_sound` 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖与 ffmpeg;`text_to_image` 需要生成图片依赖;`music_play` 需要协议端配置音卡签名;`render_markdown` / `render_html` 需要配置 md 和 html 图片渲染后端。 -> 依赖海豹内置指令的工具(如 `draw_deck`、`send_msg` 等)需要会话中先出现过指令消息(如先使用 `.r`),否则工具会提示"请先使用 .r 指令"。 +> 扩展/核心指令工具通过 OB11 核心桥注入假消息,不再要求会话先出现 `.r`;请启动 `ob11-core-bridge`,让 SealDice 的 OB11 网络依赖连接中间件 `/core`,并将插件「后端 → 核心指令中转WS地址」配置为中间件 `/control`。 --- 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 e595995..3e2b631 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" @@ -15,7 +15,7 @@ | `sensitive` | 敏感工具标记(发消息/禁言/改名等),执行日志显著记录 | | `solve(ctx, msg, session, args)` | 工具实现,返回字符串作为给 AI 的结果 | -`Tool.extensionSolve` 复用最近一次指令消息的 `cmdArgs`(`Tool.cmdArgs`),构造临时参数调用海豹扩展指令,并通过 `session.tool.listen` 监听该指令的发送消息作为返回值(10 秒超时)。因此**依赖海豹指令的工具需要先收到过指令消息**(如先使用 `.r`),否则返回提示。 +`run_ext_command` 与 `run_core_command` 通过 `aiplugin4-backends/ob11-core-bridge` 的控制 WS 注入标准 OB11 假消息,不再依赖最近一次真实指令。`run_ext_command` 的扩展名分为硬编码的 `builtin`(fun/story/coc7/deck/dnd5e/exp/log/reply)与 `non_builtin`;核心指令统一使用虚拟扩展名 `core`,例如 `core|ext`。指令前缀由「工具 → 指令前缀」配置,默认 `.`。旧的 `Tool.extensionSolve` 仍保留兼容路径,监听器改为多消息收集器,使用空闲窗口、最大消息数和超时结束。 ## 调用流程与校验链 @@ -23,7 +23,7 @@ 1. 函数是否已注册(`toolMap`)。 2. 会话工具开关 `session.toolState[name]` 是否开启。 -3. 扩展指令工具是否有可用的 `cmdArgs`。 +3. 工具参数是否完整、扩展指令是否在白名单中(`core|ext` 为例外发现入口)。 4. `sessionType` 是否匹配当前消息类型。 5. 参数 JSON 是否可解析(失败时用 `fixJsonString` 修复)。 6. 必需参数是否齐全;`Tool.validateArgs` 按 JSON Schema 的 type 做轻量校验。 @@ -49,6 +49,17 @@ 实现位置:`src/context/context.ts` 的 `addToolCallbackMessage`,调用侧由 `Tool.handleToolCall` 在结果上补充 `toolName`/`searchTarget` 字段。 +## OB11 核心指令中转 + +中间件位于 `aiplugin4-backends/backends/ob11-core-bridge`。SealDice 的 OB11 网络依赖作为 WebSocket 客户端连接中间件 `/core`;协议端(NapCat/LLOneBot 或测试模拟器)连接 `/onebot`;插件控制端连接 `/control`。控制调用使用独立 JSON 协议,不混入原始 OB11 事件。 + +- 中间件原样转发协议端与核心之间的 OB11 JSON;核心的 `send_msg`/`send_group_msg` 等 Action,以及 lane 模式下的 bot 消息事件,都可按当前调用的会话 lane 捕获。 +- 默认 `forward=false`:捕获后的 Action/消息事件不发送到协议端,但向核心返回成功 API 响应,避免核心阻塞;`forward=true` 则捕获并继续转发。工具可通过 `captureMode`、`maxMessages`、`settleMs`、`timeoutMs` 调整监听策略。 +- 同一群/私聊 lane 串行化,单次调用使用 `settleMs` 空闲窗口收集多条消息;结果含 `ambiguous`、`completedBy`,因为纯 OB11 没有标准 trace_id,无法保证跨同 lane 的绝对关联。 +- `run_core_command` 的 `command=ext` 直接调用核心 `.ext`,用于查看所有扩展名称,不需要在配置中维护扩展列表。 + +中间件配置:`AIPLUGIN4_BACKEND_HOST`(默认 `0.0.0.0`)、`AIPLUGIN4_BACKEND_PORT`(默认 `46880`)、`AIPLUGIN4_BRIDGE_TOKEN`(控制端鉴权)、`AIPLUGIN4_BRIDGE_CORE_TOKEN` / `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN`(可分别覆盖核心端/协议端鉴权),以及可选的 `AIPLUGIN4_BRIDGE_CORE_PATH`、`AIPLUGIN4_BRIDGE_ONEBOT_PATH`、`AIPLUGIN4_BRIDGE_CONTROL_PATH`。插件配置:`后端 → 核心指令中转WS地址` 填 `/control` 地址,另填 `核心指令中转Token`。SealDice 侧的 OB11 WS 配置应填 `/core` 地址。 + ## 内置工具清单 以下工具名来自 `src/tool/tools/` 各注册函数(基于当前代码): diff --git "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" index e00b868..e5ec1de 100644 --- "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" +++ "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -23,8 +23,9 @@ ## 运行与部署注意事项 - SealDice v1.4.6 分离部署(napcat/llonebot)下图片相关功能存在问题,建议 v1.5.0+。 -- 依赖海豹内置指令的工具(如 `attr_show`、`roll_check`、`draw_deck`、`send_msg` 等经 `ExtCmdInfo` 调用)需要 `Tool.cmdArgs` 非空,即会话中先出现过指令消息(如先使用 `.r`);否则工具会提示"请先使用 .r 指令"。 +- `run_ext_command` / `run_core_command` 需要启动 `ob11-core-bridge` 并配置控制 WS;核心指令白名单使用 `core|指令名`,但 `core|ext` 无需加入白名单即可查看扩展。旧的本地 `Tool.extensionSolve` 仅用于兼容第三方工具,已升级为多消息收集器。 - ob11 相关工具(发消息、取用户/群信息、禁言、打卡、AI 语音等)需要安装 ob11 网络连接依赖或 HTTP 依赖;未安装时相关工具返回"未找到ob11网络连接依赖"。 +- 中间件捕获消息默认不转发到协议端;若希望真实 QQ 也看到核心动作,需在控制协议中设置 `capture.forward=true`。核心 WS、协议 WS、插件控制 WS 分别使用 `/core`、`/onebot`、`/control` 路径。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 - 所有配置修改后自动生效(缓存最多 1 分钟,无需重载 JS);MCP 服务器增删、技能配置、音乐服务等由缓存 TTL 兜底。 - 嵌入模型输出维度取自嵌入模型配置 `[body] dimensions`(默认 1024,须与后端一致,如 text-embedding-v4 为 1024);未配置维度时记忆检索自动降级为关键词/分数检索、知识库检索自动降级为关键词匹配(嵌入仅作候选重排,加载知识库本身不请求嵌入)。 - 流式输出需要自建或使用公共后端,并在"后端 → 流式输出"配置 URL;`body.stream = true` 的模型才会走流式。 diff --git a/src/config/configs/backend.ts b/src/config/configs/backend.ts index f8b6c72..82842c4 100644 --- a/src/config/configs/backend.ts +++ b/src/config/configs/backend.ts @@ -10,6 +10,8 @@ export default class BackendConfig { seal.ext.registerStringConfig(ext, "论坛地址", "https://aiplugin-forum.fishwhite.top", 'aiplugin4 专用论坛地址,用于论坛工具;一般保持默认', "后端"); seal.ext.registerStringConfig(ext, "论坛API Token", "", '论坛注册后获取的 api_token,用于发帖等写操作的鉴权;不填则只能使用只读功能', "后端"); seal.ext.registerStringConfig(ext, "论坛签名密钥", "", '论坛注册后获取的 secret_key,用于请求签名;与「论坛API Token」配套填写', "后端"); + seal.ext.registerStringConfig(ext, "核心指令中转WS地址", "ws://127.0.0.1:46880/control", 'aiplugin4-backends/ob11-core-bridge 的控制 WebSocket 地址;留空则 run_ext_command/run_core_command 不可用', "后端"); + seal.ext.registerStringConfig(ext, "核心指令中转Token", "", '与中间件 AIPLUGIN4_BRIDGE_TOKEN 一致;不需要鉴权时留空', "后端"); } static get() { @@ -20,7 +22,9 @@ export default class BackendConfig { USAGE_CHART: seal.ext.getStringConfig(ext, "用量图表"), FORUM_URL: seal.ext.getStringConfig(ext, "论坛地址"), FORUM_API_TOKEN: seal.ext.getStringConfig(ext, "论坛API Token"), - FORUM_SECRET_KEY: seal.ext.getStringConfig(ext, "论坛签名密钥") + FORUM_SECRET_KEY: seal.ext.getStringConfig(ext, "论坛签名密钥"), + CORE_BRIDGE_WS: seal.ext.getStringConfig(ext, "核心指令中转WS地址"), + CORE_BRIDGE_TOKEN: seal.ext.getStringConfig(ext, "核心指令中转Token") } } } diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index 9f4523c..7306b69 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -10,9 +10,9 @@ export default class ToolConfig { seal.ext.registerIntConfig(ext, "工具响应压缩触发字数", 5000, "工具返回结果超过该字数时压缩后再存入上下文;设为 0 不压缩", "工具"); seal.ext.registerTemplateConfig(ext, "禁止调用的函数", [''], "每行一个禁止 AI 调用的函数名,示例:draw_deck;修改后自动生效", "工具"); seal.ext.registerTemplateConfig(ext, "默认关闭的函数", [''], "每行一个默认关闭的函数名,AI 默认无法调用,示例:get_msg;修改后自动生效", "工具"); - seal.ext.registerTemplateConfig(ext, "可调用指令白名单", ['fun|jrrp', 'story|modu', 'coc7|st', 'coc7|ra', 'coc7|sc'], "每行一个 AI 可调用的海豹指令;格式:扩展名|指令名(指令与插件同名时可只写指令名)。示例:wifeOfTheDay|今日老婆、fun|jrrp、coc7|st。默认包含内置技能所需指令,可自行增删。修改后自动生效", "工具"); - seal.ext.registerBoolConfig(ext, "是否允许调用所有指令", false, "开启后忽略白名单,允许调用所有可解析的指令;默认关闭,建议保持关闭以限制权限", "工具"); - seal.ext.registerTemplateConfig(ext, "内置扩展列表", ['fun', 'story', 'coc7', 'deck', 'dnd5e', 'exp', 'log', 'reply'], "每行一个 SealDice 核心内置扩展名,用于「允许所有指令」模式下枚举其指令;可自行增删。修改后自动生效", "工具"); + seal.ext.registerTemplateConfig(ext, "可调用指令白名单", ['fun|jrrp', 'story|modu', 'coc7|st', 'coc7|ra', 'coc7|sc'], "每行一个 AI 可调用的海豹指令;格式:扩展名|指令名,核心指令的扩展名统一写 core(如 core|ext)。run_core_command 的 core|ext 无需加入白名单。修改后自动生效", "工具"); + seal.ext.registerBoolConfig(ext, "是否允许调用所有指令", false, "开启后忽略白名单,允许调用所有可解析的扩展指令;核心指令仍通过 run_core_command 调用", "工具"); + seal.ext.registerStringConfig(ext, "指令前缀", ".", "注入到 SealDice 核心的指令前缀,通常为 .;如果核心改成其他前缀,请同步修改", "工具"); seal.ext.registerTemplateConfig(ext, "提供给AI的牌堆名称", [''], "每行一个牌堆名,示例:克苏鲁的呼唤;没有的话建议把 draw_deck 加入不允许调用", "工具"); seal.ext.registerBoolConfig(ext, "是否启用MCP", false, "MCP 功能总开关;默认关闭,避免未安装 MCP 后端时启动或对话报错。开启后才会解析下方「MCP服务器配置」并连接/注册 MCP 工具", "工具"); seal.ext.registerTemplateConfig(ext, "MCP服务器配置", [ @@ -41,33 +41,33 @@ export default class ToolConfig { name: 今日人品 description: 查询指定用户的今日人品值 --- -使用 run_command 工具执行:action=call,command="fun|jrrp",args=["用户名或QQ号"];fun|jrrp 需在「可调用指令白名单」中`, +使用 run_ext_command 工具执行:action=call,command="fun|jrrp",args=["用户名或QQ号"];fun|jrrp 需在「可调用指令白名单」中`, `--- name: COC模组抽取 description: 随机抽取一个 COC 模组 --- -使用 run_command 工具执行:action=call,command="story|modu",args=["roll"];story|modu 需在「可调用指令白名单」中`, +使用 run_ext_command 工具执行:action=call,command="story|modu",args=["roll"];story|modu 需在「可调用指令白名单」中`, `--- name: COC模组搜索 description: 按关键词搜索 COC 模组 --- -使用 run_command 工具执行:action=call,command="story|modu",args=["search","关键词"];story|modu 需在「可调用指令白名单」中`, +使用 run_ext_command 工具执行:action=call,command="story|modu",args=["search","关键词"];story|modu 需在「可调用指令白名单」中`, `--- name: 属性展示 description: 展示指定玩家的 COC 全部个人属性 --- -使用 run_command 工具执行:action=call,command="coc7|st",args=["show","玩家名称或QQ号"];coc7|st 需在「可调用指令白名单」中`, +使用 run_ext_command 工具执行:action=call,command="coc7|st",args=["show","玩家名称或QQ号"];coc7|st 需在「可调用指令白名单」中`, `--- name: 属性检定 description: 对指定玩家进行一次属性/技能检定(ra) --- -使用 run_command 工具执行:action=call,command="coc7|ra",args 按顺序:奖励/惩罚骰(可选,如 b、p3)、检定表达式(含难度等级或数值运算时直接用,普通属性名时用该属性,属性为0时补50)、检定原因(可选);coc7|ra 需在「可调用指令白名单」中`, +使用 run_ext_command 工具执行:action=call,command="coc7|ra",args 按顺序:奖励/惩罚骰(可选,如 b、p3)、检定表达式(含难度等级或数值运算时直接用,普通属性名时用该属性,属性为0时补50)、检定原因(可选);coc7|ra 需在「可调用指令白名单」中`, `--- name: san检定 description: 对指定玩家进行 san check(sc) --- -使用 run_command 工具执行:action=call,command="coc7|sc",args 按顺序:奖励/惩罚骰(可选,如 b、p2)、表达式(成功时掉san/失败时掉san,如 0/1d6、0/1);coc7|sc 需在「可调用指令白名单」中` - ], "每条配置项一个技能,仅支持标准 SKILL.md 格式:以 --- 开头的 YAML frontmatter 里写 name(必填)/description(可选),正文为技能内容;可直接粘贴其他 agent(Claude/Codex/Cursor)的技能文件。\n格式定义见 https://agentskills.io/specification (SKILL.md 开放规范,Claude/Codex 通用,国内可访问)。\n默认包含基于 run_command 统一调用的指令技能(今日人品/COC模组/属性展示/检定等),指令需加入「可调用指令白名单」,可自行增删。修改后自动生效(缓存最多 1 分钟)。AI 可通过 use_skill 工具按需调用", "工具"); +使用 run_ext_command 工具执行:action=call,command="coc7|sc",args 按顺序:奖励/惩罚骰(可选,如 b、p2)、表达式(成功时掉san/失败时掉san,如 0/1d6、0/1);coc7|sc 需在「可调用指令白名单」中` + ], "每条配置项一个技能,仅支持标准 SKILL.md 格式:以 --- 开头的 YAML frontmatter 里写 name(必填)/description(可选),正文为技能内容;可直接粘贴其他 agent(Claude/Codex/Cursor)的技能文件。\n格式定义见 https://agentskills.io/specification (SKILL.md 开放规范,Claude/Codex 通用,国内可访问)。\n默认包含基于 run_ext_command 统一调用的扩展技能(今日人品/COC模组/属性展示/检定等),指令需加入「可调用指令白名单」,可自行增删。修改后自动生效(缓存最多 1 分钟)。AI 可通过 use_skill 工具按需调用", "工具"); seal.ext.registerTemplateConfig(ext, "音乐服务配置", [ `{ "platform": "网易云", @@ -118,7 +118,7 @@ description: 对指定玩家进行 san check(sc) DEFAULT_CLOSED: seal.ext.getTemplateConfig(ext, "默认关闭的函数"), CMD_WHITELIST: seal.ext.getTemplateConfig(ext, "可调用指令白名单"), ALLOW_ALL_CMDS: seal.ext.getBoolConfig(ext, "是否允许调用所有指令"), - BUILTIN_EXT_NAMES: seal.ext.getTemplateConfig(ext, "内置扩展列表"), + COMMAND_PREFIX: seal.ext.getStringConfig(ext, "指令前缀"), DECKS: seal.ext.getTemplateConfig(ext, "提供给AI的牌堆名称"), MCP_ENABLED: seal.ext.getBoolConfig(ext, "是否启用MCP"), TTS_CHARACTER: seal.ext.getOptionConfig(ext, "ai语音使用的音色"), diff --git a/src/config/configs/trigger.ts b/src/config/configs/trigger.ts index 7089264..ee30ee5 100644 --- a/src/config/configs/trigger.ts +++ b/src/config/configs/trigger.ts @@ -4,14 +4,14 @@ import { getRegexConfig } from "../config"; export default class TriggerConfig { static register() { - seal.ext.registerIntConfig(ext, "默认计数器", 10, "计数器模式下达到该条数触发回复", "消息触发"); - seal.ext.registerFloatConfig(ext, "默认计时器", 60, "计时器模式下间隔多少秒触发回复", "消息触发"); - seal.ext.registerFloatConfig(ext, "默认概率", 10, "概率模式下每条消息触发回复的概率(%)", "消息触发"); - seal.ext.registerStringConfig(ext, "默认触发活跃时间", "10:00-20:00-5", "格式:HH:mm-HH:mm-次数,示例 10:00-20:00-5 表示 10:00-20:00 之间最多触发 5 次", "消息触发"); seal.ext.registerTemplateConfig(ext, "触发正则表达式", [ "\\[CQ:at,qq=3893625976\\]", "^正确.*[。?!?!]$" ], "每行一个正则,任一命中即触发回复(如 @机器人 或包含关键词);示例:^你好.*;修改后自动生效(缓存最多 1 分钟)", "消息触发"); + seal.ext.registerIntConfig(ext, "默认计数器", 10, "计数器模式下达到该条数触发回复", "消息触发"); + seal.ext.registerFloatConfig(ext, "默认计时器", 60, "计时器模式下间隔多少秒触发回复", "消息触发"); + seal.ext.registerFloatConfig(ext, "默认概率", 10, "概率模式下每条消息触发回复的概率(%)", "消息触发"); + seal.ext.registerStringConfig(ext, "默认触发活跃时间", "10:00-20:00-5", "格式:HH:mm-HH:mm-次数,示例 10:00-20:00-5 表示 10:00-20:00 之间最多触发 5 次", "消息触发"); seal.ext.registerStringConfig(ext, "触发需要满足的条件", '1', "额外的豹语表达式条件,命中为 1 才触发;示例:$t群号_RAW=='2001',不需要额外条件时填 1", "消息触发"); seal.ext.registerIntConfig(ext, "触发次数上限", 3, "消息触发令牌桶容量,达到上限后需等待补充", "消息触发"); seal.ext.registerIntConfig(ext, "触发次数补充间隔", 3, "令牌桶补充间隔(秒)", "消息触发"); diff --git a/src/integration/core_bridge/client.ts b/src/integration/core_bridge/client.ts new file mode 100644 index 0000000..93e246d --- /dev/null +++ b/src/integration/core_bridge/client.ts @@ -0,0 +1,135 @@ +import Config from "../../config/config"; +import { CoreBridgeInvocation, CoreBridgeResult } from "./types"; + +const WS_OPEN = 1; +let sequence = 0; + +function getWebSocket(): any { + const ctor = (globalThis as any).WebSocket; + if (typeof ctor !== 'function') throw new Error('当前海豹环境没有可用的 WebSocket;请安装支持 WebSocket 的 ob11 网络连接依赖'); + return ctor; +} + +function nextId(): string { + sequence = (sequence + 1) % 100000; + return `invoke_${Date.now().toString(36)}_${sequence.toString(36)}`; +} + +export class CoreBridgeClient { + private static ws: any = null; + private static connecting: Promise | null = null; + private static pending: { [id: string]: { resolve: (result: CoreBridgeResult) => void, reject: (error: Error) => void, timer: any } } = {}; + + static reset(): void { + this.failPending(new Error('核心指令中转 WS 已重置')); + if (this.ws && typeof this.ws.close === 'function') { + try { this.ws.close(); } catch (_) { /* ignore */ } + } + this.ws = null; + this.connecting = null; + } + + private static failPending(error: Error): void { + const pending = this.pending; + this.pending = {}; + Object.keys(pending).forEach(id => { + clearTimeout(pending[id].timer); + pending[id].reject(error); + }); + } + + private static async connect(): Promise { + const configuredUrl = Config.backend.CORE_BRIDGE_WS; + if (!configuredUrl) throw new Error('未配置「后端 → 核心指令中转WS地址」'); + if (this.ws && this.ws.readyState === WS_OPEN) return this.ws; + if (this.connecting) return this.connecting; + const WebSocketCtor = getWebSocket(); + const configuredToken = Config.backend.CORE_BRIDGE_TOKEN; + const url = configuredToken + ? `${configuredUrl}${configuredUrl.indexOf('?') >= 0 ? '&' : '?'}access_token=${encodeURIComponent(configuredToken)}` + : configuredUrl; + this.connecting = new Promise((resolve, reject) => { + let settled = false; + const ws = new WebSocketCtor(url); + const fail = (error: Error) => { + if (!settled) { settled = true; reject(error); } + this.failPending(error); + if (this.ws === ws) this.ws = null; + }; + ws.onopen = () => { + ws.send(JSON.stringify({ + type: 'hello', protocol: 'aiplugin4-core-bridge', version: 1, + client: 'aiplugin4', token: Config.backend.CORE_BRIDGE_TOKEN + })); + }; + ws.onmessage = (event: any) => { + let packet: any; + try { packet = JSON.parse(String(event && event.data !== undefined ? event.data : event)); } + catch (_) { return; } + if (packet.type === 'hello.ok') { + settled = true; this.ws = ws; resolve(ws); return; + } + if (packet.type === 'command.result' && packet.id) { + const waiter = this.pending[String(packet.id)]; + if (!waiter) return; + clearTimeout(waiter.timer); delete this.pending[String(packet.id)]; waiter.resolve(packet as CoreBridgeResult); + } + }; + ws.onerror = () => fail(new Error('核心指令中转 WS 连接失败')); + ws.onclose = () => { + const error = new Error('核心指令中转 WS 已断开'); + this.failPending(error); + if (this.ws === ws) this.ws = null; + if (!settled) fail(error); + }; + }).then(value => { this.connecting = null; return value; }, error => { this.connecting = null; throw error; }); + return this.connecting; + } + + static async invoke(request: CoreBridgeInvocation): Promise { + const ws = await this.connect(); + const id = nextId(); + const timeoutMs = Math.max(1000, Math.min(Number(request.timeoutMs || 10000), 120000)); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + delete this.pending[id]; reject(new Error(`核心指令等待超时(${timeoutMs}ms)`)); + }, timeoutMs + 1000); + this.pending[id] = { resolve, reject, timer }; + try { + ws.send(JSON.stringify({ type: 'command.invoke', id, ...request, timeoutMs })); + } catch (e) { + clearTimeout(timer); delete this.pending[id]; reject(e instanceof Error ? e : new Error(String(e))); + } + }); + } + + static async call(ctx: seal.MsgContext, command: string, args: string[], capture?: CoreBridgeInvocation['capture'], timeoutMs?: number): Promise { + const prefix = Config.tool.COMMAND_PREFIX; + const raw = `${prefix}${command}${args.length ? ` ${args.join(' ')}` : ''}`.trim(); + const target: CoreBridgeInvocation['target'] = { + selfId: String(ctx.endPoint.userId || '').replace(/^.+:/, ''), + messageType: ctx.isPrivate ? 'private' : 'group', + userId: String(ctx.player && ctx.player.userId || '').replace(/^.+:/, '') + }; + if (!ctx.isPrivate) target.groupId = String(ctx.group && ctx.group.groupId || '').replace(/^.+:/, ''); + return this.invoke({ + target, + actor: { + userId: target.userId || target.selfId, + nickname: String(ctx.player && ctx.player.name || 'AI'), + role: 'member' + }, + command: { raw, name: command, args }, + capture, + timeoutMs + }); + } +} + +export function formatCoreBridgeResult(result: CoreBridgeResult): string { + if (!result.ok) return `中转执行失败:${result.error || '未知错误'}`; + const texts = (result.messages || []).map(item => item.text || '').filter(Boolean); + const body = texts.length ? texts.join('\n') : '核心未返回文本消息'; + const flags = [result.ambiguous ? '消息关联存在歧义' : '', result.completedBy ? `结束方式:${result.completedBy}` : ''].filter(Boolean); + return `${body}${flags.length ? `\n(${flags.join(',')})` : ''}`; +} diff --git a/src/integration/core_bridge/types.ts b/src/integration/core_bridge/types.ts new file mode 100644 index 0000000..907a00e --- /dev/null +++ b/src/integration/core_bridge/types.ts @@ -0,0 +1,41 @@ +export interface CoreBridgeTarget { + selfId: string; + messageType: 'group' | 'private'; + groupId?: string; + userId?: string; +} + +export interface CoreBridgeCapture { + mode?: 'reply_only' | 'lane'; + forward?: boolean; + maxMessages?: number; + settleMs?: number; +} + +export interface CoreBridgeInvocation { + target: CoreBridgeTarget; + actor: { userId: string; nickname: string; role: string }; + command: { raw: string; name: string; args: string[] }; + capture?: CoreBridgeCapture; + timeoutMs?: number; +} + +export interface CoreBridgeMessage { + messageId?: string; + action?: string; + segments?: any[]; + text?: string; + source?: string; + forwarded?: boolean; + intercepted?: boolean; +} + +export interface CoreBridgeResult { + ok: boolean; + messages?: CoreBridgeMessage[]; + completedBy?: string; + ambiguous?: boolean; + forwardedCount?: number; + interceptedCount?: number; + error?: string; +} diff --git a/src/pipeline.ts b/src/pipeline.ts index 3ae007f..11f5b70 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -430,7 +430,7 @@ export class MessagePipeline { /** 指令消息:记录 cmdArgs,并按配置决定是否写入会话上下文 */ static handleCommand(ctx: seal.MsgContext, msg: seal.Message, cmdArgs: seal.CmdArgs): void { - // 每次收到指令都刷新当前会话的 cmdArgs,供 run_command 等指令工具复用最新指令对象 + // 每次收到指令都刷新当前会话的 cmdArgs,供旧版本地扩展调用兼容逻辑复用最近指令对象 Tool.setCmdArgs(ctx, cmdArgs); const { RECEIVE_CMD: allcmd } = Config.received; @@ -476,7 +476,7 @@ export class MessagePipeline { ? messageArray.map(item => item.type === 'text' ? ((item.data && item.data.text) || '') : `[${item.type}]`).join('') : message; - session.tool.listen.resolve?.(messageText); // 将消息传递给监听工具 + session.tool.listen.push?.(messageText); // 分发给所有监听器,支持多条消息与并发调用 const { RECEIVE_MSG_BY_BOT: allmsg } = Config.received; if (!allmsg) return; diff --git a/src/session/session.ts b/src/session/session.ts index 95fd297..9e56ef9 100644 --- a/src/session/session.ts +++ b/src/session/session.ts @@ -144,20 +144,56 @@ export class Session { } this.memory = new SessionMemoryService(); this.lastCtx = null; + const waiters: { resolve: (messages: string[]) => void, reject: (error: Error) => void, messages: string[], settleTimer: any, timeoutTimer: any, maxMessages: number, settleMs: number }[] = []; + const listen: ToolListen = { + timeoutId: null, + resolve: null, + reject: null, + cleanup: () => { + if (listen.timeoutId) clearTimeout(listen.timeoutId); + listen.timeoutId = null; + listen.resolve = null; + listen.reject = null; + while (waiters.length) { + const waiter = waiters.shift()!; + clearTimeout(waiter.settleTimer); + clearTimeout(waiter.timeoutTimer); + waiter.reject(new Error('监听已清理')); + } + }, + push: (content: string) => { + for (let i = waiters.length - 1; i >= 0; i--) { + const waiter = waiters[i]; + if (waiter.messages.length < waiter.maxMessages) waiter.messages.push(content); + if (waiter.settleTimer) clearTimeout(waiter.settleTimer); + if (waiter.messages.length >= waiter.maxMessages) { + waiters.splice(i, 1); + clearTimeout(waiter.timeoutTimer); + waiter.resolve(waiter.messages); + } else { + waiter.settleTimer = setTimeout(() => { + const index = waiters.indexOf(waiter); + if (index >= 0) waiters.splice(index, 1); + clearTimeout(waiter.timeoutTimer); + waiter.resolve(waiter.messages); + }, waiter.settleMs); + } + } + }, + waitFor: (timeoutMs = 10000, settleMs = 400, maxMessages = 20) => new Promise((resolve, reject) => { + const waiter = { resolve, reject, messages: [] as string[], settleTimer: null as any, timeoutTimer: null as any, maxMessages: Math.max(1, maxMessages), settleMs: Math.max(0, settleMs) }; + waiter.timeoutTimer = setTimeout(() => { + const index = waiters.indexOf(waiter); + if (index >= 0) waiters.splice(index, 1); + resolve(waiter.messages); + }, Math.max(1, timeoutMs)); + waiters.push(waiter); + }) + }; this.tool = { state: {} as ToolState, callCount: 0, - listen: { - timeoutId: null, - resolve: null, - reject: null, - cleanup: () => { - if (this.tool.listen.timeoutId) clearTimeout(this.tool.listen.timeoutId); - this.tool.listen.timeoutId = null; - this.tool.listen.resolve = null; - this.tool.listen.reject = null; - } - } + listen } } diff --git a/src/tool/command_catalog.ts b/src/tool/command_catalog.ts new file mode 100644 index 0000000..1269b62 --- /dev/null +++ b/src/tool/command_catalog.ts @@ -0,0 +1,116 @@ +import Config from "../config/config"; +import { NAME } from "../config/static_config"; + +/** SealDice 当前核心内置扩展。扩展名是稳定的产品边界,不再暴露为用户配置。 */ +export const BUILTIN_EXT_NAMES = ['fun', 'story', 'coc7', 'deck', 'dnd5e', 'exp', 'log', 'reply']; + +export interface ResolvedCommand { + extName: string; + cmd: string; + help: string; + kind: 'builtin' | 'non_builtin' | 'core'; +} + +function helpOf(ext: seal.ExtInfo, cmd: string): string { + const item = ext.cmdMap?.[cmd]; + return item && item.help ? item.help : ''; +} + +function isCommand(ext: seal.ExtInfo | null, cmd: string): boolean { + return !!ext && !!ext.cmdMap && Object.prototype.hasOwnProperty.call(ext.cmdMap, cmd); +} + +function kindOf(extName: string): 'builtin' | 'non_builtin' | 'core' { + if (extName === 'core') return 'core'; + return BUILTIN_EXT_NAMES.indexOf(extName) >= 0 ? 'builtin' : 'non_builtin'; +} + +export function splitEntry(entry: string): { extName: string; cmd: string } | null { + const text = String(entry || '').trim(); + if (!text) return null; + const index = text.indexOf('|'); + if (index < 0) return { extName: '', cmd: text }; + const extName = text.slice(0, index).trim(); + const cmd = text.slice(index + 1).trim(); + return extName && cmd ? { extName, cmd } : null; +} + +/** 解析本地扩展 cmdMap;core 是虚拟扩展名,不要求 seal.ext.find('core') 存在。 */ +export function resolveEntry(entry: string): ResolvedCommand | null { + const parsed = splitEntry(entry); + if (!parsed) return null; + if (parsed.extName === 'core') return { extName: 'core', cmd: parsed.cmd, help: '', kind: 'core' }; + if (parsed.extName) { + const ext = seal.ext.find(parsed.extName); + return isCommand(ext, parsed.cmd) ? { extName: ext!.name || parsed.extName, cmd: parsed.cmd, help: helpOf(ext!, parsed.cmd), kind: kindOf(ext!.name || parsed.extName) } : null; + } + const sameName = seal.ext.find(parsed.cmd); + if (isCommand(sameName, parsed.cmd)) { + const extName = sameName!.name || parsed.cmd; + return { extName, cmd: parsed.cmd, help: helpOf(sameName!, parsed.cmd), kind: kindOf(extName) }; + } + for (const extName of BUILTIN_EXT_NAMES) { + const ext = seal.ext.find(extName); + if (isCommand(ext, parsed.cmd)) return { extName: ext!.name || extName, cmd: parsed.cmd, help: helpOf(ext!, parsed.cmd), kind: 'builtin' }; + } + return null; +} + +export function whitelistEntries(): string[] { + return Config.tool.CMD_WHITELIST.map(item => String(item || '').trim()).filter(Boolean); +} + +export function isAllowedExtension(rc: ResolvedCommand): boolean { + if (Config.tool.ALLOW_ALL_CMDS) return true; + return whitelistEntries().some(entry => { + const item = resolveEntry(entry); + return !!item && item.kind !== 'core' && `${item.extName}|${item.cmd}` === `${rc.extName}|${rc.cmd}`; + }); +} + +export function isAllowedCore(command: string): boolean { + const cmd = String(command || '').trim(); + // .ext 是核心提供的扩展发现入口,不应被白名单挡住。 + if (cmd === 'ext' || Config.tool.ALLOW_ALL_CMDS) return true; + return whitelistEntries().some(entry => { + const item = splitEntry(entry); + return !!item && item.extName === 'core' && item.cmd === cmd; + }); +} + +export function collectCommands(kind: 'builtin' | 'non_builtin' | 'all' = 'all'): ResolvedCommand[] { + const seen: { [key: string]: boolean } = {}; + const result: ResolvedCommand[] = []; + const push = (item: ResolvedCommand | null) => { + if (!item || item.kind === 'core' || (kind !== 'all' && item.kind !== kind)) return; + const key = `${item.extName}|${item.cmd}`; + if (seen[key]) return; + seen[key] = true; + result.push(item); + }; + for (const entry of whitelistEntries()) push(resolveEntry(entry)); + const extNames = kind === 'builtin' || kind === 'all' ? BUILTIN_EXT_NAMES : []; + for (const extName of extNames) { + const ext = seal.ext.find(extName); + if (!ext || !ext.cmdMap) continue; + for (const cmd of Object.keys(ext.cmdMap)) push({ extName: ext.name || extName, cmd, help: helpOf(ext, cmd), kind: 'builtin' }); + } + // 非内置扩展无法通过公开 SealDice API 全量枚举;白名单条目仍可可靠解析。 + if (kind === 'non_builtin' || kind === 'all') { + const self = seal.ext.find(NAME); + if (self && self.cmdMap) for (const cmd of Object.keys(self.cmdMap)) push({ extName: self.name || NAME, cmd, help: helpOf(self, cmd), kind: 'non_builtin' }); + } + return result; +} + +export function extensionNames(): string[] { + const names: { [name: string]: boolean } = { core: true }; + BUILTIN_EXT_NAMES.forEach(name => { names[name] = true; }); + whitelistEntries().forEach(entry => { + const parsed = splitEntry(entry); + if (parsed && parsed.extName) names[parsed.extName] = true; + }); + const self = seal.ext.find(NAME); + if (self && self.name) names[self.name] = true; + return Object.keys(names).sort(); +} diff --git a/src/tool/tool.ts b/src/tool/tool.ts index 86a31e3..b466572 100644 --- a/src/tool/tool.ts +++ b/src/tool/tool.ts @@ -24,8 +24,8 @@ export const CORE_TOOL_NAMES: string[] = [ 'search_tools', // 按需发现工具(返回完整参数说明) 'call_tool', // 统一执行入口(调用任意已开启工具) 'use_skill', // 技能调用 - 'run_command', // 海豹指令调用 - 'get_cmd_help' // 指令帮助 + 'run_ext_command', // 扩展指令调用 + 'run_core_command' // 核心指令调用 ]; const ON_DEMAND_PROMPT_LIMIT = 20; @@ -116,7 +116,7 @@ export default class Tool { cmdArgs.amIBeMentionedFirst = at?.[0]?.userId === ctx.endPoint.userId; cmdArgs.cleanArgs = cmdArgs.args.join(' '); cmdArgs.specialExecuteTimes = 0; - cmdArgs.rawText = `.${cmdArgs.command} ${cmdArgs.rawArgs} ${at.map(item => `[CQ:at,qq=${item.userId.replace(/^.+:/, '')}]`).join(' ')}`; + cmdArgs.rawText = `${Config.tool.COMMAND_PREFIX}${cmdArgs.command} ${cmdArgs.rawArgs} ${at.map(item => `[CQ:at,qq=${item.userId.replace(/^.+:/, '')}]`).join(' ')}`; const ext = seal.ext.find(eci.extName); if (!ext || !Object.prototype.hasOwnProperty.call(ext.cmdMap, eci.cmd)) { @@ -124,34 +124,22 @@ export default class Tool { return ['', false]; } - listen.reject?.(new Error('中断当前监听')); - - return new Promise(( - resolve: (result: [string, boolean]) => void, - reject: (err: Error) => void - ) => { - listen.timeoutId = setTimeout(() => { - reject(new Error('监听消息超时')); - listen.cleanup(); - }, 10 * 1000); - listen.resolve = (content: string) => { - resolve([content, true]); - listen.cleanup(); - }; - listen.reject = (err: Error) => { - reject(err); - listen.cleanup(); - }; - try { - ext.cmdMap[eci.cmd].solve(ctx, msg, cmdArgs); - } catch (err) { - reject(new Error(`solve中发生错误:${err instanceof Error ? err.message : String(err)}`)); - listen.cleanup(); - } - }).catch((err) => { + // 旧版本地扩展调用兼容:使用消息收集器而不是单一 resolve,支持多条回复、空闲窗口和并发等待。 + const responsePromise = listen.waitFor + ? listen.waitFor(10000, 400, 20) + : new Promise(resolve => { + listen.timeoutId = setTimeout(() => resolve([]), 10000); + listen.resolve = content => { resolve([content]); listen.cleanup(); }; + }); + try { + ext.cmdMap[eci.cmd].solve(ctx, msg, cmdArgs); + } catch (err) { + listen.cleanup(); Logger.error(`在extensionSolve中: 调用函数失败:${err instanceof Error ? err.message : String(err)}`); return ['', false]; - }); + } + const messages = await responsePromise; + return [messages.join('\n'), messages.length > 0]; } static async handleToolCall(ctx: seal.MsgContext, msg: seal.Message, session: Session, tool_call: ToolCall): Promise<{ result: ToolCallResult, callBack: boolean }> { diff --git a/src/tool/tools/core/tool_cmd.ts b/src/tool/tools/core/tool_cmd.ts index 4da407b..87a4f6a 100644 --- a/src/tool/tools/core/tool_cmd.ts +++ b/src/tool/tools/core/tool_cmd.ts @@ -1,186 +1,141 @@ -// 指令调用工具:读取海豹扩展 cmdMap 列出可用指令、查看指令帮助,并调用指令获取返回 +// 海豹指令工具:扩展指令与核心指令走独立工具,并通过 OB11 核心桥注入假消息。 import Config from "../../../config/config"; -import { NAME } from "../../../config/static_config"; import Logger from "../../../logger"; +import { CoreBridgeClient, formatCoreBridgeResult } from "../../../integration/core_bridge/client"; +import { collectCommands, extensionNames, isAllowedCore, isAllowedExtension, resolveEntry, splitEntry, ResolvedCommand } from "../../command_catalog"; import Tool from "../../tool"; -interface ResolvedCmd { - extName: string; - cmd: string; - help: string; +function commandText(item: ResolvedCommand): string { + return `${item.extName}|${item.cmd}`; } -function getCmdHelp(ext: seal.ExtInfo, cmd: string): string { - const item = ext.cmdMap?.[cmd]; - return item && item.help ? item.help : ''; +function formatList(items: ResolvedCommand[]): string { + return items.length + ? `可调用扩展指令(共 ${items.length} 个):\n${items.map((item, index) => `${index + 1}. ${commandText(item)}`).join('\n')}` + : '当前没有可列举的扩展指令'; } -/** 解析条目:支持 扩展名|指令名 或裸 指令名(优先按同名扩展解析,其次扫描内置扩展) */ -function resolveEntry(entry: string): ResolvedCmd | null { - const s = (entry || '').trim(); - if (!s) return null; - const pipe = s.indexOf('|'); - if (pipe !== -1) { - const extName = s.slice(0, pipe).trim(); - const cmd = s.slice(pipe + 1).trim(); - if (!extName || !cmd) return null; - const ext = seal.ext.find(extName); - if (ext && ext.cmdMap && Object.prototype.hasOwnProperty.call(ext.cmdMap, cmd)) { - return { extName: ext.name || extName, cmd, help: getCmdHelp(ext, cmd) }; - } - return null; - } - const byName = seal.ext.find(s); - if (byName && byName.cmdMap && Object.prototype.hasOwnProperty.call(byName.cmdMap, s)) { - return { extName: byName.name || s, cmd: s, help: getCmdHelp(byName, s) }; - } - for (const extName of Config.tool.BUILTIN_EXT_NAMES) { - const ext = seal.ext.find(extName); - if (ext && ext.cmdMap && Object.prototype.hasOwnProperty.call(ext.cmdMap, s)) { - return { extName, cmd: s, help: getCmdHelp(ext, s) }; - } - } - return null; -} - -/** 指令是否允许调用:开启「允许所有指令」时放行,否则必须在白名单内 */ -function isAllowed(command: string): boolean { - if (Config.tool.ALLOW_ALL_CMDS) return true; - return Config.tool.CMD_WHITELIST.some(entry => { - const rc = resolveEntry(entry); - return rc !== null && (rc.cmd === command || `${rc.extName}|${rc.cmd}` === command); - }); +function parseExtCommand(extension: string, command: string): { requested: string; resolved: ResolvedCommand | null } { + const requested = extension ? `${extension}|${command}` : command; + return { requested, resolved: resolveEntry(requested) }; } -/** 收集可列举指令列表 */ -function collectCommands(all: boolean): ResolvedCmd[] { - const seen: { [key: string]: boolean } = {}; - const result: ResolvedCmd[] = []; - const push = (rc: ResolvedCmd | null) => { - if (!rc) return; - const key = `${rc.extName}/${rc.cmd}`; - if (seen[key]) return; - seen[key] = true; - result.push(rc); +function captureOptions(args: { [key: string]: any }, defaultMaxMessages: number, defaultSettleMs: number): { + capture: { mode: 'reply_only' | 'lane'; forward: boolean; maxMessages: number; settleMs: number }; + timeoutMs?: number; +} { + const forward = args && args.forward === true; + const requestedMode = args && (args.captureMode === 'lane' || args.captureMode === 'reply_only') ? args.captureMode : undefined; + const maxMessages = Number(args && args.maxMessages); + const settleMs = Number(args && args.settleMs); + const timeoutMs = Number(args && args.timeoutMs); + return { + capture: { + mode: requestedMode || (forward ? 'lane' : 'reply_only'), + forward, + maxMessages: Number.isFinite(maxMessages) && maxMessages > 0 ? maxMessages : defaultMaxMessages, + settleMs: Number.isFinite(settleMs) && settleMs >= 0 ? settleMs : defaultSettleMs + }, + timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined }; - const { CMD_WHITELIST } = Config.tool; - for (const entry of CMD_WHITELIST) push(resolveEntry(entry)); - if (all) { - // 查看全部:核心内置扩展 + 插件自身扩展(第三方插件指令无法枚举,需加入白名单) - for (const extName of Config.tool.BUILTIN_EXT_NAMES) { - const ext = seal.ext.find(extName); - if (ext && ext.cmdMap) { - for (const cmd of Object.keys(ext.cmdMap)) { - push({ extName: ext.name || extName, cmd, help: getCmdHelp(ext, cmd) }); - } - } - } - const self = seal.ext.find(NAME); - if (self && self.cmdMap) { - for (const cmd of Object.keys(self.cmdMap)) { - push({ extName: self.name || NAME, cmd, help: getCmdHelp(self, cmd) }); - } - } - } - return result; } export function registerCmdTool() { - const tool = new Tool({ + const extTool = new Tool({ type: 'function', function: { - name: 'run_command', - description: `读取海豹扩展指令表(cmdMap)并调用海豹指令。action=list:列出可用指令名(不含帮助),默认只列白名单内指令,all=true 时额外列出核心内置扩展与插件自身的指令(第三方插件指令无法枚举,需加入白名单才能列出);action=call:调用 command 指定的指令并返回执行结果,默认仅能调用白名单内的指令(开启「是否允许调用所有指令」后可调用任意指令)。查看指令帮助请用 get_cmd_help 工具。注意:调用指令需要在最近收到过一条指令消息(如 .r)后才能获取返回。`, + name: 'run_ext_command', + description: `通过核心桥向 SealDice 注入一条假消息并执行扩展指令。扩展分为 builtin(fun/story/coc7/deck/dnd5e/exp/log/reply)与 non_builtin(第三方扩展及本插件),无需配置内置扩展列表。action=list 可按 kind 列出指令;action=call 执行指令。可调用指令仍受「可调用指令白名单」约束,白名单格式为扩展名|指令名。核心指令请使用 run_core_command。`, parameters: { type: 'object', properties: { - action: { - type: 'string', - enum: ['list', 'call'], - description: 'list=列出指令名(帮助请用 get_cmd_help);call=调用指令' - }, - command: { - type: 'string', - description: '指令名(如 今日老婆、jrrp),也支持 扩展名|指令名 格式;list 时留空表示列出全部' - }, - all: { - type: 'boolean', - description: '仅 list 使用:true 时额外列出全部可解析指令(核心内置 + 插件自身),默认 false 只列白名单' - }, - args: { - type: 'array', - items: { type: 'string' }, - description: '调用指令时传入的参数列表(按顺序)' - } + action: { type: 'string', enum: ['list', 'call'], description: 'list=列出扩展指令;call=执行扩展指令' }, + kind: { type: 'string', enum: ['builtin', 'non_builtin', 'all'], description: 'list 时筛选扩展类型,默认 all' }, + extension: { type: 'string', description: '扩展名;call 时可填写,核心扩展名不是这里的 core' }, + command: { type: 'string', description: '指令名;也支持直接填写 扩展名|指令名' }, + args: { type: 'array', items: { type: 'string' }, description: '指令参数,按顺序填写' }, + forward: { type: 'boolean', description: '是否把捕获到的核心发送消息继续转发给协议端,默认 false(避免重复发送)' }, + captureMode: { type: 'string', enum: ['reply_only', 'lane'], description: '消息捕获范围;forward=true 且希望捕获协议端回复时建议使用 lane' }, + maxMessages: { type: 'integer', minimum: 1, maximum: 50, description: '最多收集多少条消息' }, + settleMs: { type: 'integer', minimum: 0, maximum: 10000, description: '收到消息后等待多久没有新消息才结束' }, + timeoutMs: { type: 'integer', minimum: 100, maximum: 120000, description: '最长等待时间,单位毫秒' } }, required: ['action'] } } }); - tool.sensitive = true; // 调用指令可能发送消息/执行操作,调用会显著记录 - tool.solve = async (ctx, msg, session, args) => { - const { action, command = '', args: cmdArgs = [], all = false } = args || {}; + extTool.sensitive = true; + extTool.solve = async (ctx, _msg, _session, args) => { + const action = String(args && args.action || ''); if (action === 'list') { - const list = collectCommands(all === true); - if (list.length === 0) { - return all === true - ? '当前没有可列举的指令' - : '可调用指令白名单为空:请先在「工具」配置的「可调用指令白名单」中添加指令'; - } - return `可调用指令(共 ${list.length} 个):\n${list.map((rc, i) => `${i + 1}. ${rc.extName}|${rc.cmd}`).join('\n')}\n查看帮助:调用 get_cmd_help(command=指令名,如 fun|jrrp)`; + const kind = args && args.kind === 'builtin' || args && args.kind === 'non_builtin' ? args.kind : 'all'; + return `${formatList(collectCommands(kind))}\n扩展名称(核心 .ext 可查看完整列表):${extensionNames().join('、')}`; } - if (action === 'call') { - const cmdStr = (command || '').trim(); - if (!cmdStr) return '调用指令时 command 不能为空'; - if (!isAllowed(cmdStr)) { - return `指令 ${cmdStr} 不在可调用白名单内,无法调用`; - } - const rc = resolveEntry(cmdStr); - if (!rc) { - return `无法解析指令 ${cmdStr}:请确认插件已安装,或使用 扩展名|指令名 格式(如 fun|jrrp)`; - } - const [content, success] = await Tool.extensionSolve( - ctx, msg, session.tool.listen, - { extName: rc.extName, cmd: rc.cmd, staticArgs: [] }, - (cmdArgs || []).map(String), [], [] - ); - if (!success) { - Logger.warning(`[run_command] 调用指令 ${rc.cmd} 失败`); - return `指令 ${rc.cmd} 调用失败:请先让用户发送一条指令消息(如 .r)后再试`; - } - return `指令 ${rc.cmd} 返回:\n${content}`; + if (action !== 'call') return 'action 仅支持 list 或 call'; + const extension = String(args && args.extension || '').trim(); + const command = String(args && args.command || '').trim(); + if (!command) return '调用扩展指令时 command 不能为空'; + const parsed = parseExtCommand(extension, command); + const rc = parsed.resolved; + if (!rc) return `无法解析扩展指令 ${parsed.requested}:请确认扩展已安装,并使用 扩展名|指令名 格式`; + if (rc.kind === 'core') return '核心指令请使用 run_core_command,不要使用 run_ext_command'; + const requested = `${rc.extName}|${rc.cmd}`; + if (!isAllowedExtension(rc)) return `扩展指令 ${requested} 不在可调用指令白名单内,无法调用`; + const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; + const options = captureOptions(args, 20, 400); + try { + const result = await CoreBridgeClient.call(ctx, rc.cmd, cmdArgs, options.capture, options.timeoutMs); + return `扩展指令 ${requested} 返回:\n${formatCoreBridgeResult(result)}`; + } catch (e) { + Logger.warning(`[run_ext_command] 调用 ${requested} 失败:${e instanceof Error ? e.message : String(e)}`); + return `扩展指令 ${requested} 调用失败:${e instanceof Error ? e.message : String(e)}`; } - return 'action 仅支持 list 或 call'; }; - // 指令帮助查看工具:只读取 cmdMap 中的帮助文本,不执行任何指令 - const toolHelp = new Tool({ + const coreTool = new Tool({ type: 'function', function: { - name: 'get_cmd_help', - description: '查看海豹指令的帮助文本(只读,不执行指令);先用 run_command 的 action=list 查看可用指令名,再用本工具获取某个指令的详细帮助', + name: 'run_core_command', + description: `通过核心桥向 SealDice 注入一条假消息并执行核心指令。白名单中的核心扩展名统一写作 core|指令名;核心 .ext 是扩展发现入口,不需要加入白名单,调用 command="ext" 即可查看核心当前全部扩展名称。默认指令前缀为 .,可在配置中修改。`, parameters: { type: 'object', properties: { - command: { - type: 'string', - description: '指令名(如 今日老婆、jrrp),也支持 扩展名|指令名 格式' - } + action: { type: 'string', enum: ['list', 'call'], description: 'list=列出白名单核心指令;call=执行核心指令' }, + command: { type: 'string', description: '核心指令名,如 ext、help;也支持 core|ext' }, + args: { type: 'array', items: { type: 'string' }, description: '指令参数,按顺序填写' }, + forward: { type: 'boolean', description: '是否把捕获到的核心发送消息继续转发给协议端,默认 false(避免重复发送)' }, + captureMode: { type: 'string', enum: ['reply_only', 'lane'], description: '消息捕获范围;forward=true 且希望捕获协议端回复时建议使用 lane' }, + maxMessages: { type: 'integer', minimum: 1, maximum: 50, description: '最多收集多少条消息' }, + settleMs: { type: 'integer', minimum: 0, maximum: 10000, description: '收到消息后等待多久没有新消息才结束' }, + timeoutMs: { type: 'integer', minimum: 100, maximum: 120000, description: '最长等待时间,单位毫秒' } }, - required: ['command'] + required: ['action'] } } }); - toolHelp.solve = async (_, __, ___, args) => { - const { command = '' } = args || {}; - const rc = resolveEntry(command); - if (!rc) { - return `无法解析指令 ${command}:请确认插件已安装,或使用 扩展名|指令名 格式(如 fun|jrrp)`; + coreTool.sensitive = true; + coreTool.solve = async (ctx, _msg, _session, args) => { + const action = String(args && args.action || ''); + if (action === 'list') { + const list = Config.tool.CMD_WHITELIST.map(item => splitEntry(String(item))).filter(item => item && item.extName === 'core').map(item => `core|${item!.cmd}`); + return `可调用核心指令(共 ${list.length} 个):\n${list.length ? list.join('\n') : '(除 .ext 外暂无白名单核心指令)'}\n核心扩展发现:使用 action=call、command=ext 查看全部扩展名称`; + } + if (action !== 'call') return 'action 仅支持 list 或 call'; + let command = String(args && args.command || '').trim(); + if (command.indexOf('core|') === 0) command = command.slice(5).trim(); + if (!command) return '调用核心指令时 command 不能为空'; + if (!isAllowedCore(command)) return `核心指令 core|${command} 不在可调用指令白名单内,无法调用`; + const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; + const options = captureOptions(args, 50, 500); + if (command === 'ext' && !(args && args.captureMode)) options.capture.mode = 'lane'; + try { + const result = await CoreBridgeClient.call(ctx, command, cmdArgs, options.capture, options.timeoutMs); + return `核心指令 core|${command} 返回:\n${formatCoreBridgeResult(result)}`; + } catch (e) { + Logger.warning(`[run_core_command] 调用 core|${command} 失败:${e instanceof Error ? e.message : String(e)}`); + return `核心指令 core|${command} 调用失败:${e instanceof Error ? e.message : String(e)}`; } - return rc.help - ? `指令 ${rc.cmd}(扩展:${rc.extName})帮助:\n${rc.help}` - : `指令 ${rc.cmd}(扩展:${rc.extName})暂无帮助文本`; }; - return tool; + return coreTool; } diff --git a/src/tool/types.ts b/src/tool/types.ts index 2f4e6de..290353d 100644 --- a/src/tool/types.ts +++ b/src/tool/types.ts @@ -107,5 +107,9 @@ export interface ToolListen { timeoutId: number | null, resolve: ((content: string) => void) | null, reject: ((err: Error) => void) | null, - cleanup: () => void + cleanup: () => void, + /** 将机器人消息分发给所有正在等待的调用,避免单一 resolve 丢消息。 */ + push?: (content: string) => void, + /** 收集一段空闲窗口内的多条机器人消息。 */ + waitFor?: (timeoutMs?: number, settleMs?: number, maxMessages?: number) => Promise } diff --git a/src/update.ts b/src/update.ts index 87613e6..bdc74c9 100644 --- a/src/update.ts +++ b/src/update.ts @@ -121,8 +121,9 @@ export const updateInfo: { [version: string]: string } = { - 新增 docs/10-TODO.md:MCP Streamable HTTP 环境适配后的改动清单 ## 优化 - 音乐服务配置化:网易云/qq 的域名与 Cookie 移入「工具-音乐服务配置」(平台|域名|Cookie),不再硬编码 -- cmdArgs 按会话隔离:每个会话独立保存最近一次指令,避免多会话共用同一可变对象相互污染(.ai tool call/run_command 等指令工具随之修正) -- 内置扩展列表配置化:run_command 枚举指令使用的 SealDice 核心内置扩展名单移入「工具-内置扩展列表」,可自行增删 +- 新增 OB11 核心指令中转:run_ext_command/run_core_command 通过独立控制 WS 向 SealDice 注入假消息,按群/私聊 lane 串行化并收集多条响应,支持捕获后拦截或继续转发 +- 指令监听升级为多消息收集器:使用空闲窗口、最大消息数与超时结束,降低并发调用时的消息混淆 +- 内置扩展名单改为代码常量;核心扩展名统一为 core,core|ext 无需白名单即可查看所有扩展 - 模型选取确定性:对话/图片/嵌入模型匹配不再随机抽取,固定取第一个符合用途的模型 - ob11 网络连接依赖订阅等待窗口延长(10 次×3s → 20 次×5s),兼容慢加载环境 - 用量统计过期归并修正:先收集过期 key 统一归并后再替换,避免遍历同一对象时增删丢数据;0-0-0 长期桶不再参与归并 @@ -142,9 +143,9 @@ export const updateInfo: { [version: string]: string } = { ## 修复 - 修复工具调用后孤立 tool 消息导致的 API 报错(tool 消息缺少对应 tool_calls)`, "4.13.3": `## 新功能 -- 新增 run_command 工具:读取海豹扩展指令表列出可用指令(action=list 不含帮助),并可调用白名单内的海豹指令(action=call,开启「是否允许调用所有指令」后可调用任意指令);配套 get_cmd_help 工具按名查看指令帮助 -- 新增「可调用指令白名单」「是否允许调用所有指令」配置(工具页签);指令技能(今日人品/COC 模组/属性展示/检定/san检定)统一改为通过 run_command 调用 -- 工具按需加载:search_tools/call_tool/use_skill/run_command/get_cmd_help/get_time 常驻,其余工具按需搜索并统一执行,system prompt 从约 2 万字符降到约 2800 字符 +- 新增 run_ext_command/run_core_command 工具:扩展指令区分 builtin/non_builtin,核心白名单使用 core|指令名;新增「指令前缀」配置,默认为 . +- 指令技能(今日人品/COC 模组/属性展示/检定/san检定)统一改为通过 run_ext_command 调用 +- 工具按需加载:search_tools/call_tool/use_skill/run_ext_command/run_core_command/get_time 常驻,其余工具按需搜索并统一执行,system prompt 从约 2 万字符降到约 2800 字符 ## 优化 - 更新链接改为官方 GitHub Release;商店 README 移除开发章节,只保留用户文档 ## 修复 From 9b36b408a303ebbf7bd01ef747dd5fe5b6114b97 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Mon, 17 Aug 2026 23:48:59 +0800 Subject: [PATCH 02/20] feat: route core commands through MCP bridge --- README.md | 4 +- src/config/configs/backend.ts | 4 - src/config/configs/tool.ts | 6 +- src/integration/core_bridge/client.ts | 118 ++++++-------------------- src/tool/mcp.ts | 2 +- src/tool/tools/core/tool_cmd.ts | 2 +- src/update.ts | 2 +- 7 files changed, 37 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 15d525e..3d26220 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ max_tokens = 2048 | 默认关闭的函数 | 每行一个,AI 在新会话中默认无法调用,需 `.ai tool on <函数名>` 开启 | | 提供给AI的牌堆名称 | 每行一个牌堆名,用于 `draw_deck` 工具;没有的话建议把 `draw_deck` 加入禁止调用 | | 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | -| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含三个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | +| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | | 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | | ai语音使用的音色 | 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)和 ffmpeg | @@ -429,7 +429,7 @@ max_tokens = 2048 > 依赖说明:ob11 相关工具需要安装 [ob11 网络连接依赖](https://raw.githubusercontent.com/error2913/sealdice-plugin-ob11-net-connection/refs/heads/main/dist/ob11%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%E4%BE%9D%E8%B5%96.js) 或 [http 依赖插件](https://github.com/error2913/sealdice-js/blob/main/HTTP%E4%BE%9D%E8%B5%96.js);`text_to_sound` 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖与 ffmpeg;`text_to_image` 需要生成图片依赖;`music_play` 需要协议端配置音卡签名;`render_markdown` / `render_html` 需要配置 md 和 html 图片渲染后端。 -> 扩展/核心指令工具通过 OB11 核心桥注入假消息,不再要求会话先出现 `.r`;请启动 `ob11-core-bridge`,让 SealDice 的 OB11 网络依赖连接中间件 `/core`,并将插件「后端 → 核心指令中转WS地址」配置为中间件 `/control`。 +> 扩展/核心指令工具通过 OB11 核心桥注入假消息,不再要求会话先出现 `.r`;请启动 `ob11-core-bridge`,让 SealDice 的 OB11 网络依赖连接中间件 `/core`,并在「工具 → MCP服务器配置」中启用 `ob11-core-bridge`(`http://127.0.0.1:46880/mcp`)。旧版 `/control` 仅作为兼容接口保留。 --- diff --git a/src/config/configs/backend.ts b/src/config/configs/backend.ts index 82842c4..e220e50 100644 --- a/src/config/configs/backend.ts +++ b/src/config/configs/backend.ts @@ -10,8 +10,6 @@ export default class BackendConfig { seal.ext.registerStringConfig(ext, "论坛地址", "https://aiplugin-forum.fishwhite.top", 'aiplugin4 专用论坛地址,用于论坛工具;一般保持默认', "后端"); seal.ext.registerStringConfig(ext, "论坛API Token", "", '论坛注册后获取的 api_token,用于发帖等写操作的鉴权;不填则只能使用只读功能', "后端"); seal.ext.registerStringConfig(ext, "论坛签名密钥", "", '论坛注册后获取的 secret_key,用于请求签名;与「论坛API Token」配套填写', "后端"); - seal.ext.registerStringConfig(ext, "核心指令中转WS地址", "ws://127.0.0.1:46880/control", 'aiplugin4-backends/ob11-core-bridge 的控制 WebSocket 地址;留空则 run_ext_command/run_core_command 不可用', "后端"); - seal.ext.registerStringConfig(ext, "核心指令中转Token", "", '与中间件 AIPLUGIN4_BRIDGE_TOKEN 一致;不需要鉴权时留空', "后端"); } static get() { @@ -23,8 +21,6 @@ export default class BackendConfig { FORUM_URL: seal.ext.getStringConfig(ext, "论坛地址"), FORUM_API_TOKEN: seal.ext.getStringConfig(ext, "论坛API Token"), FORUM_SECRET_KEY: seal.ext.getStringConfig(ext, "论坛签名密钥"), - CORE_BRIDGE_WS: seal.ext.getStringConfig(ext, "核心指令中转WS地址"), - CORE_BRIDGE_TOKEN: seal.ext.getStringConfig(ext, "核心指令中转Token") } } } diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index 7306b69..1db1b2c 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -32,10 +32,14 @@ export default class ToolConfig { "md-html-render": { "type": "http", "url": "http://127.0.0.1:37632/mcp" + }, + "ob11-core-bridge": { + "type": "http", + "url": "http://127.0.0.1:46880/mcp" } } }` - ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含三个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); + ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); seal.ext.registerTemplateConfig(ext, "技能配置", [ `--- name: 今日人品 diff --git a/src/integration/core_bridge/client.ts b/src/integration/core_bridge/client.ts index 93e246d..0dc7304 100644 --- a/src/integration/core_bridge/client.ts +++ b/src/integration/core_bridge/client.ts @@ -1,109 +1,45 @@ import Config from "../../config/config"; +import { callServerTool, getMCPServerByName } from "../../tool/mcp"; import { CoreBridgeInvocation, CoreBridgeResult } from "./types"; -const WS_OPEN = 1; +const BRIDGE_SERVER = 'ob11-core-bridge'; let sequence = 0; -function getWebSocket(): any { - const ctor = (globalThis as any).WebSocket; - if (typeof ctor !== 'function') throw new Error('当前海豹环境没有可用的 WebSocket;请安装支持 WebSocket 的 ob11 网络连接依赖'); - return ctor; -} - function nextId(): string { sequence = (sequence + 1) % 100000; return `invoke_${Date.now().toString(36)}_${sequence.toString(36)}`; } -export class CoreBridgeClient { - private static ws: any = null; - private static connecting: Promise | null = null; - private static pending: { [id: string]: { resolve: (result: CoreBridgeResult) => void, reject: (error: Error) => void, timer: any } } = {}; - - static reset(): void { - this.failPending(new Error('核心指令中转 WS 已重置')); - if (this.ws && typeof this.ws.close === 'function') { - try { this.ws.close(); } catch (_) { /* ignore */ } - } - this.ws = null; - this.connecting = null; - } - - private static failPending(error: Error): void { - const pending = this.pending; - this.pending = {}; - Object.keys(pending).forEach(id => { - clearTimeout(pending[id].timer); - pending[id].reject(error); - }); +function decodeResult(text: string): CoreBridgeResult { + try { + const result = JSON.parse(text); + if (result && typeof result === 'object' && typeof result.ok === 'boolean') return result as CoreBridgeResult; + } catch (_) { + // MCP 服务端应返回 JSON 文本;保留原文,便于定位不兼容的中间件。 } + throw new Error(`核心指令中转返回了无效结果:${text.slice(0, 500)}`); +} - private static async connect(): Promise { - const configuredUrl = Config.backend.CORE_BRIDGE_WS; - if (!configuredUrl) throw new Error('未配置「后端 → 核心指令中转WS地址」'); - if (this.ws && this.ws.readyState === WS_OPEN) return this.ws; - if (this.connecting) return this.connecting; - const WebSocketCtor = getWebSocket(); - const configuredToken = Config.backend.CORE_BRIDGE_TOKEN; - const url = configuredToken - ? `${configuredUrl}${configuredUrl.indexOf('?') >= 0 ? '&' : '?'}access_token=${encodeURIComponent(configuredToken)}` - : configuredUrl; - this.connecting = new Promise((resolve, reject) => { - let settled = false; - const ws = new WebSocketCtor(url); - const fail = (error: Error) => { - if (!settled) { settled = true; reject(error); } - this.failPending(error); - if (this.ws === ws) this.ws = null; - }; - ws.onopen = () => { - ws.send(JSON.stringify({ - type: 'hello', protocol: 'aiplugin4-core-bridge', version: 1, - client: 'aiplugin4', token: Config.backend.CORE_BRIDGE_TOKEN - })); - }; - ws.onmessage = (event: any) => { - let packet: any; - try { packet = JSON.parse(String(event && event.data !== undefined ? event.data : event)); } - catch (_) { return; } - if (packet.type === 'hello.ok') { - settled = true; this.ws = ws; resolve(ws); return; - } - if (packet.type === 'command.result' && packet.id) { - const waiter = this.pending[String(packet.id)]; - if (!waiter) return; - clearTimeout(waiter.timer); delete this.pending[String(packet.id)]; waiter.resolve(packet as CoreBridgeResult); - } - }; - ws.onerror = () => fail(new Error('核心指令中转 WS 连接失败')); - ws.onclose = () => { - const error = new Error('核心指令中转 WS 已断开'); - this.failPending(error); - if (this.ws === ws) this.ws = null; - if (!settled) fail(error); - }; - }).then(value => { this.connecting = null; return value; }, error => { this.connecting = null; throw error; }); - return this.connecting; +export class CoreBridgeClient { + static reset(): void { + // MCP 会话由通用 MCP 客户端统一管理;配置热加载时会按服务器配置自动重建会话。 } - static async invoke(request: CoreBridgeInvocation): Promise { - const ws = await this.connect(); - const id = nextId(); - const timeoutMs = Math.max(1000, Math.min(Number(request.timeoutMs || 10000), 120000)); - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - delete this.pending[id]; reject(new Error(`核心指令等待超时(${timeoutMs}ms)`)); - }, timeoutMs + 1000); - this.pending[id] = { resolve, reject, timer }; - try { - ws.send(JSON.stringify({ type: 'command.invoke', id, ...request, timeoutMs })); - } catch (e) { - clearTimeout(timer); delete this.pending[id]; reject(e instanceof Error ? e : new Error(String(e))); - } - }); + static async invoke(request: CoreBridgeInvocation, toolName = 'run_ext_command'): Promise { + const server = getMCPServerByName(BRIDGE_SERVER); + if (!server) throw new Error(`未配置 MCP 服务器 ${BRIDGE_SERVER};请在「工具 → MCP服务器配置」中添加 ${BRIDGE_SERVER}`); + const result = await callServerTool(server, toolName, { ...request, id: nextId() }); + return decodeResult(result); } - static async call(ctx: seal.MsgContext, command: string, args: string[], capture?: CoreBridgeInvocation['capture'], timeoutMs?: number): Promise { + static async call( + ctx: seal.MsgContext, + command: string, + args: string[], + capture?: CoreBridgeInvocation['capture'], + timeoutMs?: number, + toolName = 'run_ext_command' + ): Promise { const prefix = Config.tool.COMMAND_PREFIX; const raw = `${prefix}${command}${args.length ? ` ${args.join(' ')}` : ''}`.trim(); const target: CoreBridgeInvocation['target'] = { @@ -122,7 +58,7 @@ export class CoreBridgeClient { command: { raw, name: command, args }, capture, timeoutMs - }); + }, toolName); } } diff --git a/src/tool/mcp.ts b/src/tool/mcp.ts index 9d068e7..15294c8 100644 --- a/src/tool/mcp.ts +++ b/src/tool/mcp.ts @@ -31,7 +31,7 @@ let lastRefreshAt = 0; // 全量刷新节流:避免每条消息都重新同步 // 内置后端服务(有专用包装工具 web_read / render_markdown / render_html), // 仅作为 MCP 服务器供包装工具调用,不把其 tools/list 重复注册为 AI 工具 -const BUILTIN_BACKEND_SERVERS = new Set(['web-read', 'md-html-render']); +const BUILTIN_BACKEND_SERVERS = new Set(['web-read', 'md-html-render', 'ob11-core-bridge']); /** 服务器配置是否一致:url/token/headers 任一变化都视为新配置,需要重建会话并重新拉取工具列表 */ function sameServerConfig(a: MCPServer, b: MCPServer): boolean { diff --git a/src/tool/tools/core/tool_cmd.ts b/src/tool/tools/core/tool_cmd.ts index 87a4f6a..822d3ca 100644 --- a/src/tool/tools/core/tool_cmd.ts +++ b/src/tool/tools/core/tool_cmd.ts @@ -129,7 +129,7 @@ export function registerCmdTool() { const options = captureOptions(args, 50, 500); if (command === 'ext' && !(args && args.captureMode)) options.capture.mode = 'lane'; try { - const result = await CoreBridgeClient.call(ctx, command, cmdArgs, options.capture, options.timeoutMs); + const result = await CoreBridgeClient.call(ctx, command, cmdArgs, options.capture, options.timeoutMs, 'run_core_command'); return `核心指令 core|${command} 返回:\n${formatCoreBridgeResult(result)}`; } catch (e) { Logger.warning(`[run_core_command] 调用 core|${command} 失败:${e instanceof Error ? e.message : String(e)}`); diff --git a/src/update.ts b/src/update.ts index bdc74c9..2809622 100644 --- a/src/update.ts +++ b/src/update.ts @@ -121,7 +121,7 @@ export const updateInfo: { [version: string]: string } = { - 新增 docs/10-TODO.md:MCP Streamable HTTP 环境适配后的改动清单 ## 优化 - 音乐服务配置化:网易云/qq 的域名与 Cookie 移入「工具-音乐服务配置」(平台|域名|Cookie),不再硬编码 -- 新增 OB11 核心指令中转:run_ext_command/run_core_command 通过独立控制 WS 向 SealDice 注入假消息,按群/私聊 lane 串行化并收集多条响应,支持捕获后拦截或继续转发 +- 新增 OB11 核心指令中转:run_ext_command/run_core_command 通过 MCP Streamable HTTP 调用中间件,由中间件向 SealDice 注入假消息;旧版控制 WS 兼容保留,按群/私聊 lane 串行化并收集多条响应,支持捕获后拦截或继续转发 - 指令监听升级为多消息收集器:使用空闲窗口、最大消息数与超时结束,降低并发调用时的消息混淆 - 内置扩展名单改为代码常量;核心扩展名统一为 core,core|ext 无需白名单即可查看所有扩展 - 模型选取确定性:对话/图片/嵌入模型匹配不再随机抽取,固定取第一个符合用途的模型 From d82daccd814599a6807421a1bbd070f84f389c84 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 03:37:14 +0800 Subject: [PATCH 03/20] =?UTF-8?q?docs:=20=E5=90=8E=E7=AB=AF=E6=B8=85?= =?UTF-8?q?=E5=8D=95=E8=A1=A5=E5=85=85=20ob11-core-bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...205\263\345\220\216\347\253\257\351\241\271\347\233\256.md" | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 57b6f8b..c5e6d4f 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" @@ -12,8 +12,9 @@ | `web-read` | 网页 URL 内容读取 | 46799 | 工具 → MCP服务器配置(web-read) | | `md-html-render` | Markdown/HTML 渲染为图片 | 37632 | 工具 → MCP服务器配置(md-html-render) | | `mcp-files-exec` | 本地文件读写/执行 | 3910 | 工具 → MCP服务器配置(mcp-files-exec) | +| `ob11-core-bridge` | SealDice 核心反向 WS / OB11 协议端 / MCP 透明中转(注入消息执行核心/扩展指令) | 46880 | 工具 → MCP服务器配置(ob11-core-bridge) | -`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。 +`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。其中 `web-read` / `md-html-render` 由内置包装工具(`web_read` / `render_markdown` / `render_html`)调用,不重复注册为 AI 工具;`mcp-files-exec` 的工具(文件读写/执行)与 `ob11-core-bridge` 的 `run_ext_command` / `run_core_command` 直接注册为 AI 工具。这些后端的 REST 接口已移除,仅提供 MCP(`ob11-core-bridge` 另提供 `/control` 兼容旧版插件控制 WS)。 ## 部署与一键管理 From 48181a045e95392b43727d0627ca61efbc62e44b Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 03:58:44 +0800 Subject: [PATCH 04/20] =?UTF-8?q?docs:=20ob11-core-bridge=20=E5=85=A8?= =?UTF-8?q?=E9=9D=A2=E6=94=B9=E6=AD=A3=E5=90=91=20WS=20+=20MCP=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AF=A6=E7=BB=86=E4=BD=BF=E7=94=A8=E6=8C=87?= =?UTF-8?q?=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除文档中残留的旧版 /control 插件控制 WS、反向 WS、CORE_URL 描述 - docs/04 拓扑与环境变量改写为:SealDice 正向连接 /core(/core/ws)、协议端 /onebot、插件走 MCP /mcp - docs/08 表格改为「核心正向 WS」并去掉 /control 兼容说明 - docs/09 与 README 同步:通过 MCP 接入、capture.forward 说明 - 新增 docs/11-ob11-core-bridge-使用指南.md:部署/SealDice/协议端/MCP 接入、工具参数、白名单、捕获与转发语义、并发与多核心、排障 - docs/README 导航表补充第 11 篇 --- README.md | 2 +- ...45\345\205\267\347\263\273\347\273\237.md" | 6 +- ...16\347\253\257\351\241\271\347\233\256.md" | 4 +- ...70\350\247\201\351\227\256\351\242\230.md" | 4 +- ...77\347\224\250\346\214\207\345\215\227.md" | 183 ++++++++++++++++++ docs/README.md | 1 + 6 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 "docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" diff --git a/README.md b/README.md index 3d26220..f27ab93 100644 --- a/README.md +++ b/README.md @@ -429,7 +429,7 @@ max_tokens = 2048 > 依赖说明:ob11 相关工具需要安装 [ob11 网络连接依赖](https://raw.githubusercontent.com/error2913/sealdice-plugin-ob11-net-connection/refs/heads/main/dist/ob11%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%E4%BE%9D%E8%B5%96.js) 或 [http 依赖插件](https://github.com/error2913/sealdice-js/blob/main/HTTP%E4%BE%9D%E8%B5%96.js);`text_to_sound` 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖与 ffmpeg;`text_to_image` 需要生成图片依赖;`music_play` 需要协议端配置音卡签名;`render_markdown` / `render_html` 需要配置 md 和 html 图片渲染后端。 -> 扩展/核心指令工具通过 OB11 核心桥注入假消息,不再要求会话先出现 `.r`;请启动 `ob11-core-bridge`,让 SealDice 的 OB11 网络依赖连接中间件 `/core`,并在「工具 → MCP服务器配置」中启用 `ob11-core-bridge`(`http://127.0.0.1:46880/mcp`)。旧版 `/control` 仅作为兼容接口保留。 +> 扩展/核心指令工具通过 OB11 核心桥注入假消息,不再要求会话先出现 `.r`;请启动 `ob11-core-bridge`,让 SealDice 的 OB11 网络依赖连接中间件 `/core`,并在「工具 → MCP服务器配置」中启用 `ob11-core-bridge`(`http://127.0.0.1:46880/mcp`)。 --- 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 3e2b631..5d77d37 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" @@ -15,7 +15,7 @@ | `sensitive` | 敏感工具标记(发消息/禁言/改名等),执行日志显著记录 | | `solve(ctx, msg, session, args)` | 工具实现,返回字符串作为给 AI 的结果 | -`run_ext_command` 与 `run_core_command` 通过 `aiplugin4-backends/ob11-core-bridge` 的控制 WS 注入标准 OB11 假消息,不再依赖最近一次真实指令。`run_ext_command` 的扩展名分为硬编码的 `builtin`(fun/story/coc7/deck/dnd5e/exp/log/reply)与 `non_builtin`;核心指令统一使用虚拟扩展名 `core`,例如 `core|ext`。指令前缀由「工具 → 指令前缀」配置,默认 `.`。旧的 `Tool.extensionSolve` 仍保留兼容路径,监听器改为多消息收集器,使用空闲窗口、最大消息数和超时结束。 +`run_ext_command` 与 `run_core_command` 通过 `aiplugin4-backends/ob11-core-bridge`(MCP)注入标准 OB11 假消息,不再依赖最近一次真实指令。`run_ext_command` 的扩展名分为硬编码的 `builtin`(fun/story/coc7/deck/dnd5e/exp/log/reply)与 `non_builtin`;核心指令统一使用虚拟扩展名 `core`,例如 `core|ext`。指令前缀由「工具 → 指令前缀」配置,默认 `.`。旧的 `Tool.extensionSolve` 仍保留兼容路径,监听器改为多消息收集器,使用空闲窗口、最大消息数和超时结束。 ## 调用流程与校验链 @@ -51,14 +51,14 @@ ## OB11 核心指令中转 -中间件位于 `aiplugin4-backends/backends/ob11-core-bridge`。SealDice 的 OB11 网络依赖作为 WebSocket 客户端连接中间件 `/core`;协议端(NapCat/LLOneBot 或测试模拟器)连接 `/onebot`;插件控制端连接 `/control`。控制调用使用独立 JSON 协议,不混入原始 OB11 事件。 +中间件位于 `aiplugin4-backends/backends/ob11-core-bridge`。SealDice 的 OB11 网络依赖作为 WebSocket 客户端主动连接中间件 `/core`(或 `/core/ws`);协议端(NapCat/LLOneBot 或测试模拟器)连接 `/onebot`;插件通过 MCP(`/mcp`)调用 `run_ext_command` / `run_core_command`,不直接连接中转 WS。 - 中间件原样转发协议端与核心之间的 OB11 JSON;核心的 `send_msg`/`send_group_msg` 等 Action,以及 lane 模式下的 bot 消息事件,都可按当前调用的会话 lane 捕获。 - 默认 `forward=false`:捕获后的 Action/消息事件不发送到协议端,但向核心返回成功 API 响应,避免核心阻塞;`forward=true` 则捕获并继续转发。工具可通过 `captureMode`、`maxMessages`、`settleMs`、`timeoutMs` 调整监听策略。 - 同一群/私聊 lane 串行化,单次调用使用 `settleMs` 空闲窗口收集多条消息;结果含 `ambiguous`、`completedBy`,因为纯 OB11 没有标准 trace_id,无法保证跨同 lane 的绝对关联。 - `run_core_command` 的 `command=ext` 直接调用核心 `.ext`,用于查看所有扩展名称,不需要在配置中维护扩展列表。 -中间件配置:`AIPLUGIN4_BACKEND_HOST`(默认 `0.0.0.0`)、`AIPLUGIN4_BACKEND_PORT`(默认 `46880`)、`AIPLUGIN4_BRIDGE_TOKEN`(控制端鉴权)、`AIPLUGIN4_BRIDGE_CORE_TOKEN` / `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN`(可分别覆盖核心端/协议端鉴权),以及可选的 `AIPLUGIN4_BRIDGE_CORE_PATH`、`AIPLUGIN4_BRIDGE_ONEBOT_PATH`、`AIPLUGIN4_BRIDGE_CONTROL_PATH`。插件配置:`后端 → 核心指令中转WS地址` 填 `/control` 地址,另填 `核心指令中转Token`。SealDice 侧的 OB11 WS 配置应填 `/core` 地址。 +中间件配置:`AIPLUGIN4_BACKEND_HOST`(默认 `0.0.0.0`)、`AIPLUGIN4_BACKEND_PORT`(默认 `46880`)、`AIPLUGIN4_BRIDGE_TOKEN`(默认鉴权 token,同时作为核心端/协议端/MCP 的默认 token)、`AIPLUGIN4_BRIDGE_CORE_TOKEN` / `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN`(可分别覆盖核心端/协议端鉴权),以及可选的 `AIPLUGIN4_BRIDGE_CORE_PATH`(默认 `/core`,同时接受 `/core/ws`)、`AIPLUGIN4_BRIDGE_ONEBOT_PATH`(默认 `/onebot`)、`AIPLUGIN4_BRIDGE_MCP_PATH`(默认 `/mcp`)。插件在「工具 → MCP服务器配置」中接入 `ob11-core-bridge`(`http://127.0.0.1:46880/mcp`);SealDice 侧的 OB11 正向 WS 配置填 `/core` 地址。 ## 内置工具清单 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 c5e6d4f..78aee76 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" @@ -12,9 +12,9 @@ | `web-read` | 网页 URL 内容读取 | 46799 | 工具 → MCP服务器配置(web-read) | | `md-html-render` | Markdown/HTML 渲染为图片 | 37632 | 工具 → MCP服务器配置(md-html-render) | | `mcp-files-exec` | 本地文件读写/执行 | 3910 | 工具 → MCP服务器配置(mcp-files-exec) | -| `ob11-core-bridge` | SealDice 核心反向 WS / OB11 协议端 / MCP 透明中转(注入消息执行核心/扩展指令) | 46880 | 工具 → MCP服务器配置(ob11-core-bridge) | +| `ob11-core-bridge` | SealDice 核心正向 WS / OB11 协议端 / MCP 透明中转(注入消息执行核心/扩展指令) | 46880 | 工具 → MCP服务器配置(ob11-core-bridge) | -`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。其中 `web-read` / `md-html-render` 由内置包装工具(`web_read` / `render_markdown` / `render_html`)调用,不重复注册为 AI 工具;`mcp-files-exec` 的工具(文件读写/执行)与 `ob11-core-bridge` 的 `run_ext_command` / `run_core_command` 直接注册为 AI 工具。这些后端的 REST 接口已移除,仅提供 MCP(`ob11-core-bridge` 另提供 `/control` 兼容旧版插件控制 WS)。 +`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。其中 `web-read` / `md-html-render` 由内置包装工具(`web_read` / `render_markdown` / `render_html`)调用,不重复注册为 AI 工具;`mcp-files-exec` 的工具(文件读写/执行)与 `ob11-core-bridge` 的 `run_ext_command` / `run_core_command` 直接注册为 AI 工具。这些后端的 REST 接口已移除,仅提供 MCP。 ## 部署与一键管理 diff --git "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" index e5ec1de..2953a6a 100644 --- "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" +++ "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -23,9 +23,9 @@ ## 运行与部署注意事项 - SealDice v1.4.6 分离部署(napcat/llonebot)下图片相关功能存在问题,建议 v1.5.0+。 -- `run_ext_command` / `run_core_command` 需要启动 `ob11-core-bridge` 并配置控制 WS;核心指令白名单使用 `core|指令名`,但 `core|ext` 无需加入白名单即可查看扩展。旧的本地 `Tool.extensionSolve` 仅用于兼容第三方工具,已升级为多消息收集器。 +- `run_ext_command` / `run_core_command` 需要启动 `ob11-core-bridge` 并通过 MCP 接入(SealDice 正向连接 `/core`);核心指令白名单使用 `core|指令名`,但 `core|ext` 无需加入白名单即可查看扩展。旧的本地 `Tool.extensionSolve` 仅用于兼容第三方工具,已升级为多消息收集器。 - ob11 相关工具(发消息、取用户/群信息、禁言、打卡、AI 语音等)需要安装 ob11 网络连接依赖或 HTTP 依赖;未安装时相关工具返回"未找到ob11网络连接依赖"。 -- 中间件捕获消息默认不转发到协议端;若希望真实 QQ 也看到核心动作,需在控制协议中设置 `capture.forward=true`。核心 WS、协议 WS、插件控制 WS 分别使用 `/core`、`/onebot`、`/control` 路径。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 +- 中间件捕获消息默认不转发到协议端;若希望真实 QQ 也看到核心动作,需在工具参数中设置 `capture.forward=true`。核心 WS 与协议 WS 分别使用 `/core`、`/onebot` 路径,插件通过 MCP `/mcp` 调用。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 - 所有配置修改后自动生效(缓存最多 1 分钟,无需重载 JS);MCP 服务器增删、技能配置、音乐服务等由缓存 TTL 兜底。 - 嵌入模型输出维度取自嵌入模型配置 `[body] dimensions`(默认 1024,须与后端一致,如 text-embedding-v4 为 1024);未配置维度时记忆检索自动降级为关键词/分数检索、知识库检索自动降级为关键词匹配(嵌入仅作候选重排,加载知识库本身不请求嵌入)。 - 流式输出需要自建或使用公共后端,并在"后端 → 流式输出"配置 URL;`body.stream = true` 的模型才会走流式。 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" new file mode 100644 index 0000000..440407e --- /dev/null +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -0,0 +1,183 @@ +# 11 - ob11-core-bridge 使用指南 + +`ob11-core-bridge` 是 aiplugin4 的配套中转后端(位于独立仓库 [aiplugin4-backends](https://github.com/error2913/aiplugin4-backends))。SealDice 以 OB11 正向 WebSocket 客户端身份主动连到中间件,OB11 协议端连到中间件,插件通过 MCP 调用中间件:**注入假消息 → 执行核心/扩展指令 → 收集 bot 响应**,并把结果回传给 AI。 + +## 为什么需要它 + +SealDice 的 JS 插件无法直接调用核心指令(`.ext`、`.help` 等)与大多数扩展指令。中间件把 SealDice 当成一个普通 OB11 正向 WS 客户端接入,插件端通过 MCP 发起调用,中间件向核心 WS 注入一条“假消息”(`指令前缀 + 指令文本`),再监听核心为响应这条消息而发出的 `send_*_msg` Action 与消息事件,把多消息结果聚合后返回给调用方。 + +## 拓扑 + +```text +SealDice(OB11 正向 WS 客户端) <──> /core(或 /core/ws) +OB11 协议端 / 模拟器 <──> /onebot +aiplugin4 插件(MCP 客户端) <──> /mcp +``` + +| 端点 | 用途 | +| --- | --- | +| `/core`、`/core/ws` | SealDice 核心正向 WS,海豹**主动连接**本端点 | +| `/onebot` | OB11 协议端(NapCat / LLOneBot / 测试模拟器)的 WS | +| `/mcp` | Streamable HTTP MCP 端点,提供 `run_ext_command` / `run_core_command` | +| `/healthz` | 健康检查 | + +默认监听 `0.0.0.0:46880`。中间件只做入站监听,没有出站连接(无 `CORE_URL` 之类的配置);海豹和协议端都要主动连过来。 + +## 部署 + +### 方式一:launcher 一键管理(推荐) + +```bash +python launcher.py start ob11-core-bridge +``` + +首次启动自动创建依赖并后台运行;也可以在 WebUI(默认 `http://127.0.0.1:8910`)里安装、启动、改端口与 token。 + +### 方式二:手动运行 + +```bash +cd ob11-core-bridge # 后端独立包目录 +npm install +npm start +``` + +### 环境变量 + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| `AIPLUGIN4_BACKEND_HOST` | `0.0.0.0` | 监听地址 | +| `AIPLUGIN4_BACKEND_PORT` | `46880` | 监听端口 | +| `AIPLUGIN4_BRIDGE_CORE_PATH` | `/core` | 核心 WS 路径(默认同时接受 `/core/ws`) | +| `AIPLUGIN4_BRIDGE_CORE_PATHS` | `/core,/core/ws` | 核心 WS 路径集合(逗号分隔,覆盖默认) | +| `AIPLUGIN4_BRIDGE_ONEBOT_PATH` | `/onebot` | 协议端 WS 路径 | +| `AIPLUGIN4_BRIDGE_MCP_PATH` | `/mcp` | MCP Streamable HTTP 路径 | +| `AIPLUGIN4_BRIDGE_TOKEN` | 空 | 默认鉴权 token,同时作为核心端/协议端/MCP 的默认 token | +| `AIPLUGIN4_BRIDGE_CORE_TOKEN` | 跟随 `AIPLUGIN4_BRIDGE_TOKEN` | 核心端(SealDice 连接)token | +| `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN` | 跟随 `AIPLUGIN4_BRIDGE_TOKEN` | 协议端 token | + +> 注意:这里**没有 `CORE_URL`**。旧版“插件连接中间件、中间件反向连海豹”的方案已移除,现在是海豹主动连中间件。 + +## SealDice 侧配置 + +在 SealDice 面板「连接」中新增 **OB11 正向 WebSocket**,目标地址填: + +```text +ws://127.0.0.1:46880/core +``` + +若中间件设置了 token,把 access_token 填 `AIPLUGIN4_BRIDGE_CORE_TOKEN`(未单独设置时用 `AIPLUGIN4_BRIDGE_TOKEN`)。 + +## 协议端侧配置 + +NapCat / LLOneBot 等协议端以 OB11 正向或反向 WS 客户端身份连接中间件 `/onebot`(token 用 `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN`)。中间件在核心与协议端之间原样转发 OB11 JSON。 + +> 协议端不是必需的:仅做“注入指令 + 收集响应”时可以不连 `/onebot`。默认 `forward=false` 会拦截核心发出的 Action,即使没有协议端也不会失败。 + +## 插件侧接入(aiplugin4) + +1. 打开「工具 → 是否启用MCP」总开关。 +2. 在「工具 → MCP服务器配置」中确认或添加 `ob11-core-bridge`(默认配置已包含): + +```json +{ + "mcpServers": { + "ob11-core-bridge": { + "type": "http", + "url": "http://127.0.0.1:46880/mcp" + } + } +} +``` + +设置 token 时补充请求头(或使用 `token` 简写字段自动生成 Bearer 头): + +```json +"ob11-core-bridge": { + "type": "http", + "url": "http://127.0.0.1:46880/mcp", + "headers": { "Authorization": "Bearer " } +} +``` + +3. 按需配置「可调用指令白名单」与「指令前缀」,AI 即可通过 `run_ext_command` / `run_core_command` 调用海豹指令。 + +## 工具与参数 + +两个工具由中间件通过 MCP 注册,插件直接注册为 AI 工具(敏感工具,执行会显著记录)。 + +### run_ext_command — 扩展指令 + +- `action=list`:按 `kind`(builtin / non_builtin / all)列出可调用扩展指令,并附当前已知扩展名 +- `action=call`:执行扩展指令 +- `extension` + `command`:`扩展名|指令名`;`command` 也支持直接填 `扩展名|指令名` +- `args`:指令参数,按顺序 + +扩展分为两类: +- `builtin`:fun / story / coc7 / deck / dnd5e / exp / log / reply —— 已硬编码在插件里,**无需在配置中维护内置扩展列表** +- `non_builtin`:第三方扩展及本插件 + +### run_core_command — 核心指令 + +- `action=list`:列出白名单中的核心指令 +- `action=call`:执行核心指令(如 `ext`、`help`;也支持 `core|ext` 写法) + +### 公共参数 + +| 参数 | 说明 | +| --- | --- | +| `forward` | 是否把捕获到的核心发送消息继续转发给协议端,默认 `false`(拦截) | +| `captureMode` | `reply_only` / `lane`;`forward=true` 且要捕获协议端产生的 bot 回复时建议用 `lane` | +| `maxMessages` | 最多收集消息数(1–50;ext 默认 20,core 默认 50) | +| `settleMs` | 收到消息后空闲多少毫秒无新消息即结束(0–10000;ext 默认 400,core 默认 500) | +| `timeoutMs` | 最长等待毫秒数(100–120000,默认 10000) | + +### 返回结果 + +| 字段 | 说明 | +| --- | --- | +| `ok` | 是否成功 | +| `messages` | 收集到的消息:`messageId` / `action` / `segments` / `text` / `source`(action/event)/ `forwarded` / `intercepted` | +| `completedBy` | 结束方式:`idle`(空闲窗口)/ `max_messages`(达到上限)/ `timeout` / `disconnect` | +| `ambiguous` | 是否因无法区分并发消息而标记歧义 | +| `forwardedCount` / `interceptedCount` | 转发 / 拦截计数 | +| `error` | 失败原因(如核心未连接、target 缺少群/私聊 id) | + +## 指令白名单 + +「可调用指令白名单」每行一条,格式 `扩展名|指令名`: + +- 内置扩展已硬编码,**无需填写扩展列表**;第三方扩展写实际扩展名 +- 核心指令的扩展名统一写 **`core`**(如 `core|help`) +- `core|ext` 是扩展发现入口,**无需加入白名单**:`run_core_command` 执行 `command=ext`(即核心 `.ext`)即可查看核心当前全部扩展名称 +- 开启「是否允许调用所有指令」后忽略扩展指令白名单(核心指令仍受 `run_core_command` 白名单约束) + +## 指令前缀 + +「工具 → 指令前缀」配置,默认 `.`。海豹核心通常用 `.` 作为指令前缀;如果核心改成其他前缀(如 `/`),请同步修改该配置,否则注入的假消息不会被核心识别为指令。 + +## 捕获与转发语义 + +- `forward=false`(默认):捕获核心发出的 `send_*_msg` Action / 消息事件后**拦截**,不送到协议端;仍向核心返回 Action 成功响应,避免核心重试或阻塞。适合“AI 内部执行指令、不打扰群聊”的场景。 +- `forward=true`:捕获后**继续转发**到协议端,并把协议端 API 响应按 `echo` 路由回核心;如需收集协议端产生的 bot 消息,`captureMode` 用 `lane`。 +- `reply_only`:只收集带 reply 引用(指向注入消息的虚拟 message_id)的响应,最精准;协议端不生成 reply 引用时可能收集不到,此时标记 `ambiguous=true`。 +- `lane`:按 `self_id + 群/私聊 + 对方 id` 捕获该会话内的 bot 消息,适合没有 reply 引用的场景。 +- 收到第一条消息后进入 `settleMs` 空闲窗口;达到 `maxMessages` 或 `timeoutMs` 结束。 + +## 并发与消息混淆 + +- 同一 lane 的调用**串行**执行(队列),不同 lane **并行**,避免同一群同时注入多条指令时响应混在一起。 +- 纯 OB11 没有标准 trace_id,中间件无法从 Action 本身区分“本次命令的响应”与“外部并发发送”,极端并发下可能返回 `ambiguous=true`。业务侧建议:短 `settleMs` + lane 串行 + 唯一目标(`selfId` / `messageType` / `groupId` / `userId`)来降低混淆。 + +## 多核心 + +多个海豹核心可同时连接中间件:核心连接后上报的 `self_id` 用于路由,调用与事件按 `self_id` 区分。插件端从当前消息上下文自动取 `selfId` 填入 `target`,`run_ext_command` / `run_core_command` 会命中对应的核心。 + +## 排障 + +| 现象 | 检查 | +| --- | --- | +| 工具报「未配置 MCP 服务器 ob11-core-bridge」 | 「是否启用MCP」总开关、MCP服务器配置中的服务器名与 url | +| 执行无响应 / 超时 | SealDice 是否已连上 `/core`(看中间件日志)、指令前缀是否正确、目标群/私聊 id 是否正确、指令是否在白名单 | +| 返回 `ambiguous=true` | 同 lane 并发或缺少 reply 引用;调整 `captureMode` / `settleMs` | +| 鉴权失败 | 核对 core token / protocol token / MCP headers 与中间件 token 一致 | +| 提示核心 WS 未连接 | 检查 SealDice「连接」里的 OB11 正向 WS 是否指向 `/core` 且未报错 | \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 3b2bb36..640c537 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,7 @@ AI 骰娘4 是一个运行在 [SealDice](https://docs.sealdice.com/) 上的 JS | [08-相关后端项目](08-相关后端项目.md) | 配套后端服务(独立仓库 aiplugin4-backends)的说明 | | [09-注意事项与常见问题](09-注意事项与常见问题.md) | 与 README 的差异、已知问题、排障提示 | | [10-TODO 与后续计划](10-TODO.md) | 待办事项、环境适配后需落地的改动清单 | +| [11-ob11-core-bridge-使用指南](11-ob11-core-bridge-使用指南.md) | OB11 核心桥中间件:部署、SealDice/协议端/MCP 接入、工具参数与排障 | | [模型配置参考](MODELS-chat.md) | 对话 / 图片 / 嵌入三种模型的完整 TOML 示例与兼容性说明([对话](MODELS-chat.md) · [图片](MODELS-image.md) · [嵌入](MODELS-embedding.md)) | ## 阅读建议 From 5e251a1e80a6ff85332e7d62a1c56800e56ecb98 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 04:13:54 +0800 Subject: [PATCH 05/20] fix: sync seal.d.ts and align command solve return type --- src/cmd/root_cmd.ts | 5 ++-- src/integration/core_bridge/client.ts | 1 + src/tool/tools/core/tool_cmd.ts | 4 +-- types/seal.d.ts | 40 +++++++++++++++++++++++---- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/cmd/root_cmd.ts b/src/cmd/root_cmd.ts index df3baa0..f033dd6 100644 --- a/src/cmd/root_cmd.ts +++ b/src/cmd/root_cmd.ts @@ -134,11 +134,12 @@ export function registerCmd() { } // 兜住子命令异步异常,避免“无响应”(SealDice 不会消费未捕获的 Promise 拒绝) - return Promise.resolve(SubCmd.map[subCmd].solve({ ctx, msg, cmdArgs, epId, uid, gid, sid, session, page, ret })).catch((e) => { + // 同步返回 ret:新版 seal.d.ts 中 solve 仅接受同步 CmdExecuteResult + void Promise.resolve(SubCmd.map[subCmd].solve({ ctx, msg, cmdArgs, epId, uid, gid, sid, session, page, ret })).catch((e) => { logger.error(`指令.ai执行失败:${e.message}`); seal.replyToSender(ctx, msg, `指令.ai执行失败:${e.message}`); - return ret; }); + return ret; } else if (subCmd === 'help') { // .ai help <一级子指令>:查看对应子命令帮助;不带参数时展示根命令帮助 const target = aliasToCmd(cmdArgs.getArgN(2)); diff --git a/src/integration/core_bridge/client.ts b/src/integration/core_bridge/client.ts index 0dc7304..36c22c1 100644 --- a/src/integration/core_bridge/client.ts +++ b/src/integration/core_bridge/client.ts @@ -1,5 +1,6 @@ import Config from "../../config/config"; import { callServerTool, getMCPServerByName } from "../../tool/mcp"; + import { CoreBridgeInvocation, CoreBridgeResult } from "./types"; const BRIDGE_SERVER = 'ob11-core-bridge'; diff --git a/src/tool/tools/core/tool_cmd.ts b/src/tool/tools/core/tool_cmd.ts index 822d3ca..8eee2ee 100644 --- a/src/tool/tools/core/tool_cmd.ts +++ b/src/tool/tools/core/tool_cmd.ts @@ -1,8 +1,8 @@ // 海豹指令工具:扩展指令与核心指令走独立工具,并通过 OB11 核心桥注入假消息。 import Config from "../../../config/config"; -import Logger from "../../../logger"; import { CoreBridgeClient, formatCoreBridgeResult } from "../../../integration/core_bridge/client"; -import { collectCommands, extensionNames, isAllowedCore, isAllowedExtension, resolveEntry, splitEntry, ResolvedCommand } from "../../command_catalog"; +import Logger from "../../../logger"; +import { collectCommands, extensionNames, isAllowedCore, isAllowedExtension, ResolvedCommand, resolveEntry, splitEntry } from "../../command_catalog"; import Tool from "../../tool"; function commandText(item: ResolvedCommand): string { diff --git a/types/seal.d.ts b/types/seal.d.ts index ca99934..0498180 100644 --- a/types/seal.d.ts +++ b/types/seal.d.ts @@ -32,8 +32,9 @@ declare namespace seal { /** * 使用当前消息上下文的账号发送一条“通知”给通知列表(骰主)。 * @param text 通知内容 + * @param noticeTypes 通知分类(可选),如 group / ban / invite / system 等,用于颗粒化过滤 */ - notice(text: string): void; + notice(text: string, ...noticeTypes: string[]): void; } /** @@ -152,7 +153,8 @@ declare namespace seal { | 5 // 回复 Reply | 6 // 语音 Record | 7 // 表情 Face - | 8; // 戳一戳 Poke + | 8 // 戳一戳 Poke + | -1; // 兜底未知类型 Default /** * 消息段(富文本元素)。 @@ -169,7 +171,8 @@ declare namespace seal { | ReplyElement | RecordElement | FaceElement - | PokeElement; + | PokeElement + | DefaultElement; /** 文本元素(Go message.TextElement) */ export interface TextElement { @@ -183,6 +186,8 @@ declare namespace seal { export interface AtElement { /** 目标用户 ID */ target: string; + /** 目标是否为机器人 */ + isRobot: boolean; /** 元素类型(At = 1) */ type(): MessageElementType; } @@ -255,12 +260,26 @@ declare namespace seal { type(): MessageElementType; } + /** + * 默认元素(Go message.DefaultElement),兜底未注册的 CQ 元素类型。 + * 注意:字段 type(字符串)会遮蔽 type() 方法,因此本类型没有 type() 方法, + * 实际类型只能通过该原始类型名判断。 + */ + export interface DefaultElement { + /** 原始元素类型名(CQ 码 type,如 video / xml 等) */ + type: string; + /** 元素原始数据(JSON 文本) */ + data: any; + } + /** 发送者信息 */ export interface Sender { /** 昵称 */ nickname: string; /** 用户 ID */ userId: string; + /** 是否为机器人 */ + isRobot: boolean; } /** @@ -308,6 +327,10 @@ declare namespace seal { export interface AtInfo { /** 被 @ 的用户 ID */ userId: string; + /** 被 @ 的账号是否为机器人 */ + isRobot: boolean; + /** 被 @ 用户的昵称(v1.6.1 起,平台提供时填充) */ + name: string; } /** 关键字参数,如 `.ra 50 --key=20 --asm` */ @@ -384,7 +407,7 @@ declare namespace seal { /** 指令定义 */ export interface CmdItemInfo { /** 指令执行函数,返回执行结果 */ - solve: (ctx: MsgContext, msg: Message, cmdArgs: CmdArgs) => CmdExecuteResult | Promise; + solve: (ctx: MsgContext, msg: Message, cmdArgs: CmdArgs) => CmdExecuteResult; /** 指令名称 */ name: string; /** 长帮助,带换行的较详细说明 */ @@ -426,6 +449,8 @@ declare namespace seal { version: string; /** 是否自动开启 */ autoActive: boolean; + /** 跟随开关:指定扩展开启/关闭时,本扩展同步开启/关闭 */ + activeWith: string[]; /** 指令映射:key 为指令名,value 为指令定义 */ cmdMap: { [key: string]: CmdItemInfo }; /** 作者 */ @@ -434,6 +459,8 @@ declare namespace seal { isLoaded: boolean; /** 获取扩展介绍文本 */ getDescText(): string; + /** 获取扩展所属 .sealpack 的包级配置;不属于任何包时返回空对象 */ + getPackageConfig(): { [key: string]: any }; /** 监听 加载时 事件 */ onLoad: () => void; /** 指令过滤后剩下的非指令消息 */ @@ -574,6 +601,8 @@ declare namespace seal { deprecated: boolean; /** 描述 */ description: string; + /** 配置分组(非空时 WebUI 会生成对应的二级配置页签) */ + group: string; } /** @@ -796,7 +825,8 @@ declare namespace seal { value: string, fn: (taskCtx: JsScriptTaskCtx) => void, key?: string, - desc?: string + desc?: string, + group?: string ): JsScriptTask; }; From d012c40c97643b94ec9934cb2c45d619ad85c520 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 04:31:19 +0800 Subject: [PATCH 06/20] =?UTF-8?q?docs:=20=E5=8D=8F=E8=AE=AE=E7=AB=AF?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E4=B8=AD=E9=97=B4=E4=BB=B6=E5=87=BA=E7=AB=99?= =?UTF-8?q?=E4=B8=BB=E5=8A=A8=E8=BF=9E=E6=8E=A5=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=8C=87=E5=8D=97=E4=B8=8E=E5=B7=A5=E5=85=B7=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?/FAQ=20=E5=90=8C=E6=AD=A5=20WebUI=20=E9=85=8D=E7=BD=AE=E8=AF=B4?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...45\345\205\267\347\263\273\347\273\237.md" | 4 +-- ...70\350\247\201\351\227\256\351\242\230.md" | 2 +- ...77\347\224\250\346\214\207\345\215\227.md" | 25 ++++++++++++------- 3 files changed, 19 insertions(+), 12 deletions(-) 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 5d77d37..8ecaf96 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" @@ -51,14 +51,14 @@ ## OB11 核心指令中转 -中间件位于 `aiplugin4-backends/backends/ob11-core-bridge`。SealDice 的 OB11 网络依赖作为 WebSocket 客户端主动连接中间件 `/core`(或 `/core/ws`);协议端(NapCat/LLOneBot 或测试模拟器)连接 `/onebot`;插件通过 MCP(`/mcp`)调用 `run_ext_command` / `run_core_command`,不直接连接中转 WS。 +中间件位于 `aiplugin4-backends/backends/ob11-core-bridge`。SealDice 的 OB11 网络依赖作为 WebSocket 客户端主动连接中间件 `/core`(或 `/core/ws`);中间件再出站主动连接协议端(NapCat/LLOneBot 或测试模拟器)的 WS 地址(启动即连、带指数退避重连);插件通过 MCP(`/mcp`)调用 `run_ext_command` / `run_core_command`,不直接连接中转 WS。 - 中间件原样转发协议端与核心之间的 OB11 JSON;核心的 `send_msg`/`send_group_msg` 等 Action,以及 lane 模式下的 bot 消息事件,都可按当前调用的会话 lane 捕获。 - 默认 `forward=false`:捕获后的 Action/消息事件不发送到协议端,但向核心返回成功 API 响应,避免核心阻塞;`forward=true` 则捕获并继续转发。工具可通过 `captureMode`、`maxMessages`、`settleMs`、`timeoutMs` 调整监听策略。 - 同一群/私聊 lane 串行化,单次调用使用 `settleMs` 空闲窗口收集多条消息;结果含 `ambiguous`、`completedBy`,因为纯 OB11 没有标准 trace_id,无法保证跨同 lane 的绝对关联。 - `run_core_command` 的 `command=ext` 直接调用核心 `.ext`,用于查看所有扩展名称,不需要在配置中维护扩展列表。 -中间件配置:`AIPLUGIN4_BACKEND_HOST`(默认 `0.0.0.0`)、`AIPLUGIN4_BACKEND_PORT`(默认 `46880`)、`AIPLUGIN4_BRIDGE_TOKEN`(默认鉴权 token,同时作为核心端/协议端/MCP 的默认 token)、`AIPLUGIN4_BRIDGE_CORE_TOKEN` / `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN`(可分别覆盖核心端/协议端鉴权),以及可选的 `AIPLUGIN4_BRIDGE_CORE_PATH`(默认 `/core`,同时接受 `/core/ws`)、`AIPLUGIN4_BRIDGE_ONEBOT_PATH`(默认 `/onebot`)、`AIPLUGIN4_BRIDGE_MCP_PATH`(默认 `/mcp`)。插件在「工具 → MCP服务器配置」中接入 `ob11-core-bridge`(`http://127.0.0.1:46880/mcp`);SealDice 侧的 OB11 正向 WS 配置填 `/core` 地址。 +中间件配置:`AIPLUGIN4_BACKEND_HOST`(默认 `0.0.0.0`)、`AIPLUGIN4_BACKEND_PORT`(默认 `46880`)、`AIPLUGIN4_BRIDGE_TOKEN`(MCP 鉴权 token)、`AIPLUGIN4_BRIDGE_CORE_TOKEN`(核心端鉴权)、`AIPLUGIN4_BRIDGE_PROTOCOL_URL` / `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN`(协议端地址与 token,中间件出站主动连接;也可在 WebUI「⚙ 配置」填写,重启生效),以及可选的 `AIPLUGIN4_BRIDGE_CORE_PATH`(默认 `/core`,同时接受 `/core/ws`)、`AIPLUGIN4_BRIDGE_MCP_PATH`(默认 `/mcp`)。插件在「工具 → MCP服务器配置」中接入 `ob11-core-bridge`(`http://127.0.0.1:46880/mcp`);SealDice 侧的 OB11 正向 WS 配置填 `/core` 地址。 ## 内置工具清单 diff --git "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" index 2953a6a..96b6504 100644 --- "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" +++ "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -25,7 +25,7 @@ - SealDice v1.4.6 分离部署(napcat/llonebot)下图片相关功能存在问题,建议 v1.5.0+。 - `run_ext_command` / `run_core_command` 需要启动 `ob11-core-bridge` 并通过 MCP 接入(SealDice 正向连接 `/core`);核心指令白名单使用 `core|指令名`,但 `core|ext` 无需加入白名单即可查看扩展。旧的本地 `Tool.extensionSolve` 仅用于兼容第三方工具,已升级为多消息收集器。 - ob11 相关工具(发消息、取用户/群信息、禁言、打卡、AI 语音等)需要安装 ob11 网络连接依赖或 HTTP 依赖;未安装时相关工具返回"未找到ob11网络连接依赖"。 -- 中间件捕获消息默认不转发到协议端;若希望真实 QQ 也看到核心动作,需在工具参数中设置 `capture.forward=true`。核心 WS 与协议 WS 分别使用 `/core`、`/onebot` 路径,插件通过 MCP `/mcp` 调用。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 +- 中间件捕获消息默认不转发到协议端;若希望真实 QQ 也看到核心动作,需在工具参数中设置 `capture.forward=true`。海豹核心连接 `/core`,协议端由中间件出站主动连接(WebUI「⚙ 配置」填地址,重启生效),插件通过 MCP `/mcp` 调用。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 - 所有配置修改后自动生效(缓存最多 1 分钟,无需重载 JS);MCP 服务器增删、技能配置、音乐服务等由缓存 TTL 兜底。 - 嵌入模型输出维度取自嵌入模型配置 `[body] dimensions`(默认 1024,须与后端一致,如 text-embedding-v4 为 1024);未配置维度时记忆检索自动降级为关键词/分数检索、知识库检索自动降级为关键词匹配(嵌入仅作候选重排,加载知识库本身不请求嵌入)。 - 流式输出需要自建或使用公共后端,并在"后端 → 流式输出"配置 URL;`body.stream = true` 的模型才会走流式。 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index 440407e..12a424f 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -1,6 +1,6 @@ # 11 - ob11-core-bridge 使用指南 -`ob11-core-bridge` 是 aiplugin4 的配套中转后端(位于独立仓库 [aiplugin4-backends](https://github.com/error2913/aiplugin4-backends))。SealDice 以 OB11 正向 WebSocket 客户端身份主动连到中间件,OB11 协议端连到中间件,插件通过 MCP 调用中间件:**注入假消息 → 执行核心/扩展指令 → 收集 bot 响应**,并把结果回传给 AI。 +`ob11-core-bridge` 是 aiplugin4 的配套中转后端(位于独立仓库 [aiplugin4-backends](https://github.com/error2913/aiplugin4-backends))。SealDice 以 OB11 正向 WebSocket 客户端身份主动连到中间件,中间件再**出站主动连接** OB11 协议端,插件通过 MCP 调用中间件:**注入假消息 → 执行核心/扩展指令 → 收集 bot 响应**,并把结果回传给 AI。 ## 为什么需要它 @@ -10,18 +10,18 @@ SealDice 的 JS 插件无法直接调用核心指令(`.ext`、`.help` 等) ```text SealDice(OB11 正向 WS 客户端) <──> /core(或 /core/ws) -OB11 协议端 / 模拟器 <──> /onebot +OB11 协议端 / 模拟器 <── 中间件启动时出站主动连接 aiplugin4 插件(MCP 客户端) <──> /mcp ``` | 端点 | 用途 | | --- | --- | | `/core`、`/core/ws` | SealDice 核心正向 WS,海豹**主动连接**本端点 | -| `/onebot` | OB11 协议端(NapCat / LLOneBot / 测试模拟器)的 WS | +| 协议端(出站) | 中间件作为 WS **客户端**,启动时主动连接你在配置里填写的协议端地址(带指数退避重连) | | `/mcp` | Streamable HTTP MCP 端点,提供 `run_ext_command` / `run_core_command` | | `/healthz` | 健康检查 | -默认监听 `0.0.0.0:46880`。中间件只做入站监听,没有出站连接(无 `CORE_URL` 之类的配置);海豹和协议端都要主动连过来。 +默认监听 `0.0.0.0:46880`。海豹**主动连入** `/core`;协议端由中间件**出站主动连接**(不支持反向 WS,协议端无需也无法连入中间件)。 ## 部署 @@ -49,11 +49,11 @@ npm start | `AIPLUGIN4_BACKEND_PORT` | `46880` | 监听端口 | | `AIPLUGIN4_BRIDGE_CORE_PATH` | `/core` | 核心 WS 路径(默认同时接受 `/core/ws`) | | `AIPLUGIN4_BRIDGE_CORE_PATHS` | `/core,/core/ws` | 核心 WS 路径集合(逗号分隔,覆盖默认) | -| `AIPLUGIN4_BRIDGE_ONEBOT_PATH` | `/onebot` | 协议端 WS 路径 | +| `AIPLUGIN4_BRIDGE_PROTOCOL_URL` | 空 | 协议端 WS 地址(中间件启动时主动连接;WebUI「⚙ 配置」可填,重启生效) | | `AIPLUGIN4_BRIDGE_MCP_PATH` | `/mcp` | MCP Streamable HTTP 路径 | -| `AIPLUGIN4_BRIDGE_TOKEN` | 空 | 默认鉴权 token,同时作为核心端/协议端/MCP 的默认 token | +| `AIPLUGIN4_BRIDGE_TOKEN` | 空 | MCP 鉴权 token | | `AIPLUGIN4_BRIDGE_CORE_TOKEN` | 跟随 `AIPLUGIN4_BRIDGE_TOKEN` | 核心端(SealDice 连接)token | -| `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN` | 跟随 `AIPLUGIN4_BRIDGE_TOKEN` | 协议端 token | +| `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN` | 空 | 协议端 token(可选;同时以 access_token 查询参数与 Bearer 头发送) | > 注意:这里**没有 `CORE_URL`**。旧版“插件连接中间件、中间件反向连海豹”的方案已移除,现在是海豹主动连中间件。 @@ -69,9 +69,16 @@ ws://127.0.0.1:46880/core ## 协议端侧配置 -NapCat / LLOneBot 等协议端以 OB11 正向或反向 WS 客户端身份连接中间件 `/onebot`(token 用 `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN`)。中间件在核心与协议端之间原样转发 OB11 JSON。 +中间件在**启动时**作为 WS 客户端主动连接 OB11 协议端(NapCat / LLOneBot / 测试模拟器等)的 WS 地址,并在核心与协议端之间原样转发 OB11 JSON。协议端无需配置任何指向中间件的连接(不支持反向 WS)。 -> 协议端不是必需的:仅做“注入指令 + 收集响应”时可以不连 `/onebot`。默认 `forward=false` 会拦截核心发出的 Action,即使没有协议端也不会失败。 +在 aiplugin4「后端」页的 ob11-core-bridge 卡片「⚙ 配置」中填写: + +- **协议端 WebSocket 地址**:如 `ws://127.0.0.1:6700`(对应环境变量 `AIPLUGIN4_BRIDGE_PROTOCOL_URL`) +- **协议端 token**(可选):会同时以 `access_token` 查询参数和 `Authorization: Bearer` 头发送(对应 `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN`) + +> 配置保存后需**重启该后端**才生效。协议端未配置或不可达时,中间件会按指数退避(1s 起、上限 30s)持续重试,连上后自动恢复转发;期间核心发来的 API 请求立即返回 `status: failed`,不会静默等待超时。 +> +> 协议端不是必需的:仅做“注入指令 + 收集响应”时可以不配协议端。默认 `forward=false` 会拦截核心发出的 Action,即使没有协议端也不会失败。 ## 插件侧接入(aiplugin4) From 7dbf2e5adf5804fbad8e508eee3bcfdd9bd4fd1a Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 05:28:03 +0800 Subject: [PATCH 07/20] =?UTF-8?q?refactor:=20run=5Fext=5Fcommand=20?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E7=9B=B4=E8=B0=83=E6=89=A9=E5=B1=95=20solve?= =?UTF-8?q?=EF=BC=8Crun=5Fcore=5Fcommand=20=E8=B5=B0=20MCP=20=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E6=A1=A5=EF=BC=9BMCP=20=E4=BC=9A=E8=AF=9D=E5=A4=B1?= =?UTF-8?q?=E6=95=88/=E6=9C=AA=E5=88=9D=E5=A7=8B=E5=8C=96=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=87=8D=E8=BF=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- ...45\345\205\267\347\263\273\347\273\237.md" | 10 +- ...16\347\253\257\351\241\271\347\233\256.md" | 4 +- ...70\350\247\201\351\227\256\351\242\230.md" | 2 +- ...77\347\224\250\346\214\207\345\215\227.md" | 37 +++-- src/tool/extension_executor.ts | 137 ++++++++++++++++++ src/tool/mcp.ts | 32 ++-- src/tool/tools/core/tool_cmd.ts | 31 ++-- 8 files changed, 208 insertions(+), 47 deletions(-) create mode 100644 src/tool/extension_executor.ts diff --git a/README.md b/README.md index f27ab93..93ec2fb 100644 --- a/README.md +++ b/README.md @@ -429,7 +429,7 @@ max_tokens = 2048 > 依赖说明:ob11 相关工具需要安装 [ob11 网络连接依赖](https://raw.githubusercontent.com/error2913/sealdice-plugin-ob11-net-connection/refs/heads/main/dist/ob11%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%E4%BE%9D%E8%B5%96.js) 或 [http 依赖插件](https://github.com/error2913/sealdice-js/blob/main/HTTP%E4%BE%9D%E8%B5%96.js);`text_to_sound` 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖与 ffmpeg;`text_to_image` 需要生成图片依赖;`music_play` 需要协议端配置音卡签名;`render_markdown` / `render_html` 需要配置 md 和 html 图片渲染后端。 -> 扩展/核心指令工具通过 OB11 核心桥注入假消息,不再要求会话先出现 `.r`;请启动 `ob11-core-bridge`,让 SealDice 的 OB11 网络依赖连接中间件 `/core`,并在「工具 → MCP服务器配置」中启用 `ob11-core-bridge`(`http://127.0.0.1:46880/mcp`)。 +> 扩展/核心指令工具不再要求会话先出现 `.r`:`run_ext_command` 在插件内本地直调扩展 `solve`,无需中间件;`run_core_command` 通过 OB11 核心桥注入假消息,需启动 `ob11-core-bridge`(SealDice 的 OB11 网络依赖连接中间件 `/core`,并在「工具 → MCP服务器配置」中启用 `ob11-core-bridge`,`http://127.0.0.1:46880/mcp`)。 --- 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 8ecaf96..e8e1c54 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" @@ -15,7 +15,7 @@ | `sensitive` | 敏感工具标记(发消息/禁言/改名等),执行日志显著记录 | | `solve(ctx, msg, session, args)` | 工具实现,返回字符串作为给 AI 的结果 | -`run_ext_command` 与 `run_core_command` 通过 `aiplugin4-backends/ob11-core-bridge`(MCP)注入标准 OB11 假消息,不再依赖最近一次真实指令。`run_ext_command` 的扩展名分为硬编码的 `builtin`(fun/story/coc7/deck/dnd5e/exp/log/reply)与 `non_builtin`;核心指令统一使用虚拟扩展名 `core`,例如 `core|ext`。指令前缀由「工具 → 指令前缀」配置,默认 `.`。旧的 `Tool.extensionSolve` 仍保留兼容路径,监听器改为多消息收集器,使用空闲窗口、最大消息数和超时结束。 +`run_ext_command` 在插件内**本地直接调用**扩展的 `solve`(构造独立的 `CmdArgs`,不依赖最近一次真实指令,也无需 MCP/中间件);`run_core_command` 通过 `aiplugin4-backends/ob11-core-bridge`(MCP)注入标准 OB11 假消息执行核心指令。`run_ext_command` 的扩展名分为硬编码的 `builtin`(fun/story/coc7/deck/dnd5e/exp/log/reply)与 `non_builtin`;核心指令统一使用虚拟扩展名 `core`,例如 `core|ext`。指令前缀由「工具 → 指令前缀」配置,默认 `.`。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时结束)。 ## 调用流程与校验链 @@ -51,12 +51,12 @@ ## OB11 核心指令中转 -中间件位于 `aiplugin4-backends/backends/ob11-core-bridge`。SealDice 的 OB11 网络依赖作为 WebSocket 客户端主动连接中间件 `/core`(或 `/core/ws`);中间件再出站主动连接协议端(NapCat/LLOneBot 或测试模拟器)的 WS 地址(启动即连、带指数退避重连);插件通过 MCP(`/mcp`)调用 `run_ext_command` / `run_core_command`,不直接连接中转 WS。 +中间件位于 `aiplugin4-backends/backends/ob11-core-bridge`,**仅用于核心指令**(插件无法在 JS 侧直接调用 `.ext`/`.help` 等核心指令)。SealDice 的 OB11 网络依赖作为 WebSocket 客户端主动连接中间件 `/core`(或 `/core/ws`);中间件再出站主动连接协议端(NapCat/LLOneBot 或测试模拟器)的 WS 地址(启动即连、带指数退避重连);插件通过 MCP(`/mcp`)调用 `run_core_command` 注入假消息,不直接连接中转 WS。扩展指令由 `run_ext_command` 在插件内本地直调扩展 `solve`,不经过中间件。 - 中间件原样转发协议端与核心之间的 OB11 JSON;核心的 `send_msg`/`send_group_msg` 等 Action,以及 lane 模式下的 bot 消息事件,都可按当前调用的会话 lane 捕获。 -- 默认 `forward=false`:捕获后的 Action/消息事件不发送到协议端,但向核心返回成功 API 响应,避免核心阻塞;`forward=true` 则捕获并继续转发。工具可通过 `captureMode`、`maxMessages`、`settleMs`、`timeoutMs` 调整监听策略。 +- 默认 `forward=false`:捕获后的 Action/消息事件不发送到协议端,但向核心返回成功 API 响应,避免核心阻塞;`forward=true` 则捕获并继续转发。`run_core_command` 可通过 `captureMode`、`maxMessages`、`settleMs`、`timeoutMs` 调整监听策略。 - 同一群/私聊 lane 串行化,单次调用使用 `settleMs` 空闲窗口收集多条消息;结果含 `ambiguous`、`completedBy`,因为纯 OB11 没有标准 trace_id,无法保证跨同 lane 的绝对关联。 -- `run_core_command` 的 `command=ext` 直接调用核心 `.ext`,用于查看所有扩展名称,不需要在配置中维护扩展列表。 +- `run_core_command` 的 `command=ext` 直接调用核心 `.ext`,用于查看所有扩展名称,不需要在配置中维护扩展列表。`run_ext_command` 的 `action=list` 仅能列出白名单 + 内置扩展 + 插件自身指令,完整扩展列表以 `run_core_command` 执行 `ext` 为准。 中间件配置:`AIPLUGIN4_BACKEND_HOST`(默认 `0.0.0.0`)、`AIPLUGIN4_BACKEND_PORT`(默认 `46880`)、`AIPLUGIN4_BRIDGE_TOKEN`(MCP 鉴权 token)、`AIPLUGIN4_BRIDGE_CORE_TOKEN`(核心端鉴权)、`AIPLUGIN4_BRIDGE_PROTOCOL_URL` / `AIPLUGIN4_BRIDGE_PROTOCOL_TOKEN`(协议端地址与 token,中间件出站主动连接;也可在 WebUI「⚙ 配置」填写,重启生效),以及可选的 `AIPLUGIN4_BRIDGE_CORE_PATH`(默认 `/core`,同时接受 `/core/ws`)、`AIPLUGIN4_BRIDGE_MCP_PATH`(默认 `/mcp`)。插件在「工具 → MCP服务器配置」中接入 `ob11-core-bridge`(`http://127.0.0.1:46880/mcp`);SealDice 侧的 OB11 正向 WS 配置填 `/core` 地址。 @@ -108,7 +108,7 @@ - 配置项 `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`。 -- 工具列表有 60 秒 TTL 缓存;会话失效(`会话不存在/已失效/session`)时重新 initialize 并重试一次。 +- 工具列表有 60 秒 TTL 缓存;会话失效/服务器未初始化(会话不存在/已失效/未初始化/Server not initialized)时自动重新 initialize 并重试一次。 - MCP 配置修改后会按服务器名重建会话并同步工具差集;删除服务器或服务器返回的工具会从注册表移除。MCP 总开关关闭时不建立连接,system prompt 也不会展示不可调用的 MCP/技能入口。 - 错误信息把 JSON-RPC 错误码映射为可读中文。 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 78aee76..db97992 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" @@ -12,9 +12,9 @@ | `web-read` | 网页 URL 内容读取 | 46799 | 工具 → MCP服务器配置(web-read) | | `md-html-render` | Markdown/HTML 渲染为图片 | 37632 | 工具 → MCP服务器配置(md-html-render) | | `mcp-files-exec` | 本地文件读写/执行 | 3910 | 工具 → MCP服务器配置(mcp-files-exec) | -| `ob11-core-bridge` | SealDice 核心正向 WS / OB11 协议端 / MCP 透明中转(注入消息执行核心/扩展指令) | 46880 | 工具 → MCP服务器配置(ob11-core-bridge) | +| `ob11-core-bridge` | SealDice 核心正向 WS / OB11 协议端 / MCP 透明中转(注入消息执行核心指令;扩展指令由插件本地执行) | 46880 | 工具 → MCP服务器配置(ob11-core-bridge) | -`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。其中 `web-read` / `md-html-render` 由内置包装工具(`web_read` / `render_markdown` / `render_html`)调用,不重复注册为 AI 工具;`mcp-files-exec` 的工具(文件读写/执行)与 `ob11-core-bridge` 的 `run_ext_command` / `run_core_command` 直接注册为 AI 工具。这些后端的 REST 接口已移除,仅提供 MCP。 +`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。其中 `web-read` / `md-html-render` 由内置包装工具(`web_read` / `render_markdown` / `render_html`)调用,不重复注册为 AI 工具;`mcp-files-exec` 的工具(文件读写/执行)直接注册为 AI 工具;`ob11-core-bridge` 的 `run_core_command` 直接注册为 AI 工具(`run_ext_command` 由插件本地实现,不依赖该中间件)。这些后端的 REST 接口已移除,仅提供 MCP。 ## 部署与一键管理 diff --git "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" index 96b6504..a9cb65e 100644 --- "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" +++ "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -23,7 +23,7 @@ ## 运行与部署注意事项 - SealDice v1.4.6 分离部署(napcat/llonebot)下图片相关功能存在问题,建议 v1.5.0+。 -- `run_ext_command` / `run_core_command` 需要启动 `ob11-core-bridge` 并通过 MCP 接入(SealDice 正向连接 `/core`);核心指令白名单使用 `core|指令名`,但 `core|ext` 无需加入白名单即可查看扩展。旧的本地 `Tool.extensionSolve` 仅用于兼容第三方工具,已升级为多消息收集器。 +- `run_core_command` 需要启动 `ob11-core-bridge` 并通过 MCP 接入(SealDice 正向连接 `/core`);`run_ext_command` 在插件内**本地直调扩展 `solve`**,无需中间件/MCP。核心指令白名单使用 `core|指令名`,但 `core|ext` 无需加入白名单即可查看扩展。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时)。 - ob11 相关工具(发消息、取用户/群信息、禁言、打卡、AI 语音等)需要安装 ob11 网络连接依赖或 HTTP 依赖;未安装时相关工具返回"未找到ob11网络连接依赖"。 - 中间件捕获消息默认不转发到协议端;若希望真实 QQ 也看到核心动作,需在工具参数中设置 `capture.forward=true`。海豹核心连接 `/core`,协议端由中间件出站主动连接(WebUI「⚙ 配置」填地址,重启生效),插件通过 MCP `/mcp` 调用。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 - 所有配置修改后自动生效(缓存最多 1 分钟,无需重载 JS);MCP 服务器增删、技能配置、音乐服务等由缓存 TTL 兜底。 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index 12a424f..cb09a6f 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -1,10 +1,10 @@ # 11 - ob11-core-bridge 使用指南 -`ob11-core-bridge` 是 aiplugin4 的配套中转后端(位于独立仓库 [aiplugin4-backends](https://github.com/error2913/aiplugin4-backends))。SealDice 以 OB11 正向 WebSocket 客户端身份主动连到中间件,中间件再**出站主动连接** OB11 协议端,插件通过 MCP 调用中间件:**注入假消息 → 执行核心/扩展指令 → 收集 bot 响应**,并把结果回传给 AI。 +`ob11-core-bridge` 是 aiplugin4 的配套中转后端(位于独立仓库 [aiplugin4-backends](https://github.com/error2913/aiplugin4-backends))。SealDice 以 OB11 正向 WebSocket 客户端身份主动连到中间件,中间件再**出站主动连接** OB11 协议端,插件通过 MCP 调用中间件:**注入假消息 → 执行核心指令 → 收集 bot 响应**,并把结果回传给 AI。扩展指令由插件本地直调扩展 `solve` 执行(`run_ext_command`),不经过中间件。 ## 为什么需要它 -SealDice 的 JS 插件无法直接调用核心指令(`.ext`、`.help` 等)与大多数扩展指令。中间件把 SealDice 当成一个普通 OB11 正向 WS 客户端接入,插件端通过 MCP 发起调用,中间件向核心 WS 注入一条“假消息”(`指令前缀 + 指令文本`),再监听核心为响应这条消息而发出的 `send_*_msg` Action 与消息事件,把多消息结果聚合后返回给调用方。 +SealDice 的 JS 插件无法直接调用核心指令(`.ext`、`.help` 等)——核心指令没有公开的 JS 调用入口;扩展指令则可直接调用 `cmdMap[cmd].solve` 在插件内本地执行(`run_ext_command`)。中间件把 SealDice 当成一个普通 OB11 正向 WS 客户端接入,插件端通过 MCP 发起 `run_core_command`,中间件向核心 WS 注入一条“假消息”(`指令前缀 + 指令文本`),再监听核心为响应这条消息而发出的 `send_*_msg` Action 与消息事件,把多消息结果聚合后返回给调用方。 ## 拓扑 @@ -18,7 +18,7 @@ aiplugin4 插件(MCP 客户端) <──> /mcp | --- | --- | | `/core`、`/core/ws` | SealDice 核心正向 WS,海豹**主动连接**本端点 | | 协议端(出站) | 中间件作为 WS **客户端**,启动时主动连接你在配置里填写的协议端地址(带指数退避重连) | -| `/mcp` | Streamable HTTP MCP 端点,提供 `run_ext_command` / `run_core_command` | +| `/mcp` | Streamable HTTP MCP 端点,提供 `run_core_command`(核心指令注入);扩展指令由插件本地执行 | | `/healthz` | 健康检查 | 默认监听 `0.0.0.0:46880`。海豹**主动连入** `/core`;协议端由中间件**出站主动连接**(不支持反向 WS,协议端无需也无法连入中间件)。 @@ -106,11 +106,11 @@ ws://127.0.0.1:46880/core } ``` -3. 按需配置「可调用指令白名单」与「指令前缀」,AI 即可通过 `run_ext_command` / `run_core_command` 调用海豹指令。 +3. 按需配置「可调用指令白名单」与「指令前缀」。`run_ext_command` 在插件内**本地直接执行扩展指令**,无需中间件;`run_core_command` 经 MCP 调用中间件执行核心指令。 ## 工具与参数 -两个工具由中间件通过 MCP 注册,插件直接注册为 AI 工具(敏感工具,执行会显著记录)。 +两个工具都由插件注册为 AI 工具(敏感工具,执行会显著记录)。`run_ext_command` 在插件内本地直调扩展 `solve`,不依赖中间件;`run_core_command` 经中间件 MCP 注入假消息执行核心指令,需启动 `ob11-core-bridge`。 ### run_ext_command — 扩展指令 @@ -119,6 +119,8 @@ ws://127.0.0.1:46880/core - `extension` + `command`:`扩展名|指令名`;`command` 也支持直接填 `扩展名|指令名` - `args`:指令参数,按顺序 +本地执行:直接调用扩展 `cmdMap[cmd].solve`(构造全新 `CmdArgs`,不要求会话先出现 `.r`),并复用多消息收集器收集扩展发出的多条回复。无需 MCP/中间件;核心内置扩展与第三方扩展均可调用,仍受「可调用指令白名单」约束。 + 扩展分为两类: - `builtin`:fun / story / coc7 / deck / dnd5e / exp / log / reply —— 已硬编码在插件里,**无需在配置中维护内置扩展列表** - `non_builtin`:第三方扩展及本插件 @@ -128,15 +130,17 @@ ws://127.0.0.1:46880/core - `action=list`:列出白名单中的核心指令 - `action=call`:执行核心指令(如 `ext`、`help`;也支持 `core|ext` 写法) -### 公共参数 +### 参数 -| 参数 | 说明 | -| --- | --- | -| `forward` | 是否把捕获到的核心发送消息继续转发给协议端,默认 `false`(拦截) | -| `captureMode` | `reply_only` / `lane`;`forward=true` 且要捕获协议端产生的 bot 回复时建议用 `lane` | -| `maxMessages` | 最多收集消息数(1–50;ext 默认 20,core 默认 50) | -| `settleMs` | 收到消息后空闲多少毫秒无新消息即结束(0–10000;ext 默认 400,core 默认 500) | -| `timeoutMs` | 最长等待毫秒数(100–120000,默认 10000) | +| 参数 | 适用 | 说明 | +| --- | --- | --- | +| `forward` | 仅 `run_core_command` | 是否把捕获到的核心发送消息继续转发给协议端,默认 `false`(拦截) | +| `captureMode` | 仅 `run_core_command` | `reply_only` / `lane`;`forward=true` 且要捕获协议端产生的 bot 回复时建议用 `lane` | +| `maxMessages` | 两者 | 最多收集消息数(1–50;ext 默认 20,core 默认 50) | +| `settleMs` | 两者 | 收到消息后空闲多少毫秒无新消息即结束(0–10000;ext 默认 400,core 默认 500) | +| `timeoutMs` | 两者 | 最长等待毫秒数(100–120000,默认 10000) | + +> `run_ext_command` 本地执行时直接复用会话监听器的 `waitFor(timeoutMs, settleMs, maxMessages)` 收集扩展发出的多条消息;`run_core_command` 则把这些参数传给中间件的 `capture` 策略。 ### 返回结果 @@ -149,6 +153,8 @@ ws://127.0.0.1:46880/core | `forwardedCount` / `interceptedCount` | 转发 / 拦截计数 | | `error` | 失败原因(如核心未连接、target 缺少群/私聊 id) | +> 上表是 `run_core_command` 经 MCP 返回的结果结构;`run_ext_command` 本地执行直接返回收集到的消息文本,异常时返回错误说明。 + ## 指令白名单 「可调用指令白名单」每行一条,格式 `扩展名|指令名`: @@ -177,13 +183,14 @@ ws://127.0.0.1:46880/core ## 多核心 -多个海豹核心可同时连接中间件:核心连接后上报的 `self_id` 用于路由,调用与事件按 `self_id` 区分。插件端从当前消息上下文自动取 `selfId` 填入 `target`,`run_ext_command` / `run_core_command` 会命中对应的核心。 +多个海豹核心可同时连接中间件:核心连接后上报的 `self_id` 用于路由,`run_core_command` 的调用与事件按 `self_id` 区分,插件端从当前消息上下文自动取 `selfId` 填入 `target`。`run_ext_command` 在插件内本地直调当前实例的扩展 `solve`,不涉及多核心路由。 ## 排障 | 现象 | 检查 | | --- | --- | -| 工具报「未配置 MCP 服务器 ob11-core-bridge」 | 「是否启用MCP」总开关、MCP服务器配置中的服务器名与 url | +| `run_core_command` 报「未配置 MCP 服务器 ob11-core-bridge」 | 「是否启用MCP」总开关、MCP服务器配置中的服务器名与 url | +| `run_ext_command` 执行失败 / 无响应 | 扩展是否已安装、指令名是否正确(`扩展名|指令名`)、是否在白名单、指令本身是否抛异常;与中间件无关 | | 执行无响应 / 超时 | SealDice 是否已连上 `/core`(看中间件日志)、指令前缀是否正确、目标群/私聊 id 是否正确、指令是否在白名单 | | 返回 `ambiguous=true` | 同 lane 并发或缺少 reply 引用;调整 `captureMode` / `settleMs` | | 鉴权失败 | 核对 core token / protocol token / MCP headers 与中间件 token 一致 | diff --git a/src/tool/extension_executor.ts b/src/tool/extension_executor.ts new file mode 100644 index 0000000..a390bb7 --- /dev/null +++ b/src/tool/extension_executor.ts @@ -0,0 +1,137 @@ +// 本地扩展指令执行器:直接调用 SealDice 扩展 cmdMap 的 solve,无需依赖 OB11 核心桥。 +// 仅用于扩展指令;核心指令请走 run_core_command(经 ob11-core-bridge 中间件中转)。 +import Logger from "../logger"; + +import { ResolvedCommand } from "./command_catalog"; +import { ToolListen } from "./types"; + +const RE_KEYWORD = /^--([^\s=]+)(?:=(\S+))?$/; + +interface ListenerHost { + tool: { listen: ToolListen }; +} + +/** 按海豹 ArgsParse 语义把原始参数拆成位置参数与关键字参数(--name / --name=value)。 */ +function splitKwargs(plainArgs: string[]): { args: string[]; kwargs: seal.Kwarg[] } { + const args: string[] = []; + const kwargs: seal.Kwarg[] = []; + for (const text of plainArgs) { + const m = RE_KEYWORD.exec(text); + if (m) { + const value = m[2] || ''; + kwargs.push({ name: m[1], value, valueExists: m[2] !== undefined, asBool: value !== 'false' }); + } else { + args.push(text); + } + } + return { args, kwargs }; +} + +/** + * 依据海豹核心 CmdArgs 语义构造一个全新的指令参数对象。 + * 不污染会话最近一次真实指令的 cmdArgs,也不要求会话里先收到过指令。 + * 调用扩展 solve 时 goja 会把该对象按 jsbind 标签转换回 Go *CmdArgs, + * 因此 solve 内部拿到的是带真实方法的对象,这里的方法实现仅作为类型兜底。 + */ +export function buildCmdArgs(ctx: seal.MsgContext, command: string, plainArgs: string[], at: seal.AtInfo[], prefix: string): seal.CmdArgs { + const { args, kwargs } = splitKwargs(plainArgs); + const cleanArgs = args.join(' '); + const rawArgs = [cleanArgs, kwargs.map(item => `--${item.name}${item.valueExists ? `=${item.value}` : ''}`).join(' ')].filter(Boolean).join(' '); + const atText = at.map(item => `[CQ:at,qq=${item.userId.replace(/^.+:/, '')}]`).join(' '); + const rawText = [prefix + command, rawArgs, atText].filter(Boolean).join(' ').trim(); + const amIBeMentioned = at.some(item => item.userId === ctx.endPoint.userId); + const amIBeMentionedFirst = amIBeMentioned && at[0]?.userId === ctx.endPoint.userId; + + return { + command, + args, + kwargs, + at, + rawArgs, + cleanArgs, + amIBeMentioned, + amIBeMentionedFirst, + specialExecuteTimes: 0, + rawText, + getArgN: n => (n >= 1 && n <= args.length ? args[n - 1] : ''), + getKwarg: key => kwargs.find(item => item.name === key) || null, + getRestArgsFrom: n => { + const list: string[] = []; + for (let i = Math.max(1, n); i <= args.length; i++) { + const info = args[i - 1]; + if (info) list.push(info); + else break; + } + return list.join(' '); + }, + isArgEqual: (n, ...s) => n >= 1 && n <= args.length && s.some(item => item.toLowerCase() === args[n - 1].toLowerCase()), + eatPrefixWith: (...s) => { + for (const item of s) { + if (cleanArgs.length >= item.length && cleanArgs.slice(0, item.length).toLowerCase() === item.toLowerCase()) { + return [cleanArgs.slice(item.length).trim(), true]; + } + } + return ['', false]; + }, + chopPrefixToArgsWith: (...s) => { + if (args.length === 0) return false; + const text = args[0]; + for (const item of s) { + if (text.length >= item.length && text.slice(0, item.length).toLowerCase() === item.toLowerCase()) { + const base = [item]; + const rest = text.slice(item.length).trim(); + if (rest) base.push(rest); + args.splice(0, 1, ...base); + return true; + } + } + return false; + } + }; +} + +export interface LocalExecutionOptions { + prefix?: string; + timeoutMs?: number; + settleMs?: number; + maxMessages?: number; + at?: seal.AtInfo[]; +} + +/** 本地执行一条已解析的扩展指令,并用会话监听器收集多条 bot 回复。 */ +export async function executeExtensionLocally( + ctx: seal.MsgContext, + msg: seal.Message, + session: ListenerHost, + rc: ResolvedCommand, + plainArgs: string[], + options: LocalExecutionOptions = {} +): Promise { + const ext = seal.ext.find(rc.extName); + if (!ext || !ext.cmdMap || !Object.prototype.hasOwnProperty.call(ext.cmdMap, rc.cmd)) { + throw new Error(`扩展 ${rc.extName} 未找到或缺少指令 ${rc.cmd}`); + } + const prefix = options.prefix != null ? options.prefix : '.'; + const timeoutMs = options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 10000; + const settleMs = options.settleMs != null && options.settleMs >= 0 ? options.settleMs : 400; + const maxMessages = options.maxMessages && options.maxMessages > 0 ? options.maxMessages : 20; + const cmdArgs = buildCmdArgs(ctx, rc.cmd, plainArgs, options.at || [], prefix); + + const listen = session.tool.listen; + const responsePromise = listen.waitFor + ? listen.waitFor(timeoutMs, settleMs, maxMessages) + : Promise.resolve([] as string[]); + + let solved = false; + try { + const result = ext.cmdMap[rc.cmd].solve(ctx, msg, cmdArgs) as { solved?: boolean } | undefined; + solved = !!(result && result.solved); + } catch (e) { + Logger.warning(`[run_ext_command] 本地执行 ${rc.extName}|${rc.cmd} 抛异常:${e instanceof Error ? e.message : String(e)}`); + throw new Error(`指令执行抛异常:${e instanceof Error ? e.message : String(e)}`); + } + + const messages = await responsePromise; + if (messages.length) return messages.join('\n'); + return solved ? '海豹未返回文本消息' : '指令未响应(扩展返回 solved=false),海豹未返回文本消息'; +} diff --git a/src/tool/mcp.ts b/src/tool/mcp.ts index 15294c8..972201a 100644 --- a/src/tool/mcp.ts +++ b/src/tool/mcp.ts @@ -216,21 +216,30 @@ async function getSessionId(server: MCPServer, force = false): Promise { return sessionId; } -async function callTool(server: MCPServer, name: string, args: any): Promise { +/** 判断 MCP 错误是否属于会话失效/服务器未初始化,需要重新 initialize */ +function isSessionInvalidError(e: unknown): boolean { + const msg = e instanceof Error ? e.message : String(e); + return /会话不存在|已失效|session|未初始化|not initialized|Mcp-Session-Id|Server not initialized/i.test(msg); +} + +/** 获取会话并执行请求;会话失效或服务器重启后未初始化时,重新 initialize 后重试一次 */ +async function withSessionRetry(server: MCPServer, fn: (sessionId: string) => Promise): Promise<{ sessionId: string, value: T }> { + let sessionId = await getSessionId(server); try { - const sessionId = await getSessionId(server); - return await doCallTool(server, sessionId, name, args); + return { sessionId, value: await fn(sessionId) }; } catch (e) { - // 会话失效时重新 initialize 并重试一次 - if (e instanceof Error && /会话不存在|已失效|session/i.test(e instanceof Error ? e.message : String(e))) { - Logger.warning(`MCP 会话失效,重新初始化后重试: ${server.name}`); - const sessionId = await getSessionId(server, true); - return await doCallTool(server, sessionId, name, args); - } - throw e; + if (!isSessionInvalidError(e)) throw e; + Logger.warning(`MCP 会话失效,重新初始化后重试: ${server.name}`); + sessionId = await getSessionId(server, true); + return { sessionId, value: await fn(sessionId) }; } } +async function callTool(server: MCPServer, name: string, args: any): Promise { + const { value } = await withSessionRetry(server, sessionId => doCallTool(server, sessionId, name, args)); + return value; +} + /** 供内置工具直接调用某个 MCP 服务器的工具(如 web-read / md-html-render 后端) */ export async function callServerTool(server: MCPServer, toolName: string, args: any): Promise { return await callTool(server, toolName, args || {}); @@ -245,8 +254,7 @@ async function syncTools(server: MCPServer, force = false): Promise listTools(server, sessionId)); serverStates[server.name] = { server, sessionId, tools, toolsFetchedAt: Date.now() }; // 清理本服务器已注册但 tools/list 不再返回的工具(服务器内工具删除后热加载生效) diff --git a/src/tool/tools/core/tool_cmd.ts b/src/tool/tools/core/tool_cmd.ts index 8eee2ee..31b97c6 100644 --- a/src/tool/tools/core/tool_cmd.ts +++ b/src/tool/tools/core/tool_cmd.ts @@ -1,8 +1,9 @@ -// 海豹指令工具:扩展指令与核心指令走独立工具,并通过 OB11 核心桥注入假消息。 +// 海豹指令工具:扩展指令本地直调 solve;核心指令经 OB11 核心桥中转(run_core_command)。 import Config from "../../../config/config"; import { CoreBridgeClient, formatCoreBridgeResult } from "../../../integration/core_bridge/client"; import Logger from "../../../logger"; import { collectCommands, extensionNames, isAllowedCore, isAllowedExtension, ResolvedCommand, resolveEntry, splitEntry } from "../../command_catalog"; +import { executeExtensionLocally } from "../../extension_executor"; import Tool from "../../tool"; function commandText(item: ResolvedCommand): string { @@ -20,6 +21,12 @@ function parseExtCommand(extension: string, command: string): { requested: strin return { requested, resolved: resolveEntry(requested) }; } +/** 解析可选数字参数;非法或负数视为未提供,由执行器回退到默认值。 */ +function optionNumber(value: any): number | undefined { + const n = Number(value); + return Number.isFinite(n) && n >= 0 ? n : undefined; +} + function captureOptions(args: { [key: string]: any }, defaultMaxMessages: number, defaultSettleMs: number): { capture: { mode: 'reply_only' | 'lane'; forward: boolean; maxMessages: number; settleMs: number }; timeoutMs?: number; @@ -40,12 +47,12 @@ function captureOptions(args: { [key: string]: any }, defaultMaxMessages: number }; } -export function registerCmdTool() { +export function registerCmdTool(): { extTool: Tool; coreTool: Tool } { const extTool = new Tool({ type: 'function', function: { name: 'run_ext_command', - description: `通过核心桥向 SealDice 注入一条假消息并执行扩展指令。扩展分为 builtin(fun/story/coc7/deck/dnd5e/exp/log/reply)与 non_builtin(第三方扩展及本插件),无需配置内置扩展列表。action=list 可按 kind 列出指令;action=call 执行指令。可调用指令仍受「可调用指令白名单」约束,白名单格式为扩展名|指令名。核心指令请使用 run_core_command。`, + description: `在 SealDice 本地直接调用扩展指令的 solve,不依赖核心桥/中间件,无需 MCP 与协议端。扩展分为 builtin(fun/story/coc7/deck/dnd5e/exp/log/reply)与 non_builtin(第三方扩展及本插件),无需配置内置扩展列表。action=list 可按 kind 列出指令;action=call 执行指令,并收集扩展发出的多条消息作为返回。可调用指令仍受「可调用指令白名单」约束,白名单格式为扩展名|指令名。核心指令请使用 run_core_command。`, parameters: { type: 'object', properties: { @@ -54,9 +61,7 @@ export function registerCmdTool() { extension: { type: 'string', description: '扩展名;call 时可填写,核心扩展名不是这里的 core' }, command: { type: 'string', description: '指令名;也支持直接填写 扩展名|指令名' }, args: { type: 'array', items: { type: 'string' }, description: '指令参数,按顺序填写' }, - forward: { type: 'boolean', description: '是否把捕获到的核心发送消息继续转发给协议端,默认 false(避免重复发送)' }, - captureMode: { type: 'string', enum: ['reply_only', 'lane'], description: '消息捕获范围;forward=true 且希望捕获协议端回复时建议使用 lane' }, - maxMessages: { type: 'integer', minimum: 1, maximum: 50, description: '最多收集多少条消息' }, + maxMessages: { type: 'integer', minimum: 1, maximum: 50, description: '最多收集多少条回复消息' }, settleMs: { type: 'integer', minimum: 0, maximum: 10000, description: '收到消息后等待多久没有新消息才结束' }, timeoutMs: { type: 'integer', minimum: 100, maximum: 120000, description: '最长等待时间,单位毫秒' } }, @@ -65,7 +70,7 @@ export function registerCmdTool() { } }); extTool.sensitive = true; - extTool.solve = async (ctx, _msg, _session, args) => { + extTool.solve = async (ctx, msg, session, args) => { const action = String(args && args.action || ''); if (action === 'list') { const kind = args && args.kind === 'builtin' || args && args.kind === 'non_builtin' ? args.kind : 'all'; @@ -82,10 +87,14 @@ export function registerCmdTool() { const requested = `${rc.extName}|${rc.cmd}`; if (!isAllowedExtension(rc)) return `扩展指令 ${requested} 不在可调用指令白名单内,无法调用`; const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; - const options = captureOptions(args, 20, 400); try { - const result = await CoreBridgeClient.call(ctx, rc.cmd, cmdArgs, options.capture, options.timeoutMs); - return `扩展指令 ${requested} 返回:\n${formatCoreBridgeResult(result)}`; + const content = await executeExtensionLocally(ctx, msg, session, rc, cmdArgs, { + prefix: Config.tool.COMMAND_PREFIX, + timeoutMs: optionNumber(args && args.timeoutMs), + settleMs: optionNumber(args && args.settleMs), + maxMessages: optionNumber(args && args.maxMessages) + }); + return `扩展指令 ${requested} 返回:\n${content}`; } catch (e) { Logger.warning(`[run_ext_command] 调用 ${requested} 失败:${e instanceof Error ? e.message : String(e)}`); return `扩展指令 ${requested} 调用失败:${e instanceof Error ? e.message : String(e)}`; @@ -137,5 +146,5 @@ export function registerCmdTool() { } }; - return coreTool; + return { extTool, coreTool }; } From 0acff32478aa661d1ce4bc3f50f5eba6b5cdad56 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 05:58:02 +0800 Subject: [PATCH 08/20] =?UTF-8?q?refactor:=20MCP=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=8D=B3=E6=8F=92=E5=8D=B3=E7=94=A8=E9=80=82=E9=85=8D=EF=BC=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=86=85=E7=BD=AE=E5=8C=85=E8=A3=85=E5=B7=A5?= =?UTF-8?q?=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- ...45\345\205\267\347\263\273\347\273\237.md" | 6 +- ...44\344\270\216\351\205\215\347\275\256.md" | 2 +- ...16\347\253\257\351\241\271\347\233\256.md" | 2 +- ...77\347\224\250\346\214\207\345\215\227.md" | 17 +- src/config/configs/tool.ts | 167 ++++++++- src/integration/core_bridge/client.ts | 72 ---- src/tool/mcp.ts | 92 +++-- src/tool/mcp/adapters.ts | 345 ++++++++++++++++++ src/tool/mcp/types.ts | 47 +++ src/tool/tools/core/tool_cmd.ts | 74 +--- src/tool/tools/image/init.ts | 2 - src/tool/tools/image/tool_render.ts | 245 ------------- src/tool/tools/web/tool_web.ts | 74 ---- 14 files changed, 646 insertions(+), 505 deletions(-) delete mode 100644 src/integration/core_bridge/client.ts create mode 100644 src/tool/mcp/adapters.ts create mode 100644 src/tool/mcp/types.ts delete mode 100644 src/tool/tools/image/tool_render.ts diff --git a/README.md b/README.md index 93ec2fb..68dfe87 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ max_tokens = 2048 | 默认关闭的函数 | 每行一个,AI 在新会话中默认无法调用,需 `.ai tool on <函数名>` 开启 | | 提供给AI的牌堆名称 | 每行一个牌堆名,用于 `draw_deck` 工具;没有的话建议把 `draw_deck` 加入禁止调用 | | 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | -| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | +| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。每台服务器可配 `tools` 适配块(`hidden`/`exposeAs`/`adapter`/`remoteTools`/`description`/`parameters` 等),默认配置把 `web_read`、`render_markdown`、`render_html`、`run_core_command` 映射为原工具名。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | | 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | | ai语音使用的音色 | 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)和 ffmpeg | @@ -410,7 +410,7 @@ max_tokens = 2048 | 消息 | `send_msg`、`get_msg`、`delete_msg`、`send_forward_msg`、`get_context` | | 定时 | `set_timer`、`show_timer_list`、`cancel_timer` | | 触发 | `set_trigger_condition` | -| 指令 | `run_ext_command`(调用扩展指令)、`run_core_command`(调用核心指令) | +| 指令 | `run_ext_command`(本地执行扩展指令)、`run_core_command`(经 MCP 调用核心指令) | | 工具调度 | `search_tools`(按需搜索工具)、`call_tool`(统一执行任意工具) | | 语音 | `record`、`text_to_sound` | | 网页 | `web_search`、`web_read` | @@ -423,7 +423,7 @@ max_tokens = 2048 | 音乐 | `music_play` | | 黑名单 | `suggest_block`(AI 建议拉黑,带冷却;默认需骰主确认)、`unblock_user`、`get_block_list` | | 论坛 | `forum_get_posts`、`forum_get_post_detail`、`forum_search`、`forum_create_post`、`forum_manage_comment`、`forum_get_activity`、`forum_manage_post` | -| MCP / 技能 | `<服务器名>_<工具名>`(MCP 工具)、`use_skill`(技能) | +| MCP / 技能 | `<服务器名>_<工具名>`(MCP 工具,可用 `tools.exposeAs` 覆盖)、`use_skill`(技能) | > 指令类技能(今日人品、COC 模组抽取/搜索、属性展示、属性检定、san 检定等)通过 `use_skill` 按需获取内容,内部统一使用 `run_ext_command` / `run_core_command` 调用海豹指令,对应指令需加入「可调用指令白名单」。 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 e8e1c54..761a804 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" @@ -89,7 +89,7 @@ 外部注册: -- MCP 工具:`<服务器名>_<工具名>`(见下节)。 +- MCP 工具:`<服务器名>_<工具名>`,或 `tools` 适配配置里 `exposeAs` 指定的名字(见下节)。 - 技能工具:`use_skill`(见技能节)。 依赖说明:ob11 相关工具(收发消息、用户/群信息、禁言、打卡等)需要安装 ob11 网络连接依赖或 HTTP 依赖;`text_to_sound` 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)与 ffmpeg;`music_play` 需要协议端配置音卡签名;`text_to_image` 需要生成图片依赖(tti)。 @@ -105,9 +105,9 @@ ## 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 时服务器会话自动重建(热加载) +- 配置项 `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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。每个服务器可带可选 `tools` 适配块:键为远端工具名,值可配 `hidden`(仅作底层工具不暴露给 AI)、`exposeAs`(暴露给 AI 的工具名)、`adapter`(text/image/web_read/render_markdown/render_html/core_bridge_core)、`description`/`parameters`(覆盖 AI 可见的描述与参数)、`output`/`format`(图片输出与保存格式)、`remoteTool`/`remoteTools`(实际调用的远端工具名映射)、`sensitive`(敏感工具标记)。默认配置通过 `tools` 适配把 `web_read`(底层 `screenshot_url`/`scrape_url`)、`render_markdown`/`render_html`、`run_core_command` 暴露为原工具名,并保留网页截图、Markdown/HTML 渲染、核心指令多消息收集等原有处理逻辑;`run_ext_command` 由插件本地注册,远端同名工具默认隐藏。改 URL/Token 时服务器会话自动重建(热加载) - 协议:Streamable HTTP 上的 JSON-RPC(`initialize` → `notifications/initialized` → `tools/list` → `tools/call`),带 `Mcp-Session-Id` 会话头,兼容 SSE 响应。 -- 工具以 `<服务器名>_<工具名>` 注册为敏感工具,参数透传 `inputSchema`。 +- 未配置 `tools` 适配的工具按通用规则以 `<服务器名>_<工具名>` 注册为敏感工具,参数透传 `inputSchema`;返回纯文本时直接回传,返回标准 image 块或 base64 文本时保存为 `[img:...]` 并返回图片 ID。 - 工具列表有 60 秒 TTL 缓存;会话失效/服务器未初始化(会话不存在/已失效/未初始化/Server not initialized)时自动重新 initialize 并重试一次。 - MCP 配置修改后会按服务器名重建会话并同步工具差集;删除服务器或服务器返回的工具会从注册表移除。MCP 总开关关闭时不建立连接,system prompt 也不会展示不可调用的 MCP/技能入口。 - 错误信息把 JSON-RPC 错误码映射为可读中文。 diff --git "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" index 5fcb741..e16126f 100644 --- "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" +++ "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" @@ -105,7 +105,7 @@ | 对话 | 角色扮演设定、示例对话、对话保存轮数、上下文最大token、消息压缩阈值 | 每条设定第一行为角色设定名称(>20 截断),其余为设定;`$gSYSPROMPT` 选择 | | 消息接收 | 接收图片/指令消息/骰子发送的消息、忽略消息豹语条件、忽略消息正则表达式 | 接收图片只负责记录图片 URL;自动识别仍由图片识别条件和图片模型配置决定 | | 消息触发 | 触发正则表达式、触发需要满足的条件、触发次数上限、触发次数补充间隔 | 令牌桶配额 | -| 工具 | 开启调用函数功能、切换为提示词工程、允许连续调用函数次数、工具响应压缩触发字数、禁止/默认关闭的函数、是否启用MCP、MCP服务器配置、技能配置、ai语音使用的音色 | 「是否启用MCP」默认关闭,开启后才会解析并连接 MCP 服务器;MCP 仅支持标准 mcpServers JSON(默认含 mcp-files-exec/web-read/md-html-render,格式见 modelcontextprotocol.io/specification/latest),技能仅支持标准 SKILL.md(格式见 agentskills.io/specification) | +| 工具 | 开启调用函数功能、切换为提示词工程、允许连续调用函数次数、工具响应压缩触发字数、禁止/默认关闭的函数、是否启用MCP、MCP服务器配置、技能配置、ai语音使用的音色 | 「是否启用MCP」默认关闭,开启后才会解析并连接 MCP 服务器;MCP 仅支持标准 mcpServers JSON(默认含 mcp-files-exec/web-read/md-html-render/ob11-core-bridge,可用 tools 适配块把远端工具映射为 web_read/render_markdown/render_html/run_core_command,格式见 modelcontextprotocol.io/specification/latest),技能仅支持标准 SKILL.md(格式见 agentskills.io/specification) | | 记忆 | 启用长期/总结/知识库记忆、长期记忆上限/展示数量、总结记忆上限/间隔/参与轮数、短期记忆上限、知识库注入阈值(字符)、知识库(Markdown 模板,每条一份完整文档) | 知识库只读,AI 通过 kb 工具/指令检索;修改后自动生效(缓存最多 1 分钟);Markdown 语法见 commonmark.org/help/ | | 回复 | 回复引用、回复最大字数、分段发送延时/基础延时/含图额外延时、禁止回复复读 | 回复过滤规则已内置硬编码 | | 后端 | 流式输出、图片转base64、联网搜索、用量图表、论坛地址/API Token/签名密钥 | 可自建,见 [08-相关后端项目](08-相关后端项目.md);网页读取与 md/html 渲染(MCP)在「工具 → MCP服务器配置」 | 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 db97992..065395f 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" @@ -14,7 +14,7 @@ | `mcp-files-exec` | 本地文件读写/执行 | 3910 | 工具 → MCP服务器配置(mcp-files-exec) | | `ob11-core-bridge` | SealDice 核心正向 WS / OB11 协议端 / MCP 透明中转(注入消息执行核心指令;扩展指令由插件本地执行) | 46880 | 工具 → MCP服务器配置(ob11-core-bridge) | -`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。其中 `web-read` / `md-html-render` 由内置包装工具(`web_read` / `render_markdown` / `render_html`)调用,不重复注册为 AI 工具;`mcp-files-exec` 的工具(文件读写/执行)直接注册为 AI 工具;`ob11-core-bridge` 的 `run_core_command` 直接注册为 AI 工具(`run_ext_command` 由插件本地实现,不依赖该中间件)。这些后端的 REST 接口已移除,仅提供 MCP。 +`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。插件不再内置包装工具:默认配置的 `tools` 适配块把 `web-read` / `md-html-render` / `ob11-core-bridge` 的底层 MCP 工具映射为 `web_read` / `render_markdown` / `render_html` / `run_core_command` 并保留原有行为(网页截图、渲染图片、核心指令多消息收集);`mcp-files-exec` 的工具(文件读写/执行)按通用规则注册为 AI 工具;`run_ext_command` 由插件本地实现,不依赖该中间件。这些后端的 REST 接口已移除,仅提供 MCP。 ## 部署与一键管理 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index cb09a6f..c4c3441 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -90,7 +90,14 @@ ws://127.0.0.1:46880/core "mcpServers": { "ob11-core-bridge": { "type": "http", - "url": "http://127.0.0.1:46880/mcp" + "url": "http://127.0.0.1:46880/mcp", + "tools": { + "run_ext_command": { "hidden": true }, + "run_core_command": { + "exposeAs": "run_core_command", + "adapter": "core_bridge_core" + } + } } } } @@ -106,11 +113,11 @@ ws://127.0.0.1:46880/core } ``` -3. 按需配置「可调用指令白名单」与「指令前缀」。`run_ext_command` 在插件内**本地直接执行扩展指令**,无需中间件;`run_core_command` 经 MCP 调用中间件执行核心指令。 +3. 按需配置「可调用指令白名单」与「指令前缀」。`tools` 适配块会把远端 `run_core_command` 暴露为同名 AI 工具并套用核心桥适配器;若手写配置时省略该块,`run_core_command` 会按通用规则注册为 `ob11-core-bridge_run_core_command`。`run_ext_command` 在插件内**本地直接执行扩展指令**,无需中间件;`run_core_command` 经 MCP 调用中间件执行核心指令。 ## 工具与参数 -两个工具都由插件注册为 AI 工具(敏感工具,执行会显著记录)。`run_ext_command` 在插件内本地直调扩展 `solve`,不依赖中间件;`run_core_command` 经中间件 MCP 注入假消息执行核心指令,需启动 `ob11-core-bridge`。 +`run_ext_command` 由插件本地注册为 AI 工具(敏感工具,执行会显著记录),在插件内直调扩展 `solve`,不依赖中间件;`run_core_command` 由 MCP 同步注册(默认 `tools` 适配暴露同名工具,同样为敏感工具),经中间件 MCP 注入假消息执行核心指令,需启动 `ob11-core-bridge` 并开启「是否启用MCP」。 ### run_ext_command — 扩展指令 @@ -189,9 +196,9 @@ ws://127.0.0.1:46880/core | 现象 | 检查 | | --- | --- | -| `run_core_command` 报「未配置 MCP 服务器 ob11-core-bridge」 | 「是否启用MCP」总开关、MCP服务器配置中的服务器名与 url | +| `run_core_command` 不可用 / 报 MCP 服务器未配置 | 「是否启用MCP」总开关、MCP服务器配置中的服务器名与 url、`tools` 适配块是否保留了 `run_core_command` | | `run_ext_command` 执行失败 / 无响应 | 扩展是否已安装、指令名是否正确(`扩展名|指令名`)、是否在白名单、指令本身是否抛异常;与中间件无关 | | 执行无响应 / 超时 | SealDice 是否已连上 `/core`(看中间件日志)、指令前缀是否正确、目标群/私聊 id 是否正确、指令是否在白名单 | | 返回 `ambiguous=true` | 同 lane 并发或缺少 reply 引用;调整 `captureMode` / `settleMs` | | 鉴权失败 | 核对 core token / protocol token / MCP headers 与中间件 token 一致 | -| 提示核心 WS 未连接 | 检查 SealDice「连接」里的 OB11 正向 WS 是否指向 `/core` 且未报错 | \ No newline at end of file +| 提示核心 WS 未连接 | 检查 SealDice「连接」里的 OB11 正向 WS 是否指向 `/core` 且未报错 | diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index 1db1b2c..6097ff8 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -27,19 +27,178 @@ export default class ToolConfig { }, "web-read": { "type": "http", - "url": "http://127.0.0.1:46799/mcp" + "url": "http://127.0.0.1:46799/mcp", + "tools": { + "screenshot_url": { + "hidden": true + }, + "scrape_url": { + "exposeAs": "web_read", + "adapter": "web_read", + "remoteTools": { + "screenshot": "screenshot_url", + "scrape": "scrape_url" + }, + "description": "读取网页内容或对网页截图。默认抓取网页标题/正文/链接;screenshot=true 时对网页截图并返回可发送的图片", + "parameters": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "需要读取内容或截图的网页链接" + }, + "screenshot": { + "type": "boolean", + "description": "true 时对网页截图并返回图片,false(默认)时抓取网页文本内容" + }, + "width": { + "type": "integer", + "description": "截图视口宽度,默认 1680" + }, + "height": { + "type": "integer", + "description": "截图视口高度,默认 1000" + }, + "fullPage": { + "type": "boolean", + "description": "是否截取整页(长图),默认 false" + }, + "delay": { + "type": "integer", + "description": "页面加载完成后等待的毫秒数,默认 3000" + } + }, + "required": ["url"] + } + } + } }, "md-html-render": { "type": "http", - "url": "http://127.0.0.1:37632/mcp" + "url": "http://127.0.0.1:37632/mcp", + "tools": { + "render_markdown": { + "exposeAs": "render_markdown", + "adapter": "render_markdown", + "output": "image", + "format": "unknown", + "description": "渲染 Markdown 内容为图片", + "parameters": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "要渲染的 Markdown 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用[img:xxxxxx]替代图片url(注意使用markdown语法显示图片),xxxxxx为图片id,或avatar:用户名称/ID,或group_avatar:群聊名称/ID" + }, + "name": { + "type": "string", + "description": "名称,对内容大致描述" + }, + "theme": { + "type": "string", + "enum": ["light", "dark", "gradient"], + "description": "主题样式,其中 gradient 为紫色渐变背景" + }, + "save": { + "type": "boolean", + "description": "是否保存图片" + } + }, + "required": ["content", "name", "save"] + } + }, + "render_html": { + "exposeAs": "render_html", + "adapter": "render_html", + "output": "image", + "format": "unknown", + "description": "渲染 HTML 内容为图片", + "parameters": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "要渲染的 HTML 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用[img:xxxxxx]替代图片url(注意使用html元素显示图片),xxxxxx为图片id,或avatar:用户名称/ID,或group_avatar:群聊名称/ID" + }, + "name": { + "type": "string", + "description": "名称,对内容大致描述" + }, + "save": { + "type": "boolean", + "description": "是否保存图片" + } + }, + "required": ["content", "name", "save"] + } + } + } }, "ob11-core-bridge": { "type": "http", - "url": "http://127.0.0.1:46880/mcp" + "url": "http://127.0.0.1:46880/mcp", + "tools": { + "run_ext_command": { + "hidden": true + }, + "run_core_command": { + "exposeAs": "run_core_command", + "adapter": "core_bridge_core", + "sensitive": true, + "description": "通过核心桥向 SealDice 注入一条假消息并执行核心指令。白名单中的核心扩展名统一写作 core|指令名;核心 .ext 是扩展发现入口,不需要加入白名单,调用 command=\"ext\" 即可查看核心当前全部扩展名称。默认指令前缀为 .,可在配置中修改。", + "parameters": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["list", "call"], + "description": "list=列出白名单核心指令;call=执行核心指令" + }, + "command": { + "type": "string", + "description": "核心指令名,如 ext、help;也支持 core|ext" + }, + "args": { + "type": "array", + "items": { "type": "string" }, + "description": "指令参数,按顺序填写" + }, + "forward": { + "type": "boolean", + "description": "是否把捕获到的核心发送消息继续转发给协议端,默认 false(避免重复发送)" + }, + "captureMode": { + "type": "string", + "enum": ["reply_only", "lane"], + "description": "消息捕获范围;forward=true 且希望捕获协议端回复时建议使用 lane" + }, + "maxMessages": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "description": "最多收集多少条消息" + }, + "settleMs": { + "type": "integer", + "minimum": 0, + "maximum": 10000, + "description": "收到消息后等待多久没有新消息才结束" + }, + "timeoutMs": { + "type": "integer", + "minimum": 100, + "maximum": 120000, + "description": "最长等待时间,单位毫秒" + } + }, + "required": ["action"] + } + } + } } } }` - ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); + ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)、tools(可选工具适配块:键为远端工具名,值为对象,支持 hidden=仅作底层工具不暴露给 AI、exposeAs=暴露给 AI 的工具名、adapter=适配器(text/image/web_read/render_markdown/render_html/core_bridge_core)、description/parameters=覆盖 AI 可见的描述与参数、output/format=图片输出与保存格式、remoteTool/remoteTools=实际调用的远端工具名映射、sensitive=敏感工具标记;未配置 tools 的服务器按 <服务器名>_<工具名> 通用注册)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); seal.ext.registerTemplateConfig(ext, "技能配置", [ `--- name: 今日人品 diff --git a/src/integration/core_bridge/client.ts b/src/integration/core_bridge/client.ts deleted file mode 100644 index 36c22c1..0000000 --- a/src/integration/core_bridge/client.ts +++ /dev/null @@ -1,72 +0,0 @@ -import Config from "../../config/config"; -import { callServerTool, getMCPServerByName } from "../../tool/mcp"; - -import { CoreBridgeInvocation, CoreBridgeResult } from "./types"; - -const BRIDGE_SERVER = 'ob11-core-bridge'; -let sequence = 0; - -function nextId(): string { - sequence = (sequence + 1) % 100000; - return `invoke_${Date.now().toString(36)}_${sequence.toString(36)}`; -} - -function decodeResult(text: string): CoreBridgeResult { - try { - const result = JSON.parse(text); - if (result && typeof result === 'object' && typeof result.ok === 'boolean') return result as CoreBridgeResult; - } catch (_) { - // MCP 服务端应返回 JSON 文本;保留原文,便于定位不兼容的中间件。 - } - throw new Error(`核心指令中转返回了无效结果:${text.slice(0, 500)}`); -} - -export class CoreBridgeClient { - static reset(): void { - // MCP 会话由通用 MCP 客户端统一管理;配置热加载时会按服务器配置自动重建会话。 - } - - static async invoke(request: CoreBridgeInvocation, toolName = 'run_ext_command'): Promise { - const server = getMCPServerByName(BRIDGE_SERVER); - if (!server) throw new Error(`未配置 MCP 服务器 ${BRIDGE_SERVER};请在「工具 → MCP服务器配置」中添加 ${BRIDGE_SERVER}`); - const result = await callServerTool(server, toolName, { ...request, id: nextId() }); - return decodeResult(result); - } - - static async call( - ctx: seal.MsgContext, - command: string, - args: string[], - capture?: CoreBridgeInvocation['capture'], - timeoutMs?: number, - toolName = 'run_ext_command' - ): Promise { - const prefix = Config.tool.COMMAND_PREFIX; - const raw = `${prefix}${command}${args.length ? ` ${args.join(' ')}` : ''}`.trim(); - const target: CoreBridgeInvocation['target'] = { - selfId: String(ctx.endPoint.userId || '').replace(/^.+:/, ''), - messageType: ctx.isPrivate ? 'private' : 'group', - userId: String(ctx.player && ctx.player.userId || '').replace(/^.+:/, '') - }; - if (!ctx.isPrivate) target.groupId = String(ctx.group && ctx.group.groupId || '').replace(/^.+:/, ''); - return this.invoke({ - target, - actor: { - userId: target.userId || target.selfId, - nickname: String(ctx.player && ctx.player.name || 'AI'), - role: 'member' - }, - command: { raw, name: command, args }, - capture, - timeoutMs - }, toolName); - } -} - -export function formatCoreBridgeResult(result: CoreBridgeResult): string { - if (!result.ok) return `中转执行失败:${result.error || '未知错误'}`; - const texts = (result.messages || []).map(item => item.text || '').filter(Boolean); - const body = texts.length ? texts.join('\n') : '核心未返回文本消息'; - const flags = [result.ambiguous ? '消息关联存在歧义' : '', result.completedBy ? `结束方式:${result.completedBy}` : ''].filter(Boolean); - return `${body}${flags.length ? `\n(${flags.join(',')})` : ''}`; -} diff --git a/src/tool/mcp.ts b/src/tool/mcp.ts index 972201a..d47f9f7 100644 --- a/src/tool/mcp.ts +++ b/src/tool/mcp.ts @@ -2,6 +2,8 @@ import { ext } from "../config/config"; import Logger from "../logger"; +import { runMCPAdapter } from "./mcp/adapters"; +import { MCPCallResult, MCPToolConfig } from "./mcp/types"; import Tool, { toolMap } from "./tool"; export interface MCPServer { @@ -9,6 +11,7 @@ export interface MCPServer { url: string; token: string; headers: { [key: string]: string }; + tools?: Record; } interface MCPToolDef { @@ -29,14 +32,11 @@ const serverStates: { [name: string]: MCPServerState } = {}; const mcpToolKeys = new Map(); // MCP 注册过的工具键 → 所属服务器名,仅清理这些键避免误删普通工具 let lastRefreshAt = 0; // 全量刷新节流:避免每条消息都重新同步工具列表 -// 内置后端服务(有专用包装工具 web_read / render_markdown / render_html), -// 仅作为 MCP 服务器供包装工具调用,不把其 tools/list 重复注册为 AI 工具 -const BUILTIN_BACKEND_SERVERS = new Set(['web-read', 'md-html-render', 'ob11-core-bridge']); - /** 服务器配置是否一致:url/token/headers 任一变化都视为新配置,需要重建会话并重新拉取工具列表 */ function sameServerConfig(a: MCPServer, b: MCPServer): boolean { if (a.url !== b.url || a.token !== b.token) return false; - return JSON.stringify(a.headers || {}) === JSON.stringify(b.headers || {}); + return JSON.stringify(a.headers || {}) === JSON.stringify(b.headers || {}) + && JSON.stringify(a.tools || {}) === JSON.stringify(b.tools || {}); } /** 按名称取最新配置:始终实时解析当前配置(热加载后立即生效),不保留已移除服务器的旧会话 */ @@ -80,11 +80,21 @@ function normalizeMCPServer(name: string, cfg: any): MCPServer | null { const auth = headers['Authorization'] || ''; if (!token && auth.startsWith('Bearer ')) token = auth.slice(7).trim(); + let tools: Record | undefined; + if (cfg.tools && typeof cfg.tools === 'object' && !Array.isArray(cfg.tools)) { + tools = {}; + for (const [toolName, toolCfg] of Object.entries(cfg.tools)) { + if (toolCfg && typeof toolCfg === 'object') tools[toolName] = toolCfg as MCPToolConfig; + } + if (Object.keys(tools).length === 0) tools = undefined; + } + return { name: String(cfg.name || name || '').trim(), url: String(cfg.url || '').trim(), token, - headers + headers, + ...(tools ? { tools } : {}) }; } @@ -186,7 +196,7 @@ async function listTools(server: MCPServer, sessionId: string): Promise { +async function doCallTool(server: MCPServer, sessionId: string, name: string, args: any): Promise { const res = await mcpRequest(server, { jsonrpc: '2.0', id: 3, @@ -204,7 +214,11 @@ async function doCallTool(server: MCPServer, sessionId: string, name: string, ar if (result.isError) { throw new Error(text || `MCP 工具 ${name} 返回错误`); } - return text; + return { + content: Array.isArray(result.content) ? result.content : undefined, + structuredContent: result.structuredContent, + isError: false + }; } async function getSessionId(server: MCPServer, force = false): Promise { @@ -235,16 +249,11 @@ async function withSessionRetry(server: MCPServer, fn: (sessionId: string) => } } -async function callTool(server: MCPServer, name: string, args: any): Promise { +async function callTool(server: MCPServer, name: string, args: any): Promise { const { value } = await withSessionRetry(server, sessionId => doCallTool(server, sessionId, name, args)); return value; } -/** 供内置工具直接调用某个 MCP 服务器的工具(如 web-read / md-html-render 后端) */ -export async function callServerTool(server: MCPServer, toolName: string, args: any): Promise { - return await callTool(server, toolName, args || {}); -} - /** 获取工具列表(TTL 缓存),并注册新出现的工具 */ async function syncTools(server: MCPServer, force = false): Promise { const state = serverStates[server.name]; @@ -257,8 +266,14 @@ async function syncTools(server: MCPServer, force = false): Promise listTools(server, sessionId)); serverStates[server.name] = { server, sessionId, tools, toolsFetchedAt: Date.now() }; - // 清理本服务器已注册但 tools/list 不再返回的工具(服务器内工具删除后热加载生效) - const liveKeys = new Set(tools.filter(t => t.name).map(t => `${server.name}_${t.name}`)); + // 清理本服务器已注册但 tools/list 不再返回、或已被 tools 配置隐藏的工具(服务器内工具删除后热加载生效) + const liveKeys = new Set(); + for (const t of tools) { + if (!t.name) continue; + const descriptor = server.tools && server.tools[t.name]; + if (descriptor && descriptor.hidden) continue; + liveKeys.add(descriptor && descriptor.exposeAs ? descriptor.exposeAs : `${server.name}_${t.name}`); + } for (const [key, owner] of mcpToolKeys) { if (owner === server.name && !liveKeys.has(key) && Object.prototype.hasOwnProperty.call(toolMap, key)) { Logger.info(`MCP 服务器 ${server.name} 不再提供工具 ${key},清理`); @@ -269,27 +284,56 @@ async function syncTools(server: MCPServer, force = false): Promise { + }, descriptor ? descriptor.sensitive !== false : true); + const adapterName = descriptor && descriptor.adapter; + const remoteTool = descriptor && descriptor.remoteTool ? descriptor.remoteTool : t.name; + const remoteTools = descriptor && descriptor.remoteTools; + // 闭包只捕获服务器名与远端工具名:每次调用取最新配置,避免配置热加载后仍用旧 URL/Token/适配配置 + tool.solve = async (ctx, msg, session, args) => { const current = getMCPServerByName(server.name); if (!current) return `MCP 服务器 ${server.name} 未配置`; - return await callTool(current, t.name, args || {}); + const currentDescriptor = current.tools && current.tools[t.name] || undefined; + const usedDescriptor = currentDescriptor || descriptor; + const usedRemoteTool = usedDescriptor && usedDescriptor.remoteTool ? usedDescriptor.remoteTool : remoteTool; + const usedRemoteTools = (usedDescriptor && usedDescriptor.remoteTools) || remoteTools; + return runMCPAdapter(usedDescriptor && usedDescriptor.adapter || adapterName, { + ctx, + msg, + session, + args: args || {}, + server: { + name: current.name, + url: current.url, + token: current.token, + headers: current.headers + }, + remoteTool: usedRemoteTool, + remoteTools: usedRemoteTools, + descriptor: usedDescriptor, + callRemote: async (toolName: string, callArgs: any) => callTool(current, toolName, callArgs || {}) + }); }; Logger.info(`已注册 MCP 工具 ${toolName}`); } @@ -324,8 +368,6 @@ export async function registerMCPTools() { lastRefreshAt = now; for (const server of servers) { - // 内置后端服务由专用包装工具调用,不注册为 AI 可见的 MCP 工具 - if (BUILTIN_BACKEND_SERVERS.has(server.name)) continue; try { await syncTools(server); } catch (e) { diff --git a/src/tool/mcp/adapters.ts b/src/tool/mcp/adapters.ts new file mode 100644 index 0000000..f51f1c3 --- /dev/null +++ b/src/tool/mcp/adapters.ts @@ -0,0 +1,345 @@ +// MCP 工具适配器:把 AI 侧参数/返回映射为远端 MCP 工具调用,保留网页截图、Markdown/HTML 渲染、核心桥指令等既有行为 +import Config from "../../config/config"; +import { CoreBridgeInvocation, CoreBridgeResult } from "../../integration/core_bridge/types"; +import Logger from "../../logger"; +import Image from "../../resource/image"; +import { Session } from "../../session/session"; +import { parseSpecialTokens } from "../../utils/string"; +import { generateId } from "../../utils/utils"; +import { isAllowedCore, splitEntry } from "../command_catalog"; + +import { MCPCallResult, MCPToolConfig } from "./types"; + +export interface MCPAdapterContext { + ctx: seal.MsgContext; + msg: seal.Message; + session: Session; + args: { [key: string]: any }; + server: { name: string; url: string; token: string; headers: Record }; + remoteTool?: string; + remoteTools?: Record; + descriptor?: MCPToolConfig; + callRemote: (toolName: string, args: any) => Promise; +} + +type MCPAdapter = (input: MCPAdapterContext) => Promise; + +/** 提取 MCP 结果中的文本内容(兼容 text 块、structuredContent) */ +export function mcpText(result: MCPCallResult | null | undefined): string { + if (!result) return ''; + const texts = Array.isArray(result.content) + ? result.content.map(block => (block && typeof block.text === 'string' ? block.text : '')).filter(Boolean) + : []; + if (texts.length > 0) return texts.join('\n'); + if (result.structuredContent !== undefined && result.structuredContent !== null) { + try { + return JSON.stringify(result.structuredContent); + } catch (_e) { + return String(result.structuredContent); + } + } + return ''; +} + +function normalizeBase64(text: string): string { + const value = String(text || '').trim(); + if (/^data:[^,]+;base64,/i.test(value)) return value.slice(value.indexOf(',') + 1); + return value; +} + +function looksLikeBase64(text: string): boolean { + const compact = normalizeBase64(text).replace(/\s+/g, ''); + return compact.length >= 64 && /^[A-Za-z0-9+/]+={0,2}$/.test(compact); +} + +function inferFormat(mimeType?: string): string | undefined { + const mime = String(mimeType || '').toLowerCase(); + if (mime.includes('png')) return 'png'; + if (mime.includes('jpeg') || mime.includes('jpg')) return 'jpeg'; + if (mime.includes('webp')) return 'webp'; + if (mime.includes('gif')) return 'gif'; + return undefined; +} + +/** 提取 MCP 结果中的图片数据;标准 image 块优先,兼容后端以 text 返回 base64 的旧格式 */ +export function extractMCPImage(result: MCPCallResult | null | undefined, treatTextAsBase64 = false): { data: string; mimeType?: string } | null { + const blocks = Array.isArray(result && result.content) ? result!.content! : []; + for (const block of blocks) { + if (block && block.type === 'image' && typeof block.data === 'string' && block.data) { + return { data: normalizeBase64(block.data), mimeType: block.mimeType }; + } + } + if (treatTextAsBase64) { + for (const block of blocks) { + if (block && typeof block.text === 'string' && looksLikeBase64(block.text)) { + return { data: normalizeBase64(block.text), mimeType: block.mimeType }; + } + } + } + const structured = result && result.structuredContent; + if (structured && typeof structured === 'object') { + const base64 = (structured as any).base64 || (structured as any).data; + if (typeof base64 === 'string' && looksLikeBase64(base64)) { + return { data: normalizeBase64(base64), mimeType: (structured as any).mimeType || (structured as any).mime }; + } + } + return null; +} + +async function defaultText(input: MCPAdapterContext): Promise { + const result = await input.callRemote(input.remoteTool || 'unknown', input.args || {}); + return mcpText(result); +} + +async function defaultImage(input: MCPAdapterContext): Promise { + const result = await input.callRemote(input.remoteTool || 'unknown', input.args || {}); + const image = extractMCPImage(result, true); + if (!image) throw new Error(`MCP 工具 ${input.remoteTool || 'unknown'} 未返回图片数据`); + + const img = new Image(); + img.imageId = `mcp_${generateId()}`; + img.base64 = image.data; + img.format = (input.descriptor && input.descriptor.format) || inferFormat(image.mimeType) || 'png'; + img.description = `MCP 图片[img:${img.imageId}]`; + Image.save(img); + return `成功,请使用[img:${img.imageId}]发送`; +} + +async function webReadAdapter(input: MCPAdapterContext): Promise { + const { url, screenshot = false, width, height, fullPage = false, delay } = input.args || {}; + if (!url) return 'url 不能为空'; + + if (screenshot) { + const remoteTool = (input.remoteTools && input.remoteTools.screenshot) || input.remoteTool || 'screenshot_url'; + Logger.info(`网页截图: ${url}`); + const result = await input.callRemote(remoteTool, { url, width, height, fullPage, delay }); + const image = extractMCPImage(result, true); + if (!image) throw new Error('截图结果为空'); + + const img = new Image(); + img.imageId = `web_${generateId()}`; + img.base64 = image.data; + img.format = inferFormat(image.mimeType) || 'png'; + img.description = `网页截图[img:${img.imageId}]`; + Image.save(img); + return `成功,请使用[img:${img.imageId}]发送`; + } + + const remoteTool = (input.remoteTools && input.remoteTools.scrape) || input.remoteTool || 'scrape_url'; + Logger.info(`读取网页内容: ${url}`); + const result = await input.callRemote(remoteTool, { url }); + return mcpText(result) || '网页内容为空'; +} + +async function transformContentToUrlText(ctx: seal.MsgContext, session: Session, content: string): Promise<{ text: string; images: Image[] }> { + const segs = parseSpecialTokens(content); + let text = ''; + const images: Image[] = []; + for (const seg of segs) { + switch (seg.type) { + case 'text': { + text += seg.content; + break; + } + case 'at': { + const name = seg.content; + const ui = await session.context.findUser(ctx, name); + if (ui !== null) { + text += ` @${ui.userName} `; + } else { + Logger.warning(`无法找到用户:${name}`); + text += ` @${name} `; + } + break; + } + case 'img': { + const id = seg.content; + // 兼容 [img:imageId:描述]:整体找不到时取首个冒号前作为图片 id + const image = await session.context.findImage(ctx, id) || (id.includes(':') ? await session.context.findImage(ctx, id.split(':')[0]) : null); + if (image) { + if (image.type === 'local') throw new Error(`图片[img:${id}]为本地图片,暂不支持`); + images.push(image); + text += image.url; + } else { + Logger.warning(`无法找到图片:${id}`); + } + break; + } + case 'avatar': { + const name = seg.content; + const ui = await session.context.findUser(ctx, name); + if (ui !== null) { + const image = Image.getUserAvatar(ui.userId); + images.push(image); + text += image.url; + } else { + Logger.warning(`无法找到用户:${name}`); + } + break; + } + case 'group_avatar': { + const name = seg.content; + const gi = await session.context.findGroup(ctx, name); + if (gi) { + const image = Image.getGroupAvatar(gi.groupId); + images.push(image); + text += image.url; + } else { + Logger.warning(`无法找到群聊:${name}`); + } + break; + } + } + } + return { text, images }; +} + +async function renderAdapter(input: MCPAdapterContext, kind: 'markdown' | 'html'): Promise { + const { content, name, save } = input.args || {}; + const theme = input.args && input.args.theme || 'light'; + if (!content || !content.trim()) return '内容不能为空'; + if (!name || !name.trim()) return '图片名称不能为空'; + if (kind === 'markdown' && !['light', 'dark', 'gradient'].includes(theme)) return `无效的主题: ${theme}。支持: light, dark, gradient`; + + const kws = kind === 'markdown' ? ['render', 'markdown', name, theme] : ['render', 'html', name]; + try { + const { text, images } = await transformContentToUrlText(input.ctx, input.session, content); + const hasImages = images.length > 0; + const remoteArgs = kind === 'markdown' + ? { markdown: text, theme, width: 1200, quality: 90, hasImages } + : { html: text, theme: 'light', width: 1200, quality: 90, hasImages }; + const result = await input.callRemote(input.remoteTool || (kind === 'markdown' ? 'render_markdown' : 'render_html'), remoteArgs); + const image = extractMCPImage(result, true); + if (!image) throw new Error('渲染结果为空'); + + const img = new Image(); + img.imageId = `${name}_${generateId()}`; + img.base64 = image.data; + img.format = (input.descriptor && input.descriptor.format) || 'unknown'; + img.description = kind === 'markdown' + ? `Markdown 渲染图片[img:${img.imageId}]\n主题:${theme}` + : `HTML 渲染图片[img:${img.imageId}]`; + + if (save) input.session.memory.addMemory(input.ctx, input.session, [], [], kws, [img], img.description); + return `成功,请使用[img:${img.imageId}]发送`; + } catch (err) { + Logger.error(`${kind === 'markdown' ? 'Markdown' : 'HTML'} 渲染失败: ${err instanceof Error ? err.message : String(err)}`); + return `渲染图片失败: ${err instanceof Error ? err.message : String(err)}`; + } +} + +async function renderMarkdownAdapter(input: MCPAdapterContext): Promise { + return renderAdapter(input, 'markdown'); +} + +async function renderHtmlAdapter(input: MCPAdapterContext): Promise { + return renderAdapter(input, 'html'); +} + +let sequence = 0; + +function nextId(): string { + sequence = (sequence + 1) % 100000; + return `invoke_${Date.now().toString(36)}_${sequence.toString(36)}`; +} + +function decodeCoreBridgeResult(text: string): CoreBridgeResult { + try { + const result = JSON.parse(text); + if (result && typeof result === 'object' && typeof result.ok === 'boolean') return result as CoreBridgeResult; + } catch (_e) { + // MCP 服务端应返回 JSON 文本;保留原文,便于定位不兼容的中间件。 + } + throw new Error(`核心指令中转返回了无效结果:${text.slice(0, 500)}`); +} + +function formatCoreBridgeResult(result: CoreBridgeResult): string { + if (!result.ok) return `中转执行失败:${result.error || '未知错误'}`; + const texts = (result.messages || []).map(item => item.text || '').filter(Boolean); + const body = texts.length ? texts.join('\n') : '核心未返回文本消息'; + const flags = [result.ambiguous ? '消息关联存在歧义' : '', result.completedBy ? `结束方式:${result.completedBy}` : ''].filter(Boolean); + return `${body}${flags.length ? `\n(${flags.join(',')})` : ''}`; +} + +function captureOptions(args: { [key: string]: any }, defaultMaxMessages: number, defaultSettleMs: number): { + capture: { mode: 'reply_only' | 'lane'; forward: boolean; maxMessages: number; settleMs: number }; + timeoutMs?: number; +} { + const forward = args && args.forward === true; + const requestedMode = args && (args.captureMode === 'lane' || args.captureMode === 'reply_only') ? args.captureMode : undefined; + const maxMessages = Number(args && args.maxMessages); + const settleMs = Number(args && args.settleMs); + const timeoutMs = Number(args && args.timeoutMs); + return { + capture: { + mode: requestedMode || (forward ? 'lane' : 'reply_only'), + forward, + maxMessages: Number.isFinite(maxMessages) && maxMessages > 0 ? maxMessages : defaultMaxMessages, + settleMs: Number.isFinite(settleMs) && settleMs >= 0 ? settleMs : defaultSettleMs + }, + timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined + }; +} + +async function coreBridgeAdapter(input: MCPAdapterContext): Promise { + const { ctx, args = {} } = input; + const action = String(args && args.action || ''); + if (action === 'list') { + const list = Config.tool.CMD_WHITELIST.map(item => splitEntry(String(item))).filter(item => item && item.extName === 'core').map(item => `core|${item!.cmd}`); + return `可调用核心指令(共 ${list.length} 个):\n${list.length ? list.join('\n') : '(除 .ext 外暂无白名单核心指令)'}\n核心扩展发现:使用 action=call、command=ext 查看全部扩展名称`; + } + if (action !== 'call') return 'action 仅支持 list 或 call'; + + let command = String(args && args.command || '').trim(); + if (command.indexOf('core|') === 0) command = command.slice(5).trim(); + if (!command) return '调用核心指令时 command 不能为空'; + if (!isAllowedCore(command)) return `核心指令 core|${command} 不在可调用指令白名单内,无法调用`; + + const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; + const options = captureOptions(args, 50, 500); + if (command === 'ext' && !(args && args.captureMode)) options.capture.mode = 'lane'; + + const prefix = Config.tool.COMMAND_PREFIX; + const raw = `${prefix}${command}${cmdArgs.length ? ` ${cmdArgs.join(' ')}` : ''}`.trim(); + const target: CoreBridgeInvocation['target'] = { + selfId: String(ctx.endPoint.userId || '').replace(/^.+:/, ''), + messageType: ctx.isPrivate ? 'private' : 'group', + userId: String(ctx.player && ctx.player.userId || '').replace(/^.+:/, '') + }; + if (!ctx.isPrivate) target.groupId = String(ctx.group && ctx.group.groupId || '').replace(/^.+:/, ''); + + try { + const result = await input.callRemote(input.remoteTool || 'run_core_command', { + target, + actor: { + userId: target.userId || target.selfId, + nickname: String(ctx.player && ctx.player.name || 'AI'), + role: 'member' + }, + command: { raw, name: command, args: cmdArgs }, + capture: options.capture, + timeoutMs: options.timeoutMs, + id: nextId() + }); + return `核心指令 core|${command} 返回:\n${formatCoreBridgeResult(decodeCoreBridgeResult(mcpText(result)))}`; + } catch (e) { + Logger.warning(`[run_core_command] 调用 core|${command} 失败:${e instanceof Error ? e.message : String(e)}`); + return `核心指令 core|${command} 调用失败:${e instanceof Error ? e.message : String(e)}`; + } +} + +const ADAPTERS: { [name: string]: MCPAdapter } = { + text: defaultText, + image: defaultImage, + web_read: webReadAdapter, + render_markdown: renderMarkdownAdapter, + render_html: renderHtmlAdapter, + core_bridge_core: coreBridgeAdapter +}; + +/** 按配置的适配器名执行;未指定时按 output 选择 text/image 通用适配器 */ +export async function runMCPAdapter(name: string | undefined, input: MCPAdapterContext): Promise { + const adapterName = name || (input.descriptor && input.descriptor.output === 'image' ? 'image' : 'text'); + const adapter = ADAPTERS[adapterName] || defaultText; + return adapter(input); +} diff --git a/src/tool/mcp/types.ts b/src/tool/mcp/types.ts new file mode 100644 index 0000000..93d9188 --- /dev/null +++ b/src/tool/mcp/types.ts @@ -0,0 +1,47 @@ +// MCP 通用类型:服务器归一化、工具适配配置与调用结果 + +export interface MCPContentBlock { + type?: string; + text?: string; + data?: string; + mimeType?: string; + url?: string; +} + +export interface MCPCallResult { + content?: MCPContentBlock[]; + structuredContent?: any; + isError?: boolean; +} + +/** 单个 MCP 工具的本地适配配置。未配置时按通用规则注册 `<服务器名>_<工具名>`。 */ +export interface MCPToolConfig { + /** 暴露给 AI 的工具名;默认 `<服务器名>_<工具名>` */ + exposeAs?: string; + /** 是否只作为后端远程工具,不注册为 AI 工具(如低层 screenshot_url/scrape_url) */ + hidden?: boolean; + /** 敏感工具标记;默认 true,与通用 MCP 工具一致 */ + sensitive?: boolean; + /** 输入/输出适配器名:text/image/web_read/render_markdown/render_html/core_bridge_core */ + adapter?: string; + /** 覆盖 AI 可见的工具描述 */ + description?: string; + /** 覆盖 AI 可见的参数 JSON Schema */ + parameters?: any; + /** 调用远端工具时使用的工具名;默认取配置键名 */ + remoteTool?: string; + /** 按场景映射到多个远端工具,如 { "screenshot": "screenshot_url", "scrape": "scrape_url" } */ + remoteTools?: Record; + /** 通用适配时声明输出类型:text 或 image */ + output?: 'text' | 'image'; + /** 保存 MCP 返回图片时使用的格式,如 png/unknown */ + format?: string; +} + +export interface MCPServerConfig { + name: string; + url: string; + token: string; + headers: Record; + tools?: Record; +} diff --git a/src/tool/tools/core/tool_cmd.ts b/src/tool/tools/core/tool_cmd.ts index 31b97c6..b4a0ee3 100644 --- a/src/tool/tools/core/tool_cmd.ts +++ b/src/tool/tools/core/tool_cmd.ts @@ -1,8 +1,7 @@ -// 海豹指令工具:扩展指令本地直调 solve;核心指令经 OB11 核心桥中转(run_core_command)。 +// 海豹指令工具:扩展指令本地直调 solve;核心指令由 MCP 适配器注册 run_core_command。 import Config from "../../../config/config"; -import { CoreBridgeClient, formatCoreBridgeResult } from "../../../integration/core_bridge/client"; import Logger from "../../../logger"; -import { collectCommands, extensionNames, isAllowedCore, isAllowedExtension, ResolvedCommand, resolveEntry, splitEntry } from "../../command_catalog"; +import { collectCommands, extensionNames, isAllowedExtension, ResolvedCommand, resolveEntry } from "../../command_catalog"; import { executeExtensionLocally } from "../../extension_executor"; import Tool from "../../tool"; @@ -27,27 +26,7 @@ function optionNumber(value: any): number | undefined { return Number.isFinite(n) && n >= 0 ? n : undefined; } -function captureOptions(args: { [key: string]: any }, defaultMaxMessages: number, defaultSettleMs: number): { - capture: { mode: 'reply_only' | 'lane'; forward: boolean; maxMessages: number; settleMs: number }; - timeoutMs?: number; -} { - const forward = args && args.forward === true; - const requestedMode = args && (args.captureMode === 'lane' || args.captureMode === 'reply_only') ? args.captureMode : undefined; - const maxMessages = Number(args && args.maxMessages); - const settleMs = Number(args && args.settleMs); - const timeoutMs = Number(args && args.timeoutMs); - return { - capture: { - mode: requestedMode || (forward ? 'lane' : 'reply_only'), - forward, - maxMessages: Number.isFinite(maxMessages) && maxMessages > 0 ? maxMessages : defaultMaxMessages, - settleMs: Number.isFinite(settleMs) && settleMs >= 0 ? settleMs : defaultSettleMs - }, - timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined - }; -} - -export function registerCmdTool(): { extTool: Tool; coreTool: Tool } { +export function registerCmdTool(): Tool { const extTool = new Tool({ type: 'function', function: { @@ -101,50 +80,5 @@ export function registerCmdTool(): { extTool: Tool; coreTool: Tool } { } }; - const coreTool = new Tool({ - type: 'function', - function: { - name: 'run_core_command', - description: `通过核心桥向 SealDice 注入一条假消息并执行核心指令。白名单中的核心扩展名统一写作 core|指令名;核心 .ext 是扩展发现入口,不需要加入白名单,调用 command="ext" 即可查看核心当前全部扩展名称。默认指令前缀为 .,可在配置中修改。`, - parameters: { - type: 'object', - properties: { - action: { type: 'string', enum: ['list', 'call'], description: 'list=列出白名单核心指令;call=执行核心指令' }, - command: { type: 'string', description: '核心指令名,如 ext、help;也支持 core|ext' }, - args: { type: 'array', items: { type: 'string' }, description: '指令参数,按顺序填写' }, - forward: { type: 'boolean', description: '是否把捕获到的核心发送消息继续转发给协议端,默认 false(避免重复发送)' }, - captureMode: { type: 'string', enum: ['reply_only', 'lane'], description: '消息捕获范围;forward=true 且希望捕获协议端回复时建议使用 lane' }, - maxMessages: { type: 'integer', minimum: 1, maximum: 50, description: '最多收集多少条消息' }, - settleMs: { type: 'integer', minimum: 0, maximum: 10000, description: '收到消息后等待多久没有新消息才结束' }, - timeoutMs: { type: 'integer', minimum: 100, maximum: 120000, description: '最长等待时间,单位毫秒' } - }, - required: ['action'] - } - } - }); - coreTool.sensitive = true; - coreTool.solve = async (ctx, _msg, _session, args) => { - const action = String(args && args.action || ''); - if (action === 'list') { - const list = Config.tool.CMD_WHITELIST.map(item => splitEntry(String(item))).filter(item => item && item.extName === 'core').map(item => `core|${item!.cmd}`); - return `可调用核心指令(共 ${list.length} 个):\n${list.length ? list.join('\n') : '(除 .ext 外暂无白名单核心指令)'}\n核心扩展发现:使用 action=call、command=ext 查看全部扩展名称`; - } - if (action !== 'call') return 'action 仅支持 list 或 call'; - let command = String(args && args.command || '').trim(); - if (command.indexOf('core|') === 0) command = command.slice(5).trim(); - if (!command) return '调用核心指令时 command 不能为空'; - if (!isAllowedCore(command)) return `核心指令 core|${command} 不在可调用指令白名单内,无法调用`; - const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; - const options = captureOptions(args, 50, 500); - if (command === 'ext' && !(args && args.captureMode)) options.capture.mode = 'lane'; - try { - const result = await CoreBridgeClient.call(ctx, command, cmdArgs, options.capture, options.timeoutMs, 'run_core_command'); - return `核心指令 core|${command} 返回:\n${formatCoreBridgeResult(result)}`; - } catch (e) { - Logger.warning(`[run_core_command] 调用 core|${command} 失败:${e instanceof Error ? e.message : String(e)}`); - return `核心指令 core|${command} 调用失败:${e instanceof Error ? e.message : String(e)}`; - } - }; - - return { extTool, coreTool }; + return extTool; } diff --git a/src/tool/tools/image/init.ts b/src/tool/tools/image/init.ts index ba01cf1..d5bcbba 100644 --- a/src/tool/tools/image/init.ts +++ b/src/tool/tools/image/init.ts @@ -1,11 +1,9 @@ // image.ts 子目录工具注册统一入口 import { registerImage } from "./tool_image"; import { registerMeme } from "./tool_meme"; -import { registerRender } from "./tool_render"; /** 注册 image.ts 下全部图片工具 */ export function registerImageTools() { registerImage(); registerMeme(); - registerRender(); } diff --git a/src/tool/tools/image/tool_render.ts b/src/tool/tools/image/tool_render.ts deleted file mode 100644 index ee1d124..0000000 --- a/src/tool/tools/image/tool_render.ts +++ /dev/null @@ -1,245 +0,0 @@ -// 渲染工具:Markdown/HTML 转图片(后端 md-html-render,经「工具 → MCP服务器配置」接入) -import { logger } from "../../../logger"; -import Image from "../../../resource/image"; -import { Session } from "../../../session/session"; -import { parseSpecialTokens } from "../../../utils/string"; -import { generateId } from "../../../utils/utils"; -import { callServerTool, getMCPServerByName, isMCPEnabled } from "../../mcp"; -import Tool from "../../tool"; - -interface RenderResponse { - status: string; - imageId?: string; - url?: string; - fileName?: string; - contentType?: string; - base64?: string; - message?: string; -} - -async function transformContentToUrlText(ctx: seal.MsgContext, session: Session, content: string): Promise<{ text: string, images: Image[] }> { - const segs = parseSpecialTokens(content); - let text = ''; - const images: Image[] = []; - for (const seg of segs) { - switch (seg.type) { - case 'text': { - text += seg.content; - break; - } - case 'at': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) { - text += ` @${ui.userName} `; - } else { - logger.warning(`无法找到用户:${name}`); - text += ` @${name} `; - } - break; - } - case 'img': { - const id = seg.content; - // 兼容 [img:imageId:描述]:整体找不到时取首个冒号前作为图片 id - const image = await session.context.findImage(ctx, id) || (id.includes(':') ? await session.context.findImage(ctx, id.split(':')[0]) : null); - - if (image) { - if (image.type === 'local') throw new Error(`图片[img:${id}]为本地图片,暂不支持`); - images.push(image); - text += image.url; - } else { - logger.warning(`无法找到图片:${id}`); - } - break; - } - case 'avatar': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) { - const image = Image.getUserAvatar(ui.userId); - images.push(image); - text += image.url; - } else { - logger.warning(`无法找到用户:${name}`); - } - break; - } - case 'group_avatar': { - const name = seg.content; - const gi = await session.context.findGroup(ctx, name); - if (gi) { - const image = Image.getGroupAvatar(gi.groupId); - images.push(image); - text += image.url; - } else { - logger.warning(`无法找到群聊:${name}`); - } - break; - } - } - } - return { text, images }; -} - -// 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 }); - if (!base64) throw new Error('渲染结果为空'); - return { status: 'success', base64 } as RenderResponse; -} - -// 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 }); - if (!base64) throw new Error('渲染结果为空'); - return { status: 'success', base64 } as RenderResponse; -} - -export function registerRender() { - const toolMd = new Tool({ - type: "function", - function: { - name: "render_markdown", - description: `渲染 Markdown 内容为图片`, - parameters: { - type: "object", - properties: { - content: { - type: "string", - description: "要渲染的 Markdown 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用[img:xxxxxx]替代图片url(注意使用markdown语法显示图片),xxxxxx为" + `图片id,或avatar:用户名称/ID,或group_avatar:群聊名称/ID` - }, - name: { - type: "string", - description: "名称,对内容大致描述" - }, - theme: { - type: "string", - description: "主题样式,其中 gradient 为紫色渐变背景", - enum: ["light", "dark", "gradient"], - }, - save: { - type: "boolean", - description: "是否保存图片" - } - }, - required: ["content", "name", "save"] - } - } - }); - - toolMd.solve = async (ctx, _, session, args) => { - const { content, name, theme = 'light', save } = args; - if (!content || !content.trim()) return `内容不能为空`; - if (!name || !name.trim()) return `图片名称不能为空`; - if (!['light', 'dark', 'gradient'].includes(theme)) return `无效的主题: ${theme}。支持: light, dark, gradient`; - - // 切换到当前会话ai - // 会话已由 Tool.handleToolCall 传入,直接使用 session - - const kws = ["render", "markdown", name, theme]; - - try { - const { text, images } = await transformContentToUrlText(ctx, session, content); - const hasImages = images.length > 0; - - const result = await renderMarkdown(text, theme, 1200, hasImages); - if (result.status === "success" && result.base64) { - const base64 = result.base64; - if (!base64) { - logger.error(`生成的base64为空`); - return "生成的base64为空"; - } - - const img = new Image(); - img.imageId = `${name}_${generateId()}`; - img.base64 = base64; - img.format = 'unknown'; - img.description = `Markdown 渲染图片[img:${img.imageId}] -主题:${theme}`; - - if (save) session.memory.addMemory(ctx, session, [], [], kws, [img], img.description); - - return `渲染成功,请使用[img:${img.imageId}]发送`; - } else { - throw new Error(result.message || "渲染失败"); - } - } catch (err) { - logger.error(`Markdown 渲染失败: ${err instanceof Error ? err.message : String(err)}`); - return `渲染图片失败: ${err instanceof Error ? err.message : String(err)}`; - } - } - - const toolHtml = new Tool({ - type: "function", - function: { - name: "render_html", - description: `渲染 HTML 内容为图片`, - parameters: { - type: "object", - properties: { - content: { - type: "string", - description: "要渲染的 HTML 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用[img:xxxxxx]替代图片url(注意使用html元素显示图片),xxxxxx为" + `图片id,或avatar:用户名称/ID,或group_avatar:群聊名称/ID` - }, - name: { - type: "string", - description: "名称,对内容大致描述" - }, - save: { - type: "boolean", - description: "是否保存图片" - } - }, - required: ["content", "name", "save"] - } - } - }); - - toolHtml.solve = async (ctx, _, session, args) => { - const { content, name, save } = args; - if (!content || !content.trim()) return `内容不能为空`; - if (!name || !name.trim()) return `图片名称不能为空`; - - // 切换到当前会话ai - // 会话已由 Tool.handleToolCall 传入,直接使用 session - - const kws = ["render", "html", name]; - - try { - const { text, images } = await transformContentToUrlText(ctx, session, content); - const hasImages = images.length > 0; - - const result = await renderHtml(text, 1200, hasImages); - if (result.status === "success" && result.base64) { - const base64 = result.base64; - if (!base64) { - logger.error(`生成的base64为空`); - return "生成的base64为空"; - } - - const img = new Image(); - img.imageId = `${name}_${generateId()}`; - img.base64 = base64; - img.format = 'unknown'; - img.description = `HTML 渲染图片[img:${img.imageId}]`; - - if (save) session.memory.addMemory(ctx, session, [], [], kws, [img], img.description); - - return `渲染成功,请使用[img:${img.imageId}]发送`; - } else { - throw new Error(result.message || "渲染失败"); - } - } catch (err) { - logger.error(`HTML 渲染失败: ${err instanceof Error ? err.message : String(err)}`); - return `渲染图片失败: ${err instanceof Error ? err.message : String(err)}`; - } - } -} - -// TODO:嵌入本地图片 diff --git a/src/tool/tools/web/tool_web.ts b/src/tool/tools/web/tool_web.ts index f11ddc4..71b25b9 100644 --- a/src/tool/tools/web/tool_web.ts +++ b/src/tool/tools/web/tool_web.ts @@ -1,9 +1,6 @@ // 联网工具:搜索与网页阅读 import Config from "../../../config/config"; import { logger } from "../../../logger"; -import Image from "../../../resource/image"; -import { generateId } from "../../../utils/utils"; -import { callServerTool, getMCPServerByName, isMCPEnabled } from "../../mcp"; import Tool from "../../tool"; export function registerWeb() { @@ -87,75 +84,4 @@ export function registerWeb() { } } - const tool = new Tool({ - type: "function", - function: { - name: "web_read", - description: `读取网页内容或对网页截图。默认抓取网页标题/正文/链接;screenshot=true 时对网页截图并返回可发送的图片`, - parameters: { - type: "object", - properties: { - url: { - type: "string", - description: "需要读取内容或截图的网页链接" - }, - screenshot: { - type: "boolean", - description: "true 时对网页截图并返回图片,false(默认)时抓取网页文本内容" - }, - width: { - type: "integer", - description: "截图视口宽度,默认 1680" - }, - height: { - type: "integer", - description: "截图视口高度,默认 1000" - }, - fullPage: { - type: "boolean", - description: "是否截取整页(长图),默认 false" - }, - delay: { - type: "integer", - description: "页面加载完成后等待的毫秒数,默认 3000" - } - }, - required: ["url"] - } - } - }); - 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 格式添加`; - - if (screenshot) { - try { - logger.info(`网页截图: ${url}`); - const base64 = await callServerTool(mcpServer, 'screenshot_url', { url, width, height, fullPage, delay }); - if (!base64) throw new Error('截图失败'); - - const img = new Image(); - img.imageId = `web_${generateId()}`; - img.base64 = base64; - img.format = 'png'; - img.description = `网页截图[img:${img.imageId}]`; - Image.save(img); - - return `截图成功,请使用[img:${img.imageId}]发送`; - } catch (error) { - logger.error("在web_read截图请求中出错:", error); - return `网页截图失败: ${error instanceof Error ? error.message : String(error)}`; - } - } - - try { - logger.info(`读取网页内容: ${url}`); - return await callServerTool(mcpServer, 'scrape_url', { url }); - } catch (error) { - logger.error("在web_read中请求出错:", error); - return `读取网页内容失败: ${error}`; - } - } } From 5efc1b5039369fb2a5111b91cb65136b00a0cece Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 13:39:21 +0800 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20MCP=20=E5=B7=A5=E5=85=B7=E5=90=8D?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=89=8D=E7=BC=80?= =?UTF-8?q?=EF=BC=8C=E8=BF=9C=E7=AB=AF=E5=90=8C=E5=90=8D=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B7=B3=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- ...45\267\245\345\205\267\347\263\273\347\273\237.md" | 6 +++--- ...44\275\277\347\224\250\346\214\207\345\215\227.md" | 3 +-- src/config/configs/tool.ts | 5 +---- src/tool/mcp.ts | 11 +++++++---- src/tool/mcp/types.ts | 4 ++-- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 68dfe87..d2ee27e 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ max_tokens = 2048 | 默认关闭的函数 | 每行一个,AI 在新会话中默认无法调用,需 `.ai tool on <函数名>` 开启 | | 提供给AI的牌堆名称 | 每行一个牌堆名,用于 `draw_deck` 工具;没有的话建议把 `draw_deck` 加入禁止调用 | | 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | -| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。每台服务器可配 `tools` 适配块(`hidden`/`exposeAs`/`adapter`/`remoteTools`/`description`/`parameters` 等),默认配置把 `web_read`、`render_markdown`、`render_html`、`run_core_command` 映射为原工具名。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | +| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。每台服务器可配 `tools` 适配块(`hidden`/`exposeAs`/`adapter`/`remoteTools`/`description`/`parameters` 等);未配置时默认直接用远端工具名注册,同名冲突自动跳过。默认配置把 `web_read`、`render_markdown`、`render_html`、`run_core_command` 映射为原工具名。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | | 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | | ai语音使用的音色 | 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)和 ffmpeg | @@ -423,7 +423,7 @@ max_tokens = 2048 | 音乐 | `music_play` | | 黑名单 | `suggest_block`(AI 建议拉黑,带冷却;默认需骰主确认)、`unblock_user`、`get_block_list` | | 论坛 | `forum_get_posts`、`forum_get_post_detail`、`forum_search`、`forum_create_post`、`forum_manage_comment`、`forum_get_activity`、`forum_manage_post` | -| MCP / 技能 | `<服务器名>_<工具名>`(MCP 工具,可用 `tools.exposeAs` 覆盖)、`use_skill`(技能) | +| MCP / 技能 | 远端工具名(MCP 工具,同名冲突时跳过;可用 `tools.exposeAs` 改名)、`use_skill`(技能) | > 指令类技能(今日人品、COC 模组抽取/搜索、属性展示、属性检定、san 检定等)通过 `use_skill` 按需获取内容,内部统一使用 `run_ext_command` / `run_core_command` 调用海豹指令,对应指令需加入「可调用指令白名单」。 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 761a804..d7fd0b0 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" @@ -89,7 +89,7 @@ 外部注册: -- MCP 工具:`<服务器名>_<工具名>`,或 `tools` 适配配置里 `exposeAs` 指定的名字(见下节)。 +- MCP 工具:默认直接用远端工具名注册;与本地/已有工具同名时自动跳过,可用 `tools` 适配配置里 `exposeAs` 改名(见下节)。 - 技能工具:`use_skill`(见技能节)。 依赖说明:ob11 相关工具(收发消息、用户/群信息、禁言、打卡等)需要安装 ob11 网络连接依赖或 HTTP 依赖;`text_to_sound` 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)与 ffmpeg;`music_play` 需要协议端配置音卡签名;`text_to_image` 需要生成图片依赖(tti)。 @@ -105,9 +105,9 @@ ## 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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。每个服务器可带可选 `tools` 适配块:键为远端工具名,值可配 `hidden`(仅作底层工具不暴露给 AI)、`exposeAs`(暴露给 AI 的工具名)、`adapter`(text/image/web_read/render_markdown/render_html/core_bridge_core)、`description`/`parameters`(覆盖 AI 可见的描述与参数)、`output`/`format`(图片输出与保存格式)、`remoteTool`/`remoteTools`(实际调用的远端工具名映射)、`sensitive`(敏感工具标记)。默认配置通过 `tools` 适配把 `web_read`(底层 `screenshot_url`/`scrape_url`)、`render_markdown`/`render_html`、`run_core_command` 暴露为原工具名,并保留网页截图、Markdown/HTML 渲染、核心指令多消息收集等原有处理逻辑;`run_ext_command` 由插件本地注册,远端同名工具默认隐藏。改 URL/Token 时服务器会话自动重建(热加载) +- 配置项 `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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。每个服务器可带可选 `tools` 适配块:键为远端工具名,值可配 `hidden`(仅作底层工具不暴露给 AI)、`exposeAs`(暴露给 AI 的工具名)、`adapter`(text/image/web_read/render_markdown/render_html/core_bridge_core)、`description`/`parameters`(覆盖 AI 可见的描述与参数)、`output`/`format`(图片输出与保存格式)、`remoteTool`/`remoteTools`(实际调用的远端工具名映射)、`sensitive`(敏感工具标记)。默认配置通过 `tools` 适配把 `web_read`(底层 `screenshot_url`/`scrape_url`)、`render_markdown`/`render_html`、`run_core_command` 暴露为原工具名,并保留网页截图、Markdown/HTML 渲染、核心指令多消息收集等原有处理逻辑;`run_ext_command` 由插件本地注册,远端同名工具由冲突策略自动跳过。改 URL/Token 时服务器会话自动重建(热加载) - 协议:Streamable HTTP 上的 JSON-RPC(`initialize` → `notifications/initialized` → `tools/list` → `tools/call`),带 `Mcp-Session-Id` 会话头,兼容 SSE 响应。 -- 未配置 `tools` 适配的工具按通用规则以 `<服务器名>_<工具名>` 注册为敏感工具,参数透传 `inputSchema`;返回纯文本时直接回传,返回标准 image 块或 base64 文本时保存为 `[img:...]` 并返回图片 ID。 +- 未配置 `tools` 适配的工具默认用远端工具名注册为敏感工具,参数透传 `inputSchema`;返回纯文本时直接回传,返回标准 image 块或 base64 文本时保存为 `[img:...]` 并返回图片 ID。同名冲突会跳过,需要不同的暴露名时配置 `exposeAs`。 - 工具列表有 60 秒 TTL 缓存;会话失效/服务器未初始化(会话不存在/已失效/未初始化/Server not initialized)时自动重新 initialize 并重试一次。 - MCP 配置修改后会按服务器名重建会话并同步工具差集;删除服务器或服务器返回的工具会从注册表移除。MCP 总开关关闭时不建立连接,system prompt 也不会展示不可调用的 MCP/技能入口。 - 错误信息把 JSON-RPC 错误码映射为可读中文。 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index c4c3441..022814c 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -92,7 +92,6 @@ ws://127.0.0.1:46880/core "type": "http", "url": "http://127.0.0.1:46880/mcp", "tools": { - "run_ext_command": { "hidden": true }, "run_core_command": { "exposeAs": "run_core_command", "adapter": "core_bridge_core" @@ -113,7 +112,7 @@ ws://127.0.0.1:46880/core } ``` -3. 按需配置「可调用指令白名单」与「指令前缀」。`tools` 适配块会把远端 `run_core_command` 暴露为同名 AI 工具并套用核心桥适配器;若手写配置时省略该块,`run_core_command` 会按通用规则注册为 `ob11-core-bridge_run_core_command`。`run_ext_command` 在插件内**本地直接执行扩展指令**,无需中间件;`run_core_command` 经 MCP 调用中间件执行核心指令。 +3. 按需配置「可调用指令白名单」与「指令前缀」。`tools` 适配块会把远端 `run_core_command` 暴露为同名 AI 工具并套用核心桥适配器;若手写配置时省略该块,`run_core_command` 也会直接用远端工具名注册,但不会套用核心桥适配器。`run_ext_command` 在插件内**本地直接执行扩展指令**,无需中间件;`run_core_command` 经 MCP 调用中间件执行核心指令。 ## 工具与参数 diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index 6097ff8..fa39e3d 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -138,9 +138,6 @@ export default class ToolConfig { "type": "http", "url": "http://127.0.0.1:46880/mcp", "tools": { - "run_ext_command": { - "hidden": true - }, "run_core_command": { "exposeAs": "run_core_command", "adapter": "core_bridge_core", @@ -198,7 +195,7 @@ export default class ToolConfig { } } }` - ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)、tools(可选工具适配块:键为远端工具名,值为对象,支持 hidden=仅作底层工具不暴露给 AI、exposeAs=暴露给 AI 的工具名、adapter=适配器(text/image/web_read/render_markdown/render_html/core_bridge_core)、description/parameters=覆盖 AI 可见的描述与参数、output/format=图片输出与保存格式、remoteTool/remoteTools=实际调用的远端工具名映射、sensitive=敏感工具标记;未配置 tools 的服务器按 <服务器名>_<工具名> 通用注册)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); + ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)、tools(可选工具适配块:键为远端工具名,值为对象,支持 hidden=仅作底层工具不暴露给 AI、exposeAs=暴露给 AI 的工具名、adapter=适配器(text/image/web_read/render_markdown/render_html/core_bridge_core)、description/parameters=覆盖 AI 可见的描述与参数、output/format=图片输出与保存格式、remoteTool/remoteTools=实际调用的远端工具名映射、sensitive=敏感工具标记;未配置 tools 的服务器默认直接用远端工具名注册,与本地/已有工具同名时跳过,可用 exposeAs 改名或 hidden 隐藏)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); seal.ext.registerTemplateConfig(ext, "技能配置", [ `--- name: 今日人品 diff --git a/src/tool/mcp.ts b/src/tool/mcp.ts index d47f9f7..4fddeef 100644 --- a/src/tool/mcp.ts +++ b/src/tool/mcp.ts @@ -272,7 +272,7 @@ async function syncTools(server: MCPServer, force = false): Promise callTool(current, toolName, callArgs || {}) }); }; + mcpToolKeys.set(toolName, server.name); Logger.info(`已注册 MCP 工具 ${toolName}`); } return tools; diff --git a/src/tool/mcp/types.ts b/src/tool/mcp/types.ts index 93d9188..0d0c5f8 100644 --- a/src/tool/mcp/types.ts +++ b/src/tool/mcp/types.ts @@ -14,9 +14,9 @@ export interface MCPCallResult { isError?: boolean; } -/** 单个 MCP 工具的本地适配配置。未配置时按通用规则注册 `<服务器名>_<工具名>`。 */ +/** 单个 MCP 工具的本地适配配置。未配置时默认直接用远端工具名注册,同名冲突会跳过。 */ export interface MCPToolConfig { - /** 暴露给 AI 的工具名;默认 `<服务器名>_<工具名>` */ + /** 暴露给 AI 的工具名;默认使用远端工具名,可用此字段改名避免同名冲突 */ exposeAs?: string; /** 是否只作为后端远程工具,不注册为 AI 工具(如低层 screenshot_url/scrape_url) */ hidden?: boolean; From c19a3f13692f032ec9955c0c624c111c53f9b5f6 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 13:47:42 +0800 Subject: [PATCH 10/20] =?UTF-8?q?fix:=20MCP=20=E5=8F=AA=E6=9A=B4=E9=9C=B2?= =?UTF-8?q?=20run=5Fcore=5Fcommand=EF=BC=8Cmcp-files-exec=20=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2=E4=B8=BA=20runshell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- ...45\267\245\345\205\267\347\263\273\347\273\237.md" | 2 +- ...44\273\244\344\270\216\351\205\215\347\275\256.md" | 2 +- ...45\220\216\347\253\257\351\241\271\347\233\256.md" | 2 +- src/config/configs/tool.ts | 11 ++++++++++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d2ee27e..33c02d8 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ max_tokens = 2048 | 默认关闭的函数 | 每行一个,AI 在新会话中默认无法调用,需 `.ai tool on <函数名>` 开启 | | 提供给AI的牌堆名称 | 每行一个牌堆名,用于 `draw_deck` 工具;没有的话建议把 `draw_deck` 加入禁止调用 | | 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | -| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。每台服务器可配 `tools` 适配块(`hidden`/`exposeAs`/`adapter`/`remoteTools`/`description`/`parameters` 等);未配置时默认直接用远端工具名注册,同名冲突自动跳过。默认配置把 `web_read`、`render_markdown`、`render_html`、`run_core_command` 映射为原工具名。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | +| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。每台服务器可配 `tools` 适配块(`hidden`/`exposeAs`/`adapter`/`remoteTools`/`description`/`parameters` 等);未配置时默认直接用远端工具名注册,同名冲突自动跳过。默认配置把 `web_read`、`render_markdown`、`render_html`、`run_core_command` 映射为原工具名,把 `mcp-files-exec` 的远端 `run_command` 暴露为 `runshell`,并隐藏 `ob11-core-bridge` 的远端 `run_ext_command`。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | | 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | | ai语音使用的音色 | 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)和 ffmpeg | 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 d7fd0b0..ec2535b 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" @@ -105,7 +105,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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。每个服务器可带可选 `tools` 适配块:键为远端工具名,值可配 `hidden`(仅作底层工具不暴露给 AI)、`exposeAs`(暴露给 AI 的工具名)、`adapter`(text/image/web_read/render_markdown/render_html/core_bridge_core)、`description`/`parameters`(覆盖 AI 可见的描述与参数)、`output`/`format`(图片输出与保存格式)、`remoteTool`/`remoteTools`(实际调用的远端工具名映射)、`sensitive`(敏感工具标记)。默认配置通过 `tools` 适配把 `web_read`(底层 `screenshot_url`/`scrape_url`)、`render_markdown`/`render_html`、`run_core_command` 暴露为原工具名,并保留网页截图、Markdown/HTML 渲染、核心指令多消息收集等原有处理逻辑;`run_ext_command` 由插件本地注册,远端同名工具由冲突策略自动跳过。改 URL/Token 时服务器会话自动重建(热加载) +- 配置项 `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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。每个服务器可带可选 `tools` 适配块:键为远端工具名,值可配 `hidden`(仅作底层工具不暴露给 AI)、`exposeAs`(暴露给 AI 的工具名)、`adapter`(text/image/web_read/render_markdown/render_html/core_bridge_core)、`description`/`parameters`(覆盖 AI 可见的描述与参数)、`output`/`format`(图片输出与保存格式)、`remoteTool`/`remoteTools`(实际调用的远端工具名映射)、`sensitive`(敏感工具标记)。默认配置通过 `tools` 适配把 `web_read`(底层 `screenshot_url`/`scrape_url`)、`render_markdown`/`render_html`、`run_core_command` 暴露为原工具名,把 `mcp-files-exec` 的远端 `run_command` 暴露为 `runshell`,并隐藏 `ob11-core-bridge` 的远端 `run_ext_command`;`run_ext_command` 仍由插件本地注册,不经 MCP。改 URL/Token 时服务器会话自动重建(热加载) - 协议:Streamable HTTP 上的 JSON-RPC(`initialize` → `notifications/initialized` → `tools/list` → `tools/call`),带 `Mcp-Session-Id` 会话头,兼容 SSE 响应。 - 未配置 `tools` 适配的工具默认用远端工具名注册为敏感工具,参数透传 `inputSchema`;返回纯文本时直接回传,返回标准 image 块或 base64 文本时保存为 `[img:...]` 并返回图片 ID。同名冲突会跳过,需要不同的暴露名时配置 `exposeAs`。 - 工具列表有 60 秒 TTL 缓存;会话失效/服务器未初始化(会话不存在/已失效/未初始化/Server not initialized)时自动重新 initialize 并重试一次。 diff --git "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" index e16126f..429c3ca 100644 --- "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" +++ "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" @@ -105,7 +105,7 @@ | 对话 | 角色扮演设定、示例对话、对话保存轮数、上下文最大token、消息压缩阈值 | 每条设定第一行为角色设定名称(>20 截断),其余为设定;`$gSYSPROMPT` 选择 | | 消息接收 | 接收图片/指令消息/骰子发送的消息、忽略消息豹语条件、忽略消息正则表达式 | 接收图片只负责记录图片 URL;自动识别仍由图片识别条件和图片模型配置决定 | | 消息触发 | 触发正则表达式、触发需要满足的条件、触发次数上限、触发次数补充间隔 | 令牌桶配额 | -| 工具 | 开启调用函数功能、切换为提示词工程、允许连续调用函数次数、工具响应压缩触发字数、禁止/默认关闭的函数、是否启用MCP、MCP服务器配置、技能配置、ai语音使用的音色 | 「是否启用MCP」默认关闭,开启后才会解析并连接 MCP 服务器;MCP 仅支持标准 mcpServers JSON(默认含 mcp-files-exec/web-read/md-html-render/ob11-core-bridge,可用 tools 适配块把远端工具映射为 web_read/render_markdown/render_html/run_core_command,格式见 modelcontextprotocol.io/specification/latest),技能仅支持标准 SKILL.md(格式见 agentskills.io/specification) | +| 工具 | 开启调用函数功能、切换为提示词工程、允许连续调用函数次数、工具响应压缩触发字数、禁止/默认关闭的函数、是否启用MCP、MCP服务器配置、技能配置、ai语音使用的音色 | 「是否启用MCP」默认关闭,开启后才会解析并连接 MCP 服务器;MCP 仅支持标准 mcpServers JSON(默认含 mcp-files-exec/web-read/md-html-render/ob11-core-bridge,可用 tools 适配块把远端工具映射为 web_read/render_markdown/render_html/run_core_command,并把 mcp-files-exec 的 run_command 暴露为 runshell;ob11-core-bridge 的 run_ext_command 默认隐藏,格式见 modelcontextprotocol.io/specification/latest),技能仅支持标准 SKILL.md(格式见 agentskills.io/specification) | | 记忆 | 启用长期/总结/知识库记忆、长期记忆上限/展示数量、总结记忆上限/间隔/参与轮数、短期记忆上限、知识库注入阈值(字符)、知识库(Markdown 模板,每条一份完整文档) | 知识库只读,AI 通过 kb 工具/指令检索;修改后自动生效(缓存最多 1 分钟);Markdown 语法见 commonmark.org/help/ | | 回复 | 回复引用、回复最大字数、分段发送延时/基础延时/含图额外延时、禁止回复复读 | 回复过滤规则已内置硬编码 | | 后端 | 流式输出、图片转base64、联网搜索、用量图表、论坛地址/API Token/签名密钥 | 可自建,见 [08-相关后端项目](08-相关后端项目.md);网页读取与 md/html 渲染(MCP)在「工具 → MCP服务器配置」 | 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 065395f..f84dd48 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" @@ -14,7 +14,7 @@ | `mcp-files-exec` | 本地文件读写/执行 | 3910 | 工具 → MCP服务器配置(mcp-files-exec) | | `ob11-core-bridge` | SealDice 核心正向 WS / OB11 协议端 / MCP 透明中转(注入消息执行核心指令;扩展指令由插件本地执行) | 46880 | 工具 → MCP服务器配置(ob11-core-bridge) | -`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。插件不再内置包装工具:默认配置的 `tools` 适配块把 `web-read` / `md-html-render` / `ob11-core-bridge` 的底层 MCP 工具映射为 `web_read` / `render_markdown` / `render_html` / `run_core_command` 并保留原有行为(网页截图、渲染图片、核心指令多消息收集);`mcp-files-exec` 的工具(文件读写/执行)按通用规则注册为 AI 工具;`run_ext_command` 由插件本地实现,不依赖该中间件。这些后端的 REST 接口已移除,仅提供 MCP。 +`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。插件不再内置包装工具:默认配置的 `tools` 适配块把 `web-read` / `md-html-render` / `ob11-core-bridge` 的底层 MCP 工具映射为 `web_read` / `render_markdown` / `render_html` / `run_core_command` 并保留原有行为(网页截图、渲染图片、核心指令多消息收集);`mcp-files-exec` 的远端 `run_command` 暴露为 `runshell`,其余文件工具按原远端名注册;`run_ext_command` 由插件本地实现,不依赖 MCP,并在默认 MCP 配置中隐藏。这些后端的 REST 接口已移除,仅提供 MCP。 ## 部署与一键管理 diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index fa39e3d..8a0fd20 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -23,6 +23,12 @@ export default class ToolConfig { "url": "http://127.0.0.1:3910/mcp", "headers": { "Authorization": "Bearer token" + }, + "tools": { + "run_command": { + "exposeAs": "runshell", + "adapter": "text" + } } }, "web-read": { @@ -138,6 +144,9 @@ export default class ToolConfig { "type": "http", "url": "http://127.0.0.1:46880/mcp", "tools": { + "run_ext_command": { + "hidden": true + }, "run_core_command": { "exposeAs": "run_core_command", "adapter": "core_bridge_core", @@ -195,7 +204,7 @@ export default class ToolConfig { } } }` - ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心/扩展指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)、tools(可选工具适配块:键为远端工具名,值为对象,支持 hidden=仅作底层工具不暴露给 AI、exposeAs=暴露给 AI 的工具名、adapter=适配器(text/image/web_read/render_markdown/render_html/core_bridge_core)、description/parameters=覆盖 AI 可见的描述与参数、output/format=图片输出与保存格式、remoteTool/remoteTools=实际调用的远端工具名映射、sensitive=敏感工具标记;未配置 tools 的服务器默认直接用远端工具名注册,与本地/已有工具同名时跳过,可用 exposeAs 改名或 hidden 隐藏)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); + ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)、tools(可选工具适配块:键为远端工具名,值为对象,支持 hidden=仅作底层工具不暴露给 AI、exposeAs=暴露给 AI 的工具名、adapter=适配器(text/image/web_read/render_markdown/render_html/core_bridge_core)、description/parameters=覆盖 AI 可见的描述与参数、output/format=图片输出与保存格式、remoteTool/remoteTools=实际调用的远端工具名映射、sensitive=敏感工具标记;未配置 tools 的服务器默认直接用远端工具名注册,与本地/已有工具同名时跳过,可用 exposeAs 改名或 hidden 隐藏)。默认配置把 mcp-files-exec 的远端 run_command 暴露为 runshell,隐藏 ob11-core-bridge 的远端 run_ext_command。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); seal.ext.registerTemplateConfig(ext, "技能配置", [ `--- name: 今日人品 From d16954dac039e7637f058c79aafb1d9fb4aaebf7 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 14:02:56 +0800 Subject: [PATCH 11/20] fix: align MCP tool names with backend implementations --- README.md | 2 +- ...5\267\245\345\205\267\347\263\273\347\273\237.md" | 2 +- ...4\273\244\344\270\216\351\205\215\347\275\256.md" | 2 +- ...5\220\216\347\253\257\351\241\271\347\233\256.md" | 2 +- ...4\275\277\347\224\250\346\214\207\345\215\227.md" | 7 +++---- src/config/configs/tool.ts | 12 +----------- 6 files changed, 8 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 33c02d8..f1e5dcc 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ max_tokens = 2048 | 默认关闭的函数 | 每行一个,AI 在新会话中默认无法调用,需 `.ai tool on <函数名>` 开启 | | 提供给AI的牌堆名称 | 每行一个牌堆名,用于 `draw_deck` 工具;没有的话建议把 `draw_deck` 加入禁止调用 | | 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | -| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。每台服务器可配 `tools` 适配块(`hidden`/`exposeAs`/`adapter`/`remoteTools`/`description`/`parameters` 等);未配置时默认直接用远端工具名注册,同名冲突自动跳过。默认配置把 `web_read`、`render_markdown`、`render_html`、`run_core_command` 映射为原工具名,把 `mcp-files-exec` 的远端 `run_command` 暴露为 `runshell`,并隐藏 `ob11-core-bridge` 的远端 `run_ext_command`。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | +| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。每台服务器可配 `tools` 适配块(`hidden`/`exposeAs`/`adapter`/`remoteTools`/`description`/`parameters` 等);未配置时默认直接使用远端工具名注册,同名冲突自动跳过。默认后端工具名为 `run_shell`、`web_read`、`render_markdown`、`render_html`、`run_core_command`;`run_ext_command` 仅由插件本地实现,不由 MCP 提供。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | | 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | | ai语音使用的音色 | 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)和 ffmpeg | 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 ec2535b..c71ccc2 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" @@ -105,7 +105,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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。每个服务器可带可选 `tools` 适配块:键为远端工具名,值可配 `hidden`(仅作底层工具不暴露给 AI)、`exposeAs`(暴露给 AI 的工具名)、`adapter`(text/image/web_read/render_markdown/render_html/core_bridge_core)、`description`/`parameters`(覆盖 AI 可见的描述与参数)、`output`/`format`(图片输出与保存格式)、`remoteTool`/`remoteTools`(实际调用的远端工具名映射)、`sensitive`(敏感工具标记)。默认配置通过 `tools` 适配把 `web_read`(底层 `screenshot_url`/`scrape_url`)、`render_markdown`/`render_html`、`run_core_command` 暴露为原工具名,把 `mcp-files-exec` 的远端 `run_command` 暴露为 `runshell`,并隐藏 `ob11-core-bridge` 的远端 `run_ext_command`;`run_ext_command` 仍由插件本地注册,不经 MCP。改 URL/Token 时服务器会话自动重建(热加载) +- 配置项 `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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。每个服务器可带可选 `tools` 适配块:键为远端工具名,值可配 `hidden`(仅作底层工具不暴露给 AI)、`exposeAs`(暴露给 AI 的工具名)、`adapter`(text/image/web_read/render_markdown/render_html/core_bridge_core)、`description`/`parameters`(覆盖 AI 可见的描述与参数)、`output`/`format`(图片输出与保存格式)、`remoteTool`/`remoteTools`(实际调用的远端工具名映射)、`sensitive`(敏感工具标记)。默认配置直接使用后端提供的 `run_shell`、`web_read`、`render_markdown`、`render_html`、`run_core_command`;`run_ext_command` 由插件本地注册,不经 MCP。改 URL/Token 时服务器会话自动重建(热加载) - 协议:Streamable HTTP 上的 JSON-RPC(`initialize` → `notifications/initialized` → `tools/list` → `tools/call`),带 `Mcp-Session-Id` 会话头,兼容 SSE 响应。 - 未配置 `tools` 适配的工具默认用远端工具名注册为敏感工具,参数透传 `inputSchema`;返回纯文本时直接回传,返回标准 image 块或 base64 文本时保存为 `[img:...]` 并返回图片 ID。同名冲突会跳过,需要不同的暴露名时配置 `exposeAs`。 - 工具列表有 60 秒 TTL 缓存;会话失效/服务器未初始化(会话不存在/已失效/未初始化/Server not initialized)时自动重新 initialize 并重试一次。 diff --git "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" index 429c3ca..032e759 100644 --- "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" +++ "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" @@ -105,7 +105,7 @@ | 对话 | 角色扮演设定、示例对话、对话保存轮数、上下文最大token、消息压缩阈值 | 每条设定第一行为角色设定名称(>20 截断),其余为设定;`$gSYSPROMPT` 选择 | | 消息接收 | 接收图片/指令消息/骰子发送的消息、忽略消息豹语条件、忽略消息正则表达式 | 接收图片只负责记录图片 URL;自动识别仍由图片识别条件和图片模型配置决定 | | 消息触发 | 触发正则表达式、触发需要满足的条件、触发次数上限、触发次数补充间隔 | 令牌桶配额 | -| 工具 | 开启调用函数功能、切换为提示词工程、允许连续调用函数次数、工具响应压缩触发字数、禁止/默认关闭的函数、是否启用MCP、MCP服务器配置、技能配置、ai语音使用的音色 | 「是否启用MCP」默认关闭,开启后才会解析并连接 MCP 服务器;MCP 仅支持标准 mcpServers JSON(默认含 mcp-files-exec/web-read/md-html-render/ob11-core-bridge,可用 tools 适配块把远端工具映射为 web_read/render_markdown/render_html/run_core_command,并把 mcp-files-exec 的 run_command 暴露为 runshell;ob11-core-bridge 的 run_ext_command 默认隐藏,格式见 modelcontextprotocol.io/specification/latest),技能仅支持标准 SKILL.md(格式见 agentskills.io/specification) | +| 工具 | 开启调用函数功能、切换为提示词工程、允许连续调用函数次数、工具响应压缩触发字数、禁止/默认关闭的函数、是否启用MCP、MCP服务器配置、技能配置、ai语音使用的音色 | 「是否启用MCP」默认关闭,开启后才会解析并连接 MCP 服务器;MCP 仅支持标准 mcpServers JSON(默认后端直接提供 mcp-files-exec/run_shell、web-read/web_read、md-html-render/render_markdown/render_html、ob11-core-bridge/run_core_command,run_ext_command 仅由插件本地实现,格式见 modelcontextprotocol.io/specification/latest),技能仅支持标准 SKILL.md(格式见 agentskills.io/specification) | | 记忆 | 启用长期/总结/知识库记忆、长期记忆上限/展示数量、总结记忆上限/间隔/参与轮数、短期记忆上限、知识库注入阈值(字符)、知识库(Markdown 模板,每条一份完整文档) | 知识库只读,AI 通过 kb 工具/指令检索;修改后自动生效(缓存最多 1 分钟);Markdown 语法见 commonmark.org/help/ | | 回复 | 回复引用、回复最大字数、分段发送延时/基础延时/含图额外延时、禁止回复复读 | 回复过滤规则已内置硬编码 | | 后端 | 流式输出、图片转base64、联网搜索、用量图表、论坛地址/API Token/签名密钥 | 可自建,见 [08-相关后端项目](08-相关后端项目.md);网页读取与 md/html 渲染(MCP)在「工具 → MCP服务器配置」 | 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 f84dd48..8752321 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" @@ -14,7 +14,7 @@ | `mcp-files-exec` | 本地文件读写/执行 | 3910 | 工具 → MCP服务器配置(mcp-files-exec) | | `ob11-core-bridge` | SealDice 核心正向 WS / OB11 协议端 / MCP 透明中转(注入消息执行核心指令;扩展指令由插件本地执行) | 46880 | 工具 → MCP服务器配置(ob11-core-bridge) | -`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。插件不再内置包装工具:默认配置的 `tools` 适配块把 `web-read` / `md-html-render` / `ob11-core-bridge` 的底层 MCP 工具映射为 `web_read` / `render_markdown` / `render_html` / `run_core_command` 并保留原有行为(网页截图、渲染图片、核心指令多消息收集);`mcp-files-exec` 的远端 `run_command` 暴露为 `runshell`,其余文件工具按原远端名注册;`run_ext_command` 由插件本地实现,不依赖 MCP,并在默认 MCP 配置中隐藏。这些后端的 REST 接口已移除,仅提供 MCP。 +`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。插件不再为这些后端包装或改名:后端直接提供 `run_shell`、`web_read`、`render_markdown`、`render_html`、`run_core_command`,图片与核心多消息处理仍由对应适配器保留;`run_ext_command` 完全由插件本地实现,不属于 MCP。这些后端的 REST 接口已移除,仅提供 MCP。 ## 部署与一键管理 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index 022814c..a008ac6 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -93,7 +93,6 @@ ws://127.0.0.1:46880/core "url": "http://127.0.0.1:46880/mcp", "tools": { "run_core_command": { - "exposeAs": "run_core_command", "adapter": "core_bridge_core" } } @@ -112,11 +111,11 @@ ws://127.0.0.1:46880/core } ``` -3. 按需配置「可调用指令白名单」与「指令前缀」。`tools` 适配块会把远端 `run_core_command` 暴露为同名 AI 工具并套用核心桥适配器;若手写配置时省略该块,`run_core_command` 也会直接用远端工具名注册,但不会套用核心桥适配器。`run_ext_command` 在插件内**本地直接执行扩展指令**,无需中间件;`run_core_command` 经 MCP 调用中间件执行核心指令。 +3. 按需配置「可调用指令白名单」与「指令前缀」。后端只通过 MCP 提供 `run_core_command`,插件直接套用核心桥适配器;`run_ext_command` 在插件内**本地直接执行扩展指令**,无需中间件,也不会出现在 MCP 工具列表中。 ## 工具与参数 -`run_ext_command` 由插件本地注册为 AI 工具(敏感工具,执行会显著记录),在插件内直调扩展 `solve`,不依赖中间件;`run_core_command` 由 MCP 同步注册(默认 `tools` 适配暴露同名工具,同样为敏感工具),经中间件 MCP 注入假消息执行核心指令,需启动 `ob11-core-bridge` 并开启「是否启用MCP」。 +`run_ext_command` 由插件本地注册为 AI 工具(敏感工具,执行会显著记录),在插件内直调扩展 `solve`,不依赖中间件;`run_core_command` 由 MCP 同步注册,经中间件 MCP 注入假消息执行核心指令,需启动 `ob11-core-bridge` 并开启「是否启用MCP」。MCP 工具列表只包含 `run_core_command`。 ### run_ext_command — 扩展指令 @@ -159,7 +158,7 @@ ws://127.0.0.1:46880/core | `forwardedCount` / `interceptedCount` | 转发 / 拦截计数 | | `error` | 失败原因(如核心未连接、target 缺少群/私聊 id) | -> 上表是 `run_core_command` 经 MCP 返回的结果结构;`run_ext_command` 本地执行直接返回收集到的消息文本,异常时返回错误说明。 +> 上表是 `run_core_command` 经 MCP 返回的结果结构;`run_ext_command` 不经过 MCP,本地执行直接返回收集到的消息文本,异常时返回错误说明。 ## 指令白名单 diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index 8a0fd20..d36dc87 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -23,12 +23,6 @@ export default class ToolConfig { "url": "http://127.0.0.1:3910/mcp", "headers": { "Authorization": "Bearer token" - }, - "tools": { - "run_command": { - "exposeAs": "runshell", - "adapter": "text" - } } }, "web-read": { @@ -144,11 +138,7 @@ export default class ToolConfig { "type": "http", "url": "http://127.0.0.1:46880/mcp", "tools": { - "run_ext_command": { - "hidden": true - }, "run_core_command": { - "exposeAs": "run_core_command", "adapter": "core_bridge_core", "sensitive": true, "description": "通过核心桥向 SealDice 注入一条假消息并执行核心指令。白名单中的核心扩展名统一写作 core|指令名;核心 .ext 是扩展发现入口,不需要加入白名单,调用 command=\"ext\" 即可查看核心当前全部扩展名称。默认指令前缀为 .,可在配置中修改。", @@ -204,7 +194,7 @@ export default class ToolConfig { } } }` - ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)、tools(可选工具适配块:键为远端工具名,值为对象,支持 hidden=仅作底层工具不暴露给 AI、exposeAs=暴露给 AI 的工具名、adapter=适配器(text/image/web_read/render_markdown/render_html/core_bridge_core)、description/parameters=覆盖 AI 可见的描述与参数、output/format=图片输出与保存格式、remoteTool/remoteTools=实际调用的远端工具名映射、sensitive=敏感工具标记;未配置 tools 的服务器默认直接用远端工具名注册,与本地/已有工具同名时跳过,可用 exposeAs 改名或 hidden 隐藏)。默认配置把 mcp-files-exec 的远端 run_command 暴露为 runshell,隐藏 ob11-core-bridge 的远端 run_ext_command。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); + ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)、tools(可选工具适配块:键为远端工具名,值为对象,支持 hidden=仅作底层工具不暴露给 AI、exposeAs=暴露给 AI 的工具名、adapter=适配器(text/image/web_read/render_markdown/render_html/core_bridge_core)、description/parameters=覆盖 AI 可见的描述与参数、output/format=图片输出与保存格式、remoteTool/remoteTools=实际调用的远端工具名映射、sensitive=敏感工具标记;未配置 tools 的服务器默认直接用远端工具名注册,与本地/已有工具同名时跳过,可用 exposeAs 改名或 hidden 隐藏)。默认配置直接使用后端提供的工具名:mcp-files-exec 提供 run_shell,ob11-core-bridge 仅提供 run_core_command。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); seal.ext.registerTemplateConfig(ext, "技能配置", [ `--- name: 今日人品 From faf362e84bd228e0281a4088338ac3342756e542 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 17:16:46 +0800 Subject: [PATCH 12/20] feat: discover MCP tools from standard servers --- README.md | 4 +- ...45\345\205\267\347\263\273\347\273\237.md" | 6 +- ...77\347\224\250\346\214\207\345\215\227.md" | 13 +- src/config/configs/tool.ts | 161 +----------------- src/tool/mcp.ts | 101 +++++------ src/tool/mcp/adapters.ts | 156 ++++++++--------- src/tool/mcp/types.ts | 34 +--- 7 files changed, 131 insertions(+), 344 deletions(-) diff --git a/README.md b/README.md index f1e5dcc..094b97b 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ max_tokens = 2048 | 默认关闭的函数 | 每行一个,AI 在新会话中默认无法调用,需 `.ai tool on <函数名>` 开启 | | 提供给AI的牌堆名称 | 每行一个牌堆名,用于 `draw_deck` 工具;没有的话建议把 `draw_deck` 加入禁止调用 | | 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | -| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。每台服务器可配 `tools` 适配块(`hidden`/`exposeAs`/`adapter`/`remoteTools`/`description`/`parameters` 等);未配置时默认直接使用远端工具名注册,同名冲突自动跳过。默认后端工具名为 `run_shell`、`web_read`、`render_markdown`、`render_html`、`run_core_command`;`run_ext_command` 仅由插件本地实现,不由 MCP 提供。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | +| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。工具名称、描述和参数均从远端 `tools/list` 自动发现,同名冲突自动跳过;`run_ext_command` 仅由插件本地实现,不由 MCP 提供。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | | 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | | ai语音使用的音色 | 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)和 ffmpeg | @@ -423,7 +423,7 @@ max_tokens = 2048 | 音乐 | `music_play` | | 黑名单 | `suggest_block`(AI 建议拉黑,带冷却;默认需骰主确认)、`unblock_user`、`get_block_list` | | 论坛 | `forum_get_posts`、`forum_get_post_detail`、`forum_search`、`forum_create_post`、`forum_manage_comment`、`forum_get_activity`、`forum_manage_post` | -| MCP / 技能 | 远端工具名(MCP 工具,同名冲突时跳过;可用 `tools.exposeAs` 改名)、`use_skill`(技能) | +| MCP / 技能 | 远端工具名(MCP 工具,同名冲突时跳过)、`use_skill`(技能) | > 指令类技能(今日人品、COC 模组抽取/搜索、属性展示、属性检定、san 检定等)通过 `use_skill` 按需获取内容,内部统一使用 `run_ext_command` / `run_core_command` 调用海豹指令,对应指令需加入「可调用指令白名单」。 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 c71ccc2..5d5301b 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" @@ -89,7 +89,7 @@ 外部注册: -- MCP 工具:默认直接用远端工具名注册;与本地/已有工具同名时自动跳过,可用 `tools` 适配配置里 `exposeAs` 改名(见下节)。 +- MCP 工具:默认直接按远端 `tools/list` 返回的工具名注册;与本地/已有工具同名时自动跳过。 - 技能工具:`use_skill`(见技能节)。 依赖说明:ob11 相关工具(收发消息、用户/群信息、禁言、打卡等)需要安装 ob11 网络连接依赖或 HTTP 依赖;`text_to_sound` 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)与 ffmpeg;`music_play` 需要协议端配置音卡签名;`text_to_image` 需要生成图片依赖(tti)。 @@ -105,9 +105,9 @@ ## 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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。每个服务器可带可选 `tools` 适配块:键为远端工具名,值可配 `hidden`(仅作底层工具不暴露给 AI)、`exposeAs`(暴露给 AI 的工具名)、`adapter`(text/image/web_read/render_markdown/render_html/core_bridge_core)、`description`/`parameters`(覆盖 AI 可见的描述与参数)、`output`/`format`(图片输出与保存格式)、`remoteTool`/`remoteTools`(实际调用的远端工具名映射)、`sensitive`(敏感工具标记)。默认配置直接使用后端提供的 `run_shell`、`web_read`、`render_markdown`、`render_html`、`run_core_command`;`run_ext_command` 由插件本地注册,不经 MCP。改 URL/Token 时服务器会话自动重建(热加载) +- 配置项 `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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。工具名称、描述和参数 schema 均从远端 `tools/list` 自动发现,返回内容按 MCP 标准内容块处理;`run_ext_command` 由插件本地注册,不经 MCP。改 URL/Token 时服务器会话自动重建(热加载) - 协议:Streamable HTTP 上的 JSON-RPC(`initialize` → `notifications/initialized` → `tools/list` → `tools/call`),带 `Mcp-Session-Id` 会话头,兼容 SSE 响应。 -- 未配置 `tools` 适配的工具默认用远端工具名注册为敏感工具,参数透传 `inputSchema`;返回纯文本时直接回传,返回标准 image 块或 base64 文本时保存为 `[img:...]` 并返回图片 ID。同名冲突会跳过,需要不同的暴露名时配置 `exposeAs`。 +- 远端工具默认用远端工具名注册为敏感工具,参数透传 `inputSchema`;返回纯文本时直接回传,返回标准 image 块或 base64 文本时保存为 `[img:...]` 并返回图片 ID。同名冲突会跳过。 - 工具列表有 60 秒 TTL 缓存;会话失效/服务器未初始化(会话不存在/已失效/未初始化/Server not initialized)时自动重新 initialize 并重试一次。 - MCP 配置修改后会按服务器名重建会话并同步工具差集;删除服务器或服务器返回的工具会从注册表移除。MCP 总开关关闭时不建立连接,system prompt 也不会展示不可调用的 MCP/技能入口。 - 错误信息把 JSON-RPC 错误码映射为可读中文。 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index a008ac6..378d12f 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -90,12 +90,7 @@ ws://127.0.0.1:46880/core "mcpServers": { "ob11-core-bridge": { "type": "http", - "url": "http://127.0.0.1:46880/mcp", - "tools": { - "run_core_command": { - "adapter": "core_bridge_core" - } - } + "url": "http://127.0.0.1:46880/mcp" } } } @@ -111,7 +106,7 @@ ws://127.0.0.1:46880/core } ``` -3. 按需配置「可调用指令白名单」与「指令前缀」。后端只通过 MCP 提供 `run_core_command`,插件直接套用核心桥适配器;`run_ext_command` 在插件内**本地直接执行扩展指令**,无需中间件,也不会出现在 MCP 工具列表中。 +3. 按需配置「可调用指令白名单」与「指令前缀」。后端只通过标准 MCP `tools/list` 提供 `run_core_command`,插件自动读取工具名称、描述和参数 schema;`run_ext_command` 在插件内**本地直接执行扩展指令**,无需中间件,也不会出现在 MCP 工具列表中。 ## 工具与参数 @@ -134,6 +129,8 @@ ws://127.0.0.1:46880/core - `action=list`:列出白名单中的核心指令 - `action=call`:执行核心指令(如 `ext`、`help`;也支持 `core|ext` 写法) +- 结构化模式:传 `command` 和可选 `args`,插件会按当前「指令前缀」组装核心原始消息 +- 原始消息模式:传 `raw_message`,原样注入核心;`raw_message` 不能与 `command` 或 `args` 同时使用 ### 参数 @@ -194,7 +191,7 @@ ws://127.0.0.1:46880/core | 现象 | 检查 | | --- | --- | -| `run_core_command` 不可用 / 报 MCP 服务器未配置 | 「是否启用MCP」总开关、MCP服务器配置中的服务器名与 url、`tools` 适配块是否保留了 `run_core_command` | +| `run_core_command` 不可用 / 报 MCP 服务器未配置 | 「是否启用MCP」总开关、MCP服务器配置中的服务器名与 url;确认后端 `/mcp` 可访问且 `tools/list` 返回了 `run_core_command` | | `run_ext_command` 执行失败 / 无响应 | 扩展是否已安装、指令名是否正确(`扩展名|指令名`)、是否在白名单、指令本身是否抛异常;与中间件无关 | | 执行无响应 / 超时 | SealDice 是否已连上 `/core`(看中间件日志)、指令前缀是否正确、目标群/私聊 id 是否正确、指令是否在白名单 | | 返回 `ambiguous=true` | 同 lane 并发或缺少 reply 引用;调整 `captureMode` / `settleMs` | diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index d36dc87..4da3eb1 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -28,173 +28,18 @@ export default class ToolConfig { "web-read": { "type": "http", "url": "http://127.0.0.1:46799/mcp", - "tools": { - "screenshot_url": { - "hidden": true - }, - "scrape_url": { - "exposeAs": "web_read", - "adapter": "web_read", - "remoteTools": { - "screenshot": "screenshot_url", - "scrape": "scrape_url" - }, - "description": "读取网页内容或对网页截图。默认抓取网页标题/正文/链接;screenshot=true 时对网页截图并返回可发送的图片", - "parameters": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "需要读取内容或截图的网页链接" - }, - "screenshot": { - "type": "boolean", - "description": "true 时对网页截图并返回图片,false(默认)时抓取网页文本内容" - }, - "width": { - "type": "integer", - "description": "截图视口宽度,默认 1680" - }, - "height": { - "type": "integer", - "description": "截图视口高度,默认 1000" - }, - "fullPage": { - "type": "boolean", - "description": "是否截取整页(长图),默认 false" - }, - "delay": { - "type": "integer", - "description": "页面加载完成后等待的毫秒数,默认 3000" - } - }, - "required": ["url"] - } - } - } }, "md-html-render": { "type": "http", - "url": "http://127.0.0.1:37632/mcp", - "tools": { - "render_markdown": { - "exposeAs": "render_markdown", - "adapter": "render_markdown", - "output": "image", - "format": "unknown", - "description": "渲染 Markdown 内容为图片", - "parameters": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "要渲染的 Markdown 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用[img:xxxxxx]替代图片url(注意使用markdown语法显示图片),xxxxxx为图片id,或avatar:用户名称/ID,或group_avatar:群聊名称/ID" - }, - "name": { - "type": "string", - "description": "名称,对内容大致描述" - }, - "theme": { - "type": "string", - "enum": ["light", "dark", "gradient"], - "description": "主题样式,其中 gradient 为紫色渐变背景" - }, - "save": { - "type": "boolean", - "description": "是否保存图片" - } - }, - "required": ["content", "name", "save"] - } - }, - "render_html": { - "exposeAs": "render_html", - "adapter": "render_html", - "output": "image", - "format": "unknown", - "description": "渲染 HTML 内容为图片", - "parameters": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "要渲染的 HTML 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用[img:xxxxxx]替代图片url(注意使用html元素显示图片),xxxxxx为图片id,或avatar:用户名称/ID,或group_avatar:群聊名称/ID" - }, - "name": { - "type": "string", - "description": "名称,对内容大致描述" - }, - "save": { - "type": "boolean", - "description": "是否保存图片" - } - }, - "required": ["content", "name", "save"] - } - } - } + "url": "http://127.0.0.1:37632/mcp" }, "ob11-core-bridge": { "type": "http", - "url": "http://127.0.0.1:46880/mcp", - "tools": { - "run_core_command": { - "adapter": "core_bridge_core", - "sensitive": true, - "description": "通过核心桥向 SealDice 注入一条假消息并执行核心指令。白名单中的核心扩展名统一写作 core|指令名;核心 .ext 是扩展发现入口,不需要加入白名单,调用 command=\"ext\" 即可查看核心当前全部扩展名称。默认指令前缀为 .,可在配置中修改。", - "parameters": { - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["list", "call"], - "description": "list=列出白名单核心指令;call=执行核心指令" - }, - "command": { - "type": "string", - "description": "核心指令名,如 ext、help;也支持 core|ext" - }, - "args": { - "type": "array", - "items": { "type": "string" }, - "description": "指令参数,按顺序填写" - }, - "forward": { - "type": "boolean", - "description": "是否把捕获到的核心发送消息继续转发给协议端,默认 false(避免重复发送)" - }, - "captureMode": { - "type": "string", - "enum": ["reply_only", "lane"], - "description": "消息捕获范围;forward=true 且希望捕获协议端回复时建议使用 lane" - }, - "maxMessages": { - "type": "integer", - "minimum": 1, - "maximum": 50, - "description": "最多收集多少条消息" - }, - "settleMs": { - "type": "integer", - "minimum": 0, - "maximum": 10000, - "description": "收到消息后等待多久没有新消息才结束" - }, - "timeoutMs": { - "type": "integer", - "minimum": 100, - "maximum": 120000, - "description": "最长等待时间,单位毫秒" - } - }, - "required": ["action"] - } - } - } + "url": "http://127.0.0.1:46880/mcp" } } }` - ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。默认包含四个:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)、tools(可选工具适配块:键为远端工具名,值为对象,支持 hidden=仅作底层工具不暴露给 AI、exposeAs=暴露给 AI 的工具名、adapter=适配器(text/image/web_read/render_markdown/render_html/core_bridge_core)、description/parameters=覆盖 AI 可见的描述与参数、output/format=图片输出与保存格式、remoteTool/remoteTools=实际调用的远端工具名映射、sensitive=敏感工具标记;未配置 tools 的服务器默认直接用远端工具名注册,与本地/已有工具同名时跳过,可用 exposeAs 改名或 hidden 隐藏)。默认配置直接使用后端提供的工具名:mcp-files-exec 提供 run_shell,ob11-core-bridge 仅提供 run_core_command。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); + ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。工具名称、描述和参数 schema 会在连接后通过 MCP tools/list 自动发现,不需要也不支持额外的 tools 配置块。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)。默认包含四个:mcp-files-exec(提供 run_shell)、web-read(提供 scrape_url、screenshot_url)、md-html-render(提供 render_markdown、render_html)、ob11-core-bridge(仅提供 run_core_command)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请改用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); seal.ext.registerTemplateConfig(ext, "技能配置", [ `--- name: 今日人品 diff --git a/src/tool/mcp.ts b/src/tool/mcp.ts index 4fddeef..f5635de 100644 --- a/src/tool/mcp.ts +++ b/src/tool/mcp.ts @@ -3,7 +3,7 @@ import { ext } from "../config/config"; import Logger from "../logger"; import { runMCPAdapter } from "./mcp/adapters"; -import { MCPCallResult, MCPToolConfig } from "./mcp/types"; +import { MCPCallResult } from "./mcp/types"; import Tool, { toolMap } from "./tool"; export interface MCPServer { @@ -11,7 +11,6 @@ export interface MCPServer { url: string; token: string; headers: { [key: string]: string }; - tools?: Record; } interface MCPToolDef { @@ -35,8 +34,7 @@ let lastRefreshAt = 0; // 全量刷新节流:避免每条消息都重新同步 /** 服务器配置是否一致:url/token/headers 任一变化都视为新配置,需要重建会话并重新拉取工具列表 */ function sameServerConfig(a: MCPServer, b: MCPServer): boolean { if (a.url !== b.url || a.token !== b.token) return false; - return JSON.stringify(a.headers || {}) === JSON.stringify(b.headers || {}) - && JSON.stringify(a.tools || {}) === JSON.stringify(b.tools || {}); + return JSON.stringify(a.headers || {}) === JSON.stringify(b.headers || {}); } /** 按名称取最新配置:始终实时解析当前配置(热加载后立即生效),不保留已移除服务器的旧会话 */ @@ -80,21 +78,11 @@ function normalizeMCPServer(name: string, cfg: any): MCPServer | null { const auth = headers['Authorization'] || ''; if (!token && auth.startsWith('Bearer ')) token = auth.slice(7).trim(); - let tools: Record | undefined; - if (cfg.tools && typeof cfg.tools === 'object' && !Array.isArray(cfg.tools)) { - tools = {}; - for (const [toolName, toolCfg] of Object.entries(cfg.tools)) { - if (toolCfg && typeof toolCfg === 'object') tools[toolName] = toolCfg as MCPToolConfig; - } - if (Object.keys(tools).length === 0) tools = undefined; - } - return { name: String(cfg.name || name || '').trim(), url: String(cfg.url || '').trim(), token, - headers, - ...(tools ? { tools } : {}) + headers }; } @@ -188,12 +176,30 @@ async function initialize(server: MCPServer): Promise { } async function listTools(server: MCPServer, sessionId: string): Promise { - const res = await mcpRequest(server, { jsonrpc: '2.0', id: 2, method: 'tools/list' }, sessionId); - if (res.body && res.body.error) { - throw new Error(`MCP tools/list 失败: ${formatError(res.status, res.body)}`); - } - const tools = res.body && res.body.result && res.body.result.tools; - return Array.isArray(tools) ? tools : []; + const tools: MCPToolDef[] = []; + const cursors = new Set(); + let cursor: string | undefined; + let requestId = 2; + do { + const res = await mcpRequest(server, { + jsonrpc: '2.0', + id: requestId++, + method: 'tools/list', + params: cursor ? { cursor } : {} + }, sessionId); + if (res.body && res.body.error) { + throw new Error(`MCP tools/list 失败: ${formatError(res.status, res.body)}`); + } + const result = res.body && res.body.result; + if (result && Array.isArray(result.tools)) tools.push(...result.tools); + const nextCursor = result && typeof result.nextCursor === 'string' && result.nextCursor + ? result.nextCursor + : undefined; + if (!nextCursor || cursors.has(nextCursor)) break; + cursors.add(nextCursor); + cursor = nextCursor; + } while (cursor); + return tools; } async function doCallTool(server: MCPServer, sessionId: string, name: string, args: any): Promise { @@ -266,14 +272,8 @@ async function syncTools(server: MCPServer, force = false): Promise listTools(server, sessionId)); serverStates[server.name] = { server, sessionId, tools, toolsFetchedAt: Date.now() }; - // 清理本服务器已注册但 tools/list 不再返回、或已被 tools 配置隐藏的工具(服务器内工具删除后热加载生效) - const liveKeys = new Set(); - for (const t of tools) { - if (!t.name) continue; - const descriptor = server.tools && server.tools[t.name]; - if (descriptor && descriptor.hidden) continue; - liveKeys.add(descriptor && descriptor.exposeAs ? descriptor.exposeAs : t.name); - } + // 工具名称、描述和参数 schema 全部以 MCP tools/list 为准;服务器内工具删除后热加载清理。 + const liveKeys = new Set(tools.filter(t => !!t.name).map(t => t.name)); for (const [key, owner] of mcpToolKeys) { if (owner === server.name && !liveKeys.has(key) && Object.prototype.hasOwnProperty.call(toolMap, key)) { Logger.info(`MCP 服务器 ${server.name} 不再提供工具 ${key},清理`); @@ -284,43 +284,32 @@ async function syncTools(server: MCPServer, force = false): Promise { const current = getMCPServerByName(server.name); if (!current) return `MCP 服务器 ${server.name} 未配置`; - const currentDescriptor = current.tools && current.tools[t.name] || undefined; - const usedDescriptor = currentDescriptor || descriptor; - const usedRemoteTool = usedDescriptor && usedDescriptor.remoteTool ? usedDescriptor.remoteTool : remoteTool; - const usedRemoteTools = (usedDescriptor && usedDescriptor.remoteTools) || remoteTools; - return runMCPAdapter(usedDescriptor && usedDescriptor.adapter || adapterName, { + return runMCPAdapter(t.name, { ctx, msg, session, @@ -331,14 +320,12 @@ async function syncTools(server: MCPServer, force = false): Promise callTool(current, toolName, callArgs || {}) }); }; - mcpToolKeys.set(toolName, server.name); - Logger.info(`已注册 MCP 工具 ${toolName}`); + mcpToolKeys.set(t.name, server.name); + Logger.info(`已注册 MCP 工具 ${t.name}`); } return tools; } diff --git a/src/tool/mcp/adapters.ts b/src/tool/mcp/adapters.ts index f51f1c3..f636f58 100644 --- a/src/tool/mcp/adapters.ts +++ b/src/tool/mcp/adapters.ts @@ -1,6 +1,6 @@ // MCP 工具适配器:把 AI 侧参数/返回映射为远端 MCP 工具调用,保留网页截图、Markdown/HTML 渲染、核心桥指令等既有行为 import Config from "../../config/config"; -import { CoreBridgeInvocation, CoreBridgeResult } from "../../integration/core_bridge/types"; +import { CoreBridgeResult } from "../../integration/core_bridge/types"; import Logger from "../../logger"; import Image from "../../resource/image"; import { Session } from "../../session/session"; @@ -8,7 +8,7 @@ import { parseSpecialTokens } from "../../utils/string"; import { generateId } from "../../utils/utils"; import { isAllowedCore, splitEntry } from "../command_catalog"; -import { MCPCallResult, MCPToolConfig } from "./types"; +import { MCPCallResult } from "./types"; export interface MCPAdapterContext { ctx: seal.MsgContext; @@ -16,9 +16,7 @@ export interface MCPAdapterContext { session: Session; args: { [key: string]: any }; server: { name: string; url: string; token: string; headers: Record }; - remoteTool?: string; - remoteTools?: Record; - descriptor?: MCPToolConfig; + toolName: string; callRemote: (toolName: string, args: any) => Promise; } @@ -61,6 +59,16 @@ function inferFormat(mimeType?: string): string | undefined { return undefined; } +function saveMCPImage(image: { data: string; mimeType?: string }, prefix = 'mcp'): string { + const img = new Image(); + img.imageId = `${prefix}_${generateId()}`; + img.base64 = image.data; + img.format = inferFormat(image.mimeType) || 'png'; + img.description = `MCP 图片[img:${img.imageId}]`; + Image.save(img); + return `成功,请使用[img:${img.imageId}]发送`; +} + /** 提取 MCP 结果中的图片数据;标准 image 块优先,兼容后端以 text 返回 base64 的旧格式 */ export function extractMCPImage(result: MCPCallResult | null | undefined, treatTextAsBase64 = false): { data: string; mimeType?: string } | null { const blocks = Array.isArray(result && result.content) ? result!.content! : []; @@ -87,48 +95,17 @@ export function extractMCPImage(result: MCPCallResult | null | undefined, treatT } async function defaultText(input: MCPAdapterContext): Promise { - const result = await input.callRemote(input.remoteTool || 'unknown', input.args || {}); + const result = await input.callRemote(input.toolName, input.args || {}); + const image = extractMCPImage(result, false); + if (image) return saveMCPImage(image); return mcpText(result); } async function defaultImage(input: MCPAdapterContext): Promise { - const result = await input.callRemote(input.remoteTool || 'unknown', input.args || {}); + const result = await input.callRemote(input.toolName, input.args || {}); const image = extractMCPImage(result, true); - if (!image) throw new Error(`MCP 工具 ${input.remoteTool || 'unknown'} 未返回图片数据`); - - const img = new Image(); - img.imageId = `mcp_${generateId()}`; - img.base64 = image.data; - img.format = (input.descriptor && input.descriptor.format) || inferFormat(image.mimeType) || 'png'; - img.description = `MCP 图片[img:${img.imageId}]`; - Image.save(img); - return `成功,请使用[img:${img.imageId}]发送`; -} - -async function webReadAdapter(input: MCPAdapterContext): Promise { - const { url, screenshot = false, width, height, fullPage = false, delay } = input.args || {}; - if (!url) return 'url 不能为空'; - - if (screenshot) { - const remoteTool = (input.remoteTools && input.remoteTools.screenshot) || input.remoteTool || 'screenshot_url'; - Logger.info(`网页截图: ${url}`); - const result = await input.callRemote(remoteTool, { url, width, height, fullPage, delay }); - const image = extractMCPImage(result, true); - if (!image) throw new Error('截图结果为空'); - - const img = new Image(); - img.imageId = `web_${generateId()}`; - img.base64 = image.data; - img.format = inferFormat(image.mimeType) || 'png'; - img.description = `网页截图[img:${img.imageId}]`; - Image.save(img); - return `成功,请使用[img:${img.imageId}]发送`; - } - - const remoteTool = (input.remoteTools && input.remoteTools.scrape) || input.remoteTool || 'scrape_url'; - Logger.info(`读取网页内容: ${url}`); - const result = await input.callRemote(remoteTool, { url }); - return mcpText(result) || '网页内容为空'; + if (!image) throw new Error(`MCP 工具 ${input.toolName} 未返回图片数据`); + return saveMCPImage(image); } async function transformContentToUrlText(ctx: seal.MsgContext, session: Session, content: string): Promise<{ text: string; images: Image[] }> { @@ -195,32 +172,29 @@ async function transformContentToUrlText(ctx: seal.MsgContext, session: Session, } async function renderAdapter(input: MCPAdapterContext, kind: 'markdown' | 'html'): Promise { - const { content, name, save } = input.args || {}; + const args = input.args || {}; + const source = kind === 'markdown' ? args.markdown : args.html; const theme = input.args && input.args.theme || 'light'; - if (!content || !content.trim()) return '内容不能为空'; - if (!name || !name.trim()) return '图片名称不能为空'; + if (!source || !String(source).trim()) return '内容不能为空'; if (kind === 'markdown' && !['light', 'dark', 'gradient'].includes(theme)) return `无效的主题: ${theme}。支持: light, dark, gradient`; - const kws = kind === 'markdown' ? ['render', 'markdown', name, theme] : ['render', 'html', name]; try { - const { text, images } = await transformContentToUrlText(input.ctx, input.session, content); + const { text, images } = await transformContentToUrlText(input.ctx, input.session, String(source)); const hasImages = images.length > 0; - const remoteArgs = kind === 'markdown' - ? { markdown: text, theme, width: 1200, quality: 90, hasImages } - : { html: text, theme: 'light', width: 1200, quality: 90, hasImages }; - const result = await input.callRemote(input.remoteTool || (kind === 'markdown' ? 'render_markdown' : 'render_html'), remoteArgs); + const remoteArgs = { ...args, [kind]: text, hasImages }; + const result = await input.callRemote(input.toolName, remoteArgs); const image = extractMCPImage(result, true); if (!image) throw new Error('渲染结果为空'); const img = new Image(); - img.imageId = `${name}_${generateId()}`; + img.imageId = `mcp_${kind}_${generateId()}`; img.base64 = image.data; - img.format = (input.descriptor && input.descriptor.format) || 'unknown'; + img.format = inferFormat(image.mimeType) || 'png'; img.description = kind === 'markdown' ? `Markdown 渲染图片[img:${img.imageId}]\n主题:${theme}` : `HTML 渲染图片[img:${img.imageId}]`; - if (save) input.session.memory.addMemory(input.ctx, input.session, [], [], kws, [img], img.description); + Image.save(img); return `成功,请使用[img:${img.imageId}]发送`; } catch (err) { Logger.error(`${kind === 'markdown' ? 'Markdown' : 'HTML'} 渲染失败: ${err instanceof Error ? err.message : String(err)}`); @@ -236,13 +210,6 @@ async function renderHtmlAdapter(input: MCPAdapterContext): Promise { return renderAdapter(input, 'html'); } -let sequence = 0; - -function nextId(): string { - sequence = (sequence + 1) % 100000; - return `invoke_${Date.now().toString(36)}_${sequence.toString(36)}`; -} - function decodeCoreBridgeResult(text: string): CoreBridgeResult { try { const result = JSON.parse(text); @@ -290,18 +257,35 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { } if (action !== 'call') return 'action 仅支持 list 或 call'; + const rawMessage = typeof args.raw_message === 'string' ? args.raw_message : undefined; + const hasRawMessage = rawMessage !== undefined; + const hasStructuredArgs = args.command !== undefined || args.args !== undefined; + if (hasRawMessage && hasStructuredArgs) return 'raw_message 不能与 command/args 同时使用'; + let command = String(args && args.command || '').trim(); if (command.indexOf('core|') === 0) command = command.slice(5).trim(); - if (!command) return '调用核心指令时 command 不能为空'; - if (!isAllowedCore(command)) return `核心指令 core|${command} 不在可调用指令白名单内,无法调用`; + if (!hasRawMessage && !command) return '调用核心指令时 command 不能为空'; + + let authorizedCommand = command; + if (hasRawMessage) { + const withoutPrefix = rawMessage!.trim().startsWith(Config.tool.COMMAND_PREFIX) + ? rawMessage!.trim().slice(Config.tool.COMMAND_PREFIX.length).trim() + : rawMessage!.trim(); + authorizedCommand = withoutPrefix.split(/\s+/, 1)[0].replace(/^core\|/, '').trim(); + if (!authorizedCommand) return '调用核心指令时 raw_message 不能为空'; + } + if (!isAllowedCore(authorizedCommand)) return `核心指令 core|${authorizedCommand} 不在可调用指令白名单内,无法调用`; const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; const options = captureOptions(args, 50, 500); - if (command === 'ext' && !(args && args.captureMode)) options.capture.mode = 'lane'; - - const prefix = Config.tool.COMMAND_PREFIX; - const raw = `${prefix}${command}${cmdArgs.length ? ` ${cmdArgs.join(' ')}` : ''}`.trim(); - const target: CoreBridgeInvocation['target'] = { + if (authorizedCommand === 'ext' && !(args && args.captureMode)) options.capture.mode = 'lane'; + + const target: { + selfId: string; + messageType: 'private' | 'group'; + userId: string; + groupId?: string; + } = { selfId: String(ctx.endPoint.userId || '').replace(/^.+:/, ''), messageType: ctx.isPrivate ? 'private' : 'group', userId: String(ctx.player && ctx.player.userId || '').replace(/^.+:/, '') @@ -309,37 +293,43 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { if (!ctx.isPrivate) target.groupId = String(ctx.group && ctx.group.groupId || '').replace(/^.+:/, ''); try { - const result = await input.callRemote(input.remoteTool || 'run_core_command', { + const remoteArgs: { [key: string]: any } = { + action: 'call', target, actor: { userId: target.userId || target.selfId, nickname: String(ctx.player && ctx.player.name || 'AI'), role: 'member' }, - command: { raw, name: command, args: cmdArgs }, - capture: options.capture, + maxMessages: options.capture.maxMessages, + settleMs: options.capture.settleMs, + captureMode: options.capture.mode, + forward: options.capture.forward, timeoutMs: options.timeoutMs, - id: nextId() - }); - return `核心指令 core|${command} 返回:\n${formatCoreBridgeResult(decodeCoreBridgeResult(mcpText(result)))}`; + __commandPrefix: Config.tool.COMMAND_PREFIX + }; + if (hasRawMessage) remoteArgs.raw_message = rawMessage; + else { + remoteArgs.command = command; + remoteArgs.args = cmdArgs; + } + const result = await input.callRemote(input.toolName, remoteArgs); + return `核心指令 core|${authorizedCommand} 返回:\n${formatCoreBridgeResult(decodeCoreBridgeResult(mcpText(result)))}`; } catch (e) { - Logger.warning(`[run_core_command] 调用 core|${command} 失败:${e instanceof Error ? e.message : String(e)}`); - return `核心指令 core|${command} 调用失败:${e instanceof Error ? e.message : String(e)}`; + Logger.warning(`[run_core_command] 调用 core|${authorizedCommand} 失败:${e instanceof Error ? e.message : String(e)}`); + return `核心指令 core|${authorizedCommand} 调用失败:${e instanceof Error ? e.message : String(e)}`; } } const ADAPTERS: { [name: string]: MCPAdapter } = { - text: defaultText, - image: defaultImage, - web_read: webReadAdapter, + screenshot_url: defaultImage, render_markdown: renderMarkdownAdapter, render_html: renderHtmlAdapter, - core_bridge_core: coreBridgeAdapter + run_core_command: coreBridgeAdapter }; -/** 按配置的适配器名执行;未指定时按 output 选择 text/image 通用适配器 */ +/** 适配器只负责处理特殊输入/输出;工具定义始终来自 MCP tools/list。 */ export async function runMCPAdapter(name: string | undefined, input: MCPAdapterContext): Promise { - const adapterName = name || (input.descriptor && input.descriptor.output === 'image' ? 'image' : 'text'); - const adapter = ADAPTERS[adapterName] || defaultText; + const adapter = (name && ADAPTERS[name]) || defaultText; return adapter(input); } diff --git a/src/tool/mcp/types.ts b/src/tool/mcp/types.ts index 0d0c5f8..02e682f 100644 --- a/src/tool/mcp/types.ts +++ b/src/tool/mcp/types.ts @@ -1,4 +1,4 @@ -// MCP 通用类型:服务器归一化、工具适配配置与调用结果 +// MCP 标准调用结果类型 export interface MCPContentBlock { type?: string; @@ -13,35 +13,3 @@ export interface MCPCallResult { structuredContent?: any; isError?: boolean; } - -/** 单个 MCP 工具的本地适配配置。未配置时默认直接用远端工具名注册,同名冲突会跳过。 */ -export interface MCPToolConfig { - /** 暴露给 AI 的工具名;默认使用远端工具名,可用此字段改名避免同名冲突 */ - exposeAs?: string; - /** 是否只作为后端远程工具,不注册为 AI 工具(如低层 screenshot_url/scrape_url) */ - hidden?: boolean; - /** 敏感工具标记;默认 true,与通用 MCP 工具一致 */ - sensitive?: boolean; - /** 输入/输出适配器名:text/image/web_read/render_markdown/render_html/core_bridge_core */ - adapter?: string; - /** 覆盖 AI 可见的工具描述 */ - description?: string; - /** 覆盖 AI 可见的参数 JSON Schema */ - parameters?: any; - /** 调用远端工具时使用的工具名;默认取配置键名 */ - remoteTool?: string; - /** 按场景映射到多个远端工具,如 { "screenshot": "screenshot_url", "scrape": "scrape_url" } */ - remoteTools?: Record; - /** 通用适配时声明输出类型:text 或 image */ - output?: 'text' | 'image'; - /** 保存 MCP 返回图片时使用的格式,如 png/unknown */ - format?: string; -} - -export interface MCPServerConfig { - name: string; - url: string; - token: string; - headers: Record; - tools?: Record; -} From ba024071d53957ca74432ca3d51c403443e3b68b Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Tue, 18 Aug 2026 17:38:15 +0800 Subject: [PATCH 13/20] fix: handle MCP trailing commas and CQ mention triggers --- src/config/configs/tool.ts | 2 +- src/pipeline.ts | 4 ++-- src/tool/mcp.ts | 5 +++-- src/utils/json.ts | 45 ++++++++++++++++++++++++++++++++++++++ src/utils/string.ts | 13 +++++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/utils/json.ts diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index 4da3eb1..f98f30d 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -27,7 +27,7 @@ export default class ToolConfig { }, "web-read": { "type": "http", - "url": "http://127.0.0.1:46799/mcp", + "url": "http://127.0.0.1:46799/mcp" }, "md-html-render": { "type": "http", diff --git a/src/pipeline.ts b/src/pipeline.ts index 11f5b70..795ab61 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -8,7 +8,7 @@ import Tool from "./tool/tool"; import { triggerConditionMap } from "./tool/tools/core/tool_trigger"; import { expandForwardMessage } from "./utils/ob11"; import { createCtx, createMsg } from "./utils/seal"; -import { expandMilkySegments, MessageSegment, parseCardToText, parseMusicToText, transformTextToArray, truncateText } from "./utils/string"; +import { expandMilkySegments, formatMessageSegmentsForMatching, MessageSegment, parseCardToText, parseMusicToText, transformTextToArray, truncateText } from "./utils/string"; import { getRecordMessageId, transformMsgId } from "./utils/utils"; /** 核心原生 milky 路径通常过滤掉的段,只由 ob11 依赖补充。 */ @@ -318,7 +318,7 @@ export class MessagePipeline { : transformTextToArray(message); // 正则匹配统一使用可读文本:原生路径保持原字符串,数组路径用展开后的文本 const messageText = Array.isArray(ob11Segments) || hasMilkySegments || Array.isArray(message) - ? messageArray.map(item => item.type === 'text' ? ((item.data && item.data.text) || '') : `[${item.type}]`).join('') + ? formatMessageSegmentsForMatching(messageArray, typeof message === 'string' ? message : '') : message; if (hasMilkySegments) { logger.debug(`[debug] milky 消息段展开: ${messageText.slice(0, 200)}`); diff --git a/src/tool/mcp.ts b/src/tool/mcp.ts index f5635de..f977873 100644 --- a/src/tool/mcp.ts +++ b/src/tool/mcp.ts @@ -1,6 +1,7 @@ // MCP 客户端:把外部 MCP 服务器(Streamable HTTP / JSON-RPC)的工具注册为 AI 工具 import { ext } from "../config/config"; import Logger from "../logger"; +import { parseJSONWithTrailingCommas } from "../utils/json"; import { runMCPAdapter } from "./mcp/adapters"; import { MCPCallResult } from "./mcp/types"; @@ -92,7 +93,7 @@ function getMCPServers(): MCPServer[] { const servers: MCPServer[] = []; for (const line of seal.ext.getTemplateConfig(ext, "MCP服务器配置").map(l => (l || '').trim()).filter(Boolean)) { try { - const j = JSON.parse(line); + const j = parseJSONWithTrailingCommas(line); // 标准 mcpServers 块:{"mcpServers":{"名称":{...}}}(Claude Desktop / Cursor .mcp.json) if (!j || typeof j !== 'object' || !j.mcpServers || typeof j.mcpServers !== 'object' || Array.isArray(j.mcpServers)) { Logger.error(`MCP服务器配置仅支持标准 mcpServers JSON 格式({"mcpServers":{...}}),已忽略该行: ${line.slice(0, 120)}`); @@ -103,7 +104,7 @@ function getMCPServers(): MCPServer[] { if (s) servers.push(s); } } catch (e) { - Logger.error(`MCP服务器配置解析失败(仅支持标准 mcpServers JSON 格式): ${e instanceof Error ? e.message : String(e)},内容: ${line}`); + Logger.error(`MCP服务器配置解析失败(需要标准 mcpServers JSON 格式,兼容对象/数组尾逗号): ${e instanceof Error ? e.message : String(e)},内容: ${line}`); } } return servers.filter(s => s.name && s.url); diff --git a/src/utils/json.ts b/src/utils/json.ts new file mode 100644 index 0000000..3033770 --- /dev/null +++ b/src/utils/json.ts @@ -0,0 +1,45 @@ +/** 解析配置 JSON,并兼容常见 JSONC 配置中的对象/数组尾逗号。 */ +export function parseJSONWithTrailingCommas(text: string): any { + try { + return JSON.parse(text); + } catch (firstError) { + const normalized = removeTrailingCommas(text); + if (normalized === text) throw firstError; + return JSON.parse(normalized); + } +} + +function removeTrailingCommas(text: string): string { + let result = ''; + let inString = false; + let escaped = false; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if (inString) { + result += char; + if (escaped) { + escaped = false; + } else if (char === '\\') { + escaped = true; + } else if (char === '"') { + inString = false; + } + continue; + } + + if (char === '"') { + inString = true; + result += char; + continue; + } + + if (char === ',') { + let next = i + 1; + while (next < text.length && /\s/.test(text[next])) next++; + if (text[next] === '}' || text[next] === ']') continue; + } + result += char; + } + return result; +} diff --git a/src/utils/string.ts b/src/utils/string.ts index 02887a7..1e3eccf 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -123,6 +123,19 @@ export interface MessageSegment { }; } +/** 为忽略/触发正则生成兼容 CQ 码的匹配文本,不改变消息段本身。 */ +export function formatMessageSegmentsForMatching(messageArray: MessageSegment[], fallback: string = ''): string { + const text = messageArray.map(item => { + if (item.type === 'text') return (item.data && item.data.text) || ''; + if (item.type === 'at') { + const qq = item.data && (item.data.qq || item.data.user_id); + return qq ? `[CQ:at,qq=${qq}]` : '[at]'; + } + return `[${item.type}]`; + }).join(''); + return text || fallback; +} + /** * 把海豹 milky 消息段(seal.MessageSegment,独立 Go 结构体 + type() 编号)直接映射为 * 项目内部统一段格式。不经过 CQ 码:milky 下 msg.message 只有纯文本拼接, From 3cd792f3f9aad4c51db83b2f1d88ef5aa6805e72 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Wed, 19 Aug 2026 01:21:19 +0800 Subject: [PATCH 14/20] chore: checkpoint current tool and resource changes --- README.md | 5 +- ...66\346\236\204\350\256\276\350\256\241.md" | 2 +- ...41\345\235\227\350\257\246\350\247\243.md" | 2 +- ...45\345\205\267\347\263\273\347\273\237.md" | 8 +- ...44\344\270\216\351\205\215\347\275\256.md" | 2 +- ...00\345\217\221\346\214\207\345\215\227.md" | 2 +- ...16\347\253\257\351\241\271\347\233\256.md" | 2 +- ...77\347\224\250\346\214\207\345\215\227.md" | 3 +- scripts/test-tool-listen.cjs | 66 ++++++ .../aiplugin4-test-suite/references/tool.md | 2 +- src/cmd/sub_cmd/tool.ts | 6 - src/config/configs/resource.ts | 2 +- src/config/configs/tool.ts | 38 +--- .../sealdice_command_defaults.ts | 197 ++++++++++++++++++ src/global.d.ts | 2 + src/index.ts | 5 +- src/pipeline.ts | 16 +- src/prompt/templates.ts | 5 +- src/session/session.ts | 48 +---- src/session/session_service.ts | 3 + src/session/tool_listen.ts | 75 +++++++ src/tool/command_catalog.ts | 15 +- src/tool/extension_executor.ts | 20 +- src/tool/mcp.ts | 6 + src/tool/mcp/adapters.ts | 4 +- src/tool/tool.ts | 72 +------ src/tool/tools/core/tool_cmd.ts | 2 +- src/tool/tools/resource/tool_resource.ts | 93 ++++++--- src/tool/tools/resource/tool_voice.ts | 49 ++++- src/tool/types.ts | 6 - src/update.ts | 1 + src/utils/ob11.ts | 33 +++ src/utils/resource.ts | 86 ++++++++ src/utils/string.ts | 36 +++- src/utils/utils.ts | 93 ++++++++- test-reports/aiplugin4-e2e-20260818-2308.md | 35 ++++ .../aiplugin4-mcp-free-path-20260818.md | 34 +++ 37 files changed, 831 insertions(+), 245 deletions(-) create mode 100644 scripts/test-tool-listen.cjs create mode 100644 src/config/static_config/sealdice_command_defaults.ts create mode 100644 src/session/tool_listen.ts create mode 100644 src/utils/resource.ts create mode 100644 test-reports/aiplugin4-e2e-20260818-2308.md create mode 100644 test-reports/aiplugin4-mcp-free-path-20260818.md diff --git a/README.md b/README.md index 094b97b..75f5c0e 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,8 @@ max_tokens = 2048 | 提供给AI的牌堆名称 | 每行一个牌堆名,用于 `draw_deck` 工具;没有的话建议把 `draw_deck` 加入禁止调用 | | 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | | MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。工具名称、描述和参数均从远端 `tools/list` 自动发现,同名冲突自动跳过;`run_ext_command` 仅由插件本地实现,不由 MCP 提供。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | -| 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | +| 可调用指令白名单 | 每行一个 `扩展名|指令名/别名1/别名2`;同一元素内的别名用 `/` 分隔。默认已包含当前 SealDice 核心命令、内置扩展命令及全部别名,核心扩展名统一写 `core`(如 `core|roll/r/rd`) | +| 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件;默认包含核心命令、内置扩展命令及别名的 `run_ext_command` / `run_core_command` 调用帮助。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | | ai语音使用的音色 | 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)和 ffmpeg | ### 记忆 @@ -456,7 +457,7 @@ max_tokens = 2048 - 查看日志中的调用失败原因(未注册/未经许可/参数缺失/类型不符/会话类型不符/超时等); - 用 `.ai tool` 查看开关状态,`.ai tool help <函数名>` 查看参数,`.ai tool call <函数名> --参数=值` 手动试用; - 工具在「禁止调用的函数」列表中时无法开启;新会话中「默认关闭的函数」需要 `.ai tool on <函数名>` 手动开启; -- 依赖海豹指令的工具需要先使用 `.r` 等指令。 +- 扩展指令工具不需要先使用 `.r`;`run_ext_command` 每次调用现场构造 `CmdArgs`。核心指令工具通过 `run_core_command` 走 OB11 核心桥。 **记忆/知识库检索不到** diff --git "a/docs/02-\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/02-\346\236\266\346\236\204\350\256\276\350\256\241.md" index 3e13208..0fce3c2 100644 --- "a/docs/02-\346\236\266\346\236\204\350\256\276\350\256\241.md" +++ "b/docs/02-\346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -83,7 +83,7 @@ ### 2. 指令消息(handleCommand) -- 记录 `Tool.cmdArgs`(调用海豹内置指令的工具依赖它;为空时工具会提示先使用 `.r` 等指令)。 +- 指令消息默认不写入上下文;扩展指令工具的 `CmdArgs` 由 `src/tool/extension_executor.ts` 在每次调用现场按当前参数新建,不读取会话历史,也不要求先使用 `.r`。核心指令仍通过 `run_core_command` 的 OB11 核心桥执行。 - 默认不写入上下文(`RECEIVE_CMD=false`);开启后在待机模式下把指令消息计入上下文。 ### 3. 机器人自身消息(handleBotMessage) 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 5874cc4..651cecf 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" @@ -14,7 +14,7 @@ `MessagePipeline` 是唯一的事件处理入口: - `handleNonCommand(ctx, msg)`:执行忽略/触发判定,见 [02-架构设计](02-架构设计.md)。 -- `handleCommand(ctx, msg, cmdArgs)`:记录 `Tool.cmdArgs`;可选录入上下文。 +- `handleCommand(ctx, msg, cmdArgs)`:处理收到的指令消息;可选录入上下文。扩展指令工具所需 `CmdArgs` 不从这里缓存,而是在 `run_ext_command` 调用现场构造。 - `handleBotMessage(ctx, msg)`:转发给工具监听;可选录入上下文。 - 会话统一通过 `getSession(sid)` 获取;`sid` 私聊为用户 ID、群聊为群 ID。 - CQ 码白名单 `CQ_TYPES_ALLOW = ["at","image","reply","face","poke"]`,含白名单之外 CQ 类型的消息不处理。 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 5d5301b..3bc8e24 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" @@ -9,7 +9,7 @@ | 属性 | 说明 | | --- | --- | | `toolInfo` | OpenAI function calling 格式的描述(name/description/parameters JSON Schema) | -| `ExtCmdInfo` | 海豹扩展指令信息(extName/cmd/staticArgs);非空时工具通过 `Tool.extensionSolve` 调用海豹内置指令 | +| `solve` | 工具实际执行函数;扩展指令工具使用 `run_ext_command`,由 `src/tool/extension_executor.ts` 现场构造 `CmdArgs` 后调用扩展 `solve` | | `sessionType` | 适用会话类型:`'any'`(默认)、`'group'`、`'user'` | | `callBack` | 是否回调智能体(当前统一为 true) | | `sensitive` | 敏感工具标记(发消息/禁言/改名等),执行日志显著记录 | @@ -87,6 +87,9 @@ > 注:`text_to_image` 支持可选 `image` 参数进行以图生图。参数可传图片 ID、`user_avatar:...`、`group_avatar:...`、图片 URL、data URL 或 base64;tti 模型配置需要在请求体中使用 `{image}`、`{image_url}` 或 `{image_base64}` 占位符。 +> 注:资源发送工具支持直接传 `path`。`send_image`、`send_file`、`send_video`、`record` 可接收本地绝对路径、`file://` URI,以及相对 SealDice 目录的路径;Windows 盘符路径(如 `C:\data\a.pdf`)和 UNC 路径会自动规范化。`send_file` 在 Milky 端点使用 `upload_group_file`/`upload_private_file`,OB11 端点继续使用 `file` 消息段。 +> 注:MCP 文件服务器 `mcp-files-exec` 支持把沙箱文件先导出为短时下载地址再发送。可使用 `mcp://mcp-files-exec/output/example.pdf`,或显式传 `source=mcp`、`server=mcp-files-exec`、`path=output/example.pdf`;相对 MCP 路径相对第一个沙箱根目录。 + 外部注册: - MCP 工具:默认直接按远端 `tools/list` 返回的工具名注册;与本地/已有工具同名时自动跳过。 @@ -105,7 +108,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 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。工具名称、描述和参数 schema 均从远端 `tools/list` 自动发现,返回内容按 MCP 标准内容块处理;`run_ext_command` 由插件本地注册,不经 MCP。改 URL/Token 时服务器会话自动重建(热加载) +- 配置项 `MCP服务器配置`(工具分组)只支持 JSON 格式的标准 `mcpServers` 块(Claude Desktop / Cursor `.mcp.json`):`{"mcpServers":{"名称":{"type":"http","url":"...","headers":{...}}}}`,支持任意自定义 headers 与 `token` 简写;`command`(stdio)与 `sse` 传输会明确提示后跳过。默认配置包含四个服务器:`mcp-files-exec`(文件读写、URL 下载、命令执行)、`web-read`(网页读取)、`md-html-render`(Markdown/HTML 渲染)、`ob11-core-bridge`(SealDice 核心指令中转)。工具名称、描述和参数 schema 均从远端 `tools/list` 自动发现,返回内容按 MCP 标准内容块处理;`run_ext_command` 由插件本地注册,不经 MCP。改 URL/Token 时服务器会话自动重建(热加载) - 协议:Streamable HTTP 上的 JSON-RPC(`initialize` → `notifications/initialized` → `tools/list` → `tools/call`),带 `Mcp-Session-Id` 会话头,兼容 SSE 响应。 - 远端工具默认用远端工具名注册为敏感工具,参数透传 `inputSchema`;返回纯文本时直接回传,返回标准 image 块或 base64 文本时保存为 `[img:...]` 并返回图片 ID。同名冲突会跳过。 - 工具列表有 60 秒 TTL 缓存;会话失效/服务器未初始化(会话不存在/已失效/未初始化/Server not initialized)时自动重新 initialize 并重试一次。 @@ -117,3 +120,4 @@ - 配置项 `技能配置`(工具分组)只支持标准 SKILL.md 格式:直接粘贴 Claude/Codex/Cursor 的技能文件,frontmatter 的 `name`/`description` 自动解析为技能名与摘要,正文为技能内容 - 注册 `use_skill` 工具;`getSkillSummaries()` 生成的技能摘要会追加到 system prompt 的"可用技能"段(两种工具模式都可见)。 - 技能内容支持 `{{skill:名称}}` 引用其他技能(最大解析深度 2),单次返回上限 4000 字符(超出截断并注明)。 +- 默认技能包含当前 SealDice 核心命令、内置扩展命令及全部别名的调用帮助:扩展指令使用 `run_ext_command`,核心命令(含 `core|team`)使用 `run_core_command`;默认白名单与 SealDice 源码中的命令表同步。 diff --git "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" index 032e759..47a3dc2 100644 --- "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" +++ "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" @@ -105,7 +105,7 @@ | 对话 | 角色扮演设定、示例对话、对话保存轮数、上下文最大token、消息压缩阈值 | 每条设定第一行为角色设定名称(>20 截断),其余为设定;`$gSYSPROMPT` 选择 | | 消息接收 | 接收图片/指令消息/骰子发送的消息、忽略消息豹语条件、忽略消息正则表达式 | 接收图片只负责记录图片 URL;自动识别仍由图片识别条件和图片模型配置决定 | | 消息触发 | 触发正则表达式、触发需要满足的条件、触发次数上限、触发次数补充间隔 | 令牌桶配额 | -| 工具 | 开启调用函数功能、切换为提示词工程、允许连续调用函数次数、工具响应压缩触发字数、禁止/默认关闭的函数、是否启用MCP、MCP服务器配置、技能配置、ai语音使用的音色 | 「是否启用MCP」默认关闭,开启后才会解析并连接 MCP 服务器;MCP 仅支持标准 mcpServers JSON(默认后端直接提供 mcp-files-exec/run_shell、web-read/web_read、md-html-render/render_markdown/render_html、ob11-core-bridge/run_core_command,run_ext_command 仅由插件本地实现,格式见 modelcontextprotocol.io/specification/latest),技能仅支持标准 SKILL.md(格式见 agentskills.io/specification) | +| 工具 | 开启调用函数功能、切换为提示词工程、允许连续调用函数次数、工具响应压缩触发字数、禁止/默认关闭的函数、是否启用MCP、MCP服务器配置、技能配置、ai语音使用的音色 | 「是否启用MCP」默认关闭,开启后才会解析并连接 MCP 服务器;MCP 仅支持标准 mcpServers JSON(默认后端直接提供 mcp-files-exec(read_file、download_file、run_shell)、web-read(web_read)、md-html-render/render_markdown/render_html、ob11-core-bridge/run_core_command,run_ext_command 仅由插件本地实现,格式见 modelcontextprotocol.io/specification/latest),技能仅支持标准 SKILL.md(格式见 agentskills.io/specification) | | 记忆 | 启用长期/总结/知识库记忆、长期记忆上限/展示数量、总结记忆上限/间隔/参与轮数、短期记忆上限、知识库注入阈值(字符)、知识库(Markdown 模板,每条一份完整文档) | 知识库只读,AI 通过 kb 工具/指令检索;修改后自动生效(缓存最多 1 分钟);Markdown 语法见 commonmark.org/help/ | | 回复 | 回复引用、回复最大字数、分段发送延时/基础延时/含图额外延时、禁止回复复读 | 回复过滤规则已内置硬编码 | | 后端 | 流式输出、图片转base64、联网搜索、用量图表、论坛地址/API Token/签名密钥 | 可自建,见 [08-相关后端项目](08-相关后端项目.md);网页读取与 md/html 渲染(MCP)在「工具 → MCP服务器配置」 | diff --git "a/docs/07-\345\274\200\345\217\221\346\214\207\345\215\227.md" "b/docs/07-\345\274\200\345\217\221\346\214\207\345\215\227.md" index dd05eaf..abede35 100644 --- "a/docs/07-\345\274\200\345\217\221\346\214\207\345\215\227.md" +++ "b/docs/07-\345\274\200\345\217\221\346\214\207\345\215\227.md" @@ -35,7 +35,7 @@ npm run lint # ESLint 检查 src/**/*.ts 1. 在 `src/tool/tools/` 下新建文件(或放入已有分类目录),导出一个 `registerXxx()` 函数。 2. 创建 `new Tool({ type:'function', function:{ name, description, parameters } })`,实现 `solve(ctx, msg, session, args)`,返回字符串给 AI。 -3. 可选配置:`sessionType`(group/user)、`sensitive`(敏感标记)、`ExtCmdInfo`(调用海豹内置指令)。 +3. 可选配置:`sessionType`(group/user)、`sensitive`(敏感标记)。如需调用海豹扩展指令,使用 `run_ext_command`;它会在每次调用时现场构造 `CmdArgs`,不依赖历史指令或 `.r`。 4. 在 `src/tool/tool.ts` 的 `Tool.registerTool()` 中导入并调用注册函数。 示例: 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 8752321..30f8140 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" @@ -14,7 +14,7 @@ | `mcp-files-exec` | 本地文件读写/执行 | 3910 | 工具 → MCP服务器配置(mcp-files-exec) | | `ob11-core-bridge` | SealDice 核心正向 WS / OB11 协议端 / MCP 透明中转(注入消息执行核心指令;扩展指令由插件本地执行) | 46880 | 工具 → MCP服务器配置(ob11-core-bridge) | -`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。插件不再为这些后端包装或改名:后端直接提供 `run_shell`、`web_read`、`render_markdown`、`render_html`、`run_core_command`,图片与核心多消息处理仍由对应适配器保留;`run_ext_command` 完全由插件本地实现,不属于 MCP。这些后端的 REST 接口已移除,仅提供 MCP。 +`web-read`、`md-html-render`、`mcp-files-exec` 与 `ob11-core-bridge` 均为 **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`、`http://127.0.0.1:46880/mcp`)。插件不再为这些后端包装或改名:后端直接提供 `read_file`、`download_file`、`run_shell`、`web_read`、`render_markdown`、`render_html`、`run_core_command`,图片与核心多消息处理仍由对应适配器保留;`run_ext_command` 完全由插件本地实现,不属于 MCP。这些后端的 REST 接口已移除,仅提供 MCP。 ## 部署与一键管理 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index 378d12f..072d46b 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -159,12 +159,13 @@ ws://127.0.0.1:46880/core ## 指令白名单 -「可调用指令白名单」每行一条,格式 `扩展名|指令名`: +「可调用指令白名单」每行一条,格式 `扩展名|指令名/别名1/别名2`,同一元素内的别名用 `/` 分隔: - 内置扩展已硬编码,**无需填写扩展列表**;第三方扩展写实际扩展名 - 核心指令的扩展名统一写 **`core`**(如 `core|help`) - `core|ext` 是扩展发现入口,**无需加入白名单**:`run_core_command` 执行 `command=ext`(即核心 `.ext`)即可查看核心当前全部扩展名称 - 开启「是否允许调用所有指令」后忽略扩展指令白名单(核心指令仍受 `run_core_command` 白名单约束) +- 插件默认值已按当前 SealDice 源码同步全部核心命令、内置扩展命令及别名;技能配置同时提供对应的 `run_ext_command` / `run_core_command` 调用帮助。若升级 SealDice 后新增命令,请手动补充白名单或等待插件默认值更新。 ## 指令前缀 diff --git a/scripts/test-tool-listen.cjs b/scripts/test-tool-listen.cjs new file mode 100644 index 0000000..e5d1368 --- /dev/null +++ b/scripts/test-tool-listen.cjs @@ -0,0 +1,66 @@ +const fs = require('fs'); +const path = require('path'); +const ts = require('typescript'); + +const sourcePath = path.resolve(__dirname, '../src/session/tool_listen.ts'); +if (!fs.existsSync(sourcePath)) { + throw new Error('缺少 src/session/tool_listen.ts:监听器尚未实现'); +} + +const source = fs.readFileSync(sourcePath, 'utf8'); +const output = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2017, + }, +}).outputText; +const transpiledModule = { exports: {} }; +new Function('require', 'module', 'exports', output)(require, transpiledModule, transpiledModule.exports); + +const { createToolListen } = transpiledModule.exports; +if (typeof createToolListen !== 'function') { + throw new Error('createToolListen 未导出'); +} + +async function testResolveDispatchesToWaiter() { + const listen = createToolListen(); + const resultPromise = listen.waitFor(200, 0, 1); + listen.resolve('扩展回复'); + const result = await resultPromise; + if (result.length !== 1 || result[0] !== '扩展回复') { + throw new Error(`resolve 未投递消息: ${JSON.stringify(result)}`); + } +} + +async function testRehydratedRuntimeListener() { + const persisted = JSON.parse(JSON.stringify({ timeoutId: null })); + const restored = createToolListen(); + restored.timeoutId = persisted.timeoutId; + const resultPromise = restored.waitFor(200, 0, 1); + restored.resolve('恢复后的扩展回复'); + const result = await resultPromise; + if (result.length !== 1 || result[0] !== '恢复后的扩展回复') { + throw new Error(`恢复后的 listener 无法接收消息: ${JSON.stringify(result)}`); + } +} + +async function testPushSupportsMultipleWaiters() { + const listen = createToolListen(); + const first = listen.waitFor(200, 0, 2); + const second = listen.waitFor(200, 0, 2); + listen.push('第一条'); + listen.push('第二条'); + const results = await Promise.all([first, second]); + for (const result of results) { + if (result.join('|') !== '第一条|第二条') { + throw new Error(`并发等待器收到的消息不完整: ${JSON.stringify(results)}`); + } + } +} + +Promise.all([testResolveDispatchesToWaiter(), testPushSupportsMultipleWaiters(), testRehydratedRuntimeListener()]) + .then(() => console.log('TOOL LISTEN TEST OK')) + .catch(error => { + console.error('TOOL LISTEN TEST FAIL:', error.message); + process.exitCode = 1; + }); diff --git a/skills/aiplugin4-test-suite/references/tool.md b/skills/aiplugin4-test-suite/references/tool.md index 13e5905..26a0027 100644 --- a/skills/aiplugin4-test-suite/references/tool.md +++ b/skills/aiplugin4-test-suite/references/tool.md @@ -18,5 +18,5 @@ 说明: - `tool call` 有调用副作用(如 `ban`/`whole_ban`/`record`/`text_to_sound`),只测无副作用的 `show_timer_list` 与未注册函数错误路径。 -- `tool call` 依赖 `Tool.cmdArgs`(`ExtCmdInfo.extName !== ''` 且 `Tool.cmdArgs == null` 时会要求先使用 `.r` 指令),遇此提示记 SKIP(env)。 +- `run_ext_command` 不依赖会话历史中的 `Tool.cmdArgs`,也不要求先使用 `.r`;每次调用都会按工具参数现场构造 `CmdArgs`。核心指令使用 `run_core_command`,需要 OB11 核心桥。 - TOOL-04~07 结束后按 TOOL-01 快照逐工具还原(`on`/`off <工具名>`)。 diff --git a/src/cmd/sub_cmd/tool.ts b/src/cmd/sub_cmd/tool.ts index 4bb13bf..ea4d43d 100644 --- a/src/cmd/sub_cmd/tool.ts +++ b/src/cmd/sub_cmd/tool.ts @@ -2,7 +2,6 @@ import Config from "../../config/config"; import { logger } from "../../logger"; import { toolMap } from "../../tool/tool"; -import Tool from "../../tool/tool"; import { aliasToCmd } from "../../utils/utils"; import { I, M, U } from "../privilege"; import { SubCmd, SubCmdContext } from "../root_cmd"; @@ -105,11 +104,6 @@ export function registerCmdTool() { return ret; } const tool = toolMap[val3]; - if (tool.ExtCmdInfo.extName !== '' && !Tool.getCmdArgs(ctx)) { - seal.replyToSender(ctx, msg, `暂时无法调用函数,请先使用 .r 指令`); - return ret; - } - try { const args = cmdArgs.kwargs.reduce((acc: { [key: string]: any }, kwarg) => { const valueString = kwarg.value; diff --git a/src/config/configs/resource.ts b/src/config/configs/resource.ts index 568affe..1dea582 100644 --- a/src/config/configs/resource.ts +++ b/src/config/configs/resource.ts @@ -34,7 +34,7 @@ function getPathMapConfig(key: string): { [id: string]: string } { id = trimmed.slice(0, eq).trim(); path = trimmed.slice(eq + 1).trim(); } else { - id = (trimmed.split('/').pop() || '').replace(/\.[^/.]+$/, ''); + id = (trimmed.split(/[\\/]/).pop() || '').replace(/\.[^/.]+$/, ''); } if (!id || !path) throw new Error(`本地路径格式错误:${line}`); acc[id] = path; diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index f98f30d..ab1733e 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -1,5 +1,6 @@ // 工具配置:函数调用开关/提示词工程/上限/禁用与默认关闭 import { ext } from "../config"; +import { SEALDICE_COMMAND_SKILLS, SEALDICE_COMMAND_WHITELIST } from "../static_config/sealdice_command_defaults"; export default class ToolConfig { static register() { @@ -10,7 +11,7 @@ export default class ToolConfig { seal.ext.registerIntConfig(ext, "工具响应压缩触发字数", 5000, "工具返回结果超过该字数时压缩后再存入上下文;设为 0 不压缩", "工具"); seal.ext.registerTemplateConfig(ext, "禁止调用的函数", [''], "每行一个禁止 AI 调用的函数名,示例:draw_deck;修改后自动生效", "工具"); seal.ext.registerTemplateConfig(ext, "默认关闭的函数", [''], "每行一个默认关闭的函数名,AI 默认无法调用,示例:get_msg;修改后自动生效", "工具"); - seal.ext.registerTemplateConfig(ext, "可调用指令白名单", ['fun|jrrp', 'story|modu', 'coc7|st', 'coc7|ra', 'coc7|sc'], "每行一个 AI 可调用的海豹指令;格式:扩展名|指令名,核心指令的扩展名统一写 core(如 core|ext)。run_core_command 的 core|ext 无需加入白名单。修改后自动生效", "工具"); + seal.ext.registerTemplateConfig(ext, "可调用指令白名单", SEALDICE_COMMAND_WHITELIST, "每行一个 AI 可调用的海豹指令;格式:扩展名|指令名/别名1/别名2,同一元素内的别名用 / 分隔;核心指令的扩展名统一写 core(如 core|roll/r/rd)。默认已包含当前 SealDice 源码中的全部核心命令、内置扩展命令及其别名;修改后自动生效", "工具"); seal.ext.registerBoolConfig(ext, "是否允许调用所有指令", false, "开启后忽略白名单,允许调用所有可解析的扩展指令;核心指令仍通过 run_core_command 调用", "工具"); seal.ext.registerStringConfig(ext, "指令前缀", ".", "注入到 SealDice 核心的指令前缀,通常为 .;如果核心改成其他前缀,请同步修改", "工具"); seal.ext.registerTemplateConfig(ext, "提供给AI的牌堆名称", [''], "每行一个牌堆名,示例:克苏鲁的呼唤;没有的话建议把 draw_deck 加入不允许调用", "工具"); @@ -39,39 +40,8 @@ export default class ToolConfig { } } }` - ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。工具名称、描述和参数 schema 会在连接后通过 MCP tools/list 自动发现,不需要也不支持额外的 tools 配置块。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)。默认包含四个:mcp-files-exec(提供 run_shell)、web-read(提供 scrape_url、screenshot_url)、md-html-render(提供 render_markdown、render_html)、ob11-core-bridge(仅提供 run_core_command)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请改用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); - seal.ext.registerTemplateConfig(ext, "技能配置", [ - `--- -name: 今日人品 -description: 查询指定用户的今日人品值 ---- -使用 run_ext_command 工具执行:action=call,command="fun|jrrp",args=["用户名或QQ号"];fun|jrrp 需在「可调用指令白名单」中`, - `--- -name: COC模组抽取 -description: 随机抽取一个 COC 模组 ---- -使用 run_ext_command 工具执行:action=call,command="story|modu",args=["roll"];story|modu 需在「可调用指令白名单」中`, - `--- -name: COC模组搜索 -description: 按关键词搜索 COC 模组 ---- -使用 run_ext_command 工具执行:action=call,command="story|modu",args=["search","关键词"];story|modu 需在「可调用指令白名单」中`, - `--- -name: 属性展示 -description: 展示指定玩家的 COC 全部个人属性 ---- -使用 run_ext_command 工具执行:action=call,command="coc7|st",args=["show","玩家名称或QQ号"];coc7|st 需在「可调用指令白名单」中`, - `--- -name: 属性检定 -description: 对指定玩家进行一次属性/技能检定(ra) ---- -使用 run_ext_command 工具执行:action=call,command="coc7|ra",args 按顺序:奖励/惩罚骰(可选,如 b、p3)、检定表达式(含难度等级或数值运算时直接用,普通属性名时用该属性,属性为0时补50)、检定原因(可选);coc7|ra 需在「可调用指令白名单」中`, - `--- -name: san检定 -description: 对指定玩家进行 san check(sc) ---- -使用 run_ext_command 工具执行:action=call,command="coc7|sc",args 按顺序:奖励/惩罚骰(可选,如 b、p2)、表达式(成功时掉san/失败时掉san,如 0/1d6、0/1);coc7|sc 需在「可调用指令白名单」中` - ], "每条配置项一个技能,仅支持标准 SKILL.md 格式:以 --- 开头的 YAML frontmatter 里写 name(必填)/description(可选),正文为技能内容;可直接粘贴其他 agent(Claude/Codex/Cursor)的技能文件。\n格式定义见 https://agentskills.io/specification (SKILL.md 开放规范,Claude/Codex 通用,国内可访问)。\n默认包含基于 run_ext_command 统一调用的扩展技能(今日人品/COC模组/属性展示/检定等),指令需加入「可调用指令白名单」,可自行增删。修改后自动生效(缓存最多 1 分钟)。AI 可通过 use_skill 工具按需调用", "工具"); + ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。工具名称、描述和参数 schema 会在连接后通过 MCP tools/list 自动发现,不需要也不支持额外的 tools 配置块。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)。默认包含四个:mcp-files-exec(提供 read_file、list_dir、write_file、delete_file、download_file、run_shell、export_file;默认可直接传后端绝对路径)、web-read(提供 scrape_url、screenshot_url)、md-html-render(提供 render_markdown、render_html)、ob11-core-bridge(仅提供 run_core_command)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请改用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); + seal.ext.registerTemplateConfig(ext, "技能配置", SEALDICE_COMMAND_SKILLS, "每条配置项一个技能,仅支持标准 SKILL.md 格式:以 --- 开头的 YAML frontmatter 里写 name(必填)/description(可选),正文为技能内容;默认包含当前 SealDice 核心命令、内置扩展命令及别名的调用帮助,统一说明 run_ext_command / run_core_command 的参数传递方式。修改后自动生效(缓存最多 1 分钟)。AI 可通过 use_skill 工具按需调用", "工具"); seal.ext.registerTemplateConfig(ext, "音乐服务配置", [ `{ "platform": "网易云", diff --git a/src/config/static_config/sealdice_command_defaults.ts b/src/config/static_config/sealdice_command_defaults.ts new file mode 100644 index 0000000..bdcdf01 --- /dev/null +++ b/src/config/static_config/sealdice_command_defaults.ts @@ -0,0 +1,197 @@ +// SealDice 当前源码(sealdice-core,2026-08-18)内置扩展与核心命令的默认可调用清单。 +// 命令别名也保留,确保 AI 传入任意 SealDice 可解析名称时都能通过白名单校验。 +export const SEALDICE_COMMAND_WHITELIST = [ + // 核心命令(同一扩展元素内用 / 分隔别名,含 core 扩展中的 team) + 'core|black/ban', 'core|find/查询/査詢', 'core|help', + 'core|bot', 'core|dismiss', 'core|botlist', 'core|master', + 'core|roll/r/rd/rh/rhd/rdh/rx/rxh/rhx', + 'core|ext', 'core|nn', 'core|userid', 'core|randalgo', 'core|set', + 'core|角色/ch/char/character/pc', 'core|reply', 'core|team', + + // fun + 'fun|alias', 'fun|&/a', 'fun|ping', 'fun|send', 'fun|welcome', 'fun|gugu/咕咕', + 'fun|jrrp', 'fun|text', 'fun|rsr', 'fun|ek/ekgen', 'fun|dx', + 'fun|w/ww/dxh/wh/wwh', 'fun|jsr', 'fun|drl/drlh', 'fun|check', + + // story + 'story|name', 'story|namednd', 'story|who', 'story|cnmods/modu/魔都', + + // coc7 + 'coc7|en', 'coc7|setcoc', 'coc7|ti', 'coc7|li', + 'coc7|ra/rc/rch/rah/cra/crc/crch/crah', 'coc7|rav/rcv', 'coc7|sc', + 'coc7|coc', 'coc7|st/cst', + + // deck + 'deck|draw/deck', + + // dnd5e + 'dnd5e|dnd/dndx', 'dnd5e|ri', 'dnd5e|init', 'dnd5e|st/dst', + 'dnd5e|rc/ra/rah/rch/drc', 'dnd5e|buff/dbuff', + 'dnd5e|spellslots/ss/dss/法术位', 'dnd5e|cast/dcast', + 'dnd5e|长休/longrest/dlongrest', 'dnd5e|ds/死亡豁免', + + // log + 'log|log', 'log|stat/hiy', 'log|ob', 'log|sn' +]; + +const COMMAND_CALL_RULES = ` +调用规则: +- 扩展指令使用 run_ext_command,参数格式:{"action":"call","extension":"扩展名","command":"指令名","args":["参数1","参数2"]};不要在 command 中带前缀点号。 +- 核心指令(包括 core|team)使用 run_core_command,参数格式:{"command":"指令名","args":["参数1","参数2"]}。 +- args 按 SealDice 原始指令的空格分隔顺序逐项传入;需要 @、表达式或带空格的文本时按工具 schema 传入单独字符串。 +- 下方同一行中的名称是同一条指令的别名,均已加入白名单;优先使用第一个主名称。 +- 参数不确定时,先调用对应的 help:核心用 run_core_command(command="help", args=["指令名"],扩展可调用扩展的 help 参数或先查看 SealDice 指令帮助。 +`; + +export const SEALDICE_COMMAND_SKILLS = [ + `--- +name: 今日人品 +description: 查询指定用户的今日人品值 +--- +使用 run_ext_command 工具执行:{"action":"call","extension":"fun","command":"jrrp","args":["用户名或QQ号"]};fun|jrrp 已加入默认白名单。`, + + `--- +name: COC模组抽取 +description: 随机抽取一个 COC 模组 +--- +使用 run_ext_command 工具执行:{"action":"call","extension":"story","command":"modu","args":["roll"]};story|modu 已加入默认白名单。`, + + `--- +name: COC模组搜索 +description: 按关键词搜索 COC 模组 +--- +使用 run_ext_command 工具执行:{"action":"call","extension":"story","command":"modu","args":["search","关键词"]};story|modu 已加入默认白名单。`, + + `--- +name: 属性展示 +description: 展示指定玩家的 COC 全部个人属性 +--- +使用 run_ext_command 工具执行:{"action":"call","extension":"coc7","command":"st","args":["show","玩家名称或QQ号"]};coc7|st 已加入默认白名单。`, + + `--- +name: 属性检定 +description: 对指定玩家进行一次属性或技能检定(ra) +--- +使用 run_ext_command 工具执行:{"action":"call","extension":"coc7","command":"ra","args":["奖励/惩罚骰(可选,如 b、p3)","检定表达式或属性名","检定原因(可选)"]};coc7|ra 已加入默认白名单。args 按 SealDice 原始指令顺序传入;普通属性名若属性值为 0,按原扩展规则补 50。`, + + `--- +name: san检定 +description: 对指定玩家进行 San check(sc) +--- +使用 run_ext_command 工具执行:{"action":"call","extension":"coc7","command":"sc","args":["奖励/惩罚骰(可选,如 b、p2)","成功损失/失败损失表达式(如 0/1d6、0/1)"]};coc7|sc 已加入默认白名单。`, + + `--- +name: SealDice核心指令调用帮助 +description: 调用 SealDice 核心命令与 core 扩展命令,包含命令别名、参数传递方式和安全提示 +--- +${COMMAND_CALL_RULES} +核心命令清单(主名称 / 别名): +- black / ban:黑名单与处罚操作,涉及封禁、拉黑等敏感动作前确认目标和参数。 +- find / 查询 / 査詢:查找扩展、用户或相关信息。 +- help:查询命令帮助,例如 command="help"、args=["roll"]。 +- bot:机器人开关与状态操作。 +- dismiss:退群/解散相关操作,执行前确认。 +- botlist:查看机器人列表。 +- master:查看或管理骰主信息。 +- roll / r / rd / rh / rhd / rdh / rx / rxh / rhx:骰点表达式,例如 command="roll"、args=["1d100"]。 +- ext:查看当前扩展;core|ext 无需加入白名单也可用,但默认仍列出。 +- nn:查看或设置当前角色名。 +- userid:查看当前帐号、用户和群组 ID。 +- randalgo:查看随机算法,或 args=["get","100"];set 仅限 Master。 +- set:设置骰子面数或规则,例如 args=["coc"]、args=["100"]。 +- 角色 / ch / char / character / pc:查看或设置角色卡。 +- reply:开启或关闭自定义回复,例如 args=["on"] 或 args=["off"]。 +- team:团队管理,例如 args=["list"] 或 args=["团队名","add"];需要群聊环境。 + +安全:涉及 ban/black、bot、dismiss、master、set、reply、team 等会改变状态的命令,先核对用户意图;不要把自然语言说明当作 args。`, + + `--- +name: SealDice fun扩展调用帮助 +description: 调用 fun 内置扩展的娱乐、快捷指令、骰池和功能命令 +--- +${COMMAND_CALL_RULES} +调用时 extension 固定为 "fun"。 +- alias:创建、删除或查看快捷指令;args 按 SealDice .alias 的原始顺序传入。 +- & / a:执行已有快捷指令;args=["快捷指令名", "参数..."]。 +- ping:触发一条回复;通常 args=[]。 +- send:向骰主留言;args=["留言内容"],涉及私密信息时谨慎使用。 +- welcome:查看或设置入群欢迎相关内容;参数按原命令帮助传入。 +- gugu / 咕咕:生成咕咕理由;args 可传来源。 +- jrrp:查询今日人品;args 可传用户名或 QQ 号。 +- text:文本/豹语相关功能;参数按原命令帮助传入。 +- rsr:暗影狂奔骰点;args=["骰数或表达式"]。 +- ek / ekgen:特殊规则或生成相关功能;参数按原命令帮助传入。 +- dx:掷骰,例如 args=["3c4"]。 +- w / ww / dxh / wh / wwh:同一类掷骰/暗骰命令,优先使用主名称 w 或 ww,参数按原命令帮助传入。 +- jsr:不重复投掷,例如 args=["3#","10"]。 +- drl / drlh:骰池抽取或管理;例如 drl args=[],drl new 的参数按原命令帮助传入。 +- check:校验相关命令,参数按原命令帮助传入。`, + + `--- +name: SealDice story扩展调用帮助 +description: 调用 story 内置扩展的随机姓名、人物和 COC 模组命令 +--- +${COMMAND_CALL_RULES} +调用时 extension 固定为 "story"。 +- name:随机姓名;args=["cn"、"en" 或 "jp", "数量", "性别"]。 +- namednd:生成 DND 种族姓名;args 可传种族,例如 ["精灵"]。 +- who:随机人物/身份组合;args 按原命令帮助传入,例如 ["a","b","c"]。 +- cnmods / modu / 魔都:COC 模组功能;modu args=["roll"] 随机抽取,args=["search","关键词"] 按关键词搜索。`, + + `--- +name: SealDice coc7扩展调用帮助 +description: 调用 COC7 内置扩展的角色卡、属性、检定和疯狂症状命令 +--- +${COMMAND_CALL_RULES} +调用时 extension 固定为 "coc7"。 +- en:COC 规则/角色相关设置;参数按原命令帮助传入。 +- setcoc:设置 COC 规则;参数按原命令帮助传入。 +- ti:抽取临时性疯狂症状;args=[]。 +- li:抽取总结性疯狂症状;args=[]。 +- ra / rc / rch / rah / cra / crc / crch / crah:属性或技能检定,同一实现的不同别名;args 按顺序传奖励/惩罚骰、检定表达式、原因。 +- rav / rcv:对抗/竞争检定;args 按原命令帮助传入。 +- sc:San 检定;args 按顺序传奖励/惩罚骰、成功/失败损失表达式,例如 ["0/1d6"]。 +- coc:生成 COC 角色卡;args 可传数量,例如 ["3"]。 +- st / cst:查看或管理 COC 角色属性;常用 args=["show","玩家名称或QQ号"]。`, + + `--- +name: SealDice deck扩展调用帮助 +description: 调用 SealDice 牌堆抽取命令 +--- +${COMMAND_CALL_RULES} +调用时 extension 固定为 "deck"。 +- draw / deck:从已配置牌堆抽取内容;args 通常为["牌堆名称"],需要指定抽取次数或其他选项时继续按原命令顺序传入。 +- 仅调用已配置且允许提供给 AI 的牌堆;不要猜测不存在的牌堆名称。`, + + `--- +name: SealDice dnd5e扩展调用帮助 +description: 调用 DND5E 内置扩展的角色、先攻、检定、法术位和战斗命令 +--- +${COMMAND_CALL_RULES} +调用时 extension 固定为 "dnd5e"。 +- dnd / dndx:DND 规则或角色相关设置。 +- ri:先攻/遭遇相关操作;参数按原命令帮助传入。 +- init:查看先攻列表;args=[]。 +- st / dst:查看角色属性。 +- rc / ra / rah / rch / drc:DND 检定;args 按顺序传检定表达式、难度或原因,别名可按习惯选择。 +- buff / dbuff:增益/减益效果;参数按原命令帮助传入。 +- spellslots / ss / dss / 法术位:查看或管理法术位。 +- cast / dcast:施法;args 按原命令帮助传入。 +- 长休 / longrest / dlongrest:执行长休。 +- ds / 死亡豁免:死亡豁免检定;args 按原命令帮助传入。`, + + `--- +name: SealDice log扩展调用帮助 +description: 调用 SealDice 日志、统计和日志导出命令 +--- +${COMMAND_CALL_RULES} +调用时 extension 固定为 "log"。 +- log:查看、记录或管理日志;参数按原命令帮助传入。 +- stat / hiy:查看日志统计。 +- ob:导出或查看日志内容;涉及大量输出时限制范围。 +- sn:日志相关的名称/故事操作;参数按原命令帮助传入。 + +日志命令可能读取或导出群聊历史,调用前确认范围,避免无必要地暴露隐私。` +]; + + diff --git a/src/global.d.ts b/src/global.d.ts index 5d596a5..6af0124 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -4,6 +4,8 @@ /* eslint-disable no-var */ interface NetApi { callApi(epId: string, action: string, params?: any): Promise; + /** 统一发送文件;Milky 端点会改用 upload_*_file,OB11 端点发送 file 消息段 */ + sendFile?(epId: string, scene: 'group' | 'private', peerId: string | number, file: string, name: string, parentFolderId?: string): Promise; /** 订阅依赖的事件分发(ob11 网络连接依赖:milky → OB11 转接的额外消息段) */ getEventDispatcher?(ext: any, key?: string): Promise; /** milky 消息引用 <-> OB11 唯一 message_id 双向转换(本地计算) */ diff --git a/src/index.ts b/src/index.ts index 92a3e4f..e5bef0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,9 +84,9 @@ function main() { } //接受的指令 - ext.onCommandReceived = (ctx: seal.MsgContext, msg: seal.Message, cmdArgs: seal.CmdArgs) => { + ext.onCommandReceived = (ctx: seal.MsgContext, msg: seal.Message) => { try { - MessagePipeline.handleCommand(ctx, msg, cmdArgs); + MessagePipeline.handleCommand(ctx, msg); } catch (e) { logger.error(`指令消息处理出错,错误信息:${e instanceof Error ? e.message : String(e)}`); } @@ -95,6 +95,7 @@ function main() { //骰子发送的消息 ext.onMessageSend = (ctx: seal.MsgContext, msg: seal.Message) => { try { + logger.debug(`[onMessageSend] 收到机器人出站消息 session=${ctx.isPrivate ? ctx.player?.userId : ctx.group?.groupId} text=${String(msg.message || "").slice(0, 120)}`); MessagePipeline.handleBotMessage(ctx, msg); } catch (e) { logger.error(`获取发送消息处理出错,错误信息:${e instanceof Error ? e.message : String(e)}`); diff --git a/src/pipeline.ts b/src/pipeline.ts index 795ab61..b73ea74 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -4,11 +4,10 @@ import Config, { ext } from "./config/config"; import { CQ_TYPES_ALLOW } from "./config/static_config"; import { logger } from "./logger"; import { getSession } from "./session/session_service"; -import Tool from "./tool/tool"; import { triggerConditionMap } from "./tool/tools/core/tool_trigger"; import { expandForwardMessage } from "./utils/ob11"; import { createCtx, createMsg } from "./utils/seal"; -import { expandMilkySegments, formatMessageSegmentsForMatching, MessageSegment, parseCardToText, parseMusicToText, transformTextToArray, truncateText } from "./utils/string"; +import { expandMilkySegments, formatFileSegmentText, formatMessageSegmentsForMatching, MessageSegment, parseCardToText, parseMusicToText, transformTextToArray, truncateText } from "./utils/string"; import { getRecordMessageId, transformMsgId } from "./utils/utils"; /** 核心原生 milky 路径通常过滤掉的段,只由 ob11 依赖补充。 */ @@ -106,8 +105,8 @@ export class MessagePipeline { break; } case 'file': { - // milky 转接与 NapCat 的 file 段字段略有差异(file/name/file_id),统一取可用值 - result.push({ type: 'text', data: { text: `【文件】${data.name || data.file || data.file_id || ''}` } }); + // 保留文件名之外的 path/url/file_id 等字段,供 AI 继续调用文件工具读取或导入。 + result.push({ type: 'text', data: { text: formatFileSegmentText(data) } }); break; } case 'video': { @@ -428,10 +427,8 @@ export class MessagePipeline { } } - /** 指令消息:记录 cmdArgs,并按配置决定是否写入会话上下文 */ - static handleCommand(ctx: seal.MsgContext, msg: seal.Message, cmdArgs: seal.CmdArgs): void { - // 每次收到指令都刷新当前会话的 cmdArgs,供旧版本地扩展调用兼容逻辑复用最近指令对象 - Tool.setCmdArgs(ctx, cmdArgs); + /** 指令消息:按配置决定是否写入会话上下文 */ + static handleCommand(ctx: seal.MsgContext, msg: seal.Message): void { const { RECEIVE_CMD: allcmd } = Config.received; if (!allcmd) return; @@ -476,7 +473,8 @@ export class MessagePipeline { ? messageArray.map(item => item.type === 'text' ? ((item.data && item.data.text) || '') : `[${item.type}]`).join('') : message; - session.tool.listen.push?.(messageText); // 分发给所有监听器,支持多条消息与并发调用 + const deliver = session.tool.listen.push || session.tool.listen.resolve; + deliver?.(messageText); const { RECEIVE_MSG_BY_BOT: allmsg } = Config.received; if (!allmsg) return; diff --git a/src/prompt/templates.ts b/src/prompt/templates.ts index 1c24e32..55902eb 100644 --- a/src/prompt/templates.ts +++ b/src/prompt/templates.ts @@ -38,8 +38,9 @@ const TEMPLATES: { [key: string]: string } = { - 可使用[audio:音频ID]发送本地音频,可用名称先通过 list_resources(type=audio) 查询 ## 文件与视频相关 -- 可使用send_file工具发送本地文件,可用名称先通过 list_resources(type=file) 查询 -- 可使用send_video工具发送本地视频,可用名称先通过 list_resources(type=video) 查询 +- 可使用send_file工具发送本地文件,可用名称先通过 list_resources(type=file) 查询;也可直接传 path +- 可使用send_video工具发送本地视频,可用名称先通过 list_resources(type=video) 查询;也可直接传 path +- send_image、send_file、send_video、record 的 path 支持本地绝对路径、file:// URI;MCP 沙箱文件使用 mcp://服务器名/沙箱相对路径,或 source=mcp + server + path **DYNAMIC_SECTIONS** diff --git a/src/session/session.ts b/src/session/session.ts index 9e56ef9..9acf96c 100644 --- a/src/session/session.ts +++ b/src/session/session.ts @@ -18,6 +18,7 @@ import { TypeDescriptor } from "../utils/utils"; import { getRecordMessageId, replyToSender } from "../utils/utils"; import Group from "./group"; +import { createToolListen } from "./tool_listen"; import { SessionType, State } from "./types"; import User from "./user"; @@ -144,52 +145,7 @@ export class Session { } this.memory = new SessionMemoryService(); this.lastCtx = null; - const waiters: { resolve: (messages: string[]) => void, reject: (error: Error) => void, messages: string[], settleTimer: any, timeoutTimer: any, maxMessages: number, settleMs: number }[] = []; - const listen: ToolListen = { - timeoutId: null, - resolve: null, - reject: null, - cleanup: () => { - if (listen.timeoutId) clearTimeout(listen.timeoutId); - listen.timeoutId = null; - listen.resolve = null; - listen.reject = null; - while (waiters.length) { - const waiter = waiters.shift()!; - clearTimeout(waiter.settleTimer); - clearTimeout(waiter.timeoutTimer); - waiter.reject(new Error('监听已清理')); - } - }, - push: (content: string) => { - for (let i = waiters.length - 1; i >= 0; i--) { - const waiter = waiters[i]; - if (waiter.messages.length < waiter.maxMessages) waiter.messages.push(content); - if (waiter.settleTimer) clearTimeout(waiter.settleTimer); - if (waiter.messages.length >= waiter.maxMessages) { - waiters.splice(i, 1); - clearTimeout(waiter.timeoutTimer); - waiter.resolve(waiter.messages); - } else { - waiter.settleTimer = setTimeout(() => { - const index = waiters.indexOf(waiter); - if (index >= 0) waiters.splice(index, 1); - clearTimeout(waiter.timeoutTimer); - waiter.resolve(waiter.messages); - }, waiter.settleMs); - } - } - }, - waitFor: (timeoutMs = 10000, settleMs = 400, maxMessages = 20) => new Promise((resolve, reject) => { - const waiter = { resolve, reject, messages: [] as string[], settleTimer: null as any, timeoutTimer: null as any, maxMessages: Math.max(1, maxMessages), settleMs: Math.max(0, settleMs) }; - waiter.timeoutTimer = setTimeout(() => { - const index = waiters.indexOf(waiter); - if (index >= 0) waiters.splice(index, 1); - resolve(waiter.messages); - }, Math.max(1, timeoutMs)); - waiters.push(waiter); - }) - }; + const listen = createToolListen(); this.tool = { state: {} as ToolState, callCount: 0, diff --git a/src/session/session_service.ts b/src/session/session_service.ts index f0af2a5..6b5a2ae 100644 --- a/src/session/session_service.ts +++ b/src/session/session_service.ts @@ -7,6 +7,7 @@ import MemoryService from "../memory/memory"; import { revive, TypeDescriptor } from "../utils/utils"; import { Session, Setting } from "./session"; +import { createToolListen } from "./tool_listen"; /** @@ -47,6 +48,8 @@ export class SessionService { session.sessionId = sessionId; if (sessionId.startsWith('QQ:')) session.sessionType = 'user'; session.agentName = this.agentName; + // listen 是运行时对象,函数不会被 JSON 持久化;每次恢复会话都重新创建。 + session.tool.listen = createToolListen(); // 复活嵌套对象(Context/MemoryItem/Setting) session.context = revive(Context, session.context || {}); session.context.reviveMessages(); diff --git a/src/session/tool_listen.ts b/src/session/tool_listen.ts new file mode 100644 index 0000000..d7acdd6 --- /dev/null +++ b/src/session/tool_listen.ts @@ -0,0 +1,75 @@ +import { ToolListen } from "../tool/types"; + +interface ToolWaiter { + resolve: (messages: string[]) => void; + reject: (error: Error) => void; + messages: string[]; + settleTimer: any; + timeoutTimer: any; + maxMessages: number; + settleMs: number; +} + +/** 创建不参与会话持久化的运行时消息监听器。 */ +export function createToolListen(): ToolListen { + const waiters: ToolWaiter[] = []; + const dispatch = (content: string): void => { + for (let i = waiters.length - 1; i >= 0; i--) { + const waiter = waiters[i]; + if (waiter.messages.length < waiter.maxMessages) waiter.messages.push(content); + if (waiter.settleTimer) clearTimeout(waiter.settleTimer); + if (waiter.messages.length >= waiter.maxMessages) { + waiters.splice(i, 1); + clearTimeout(waiter.timeoutTimer); + waiter.resolve(waiter.messages); + continue; + } + waiter.settleTimer = setTimeout(() => { + const index = waiters.indexOf(waiter); + if (index >= 0) waiters.splice(index, 1); + clearTimeout(waiter.timeoutTimer); + waiter.resolve(waiter.messages); + }, waiter.settleMs); + } + }; + const rejectAll = (error: Error): void => { + while (waiters.length) { + const waiter = waiters.shift()!; + clearTimeout(waiter.settleTimer); + clearTimeout(waiter.timeoutTimer); + waiter.reject(error); + } + }; + const listen: ToolListen = { + timeoutId: null, + resolve: dispatch, + reject: rejectAll, + cleanup: () => { + if (listen.timeoutId) clearTimeout(listen.timeoutId); + listen.timeoutId = null; + rejectAll(new Error('监听已清理')); + // cleanup 后恢复当前监听器的分发入口。 + listen.resolve = dispatch; + listen.reject = rejectAll; + }, + push: dispatch, + waitFor: (timeoutMs = 10000, settleMs = 400, maxMessages = 20) => new Promise((resolve, reject) => { + const waiter: ToolWaiter = { + resolve, + reject, + messages: [], + settleTimer: null, + timeoutTimer: null, + maxMessages: Math.max(1, maxMessages), + settleMs: Math.max(0, settleMs), + }; + waiter.timeoutTimer = setTimeout(() => { + const index = waiters.indexOf(waiter); + if (index >= 0) waiters.splice(index, 1); + resolve(waiter.messages); + }, Math.max(1, timeoutMs)); + waiters.push(waiter); + }), + }; + return listen; +} diff --git a/src/tool/command_catalog.ts b/src/tool/command_catalog.ts index 1269b62..0ffca6b 100644 --- a/src/tool/command_catalog.ts +++ b/src/tool/command_catalog.ts @@ -56,8 +56,21 @@ export function resolveEntry(entry: string): ResolvedCommand | null { return null; } +/** 将同一白名单元素中的 command/alias1/alias2 展开为可解析的独立条目。 */ +function expandWhitelistEntry(entry: string): string[] { + const text = String(entry || '').trim(); + if (!text) return []; + const index = text.indexOf('|'); + if (index < 0) return [text]; + const extName = text.slice(0, index).trim(); + const commands = text.slice(index + 1).split('/').map(item => item.trim()).filter(Boolean); + return extName && commands.length ? commands.map(command => `${extName}|${command}`) : []; +} + export function whitelistEntries(): string[] { - return Config.tool.CMD_WHITELIST.map(item => String(item || '').trim()).filter(Boolean); + const result: string[] = []; + Config.tool.CMD_WHITELIST.forEach(item => result.push(...expandWhitelistEntry(String(item || '')))); + return result; } export function isAllowedExtension(rc: ResolvedCommand): boolean { diff --git a/src/tool/extension_executor.ts b/src/tool/extension_executor.ts index a390bb7..983816a 100644 --- a/src/tool/extension_executor.ts +++ b/src/tool/extension_executor.ts @@ -19,7 +19,12 @@ function splitKwargs(plainArgs: string[]): { args: string[]; kwargs: seal.Kwarg[ const m = RE_KEYWORD.exec(text); if (m) { const value = m[2] || ''; - kwargs.push({ name: m[1], value, valueExists: m[2] !== undefined, asBool: value !== 'false' }); + kwargs.push({ + name: m[1], + value, + valueExists: m[2] !== undefined, + asBool: value !== '' && value !== '0' && value.toLowerCase() !== 'false', + }); } else { args.push(text); } @@ -118,15 +123,24 @@ export async function executeExtensionLocally( const cmdArgs = buildCmdArgs(ctx, rc.cmd, plainArgs, options.at || [], prefix); const listen = session.tool.listen; + // waitFor 是当前监听器的消息收集入口;resolve 仅作为旧会话对象的防御性兜底。 const responsePromise = listen.waitFor ? listen.waitFor(timeoutMs, settleMs, maxMessages) - : Promise.resolve([] as string[]); + : new Promise(resolve => { + listen.timeoutId = setTimeout(() => resolve([]), timeoutMs); + listen.resolve = content => { + resolve([content]); + listen.cleanup(); + }; + }); let solved = false; try { - const result = ext.cmdMap[rc.cmd].solve(ctx, msg, cmdArgs) as { solved?: boolean } | undefined; + const result = await Promise.resolve(ext.cmdMap[rc.cmd].solve(ctx, msg, cmdArgs)) as { solved?: boolean } | undefined; solved = !!(result && result.solved); } catch (e) { + listen.cleanup(); + await responsePromise.catch(() => []); Logger.warning(`[run_ext_command] 本地执行 ${rc.extName}|${rc.cmd} 抛异常:${e instanceof Error ? e.message : String(e)}`); throw new Error(`指令执行抛异常:${e instanceof Error ? e.message : String(e)}`); } diff --git a/src/tool/mcp.ts b/src/tool/mcp.ts index f977873..0ade0fd 100644 --- a/src/tool/mcp.ts +++ b/src/tool/mcp.ts @@ -256,6 +256,12 @@ async function withSessionRetry(server: MCPServer, fn: (sessionId: string) => } } +/** 供资源工具等内部桥接调用 MCP 工具,不依赖 MCP 工具是否已注册到 AI 工具列表。 */ +export async function callMCPTool(serverName: string, name: string, args: any = {}): Promise { + const server = getMCPServerByName(serverName); + if (!server) throw new Error(`MCP 服务器 ${serverName} 未配置`); + return callTool(server, name, args); +} async function callTool(server: MCPServer, name: string, args: any): Promise { const { value } = await withSessionRetry(server, sessionId => doCallTool(server, sessionId, name, args)); return value; diff --git a/src/tool/mcp/adapters.ts b/src/tool/mcp/adapters.ts index f636f58..9653063 100644 --- a/src/tool/mcp/adapters.ts +++ b/src/tool/mcp/adapters.ts @@ -6,7 +6,7 @@ import Image from "../../resource/image"; import { Session } from "../../session/session"; import { parseSpecialTokens } from "../../utils/string"; import { generateId } from "../../utils/utils"; -import { isAllowedCore, splitEntry } from "../command_catalog"; +import { isAllowedCore, splitEntry, whitelistEntries } from "../command_catalog"; import { MCPCallResult } from "./types"; @@ -252,7 +252,7 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { const { ctx, args = {} } = input; const action = String(args && args.action || ''); if (action === 'list') { - const list = Config.tool.CMD_WHITELIST.map(item => splitEntry(String(item))).filter(item => item && item.extName === 'core').map(item => `core|${item!.cmd}`); + const list = whitelistEntries().map(item => splitEntry(item)).filter(item => item && item.extName === 'core').map(item => `core|${item!.cmd}`); return `可调用核心指令(共 ${list.length} 个):\n${list.length ? list.join('\n') : '(除 .ext 外暂无白名单核心指令)'}\n核心扩展发现:使用 action=call、command=ext 查看全部扩展名称`; } if (action !== 'call') return 'action 仅支持 list 或 call'; diff --git a/src/tool/tool.ts b/src/tool/tool.ts index b466572..983f448 100644 --- a/src/tool/tool.ts +++ b/src/tool/tool.ts @@ -4,14 +4,13 @@ import Logger from "../logger" import { TOOLS_PROMPT_TEMPLATE } from "../prompt/templates" import { Session } from "../session/session"; import { SessionType } from "../session/types"; -import { getSessionId } from "../utils/seal"; import { fixJsonString } from "../utils/string"; import { withTimeout } from "../utils/utils"; import { registerMCPTools } from "./mcp"; import { registerSkills } from "./skills"; import { registerTools } from "./tools/init"; -import { ExtCmdInfo, ToolCall, ToolCallResult, ToolInfo, ToolInfoObject, ToolListen } from "./types"; +import { ToolCall, ToolCallResult, ToolInfo, ToolInfoObject } from "./types"; export const toolMap: { [key: string]: Tool } = {}; @@ -50,7 +49,6 @@ function formatParameterText(parameters: ToolInfoObject): string { export default class Tool { toolInfo: ToolInfo; - ExtCmdInfo: ExtCmdInfo; // 海豹指令信息 sessionType: 'any' | SessionType; // 可使用函数的会话类型 callBack: boolean; // 是否回调智能体 sensitive: boolean; // 敏感工具(发送消息/封禁/改名等),调用会显著记录 @@ -59,11 +57,6 @@ export default class Tool { constructor(info: ToolInfo, sensitive = false) { this.toolInfo = info; this.sensitive = sensitive; - this.ExtCmdInfo = { - extName: '', - cmd: '', - staticArgs: [] - } this.sessionType = "any"; this.callBack = true; this.solve = async (_, __, ___, ____) => "函数未实现"; @@ -71,23 +64,9 @@ export default class Tool { toolMap[info.function.name] = this; } - // 按会话保存最近一次收到的指令 cmdArgs,避免多个会话共用同一可变对象相互污染 - static cmdArgsMap: { [sid: string]: seal.CmdArgs } = {}; - - /** 刷新某会话的 cmdArgs(每次收到指令都会更新) */ - static setCmdArgs(ctx: seal.MsgContext, cmdArgs: seal.CmdArgs) { - Tool.cmdArgsMap[getSessionId(ctx)] = cmdArgs; - } - - /** 获取某会话最近一次指令的 cmdArgs;未收到过指令时返回 undefined */ - static getCmdArgs(ctx: seal.MsgContext): seal.CmdArgs | undefined { - return Tool.cmdArgsMap[getSessionId(ctx)]; - } - /** 清空工具注册表(用于测试/热重载) */ static reset() { for (const key of Object.keys(toolMap)) delete toolMap[key]; - Tool.cmdArgsMap = {}; } static registerTool() { @@ -98,50 +77,6 @@ export default class Tool { - /** - * 利用预存的指令信息和额外输入的参数构建一个cmdArgs并调用solve函数,监听消息并返回结果 - */ - static async extensionSolve(ctx: seal.MsgContext, msg: seal.Message, listen: ToolListen, eci: ExtCmdInfo, args: string[], kwargs: seal.Kwarg[], at: seal.AtInfo[]): Promise<[string, boolean]> { - const cmdArgs = this.getCmdArgs(ctx); - if (!cmdArgs) { - Logger.warning('扩展指令调用失败:尚未收到过指令(cmdArgs 为空)'); - return ['', false]; - } - cmdArgs.command = eci.cmd; - cmdArgs.args = eci.staticArgs.concat(args); - cmdArgs.kwargs = kwargs; - cmdArgs.at = at; - cmdArgs.rawArgs = `${cmdArgs.args.join(' ')} ${kwargs.map(item => `--${item.name}${item.valueExists ? `=${item.value}` : ``}`).join(' ')}`; - cmdArgs.amIBeMentioned = at.findIndex(item => item.userId === ctx.endPoint.userId) !== -1; - cmdArgs.amIBeMentionedFirst = at?.[0]?.userId === ctx.endPoint.userId; - cmdArgs.cleanArgs = cmdArgs.args.join(' '); - cmdArgs.specialExecuteTimes = 0; - cmdArgs.rawText = `${Config.tool.COMMAND_PREFIX}${cmdArgs.command} ${cmdArgs.rawArgs} ${at.map(item => `[CQ:at,qq=${item.userId.replace(/^.+:/, '')}]`).join(' ')}`; - - const ext = seal.ext.find(eci.extName); - if (!ext || !Object.prototype.hasOwnProperty.call(ext.cmdMap, eci.cmd)) { - Logger.warning(`扩展${eci.extName}未找到或缺少指令:${eci.cmd}`); - return ['', false]; - } - - // 旧版本地扩展调用兼容:使用消息收集器而不是单一 resolve,支持多条回复、空闲窗口和并发等待。 - const responsePromise = listen.waitFor - ? listen.waitFor(10000, 400, 20) - : new Promise(resolve => { - listen.timeoutId = setTimeout(() => resolve([]), 10000); - listen.resolve = content => { resolve([content]); listen.cleanup(); }; - }); - try { - ext.cmdMap[eci.cmd].solve(ctx, msg, cmdArgs); - } catch (err) { - listen.cleanup(); - Logger.error(`在extensionSolve中: 调用函数失败:${err instanceof Error ? err.message : String(err)}`); - return ['', false]; - } - const messages = await responsePromise; - return [messages.join('\n'), messages.length > 0]; - } - static async handleToolCall(ctx: seal.MsgContext, msg: seal.Message, session: Session, tool_call: ToolCall): Promise<{ result: ToolCallResult, callBack: boolean }> { const name = tool_call.function.name; if (!Object.prototype.hasOwnProperty.call(toolMap, name)) { @@ -154,11 +89,6 @@ export default class Tool { } const tool = toolMap[name]; - if (tool.ExtCmdInfo.extName !== '' && !this.getCmdArgs(ctx)) { - Logger.warning(`暂时无法调用函数,请先使用 .r 指令`); - return { result: { tool_call_id: tool_call.id, content: `暂时无法调用函数,请先提示用户使用 .r 指令` }, callBack: true }; - } - const msgType = msg.messageType === 'private' ? 'user' : 'group'; if (tool.sessionType !== "any" && tool.sessionType !== msgType) { Logger.warning(`调用函数失败:函数${name}可使用的场景类型为${tool.sessionType},当前场景类型为${msgType}`); diff --git a/src/tool/tools/core/tool_cmd.ts b/src/tool/tools/core/tool_cmd.ts index b4a0ee3..bb6a0ed 100644 --- a/src/tool/tools/core/tool_cmd.ts +++ b/src/tool/tools/core/tool_cmd.ts @@ -31,7 +31,7 @@ export function registerCmdTool(): Tool { type: 'function', function: { name: 'run_ext_command', - description: `在 SealDice 本地直接调用扩展指令的 solve,不依赖核心桥/中间件,无需 MCP 与协议端。扩展分为 builtin(fun/story/coc7/deck/dnd5e/exp/log/reply)与 non_builtin(第三方扩展及本插件),无需配置内置扩展列表。action=list 可按 kind 列出指令;action=call 执行指令,并收集扩展发出的多条消息作为返回。可调用指令仍受「可调用指令白名单」约束,白名单格式为扩展名|指令名。核心指令请使用 run_core_command。`, + description: `在 SealDice 本地直接调用扩展指令的 solve,不依赖核心桥/中间件,无需 MCP 与协议端。扩展分为 builtin(fun/story/coc7/deck/dnd5e/exp/log/reply)与 non_builtin(第三方扩展及本插件),无需配置内置扩展列表。action=list 可按 kind 列出指令;action=call 执行指令,并收集扩展发出的多条消息作为返回。可调用指令仍受「可调用指令白名单」约束,白名单格式为扩展名|指令名/别名1/别名2,同一元素内的别名用 / 分隔。核心指令请使用 run_core_command。`, parameters: { type: 'object', properties: { diff --git a/src/tool/tools/resource/tool_resource.ts b/src/tool/tools/resource/tool_resource.ts index 0732832..bbad413 100644 --- a/src/tool/tools/resource/tool_resource.ts +++ b/src/tool/tools/resource/tool_resource.ts @@ -1,12 +1,14 @@ // 资源工具:本地文件/视频发送(走 ob11 网络连接依赖) import Config from "../../../config/config"; import { logger } from "../../../logger"; -import { netExists, sendGroupMsg, sendPrivateMsg } from "../../../utils/ob11"; -import { resolveLocalPath } from "../../../utils/utils"; +import { netExists, sendGroupFile, sendGroupMsg, sendPrivateFile, sendPrivateMsg } from "../../../utils/ob11"; +import { resolveResourceReference } from "../../../utils/resource"; import Tool from "../../tool"; -/** 按“资源名=路径”映射构建 名称->路径 表,key 为 id 字段名(fileId/videoId) */ -function buildPathMap(items: { [key: string]: string }[], key: string): { [key: string]: string } { +type ResourceEntry = { [key: string]: string | undefined }; + +/** 按“资源名=路径”映射构建 名称->路径 表,key 为 id 字段名(imageId/fileId/videoId) */ +function buildPathMap(items: ResourceEntry[], key: string): { [key: string]: string } { const pathMap: { [key: string]: string } = {}; for (const item of items || []) { const id = item[key]; @@ -32,17 +34,27 @@ function buildResourceList(type: string): string { return sections.map(([label, names]) => `${label}:${names.length > 0 ? names.join('、') : '暂无'}`).join('\n'); } +function getConfiguredResources(key: 'imageId' | 'fileId' | 'videoId'): ResourceEntry[] { + if (key === 'imageId') { + return (Config.resource.LOCAL_IMAGES || []).map(item => ({ [key]: item.imageId, path: item.path })); + } + if (key === 'fileId') { + return (Config.resource.LOCAL_FILES || []).map(item => ({ [key]: item.fileId, path: item.path })); + } + return (Config.resource.LOCAL_VIDEOS || []).map(item => ({ [key]: item.videoId, path: item.path })); +} + function registerResourceTool( name: string, desc: string, - key: 'fileId' | 'videoId', - segmentType: 'file' | 'video' + key: 'imageId' | 'fileId' | 'videoId', + segmentType: 'image' | 'file' | 'video' ) { const tool = new Tool({ type: 'function', function: { name, - description: `${desc}。可传 name(已登记资源,先通过 list_resources 查询可用名称)或 path(本地绝对路径,或相对海豹 data 目录的路径)`, + description: `${desc}。可传 name(已登记资源,先通过 list_resources 查询可用名称)或 path(本地绝对路径、相对 SealDice 目录的路径、file:// URI,或 mcp://服务器名/沙箱路径);也可用 source=mcp、server、path 调用 MCP 文件服务器`, parameters: { type: 'object', properties: { @@ -52,7 +64,16 @@ function registerResourceTool( }, path: { type: 'string', - description: '文件路径:本地绝对路径,或相对海豹 data 目录的路径' + description: '文件路径:本地绝对路径、相对 SealDice 目录的路径、file:// URI,或 mcp://服务器名/沙箱相对路径' + }, + source: { + type: 'string', + enum: ['local', 'mcp'], + description: '资源来源;使用 MCP 文件服务器时填 mcp' + }, + server: { + type: 'string', + description: 'MCP 服务器名称,source=mcp 时使用,默认 mcp-files-exec' } }, required: [] @@ -61,14 +82,14 @@ function registerResourceTool( }); tool.sensitive = true; // 发送文件/视频属敏感操作 tool.solve = async (ctx, _, __, args) => { - const { name, path } = args; + const { name, path, source, server } = args; // 每次调用实时读取配置,保证修改配置后无需重载即可生效 - const items = key === 'fileId' ? (Config.resource.LOCAL_FILES || []) : (Config.resource.LOCAL_VIDEOS || []); + const items = getConfiguredResources(key); const pathMap = buildPathMap(items, key); const nameList = Object.keys(pathMap); - let filePath = ''; + let rawPath = ''; if (name && path) { return `name 与 path 不能同时提供,请二选一:name(已登记资源)或 path(直接传路径)`; } @@ -77,34 +98,48 @@ function registerResourceTool( logger.error(`${desc}${name}不存在`); return `${desc}${name}不存在${nameList.length !== 0 ? `,可发送的资源名有:${nameList.join('、')}` : ''}`; } - filePath = resolveLocalPath(pathMap[name]); + rawPath = pathMap[name]; } else if (path) { - filePath = resolveLocalPath(path); + rawPath = path; } else { return `必须提供 name(已登记资源)或 path(直接传路径)中的一项`; } if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - const epId = ctx.endPoint.userId; - const segment = { type: segmentType, data: { file: filePath } }; - let result = null; - if (ctx.isPrivate) { - result = await sendPrivateMsg(epId, ctx.player!.userId.replace(/^.+:/, ''), [segment]); - } else { - result = await sendGroupMsg(epId, ctx.group!.groupId.replace(/^.+:/, ''), [segment]); - } + try { + const resource = await resolveResourceReference(rawPath, source, server); + const epId = ctx.endPoint.userId; + let result = null; + const peerId = ctx.isPrivate + ? ctx.player!.userId.replace(/^.+:/, '') + : ctx.group!.groupId.replace(/^.+:/, ''); + if (segmentType === 'file') { + result = ctx.isPrivate + ? await sendPrivateFile(epId, peerId, resource.path, resource.name) + : await sendGroupFile(epId, peerId, resource.path, resource.name); + } else { + const segment = { type: segmentType, data: { file: resource.path } }; + result = ctx.isPrivate + ? await sendPrivateMsg(epId, peerId, [segment]) + : await sendGroupMsg(epId, peerId, [segment]); + } - if (result === null || result === undefined) return `${desc}发送失败,请查看ob11网络连接依赖日志`; - return `${desc}发送成功`; - } + if (result === null || result === undefined) return `${desc}发送失败,请查看ob11网络连接依赖日志`; + return `${desc}发送成功`; + } catch (e) { + logger.error(`${desc}失败:${e instanceof Error ? e.message : String(e)}`); + return `${desc}失败:${e instanceof Error ? e.message : String(e)}`; + } + }; } export function registerResourceTools() { + registerResourceTool('send_image', '发送本地图片', 'imageId', 'image'); registerResourceTool('send_file', '发送本地文件', 'fileId', 'file'); registerResourceTool('send_video', '发送本地视频', 'videoId', 'video'); - const listTool = new Tool({ + const tool = new Tool({ type: 'function', function: { name: 'list_resources', @@ -114,19 +149,19 @@ export function registerResourceTools() { properties: { type: { type: 'string', - description: '资源类型', - enum: ['image', 'audio', 'file', 'video', 'all'] + enum: ['all', 'image', 'audio', 'file', 'video'], + description: '资源类型,默认 all' } }, required: [] } } }); - listTool.solve = async (_, __, ___, args) => { + tool.solve = async (_, __, ___, args) => { const type = args && typeof args.type === 'string' ? args.type : 'all'; if (!['image', 'audio', 'file', 'video', 'all'].includes(type)) { return `未知资源类型:${type},可选值为 image/audio/file/video/all`; } return buildResourceList(type); }; -} +} \ No newline at end of file diff --git a/src/tool/tools/resource/tool_voice.ts b/src/tool/tools/resource/tool_voice.ts index 1e746e5..e20868c 100644 --- a/src/tool/tools/resource/tool_voice.ts +++ b/src/tool/tools/resource/tool_voice.ts @@ -2,7 +2,7 @@ import Config from "../../../config/config"; import { logger } from "../../../logger"; import { netExists, sendGroupAISound } from "../../../utils/ob11"; -import { resolveLocalPath } from "../../../utils/utils"; +import { resolveResourceReference } from "../../../utils/resource"; import Tool from "../../tool"; const characterMap: { [key: string]: string } = { @@ -36,22 +36,38 @@ export function registerRecord() { type: "function", function: { name: "record", - description: `发送本地语音,语音名先通过 list_resources(type=audio) 查询`, + description: `发送本地语音。可传 name(先通过 list_resources(type=audio) 查询)或 path(本地绝对路径、相对 SealDice 目录的路径、file:// URI、mcp://服务器名/沙箱相对路径)`, parameters: { type: "object", properties: { name: { type: "string", description: "语音名称" + }, + path: { + type: "string", + description: "本地语音路径、file:// URI,或 mcp://服务器名/沙箱相对路径" + }, + source: { + type: "string", + enum: ["local", "mcp"], + description: "资源来源;使用 MCP 文件服务器时填 mcp" + }, + server: { + type: "string", + description: "MCP 服务器名称,source=mcp 时使用,默认 mcp-files-exec" } }, - required: ["name"] + required: [] } } }); toolRecord.sensitive = true; // 发送语音属敏感操作 toolRecord.solve = async (ctx, msg, _, args) => { - const { name } = args; + const { name, path, source, server } = args; + + if (name && path) return 'name 与 path 不能同时提供,请二选一:name 或 path'; + if (!name && !path) return '必须提供 name(已登记资源)或 path(直接传路径)中的一项'; // 每次调用实时读取配置,保证修改配置后无需重载即可生效 const recordPathMap: { [key: string]: string } = {}; @@ -59,13 +75,24 @@ export function registerRecord() { recordPathMap[audio.audioId] = audio.path; } - if (Object.prototype.hasOwnProperty.call(recordPathMap, name)) { - seal.replyToSender(ctx, msg, `[语音:${resolveLocalPath(recordPathMap[name])}]`); - return '发送成功'; - } else { - const nameList = Object.keys(recordPathMap); - logger.error(`本地语音${name}不存在`); - return `本地语音${name}不存在${nameList.length !== 0 ? `,可发送的语音名有:${nameList.join('、')}` : ''}`; + try { + if (path) { + const resource = await resolveResourceReference(path, source, server); + seal.replyToSender(ctx, msg, `[语音:${resource.path}]`); + return '发送成功'; + } + if (Object.prototype.hasOwnProperty.call(recordPathMap, name)) { + const resource = await resolveResourceReference(recordPathMap[name]); + seal.replyToSender(ctx, msg, `[语音:${resource.path}]`); + return '发送成功'; + } else { + const nameList = Object.keys(recordPathMap); + logger.error(`本地语音${name}不存在`); + return `本地语音${name}不存在${nameList.length !== 0 ? `,可发送的语音名有:${nameList.join('、')}` : ''}`; + } + } catch (e) { + logger.error(`发送语音失败:${e instanceof Error ? e.message : String(e)}`); + return `发送语音失败:${e instanceof Error ? e.message : String(e)}`; } } diff --git a/src/tool/types.ts b/src/tool/types.ts index 290353d..ccfbb57 100644 --- a/src/tool/types.ts +++ b/src/tool/types.ts @@ -97,12 +97,6 @@ export interface ToolCallResult { callBack?: boolean // 是否把结果回调给智能体(写回上下文继续对话),false 时工具静默执行 } -export interface ExtCmdInfo { - extName: string, // 使用的扩展名称 - cmd: string, // 指令名称 - staticArgs: string[] // 参数 -} - export interface ToolListen { timeoutId: number | null, resolve: ((content: string) => void) | null, diff --git a/src/update.ts b/src/update.ts index 2809622..67ed9b8 100644 --- a/src/update.ts +++ b/src/update.ts @@ -17,6 +17,7 @@ export const updateInfo: { [version: string]: string } = { ## 工具与图片 - 增加 MCP 总开关与图生图能力:text_to_image 支持图片 ID、头像、URL、data URL 和 base64,适配 aiplugin4-dependencies 新接口 - 按需工具摘要限制数量并提供参数概览,减少 system prompt 中函数列表占用的 token +- 更新 SealDice 核心与内置扩展命令白名单:默认包含全部命令及别名,并补充对应的 Skills 调用帮助 ## 配置项更改 - 无新增或删除配置项;本版本主要为消息接收、提示词缓存、工具调用和图片处理的实现修复。升级后无需恢复默认配置。`, diff --git a/src/utils/ob11.ts b/src/utils/ob11.ts index 2983b30..7e71e37 100644 --- a/src/utils/ob11.ts +++ b/src/utils/ob11.ts @@ -35,6 +35,39 @@ export async function sendPrivateMsg(epId: string, user_id: string, message: Mes } } +/** + * 统一发送文件。新版 ob11 网络依赖提供 sendFile,可针对 Milky 正确调用 + * upload_group_file/upload_private_file;旧版依赖回退到 OB11 file 消息段。 + */ +export async function sendPrivateFile(epId: string, user_id: string, file: string, name: string): Promise { + const net = getNet(); + if (!net) return null; + try { + if (typeof net.sendFile === 'function') return await net.sendFile(epId, 'private', user_id, file, name); + return await net.callApi(epId, 'send_private_msg', { + user_id, + message: [{ type: 'file', data: { file, ...(name ? { name } : {}) } }] + }); + } catch (_e) { + logger.error(`发送私聊文件失败`); + return null; + } +} + +export async function sendGroupFile(epId: string, group_id: string, file: string, name: string): Promise { + const net = getNet(); + if (!net) return null; + try { + if (typeof net.sendFile === 'function') return await net.sendFile(epId, 'group', group_id, file, name); + return await net.callApi(epId, 'send_group_msg', { + group_id, + message: [{ type: 'file', data: { file, ...(name ? { name } : {}) } }] + }); + } catch (_e) { + logger.error(`发送群文件失败`); + return null; + } +} export async function sendGroupMsg(epId: string, group_id: string, message: MessageSegment[]): Promise { const net = getNet(); if (!net) return null; diff --git a/src/utils/resource.ts b/src/utils/resource.ts new file mode 100644 index 0000000..a0bf3ae --- /dev/null +++ b/src/utils/resource.ts @@ -0,0 +1,86 @@ +// 资源引用解析:支持本地路径、file:// URI,以及 mcp://服务器/沙箱路径。 +import { callMCPTool, getMCPServerByName } from "../tool/mcp"; +import { MCPCallResult } from "../tool/mcp/types"; + +import { resolveLocalPath } from "./utils"; + +export interface ResolvedResource { + path: string; + name: string; +} + +function safeDecode(value: string): string { + try { + return decodeURIComponent(value); + } catch (_e) { + return value; + } +} + +function basename(value: string): string { + const clean = String(value || '').split(/[?#]/, 1)[0].replace(/[\\/]+$/, ''); + const match = /[^\\/]+$/.exec(clean); + return match ? safeDecode(match[0]) : 'resource'; +} + +function parseMCPReference(value: string, source?: string, serverName?: string): { server: string; path: string } | null { + const raw = String(value || '').trim(); + if (/^mcp:\/\//i.test(raw)) { + const match = /^mcp:\/\/([^\/]+)\/(.*)$/i.exec(raw); + if (!match) throw new Error('MCP 文件路径格式应为 mcp://服务器名/沙箱相对路径'); + const server = safeDecode(match[1]).trim(); + const path = safeDecode(match[2]); + if (!server || !path) throw new Error('MCP 文件路径格式应为 mcp://服务器名/沙箱相对路径'); + return { server, path }; + } + if (String(source || '').toLowerCase() === 'mcp') { + const server = String(serverName || 'mcp-files-exec').trim(); + const path = raw.replace(/^[/\\]+/, ''); + if (!server || !path) throw new Error('MCP 文件引用需要 server 和 path'); + return { server, path }; + } + return null; +} + +function makeMCPDownloadUrl(serverName: string, value: string): string { + if (!/^\//.test(value)) return value; + const server = getMCPServerByName(serverName); + const origin = server && /^(https?:\/\/[^/]+)/i.exec(server.url); + return origin ? `${origin[1]}${value}` : value; +} + +function parseMCPResult(result: MCPCallResult): any { + const structured = result && result.structuredContent; + if (structured !== undefined && structured !== null) { + if (typeof structured === 'string') { + try { return JSON.parse(structured); } catch { return structured; } + } + return structured; + } + for (const block of result && result.content || []) { + if (block && typeof block.text === 'string') { + try { return JSON.parse(block.text); } catch { return block.text; } + } + } + return null; +} + +/** 将 MCP export_file 结果统一成可交给消息适配器的 URI/路径。 */ +export async function resolveResourceReference(value: string, source?: string, serverName?: string): Promise { + const mcp = parseMCPReference(value, source, serverName); + if (!mcp) { + const path = resolveLocalPath(value); + return { path, name: basename(path) }; + } + + const result = parseMCPResult(await callMCPTool(mcp.server, 'export_file', { path: mcp.path })); + if (!result || typeof result !== 'object') throw new Error('MCP export_file 未返回文件信息'); + const downloadValue = String(result.downloadUrl || result.url || result.fileUri || result.uri || '').trim(); + const path = makeMCPDownloadUrl(mcp.server, downloadValue); + if (!path) throw new Error('MCP export_file 未返回可发送的 downloadUrl/fileUri'); + const name = String(result.name || basename(mcp.path)).trim(); + return { + path, + name: name || basename(mcp.path) + }; +} diff --git a/src/utils/string.ts b/src/utils/string.ts index 1e3eccf..626211c 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -123,6 +123,37 @@ export interface MessageSegment { }; } +/** + * 文件消息的完整可读表示。不要只显示 name:OB11/不同适配器可能把真实路径、URL、 + * file_id、file_unique 等字段放在同一段里,AI 需要这些字段才能继续调用文件工具。 + */ +export function formatFileSegmentText(rawData: any): string { + const data = rawData && typeof rawData === 'object' ? rawData : {}; + const nestedFile = data.file && typeof data.file === 'object' ? data.file : {}; + const pick = (...values: any[]): string => { + for (const value of values) { + if (value === undefined || value === null) continue; + const text = String(value).replace(/[\r\n]+/g, ' ').trim(); + if (text) return text; + } + return ''; + }; + const name = pick(data.name, data.file_name, data.filename, nestedFile.name, nestedFile.file_name); + const path = pick(data.path, data.local_path, nestedFile.path, nestedFile.local_path); + const file = pick(typeof data.file === 'string' ? data.file : '', nestedFile.file); + const url = pick(data.url, data.file_url, data.download_url, nestedFile.url, nestedFile.file_url); + const fileId = pick(data.file_id, data.fileId, nestedFile.file_id, nestedFile.fileId); + const fileUnique = pick(data.file_unique, data.fileUnique, nestedFile.file_unique, nestedFile.fileUnique); + const size = pick(data.size, data.file_size, nestedFile.size, nestedFile.file_size); + const mime = pick(data.content_type, data.contentType, data.mime, nestedFile.content_type, nestedFile.contentType); + const label = name || path || file || url || fileId || '未知文件'; + const fields = [ + ['name', name], ['path', path], ['file', file], ['url', url], + ['file_id', fileId], ['file_unique', fileUnique], ['size', size], ['mime', mime] + ].filter(([, value]) => value).map(([key, value]) => `${key}=${value}`); + return `【文件】${label}${fields.length ? `\n${fields.join('\n')}` : ''}`; +} + /** 为忽略/触发正则生成兼容 CQ 码的匹配文本,不改变消息段本身。 */ export function formatMessageSegmentsForMatching(messageArray: MessageSegment[], fallback: string = ''): string { const text = messageArray.map(item => { @@ -168,10 +199,7 @@ export function expandMilkySegments(ctx: seal.MsgContext, segments: seal.Message break; } case 2: { // 文件 File - const url = 'url' in seg ? seg.url : ''; - const file = 'file' in seg && typeof seg.file === 'string' ? seg.file : ''; - const fileText = url || file; - result.push({ type: 'text', data: { text: fileText ? `【文件】${fileText}` : '【文件】' } }); + result.push({ type: 'text', data: { text: formatFileSegmentText(seg) } }); break; } case 3: { // 图片 Image diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 95b615a..f644d8c 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -241,14 +241,95 @@ export function getCommonItem(a: string[], b: string[]): string[] { return b.filter(u => aset.has(u)); } +/** 判断字符串是否为 URI(Windows 盘符 C:\\foo 不算 URI)。 */ +function hasUriScheme(value: string): boolean { + return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value) && !/^[a-zA-Z]:[\\/]/.test(value); +} + +/** 将 file:// URI 转回当前进程可使用的本地路径。 */ +function fileUriToLocalPath(value: string): string { + if (!/^file:\/\//i.test(value)) return value; + + const withoutScheme = value.replace(/^file:\/\//i, ''); + // file://server/share/file 是 UNC 路径;file:///C:/file 是 Windows 盘符路径。 + const slashValue = withoutScheme.replace(/\\/g, '/'); + try { + const decoded = decodeURIComponent(slashValue); + if (/^[^/]+\//.test(decoded) && !/^[a-zA-Z]:\//.test(decoded)) { + return `\\\\${decoded.replace(/\//g, '\\')}`; + } + if (/^\/[a-zA-Z]:\//.test(decoded)) return decoded.slice(1).replace(/\//g, '\\'); + if (/^[a-zA-Z]:\//.test(decoded)) return decoded.replace(/\//g, '\\'); + return decoded.startsWith('/') ? decoded : `/${decoded}`; + } catch (_e) { + // URI 编码不合法时保留原字符串,避免发送工具因解析异常直接崩溃。 + return slashValue; + } +} + /** - * 将本地资源路径解析为绝对路径: - * 相对路径(不以 / 或盘符开头)会拼接 SealDice 核心路径 Config.base.SEALDICE_PATH。 + * 规范化本地路径,但不依赖 Node 的 path/fs 模块(插件运行在海豹 JS 环境)。 + * 覆盖盘符、UNC、POSIX 绝对路径和相对 SealDice 路径,避免字符串拼接造成 + * 双分隔符、`.`/`..` 残留以及 UNC 路径被误判为相对路径。 + */ +function normalizeLocalPath(value: string): string { + const original = value; + const slashValue = value.replace(/\\/g, '/'); + const windowsStyle = /^[a-zA-Z]:\//.test(slashValue) || slashValue.startsWith('//'); + let root = ''; + let rest = slashValue; + + const drive = slashValue.match(/^([a-zA-Z]:)\//); + if (drive) { + root = `${drive[1]}/`; + rest = slashValue.slice(root.length); + } else if (slashValue.startsWith('//')) { + const uncParts = slashValue.slice(2).split('/').filter(Boolean); + if (uncParts.length >= 2) { + root = `//${uncParts[0]}/${uncParts[1]}/`; + rest = uncParts.slice(2).join('/'); + } else { + root = '//'; + rest = uncParts.join('/'); + } + } else if (slashValue.startsWith('/')) { + root = '/'; + rest = slashValue.slice(1); + } + + const parts: string[] = []; + for (const part of rest.split('/')) { + if (!part || part === '.') continue; + if (part === '..') { + if (parts.length > 0 && parts[parts.length - 1] !== '..') parts.pop(); + else if (!root) parts.push(part); + continue; + } + parts.push(part); + } + + let normalized = `${root}${parts.join('/')}`; + if (!normalized) normalized = root || (original ? '.' : ''); + if (windowsStyle) normalized = normalized.replace(/\//g, '\\'); + return normalized; +} + +/** + * 将资源配置/工具参数中的路径解析为 SealDice 进程可使用的路径。 + * 相对路径相对 Config.base.SEALDICE_PATH;mcp://、http:// 等远端 URI 不应被 + * 拼接到 SealDice 目录,原样交给上层的资源桥处理。 */ export function resolveLocalPath(p: string): string { - if (!p) return p; - if (/^([a-zA-Z]:[\\/]|\/)/.test(p)) return p; + const value = String(p || '').trim(); + if (!value) return value; + if (/^file:\/\//i.test(value)) return normalizeLocalPath(fileUriToLocalPath(value)); + if (hasUriScheme(value)) return value; + + const normalized = normalizeLocalPath(value); + const isAbsolute = /^([a-zA-Z]:[\\/]|\\\\|\/)/.test(normalized); + if (isAbsolute) return normalized; + const { SEALDICE_PATH } = Config.base; - if (!SEALDICE_PATH) return p; - return `${SEALDICE_PATH}/${p}`; + if (!SEALDICE_PATH) return normalized; + return normalizeLocalPath(`${String(SEALDICE_PATH).replace(/[\\/]$/, '')}/${normalized}`); } diff --git a/test-reports/aiplugin4-e2e-20260818-2308.md b/test-reports/aiplugin4-e2e-20260818-2308.md new file mode 100644 index 0000000..61d936e --- /dev/null +++ b/test-reports/aiplugin4-e2e-20260818-2308.md @@ -0,0 +1,35 @@ +# aiplugin4 E2E 验证报告 — 2026-08-18 + +## 环境 + +- 测试群:`1064487252`(呱呱群) +- 实际响应机器人:`3893625976`(测试姬-测试中) +- 插件面板:远程测试面板(凭据未写入报告) +- 后端:`mcp-files-exec`,远端版本 `1.0.3` + +## 部署与重载 + +- 后端面板更新接口返回成功。 +- 更新后后端状态:`version=1.0.3`、`running=true`、`deps_ready=true`。 +- 通过面板上传当前 `dist/aiplugin4.js` 并重载 JS。 +- 重载日志:`JsReload: 重载完成`,无插件加载错误;MCP 工具注册日志包含 `export_file`。 + +## 真实群测试 + +1. `.ai tool help write_file`:PASS,返回参数说明。 +2. `.ai tool call write_file --path=export-test-20260818.txt --content=codex-export-check-20260818`:PASS,返回写入 27 字符。 +3. `.ai tool call export_file --path=export-test-20260818.txt`:PASS,返回 `downloadUrl`、文件名、大小 27、过期时间和 TTL 300 秒。 +4. 通过 SSH 在后端主机对该下载地址执行 `HEAD`:PASS,HTTP 200、`Content-Length: 27`、附件文件名正确。 +5. 通过 SSH 在后端主机执行 `GET`:PASS,HTTP 200,正文与写入内容一致。 +6. `.ai tool call delete_file --path=export-test-20260818.txt`:PASS,测试文件已清理。 +7. `.ai tool`:PASS,工具列表包含 `export_file[开]`。 +8. `run_ext_command` 回归:PASS,`.ai tool call run_ext_command ... fun|jrrp` 成功;未先发送 `.r`。 + +## 后端审计日志 + +后端日志记录了 `write_file`、`export_file`、`download_export`(HEAD/GET)和 `delete_file`,下载链路实际命中远端服务。 + +## 备注 + +- 下载 token 可重复使用,直到 TTL 到期;这是当前设计,不是单次 token。 +- 后端首次更新仅重启但仍显示 `1.0.2`,原因是 GitHub release 缺少 `mcp-files-exec-1.0.3` 独立包;已补充该 release asset 后再次更新,最终状态确认到 `1.0.3`。 diff --git a/test-reports/aiplugin4-mcp-free-path-20260818.md b/test-reports/aiplugin4-mcp-free-path-20260818.md new file mode 100644 index 0000000..de23016 --- /dev/null +++ b/test-reports/aiplugin4-mcp-free-path-20260818.md @@ -0,0 +1,34 @@ +# aiplugin4 MCP 自由路径模式验证报告 — 2026-08-18 + +## 结论 + +- 后端已切换为自由路径模式:允许直接传入任意绝对路径,不再执行路径沙箱限制。 +- 危险命令拦截已关闭;`run_shell` 接受外部 `cwd`。 +- 下载大小限制为不限制(`MCP_MAX_DOWNLOAD_BYTES=0`),下载超时为 120 秒。 +- 插件已构建、上传并重载;后端工具已在插件日志中注册。 + +## 部署状态 + +- 后端:`mcp-files-exec 1.0.6`,远端面板显示 `running=true`。 +- 后端日志确认:外部路径访问开启、危险命令拦截关闭。 +- 插件重载日志确认注册:`read_file`、`list_dir`、`write_file`、`delete_file`、`export_file`、`download_file`、`run_shell`。 +- 海豹日志确认 OB11 连接完成端点映射:`QQ:3893625976`。 + +## aitool 验证 + +通过远程海豹面板的指令执行接口执行 `.ai tool`(不是直接调用 AI API): + +- PASS:工具列表可返回。 +- PASS:列表包含 `read_file`、`list_dir`、`write_file`、`delete_file`、`export_file`、`download_file`、`run_shell`,均为开启状态。 + +## QQ 测试群复验 + +- 群:`1064487252`。 +- QQ-MCP 控制账号:`3837233349`。 +- 按要求向群发送 `.ai status`、`.ai tool`,并尝试 @ `3893625976`。 +- 本轮未收到 `3893625976` 的新回复;群历史中出现“正确冻结了喵”,且当前未产生插件指令响应。该现象属于测试机器人/消息链路未响应,不能归因于 MCP 权限限制。 +- 面板侧 `.ai tool` 已验证插件命令和工具注册正常。 + +## 备注 + +当前实现刻意不恢复路径沙箱、SSRF 限制、危险命令拦截或外部路径拒绝逻辑,符合自由模式要求。 From d67d09cbdf21c7aa3f393fb2bdb74a2ad0e69ddc Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Wed, 19 Aug 2026 01:44:48 +0800 Subject: [PATCH 15/20] feat: add card recording skill and default forwarding --- ...45\345\205\267\347\263\273\347\273\237.md" | 2 +- ...70\350\247\201\351\227\256\351\242\230.md" | 2 +- ...77\347\224\250\346\214\207\345\215\227.md" | 6 +- .../sealdice_command_defaults.ts | 61 ++++++++----------- src/tool/mcp/adapters.ts | 2 +- src/tool/skills.ts | 11 +++- 6 files changed, 41 insertions(+), 43 deletions(-) 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 3bc8e24..44f441e 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" @@ -54,7 +54,7 @@ 中间件位于 `aiplugin4-backends/backends/ob11-core-bridge`,**仅用于核心指令**(插件无法在 JS 侧直接调用 `.ext`/`.help` 等核心指令)。SealDice 的 OB11 网络依赖作为 WebSocket 客户端主动连接中间件 `/core`(或 `/core/ws`);中间件再出站主动连接协议端(NapCat/LLOneBot 或测试模拟器)的 WS 地址(启动即连、带指数退避重连);插件通过 MCP(`/mcp`)调用 `run_core_command` 注入假消息,不直接连接中转 WS。扩展指令由 `run_ext_command` 在插件内本地直调扩展 `solve`,不经过中间件。 - 中间件原样转发协议端与核心之间的 OB11 JSON;核心的 `send_msg`/`send_group_msg` 等 Action,以及 lane 模式下的 bot 消息事件,都可按当前调用的会话 lane 捕获。 -- 默认 `forward=false`:捕获后的 Action/消息事件不发送到协议端,但向核心返回成功 API 响应,避免核心阻塞;`forward=true` 则捕获并继续转发。`run_core_command` 可通过 `captureMode`、`maxMessages`、`settleMs`、`timeoutMs` 调整监听策略。 +- 默认 `forward=true`:捕获后的 Action/消息事件默认继续发送到协议端;显式 `forward=false` 时拦截,但仍向核心返回成功 API 响应,避免核心阻塞。`run_core_command` 可通过 `captureMode`、`maxMessages`、`settleMs`、`timeoutMs` 调整监听策略。 - 同一群/私聊 lane 串行化,单次调用使用 `settleMs` 空闲窗口收集多条消息;结果含 `ambiguous`、`completedBy`,因为纯 OB11 没有标准 trace_id,无法保证跨同 lane 的绝对关联。 - `run_core_command` 的 `command=ext` 直接调用核心 `.ext`,用于查看所有扩展名称,不需要在配置中维护扩展列表。`run_ext_command` 的 `action=list` 仅能列出白名单 + 内置扩展 + 插件自身指令,完整扩展列表以 `run_core_command` 执行 `ext` 为准。 diff --git "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" index a9cb65e..8399e21 100644 --- "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" +++ "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -25,7 +25,7 @@ - SealDice v1.4.6 分离部署(napcat/llonebot)下图片相关功能存在问题,建议 v1.5.0+。 - `run_core_command` 需要启动 `ob11-core-bridge` 并通过 MCP 接入(SealDice 正向连接 `/core`);`run_ext_command` 在插件内**本地直调扩展 `solve`**,无需中间件/MCP。核心指令白名单使用 `core|指令名`,但 `core|ext` 无需加入白名单即可查看扩展。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时)。 - ob11 相关工具(发消息、取用户/群信息、禁言、打卡、AI 语音等)需要安装 ob11 网络连接依赖或 HTTP 依赖;未安装时相关工具返回"未找到ob11网络连接依赖"。 -- 中间件捕获消息默认不转发到协议端;若希望真实 QQ 也看到核心动作,需在工具参数中设置 `capture.forward=true`。海豹核心连接 `/core`,协议端由中间件出站主动连接(WebUI「⚙ 配置」填地址,重启生效),插件通过 MCP `/mcp` 调用。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 +- 中间件捕获消息默认转发到协议端;若希望 AI 内部执行且不打扰真实 QQ,需在工具参数中设置 `capture.forward=false`。海豹核心连接 `/core`,协议端由中间件出站主动连接(WebUI「⚙ 配置」填地址,重启生效),插件通过 MCP `/mcp` 调用。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 - 所有配置修改后自动生效(缓存最多 1 分钟,无需重载 JS);MCP 服务器增删、技能配置、音乐服务等由缓存 TTL 兜底。 - 嵌入模型输出维度取自嵌入模型配置 `[body] dimensions`(默认 1024,须与后端一致,如 text-embedding-v4 为 1024);未配置维度时记忆检索自动降级为关键词/分数检索、知识库检索自动降级为关键词匹配(嵌入仅作候选重排,加载知识库本身不请求嵌入)。 - 流式输出需要自建或使用公共后端,并在"后端 → 流式输出"配置 URL;`body.stream = true` 的模型才会走流式。 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index 072d46b..aaa3c4b 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -78,7 +78,7 @@ ws://127.0.0.1:46880/core > 配置保存后需**重启该后端**才生效。协议端未配置或不可达时,中间件会按指数退避(1s 起、上限 30s)持续重试,连上后自动恢复转发;期间核心发来的 API 请求立即返回 `status: failed`,不会静默等待超时。 > -> 协议端不是必需的:仅做“注入指令 + 收集响应”时可以不配协议端。默认 `forward=false` 会拦截核心发出的 Action,即使没有协议端也不会失败。 +> 协议端不是必需的:仅做“注入指令 + 收集响应”时请显式传 `forward=false`;默认 `forward=true` 需要协议端可连接,否则核心发出的 Action 无法继续转发。 ## 插件侧接入(aiplugin4) @@ -136,7 +136,7 @@ ws://127.0.0.1:46880/core | 参数 | 适用 | 说明 | | --- | --- | --- | -| `forward` | 仅 `run_core_command` | 是否把捕获到的核心发送消息继续转发给协议端,默认 `false`(拦截) | +| `forward` | 仅 `run_core_command` | 是否把捕获到的核心发送消息继续转发给协议端,默认 `true`(转发) | | `captureMode` | 仅 `run_core_command` | `reply_only` / `lane`;`forward=true` 且要捕获协议端产生的 bot 回复时建议用 `lane` | | `maxMessages` | 两者 | 最多收集消息数(1–50;ext 默认 20,core 默认 50) | | `settleMs` | 两者 | 收到消息后空闲多少毫秒无新消息即结束(0–10000;ext 默认 400,core 默认 500) | @@ -173,7 +173,7 @@ ws://127.0.0.1:46880/core ## 捕获与转发语义 -- `forward=false`(默认):捕获核心发出的 `send_*_msg` Action / 消息事件后**拦截**,不送到协议端;仍向核心返回 Action 成功响应,避免核心重试或阻塞。适合“AI 内部执行指令、不打扰群聊”的场景。 +- `forward=false`:捕获核心发出的 `send_*_msg` Action / 消息事件后**拦截**,不送到协议端;仍向核心返回 Action 成功响应,避免核心重试或阻塞。适合“AI 内部执行指令、不打扰群聊”的场景。 - `forward=true`:捕获后**继续转发**到协议端,并把协议端 API 响应按 `echo` 路由回核心;如需收集协议端产生的 bot 消息,`captureMode` 用 `lane`。 - `reply_only`:只收集带 reply 引用(指向注入消息的虚拟 message_id)的响应,最精准;协议端不生成 reply 引用时可能收集不到,此时标记 `ambiguous=true`。 - `lane`:按 `self_id + 群/私聊 + 对方 id` 捕获该会话内的 bot 消息,适合没有 reply 引用的场景。 diff --git a/src/config/static_config/sealdice_command_defaults.ts b/src/config/static_config/sealdice_command_defaults.ts index bdcdf01..be50791 100644 --- a/src/config/static_config/sealdice_command_defaults.ts +++ b/src/config/static_config/sealdice_command_defaults.ts @@ -6,7 +6,7 @@ export const SEALDICE_COMMAND_WHITELIST = [ 'core|bot', 'core|dismiss', 'core|botlist', 'core|master', 'core|roll/r/rd/rh/rhd/rdh/rx/rxh/rhx', 'core|ext', 'core|nn', 'core|userid', 'core|randalgo', 'core|set', - 'core|角色/ch/char/character/pc', 'core|reply', 'core|team', + 'core|角色/ch/char/character/pc', 'core|st/cst', 'core|reply', 'core|team', // fun 'fun|alias', 'fun|&/a', 'fun|ping', 'fun|send', 'fun|welcome', 'fun|gugu/咕咕', @@ -45,40 +45,28 @@ const COMMAND_CALL_RULES = ` export const SEALDICE_COMMAND_SKILLS = [ `--- -name: 今日人品 -description: 查询指定用户的今日人品值 +name: 录卡 +description: 从角色卡 Excel 表格提取可直接执行的 .st 命令,并通过 SealDice 核心录入角色卡 --- -使用 run_ext_command 工具执行:{"action":"call","extension":"fun","command":"jrrp","args":["用户名或QQ号"]};fun|jrrp 已加入默认白名单。`, - - `--- -name: COC模组抽取 -description: 随机抽取一个 COC 模组 ---- -使用 run_ext_command 工具执行:{"action":"call","extension":"story","command":"modu","args":["roll"]};story|modu 已加入默认白名单。`, - - `--- -name: COC模组搜索 -description: 按关键词搜索 COC 模组 ---- -使用 run_ext_command 工具执行:{"action":"call","extension":"story","command":"modu","args":["search","关键词"]};story|modu 已加入默认白名单。`, - - `--- -name: 属性展示 -description: 展示指定玩家的 COC 全部个人属性 ---- -使用 run_ext_command 工具执行:{"action":"call","extension":"coc7","command":"st","args":["show","玩家名称或QQ号"]};coc7|st 已加入默认白名单。`, - - `--- -name: 属性检定 -description: 对指定玩家进行一次属性或技能检定(ra) ---- -使用 run_ext_command 工具执行:{"action":"call","extension":"coc7","command":"ra","args":["奖励/惩罚骰(可选,如 b、p3)","检定表达式或属性名","检定原因(可选)"]};coc7|ra 已加入默认白名单。args 按 SealDice 原始指令顺序传入;普通属性名若属性值为 0,按原扩展规则补 50。`, - - `--- -name: san检定 -description: 对指定玩家进行 San check(sc) ---- -使用 run_ext_command 工具执行:{"action":"call","extension":"coc7","command":"sc","args":["奖励/惩罚骰(可选,如 b、p2)","成功损失/失败损失表达式(如 0/1d6、0/1)"]};coc7|sc 已加入默认白名单。`, +目标:把用户提供的角色卡表格录入当前群/私聊上下文对应的 SealDice 角色卡。优先使用表格已经生成的完整 .st 导入命令,不要凭空重排或猜测属性。 + +一、读取文件 +1. 先从当前消息的文件字段中取得真实路径或 URL(优先 path/file/url,其次 file_id);只有文件名而没有可访问路径时,明确提示用户重新发送文件或提供路径,不要猜测本地位置。 +2. 对 .xlsx/.xlsm 文件,使用 mcp-files-exec 的 read_file 或 run_shell 配合 Python/openpyxl/LibreOffice 读取;先列出工作表,优先选择名称为“简化卡 骰娘导入”、包含“简化卡”或“骰娘导入”的工作表,其次选择 Sheet2/第二个工作表。D:\COC\空白卡 下的模板仅用于识别布局,不能替代用户实际上传的角色卡。 +3. 读取公式结果时优先取已计算的缓存值;缓存为空时用 LibreOffice headless 重算后再读。不要把公式文本本身当作 .st 参数。 + +二、提取 .st +1. 扫描优先工作表的所有单元格(包括公式计算结果、富文本/换行文本),寻找以 .st 开头的完整命令;允许前后空白和全角空格,但必须保留命令原文中的字段顺序、等号、加减号、括号和分隔符。 +2. 优先选择完整的角色卡导入命令(包含角色名及多个属性),排除“.st show/.st clr/.st rm”等查询、清理或局部修改命令。若表格生成多个 .st 行,按表格顺序执行;执行前先向用户展示将执行的命令摘要并确认角色名,避免覆盖错误角色。 +3. 录入使用核心工具 run_core_command,不要使用 run_ext_command: + {"action":"call","raw_message":".st …","forward":true} + raw_message 与 command/args 不能同时传。forward 未提供时默认也是 true;如用户要求静默再显式传 false。 +4. 命令返回后用 run_core_command 再执行核心 st 查询确认(通常为 {"action":"call","command":"st","args":["show"]}),检查角色名和关键属性是否已写入;失败时不要盲目重复执行,先报告核心返回。 + +三、没有可用 .st 时的回退 +1. 先确认角色名,再调用核心角色管理:{"action":"call","command":"pc","args":["new","角色名"],"forward":true}。若角色已存在,不要覆盖,先用 pc list/st show 查询并请求确认。 +2. 新建成功后,把表格中的可识别属性按核心 st 语法分批设置,仍使用 run_core_command(command="st", args=[…]);未知字段、公式未解析或无法映射的字段列为待人工处理,不要猜值。 +3. 最后再次执行 st show 验证。回复用户时报告:文件、工作表、是否找到直接 .st、角色名、执行结果和未录入字段。`, `--- name: SealDice核心指令调用帮助 @@ -99,7 +87,8 @@ ${COMMAND_CALL_RULES} - userid:查看当前帐号、用户和群组 ID。 - randalgo:查看随机算法,或 args=["get","100"];set 仅限 Master。 - set:设置骰子面数或规则,例如 args=["coc"]、args=["100"]。 -- 角色 / ch / char / character / pc:查看或设置角色卡。 +- 角色 / ch / char / character / pc:查看或设置角色卡;录卡时用 pc new 新建角色。 +- st / cst:核心角色属性导入、查看和修改;录卡时优先使用 run_core_command。 - reply:开启或关闭自定义回复,例如 args=["on"] 或 args=["off"]。 - team:团队管理,例如 args=["list"] 或 args=["团队名","add"];需要群聊环境。 @@ -152,7 +141,7 @@ ${COMMAND_CALL_RULES} - rav / rcv:对抗/竞争检定;args 按原命令帮助传入。 - sc:San 检定;args 按顺序传奖励/惩罚骰、成功/失败损失表达式,例如 ["0/1d6"]。 - coc:生成 COC 角色卡;args 可传数量,例如 ["3"]。 -- st / cst:查看或管理 COC 角色属性;常用 args=["show","玩家名称或QQ号"]。`, +- st / cst:COC 扩展的角色属性查看/修改别名;常用 args=["show","玩家名称或QQ号"]。角色卡导入流程优先使用核心 run_core_command 的 st。`, `--- name: SealDice deck扩展调用帮助 diff --git a/src/tool/mcp/adapters.ts b/src/tool/mcp/adapters.ts index 9653063..0464c8c 100644 --- a/src/tool/mcp/adapters.ts +++ b/src/tool/mcp/adapters.ts @@ -232,7 +232,7 @@ function captureOptions(args: { [key: string]: any }, defaultMaxMessages: number capture: { mode: 'reply_only' | 'lane'; forward: boolean; maxMessages: number; settleMs: number }; timeoutMs?: number; } { - const forward = args && args.forward === true; + const forward = !(args && args.forward === false); const requestedMode = args && (args.captureMode === 'lane' || args.captureMode === 'reply_only') ? args.captureMode : undefined; const maxMessages = Number(args && args.maxMessages); const settleMs = Number(args && args.settleMs); diff --git a/src/tool/skills.ts b/src/tool/skills.ts index 9644ae4..d01d233 100644 --- a/src/tool/skills.ts +++ b/src/tool/skills.ts @@ -1,5 +1,6 @@ // Skills:配置驱动的技能,AI 通过 use_skill 工具按需获取技能内容 import { ext } from "../config/config"; +import { SEALDICE_COMMAND_SKILLS } from "../config/static_config/sealdice_command_defaults"; import Logger from "../logger"; import Tool from "./tool"; @@ -49,11 +50,19 @@ function parseSkillEntry(line: string): { name: string, description: string, con } function getSkills(): Skill[] { - return seal.ext.getTemplateConfig(ext, "技能配置") + const configured = seal.ext.getTemplateConfig(ext, "技能配置") .map(line => (line || '').replace(/\r\n/g, '\n').trim()) .filter(Boolean) .map(parseSkillEntry) .filter(s => s.name); + + // registerTemplateConfig 不会覆盖已有安装的配置。为避免升级后新默认技能(如“录卡”) + // 因旧配置持久化而不可用,补入缺失的默认技能;同名自定义技能优先保留。 + const configuredNames = new Set(configured.map(skill => skill.name)); + const defaults = SEALDICE_COMMAND_SKILLS + .map(line => parseSkillEntry(line.trim())) + .filter(skill => skill.name && !configuredNames.has(skill.name)); + return configured.concat(defaults); } /** 返回已配置的技能名称列表 */ From 62a97ccb46c12d55194d9b655b36191331de0c96 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Wed, 19 Aug 2026 04:46:55 +0800 Subject: [PATCH 16/20] feat: support command trigger and at users --- README.md | 8 ++- ...45\345\205\267\347\263\273\347\273\237.md" | 5 +- ...77\347\224\250\346\214\207\345\215\227.md" | 8 ++- .../aiplugin4-test-suite/references/tool.md | 3 +- src/config/configs/tool.ts | 4 +- .../sealdice_command_defaults.ts | 9 ++-- src/tool/command_target.ts | 45 +++++++++++++++++ src/tool/extension_executor.ts | 14 +++++- src/tool/mcp/adapters.ts | 11 +++-- src/tool/tools/core/tool_cmd.ts | 17 ++++++- src/tool/tools/seal/init.ts | 6 +-- src/tool/tools/seal/tool_deck.ts | 49 ------------------- src/update.ts | 5 +- 13 files changed, 105 insertions(+), 79 deletions(-) create mode 100644 src/tool/command_target.ts delete mode 100644 src/tool/tools/seal/tool_deck.ts diff --git a/README.md b/README.md index 75f5c0e..3520852 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,6 @@ max_tokens = 2048 | 工具响应压缩触发字数 | 工具返回结果超过该字数时压缩后再存入上下文,设为 0 不压缩(默认 5000);web_search 压缩时附带搜索目标 | | 禁止调用的函数 | 每行一个,设置后将不被允许开启 | | 默认关闭的函数 | 每行一个,AI 在新会话中默认无法调用,需 `.ai tool on <函数名>` 开启 | -| 提供给AI的牌堆名称 | 每行一个牌堆名,用于 `draw_deck` 工具;没有的话建议把 `draw_deck` 加入禁止调用 | | 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | | MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。工具名称、描述和参数均从远端 `tools/list` 自动发现,同名冲突自动跳过;`run_ext_command` 仅由插件本地实现,不由 MCP 提供。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | | 可调用指令白名单 | 每行一个 `扩展名|指令名/别名1/别名2`;同一元素内的别名用 `/` 分隔。默认已包含当前 SealDice 核心命令、内置扩展命令及全部别名,核心扩展名统一写 `core`(如 `core|roll/r/rd`) | @@ -361,8 +360,8 @@ max_tokens = 2048 | `.ai tool` | - | 列出所有工具及开关状态 | | `.ai tool help <函数名>` | `.ai tool help set_timer` | 查看指定工具的详细说明和参数需求 | | `.ai tool [on/off]` | - | 开启/关闭全部工具函数 | -| `.ai tool [on/off] <函数名>` | `.ai tool on jrrp` | 开启/关闭指定工具函数 | -| `.ai tool call <函数名> --参数=值` | `.ai tool call jrrp --name=错误` | 试用指定工具函数,输出调用返回信息;参数可尝试 JSON 解析,数字需要引号包裹 | +| `.ai tool [on/off] <函数名>` | `.ai tool on run_ext_command` | 开启/关闭指定工具函数 | +| `.ai tool call <函数名> --参数=值` | `.ai tool call run_ext_command --action=call --extension=fun --command=jrrp` | 试用指定工具函数,输出调用返回信息;参数可尝试 JSON 解析,数字需要引号包裹 | ### 忽略名单相关命令 @@ -415,7 +414,6 @@ max_tokens = 2048 | 工具调度 | `search_tools`(按需搜索工具)、`call_tool`(统一执行任意工具) | | 语音 | `record`、`text_to_sound` | | 网页 | `web_search`、`web_read` | -| TRPG | `draw_deck` | | 属性 | `attr_get`、`attr_set` | | 图片 | `image_to_text`、`text_to_image`、`meme_list`、`get_meme_info`、`meme_generator`、`render_markdown`、`render_html` | | QQ 管理 | `ban`、`whole_ban`、`get_ban_list`、`rename`、`group_sign` | @@ -457,7 +455,7 @@ max_tokens = 2048 - 查看日志中的调用失败原因(未注册/未经许可/参数缺失/类型不符/会话类型不符/超时等); - 用 `.ai tool` 查看开关状态,`.ai tool help <函数名>` 查看参数,`.ai tool call <函数名> --参数=值` 手动试用; - 工具在「禁止调用的函数」列表中时无法开启;新会话中「默认关闭的函数」需要 `.ai tool on <函数名>` 手动开启; -- 扩展指令工具不需要先使用 `.r`;`run_ext_command` 每次调用现场构造 `CmdArgs`。核心指令工具通过 `run_core_command` 走 OB11 核心桥。 +- 扩展指令工具不需要先使用 `.r`;`run_ext_command` 每次调用现场构造 `CmdArgs`。`run_ext_command` / `run_core_command` 均支持 `triggerUserId` 指定触发对象、`atUserId` 指定群聊中的 @ 对象。核心指令工具通过 `run_core_command` 走 OB11 核心桥。 **记忆/知识库检索不到** 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 44f441e..82e525c 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" @@ -15,7 +15,7 @@ | `sensitive` | 敏感工具标记(发消息/禁言/改名等),执行日志显著记录 | | `solve(ctx, msg, session, args)` | 工具实现,返回字符串作为给 AI 的结果 | -`run_ext_command` 在插件内**本地直接调用**扩展的 `solve`(构造独立的 `CmdArgs`,不依赖最近一次真实指令,也无需 MCP/中间件);`run_core_command` 通过 `aiplugin4-backends/ob11-core-bridge`(MCP)注入标准 OB11 假消息执行核心指令。`run_ext_command` 的扩展名分为硬编码的 `builtin`(fun/story/coc7/deck/dnd5e/exp/log/reply)与 `non_builtin`;核心指令统一使用虚拟扩展名 `core`,例如 `core|ext`。指令前缀由「工具 → 指令前缀」配置,默认 `.`。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时结束)。 +`run_ext_command` 在插件内**本地直接调用**扩展的 `solve`(构造独立的 `CmdArgs`,不依赖最近一次真实指令,也无需 MCP/中间件);`run_core_command` 通过 `aiplugin4-backends/ob11-core-bridge`(MCP)注入标准 OB11 假消息执行核心指令。`run_ext_command` 的扩展名分为硬编码的 `builtin`(fun/story/coc7/deck/dnd5e/exp/log/reply)与 `non_builtin`;核心指令统一使用虚拟扩展名 `core`,例如 `core|ext`。指令前缀由「工具 → 指令前缀」配置,默认 `.`。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时结束)。两种工具均支持 `triggerUserId` 指定触发对象、`atUserId` 指定 @ 对象;触发对象会替换本地临时 ctx/msg 或 OB11 假消息的 sender,@ 对象会写入消息段。 ## 调用流程与校验链 @@ -73,7 +73,7 @@ | 触发 | `set_trigger_condition` | | 语音 | `record`、`text_to_sound` | | 网页 | `web_search`、`web_read` | -| TRPG | `roll_check`、`san_check`、`jrrp`、`modu_roll`、`modu_search`、`draw_deck` | +| 指令 | `run_ext_command`、`run_core_command`(调用 SealDice 扩展与核心指令) | | 属性 | `attr_show`、`attr_get`、`attr_set` | | 图片 | `image_to_text`、`text_to_image`、`meme_list`、`get_meme_info`、`meme_generator`、`render_markdown`、`render_html` | | QQ 管理 | `ban`、`whole_ban`、`get_ban_list`、`rename`、`group_sign` | @@ -83,7 +83,6 @@ | 黑名单 | `suggest_block`(AI 建议拉黑,带冷却;默认需骰主用 `.ai block` 确认,可在工具配置关闭)、`unblock_user`、`get_block_list` | | 论坛 | `forum_get_posts`、`forum_get_post_detail`、`forum_search`、`forum_create_post`、`forum_manage_comment`、`forum_get_activity`、`forum_manage_post` | -> 注:`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}` 占位符。 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index aaa3c4b..e8401ba 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -118,6 +118,8 @@ ws://127.0.0.1:46880/core - `action=call`:执行扩展指令 - `extension` + `command`:`扩展名|指令名`;`command` 也支持直接填 `扩展名|指令名` - `args`:指令参数,按顺序 +- `triggerUserId`:可选,指定注入消息的发送者/触发对象用户 ID;不填则使用当前对话用户 +- `atUserId`:可选,指定注入消息中 @ 的用户 ID;仅群聊支持 本地执行:直接调用扩展 `cmdMap[cmd].solve`(构造全新 `CmdArgs`,不要求会话先出现 `.r`),并复用多消息收集器收集扩展发出的多条回复。无需 MCP/中间件;核心内置扩展与第三方扩展均可调用,仍受「可调用指令白名单」约束。 @@ -132,10 +134,14 @@ ws://127.0.0.1:46880/core - 结构化模式:传 `command` 和可选 `args`,插件会按当前「指令前缀」组装核心原始消息 - 原始消息模式:传 `raw_message`,原样注入核心;`raw_message` 不能与 `command` 或 `args` 同时使用 +触发对象与 @ 对象:两种工具都支持 `triggerUserId` / `atUserId`。前者写入 OB11 `user_id` / `sender.user_id`,后者写入消息的 at 段和 `[CQ:at,qq=...]` 原文;省略前者时沿用当前对话用户。私聊传 `atUserId` 会返回提示,不会伪造私聊 @。 + ### 参数 | 参数 | 适用 | 说明 | | --- | --- | --- | +| `triggerUserId` | 两者 | 指定指令消息发送者/触发对象用户 ID;默认当前对话用户 | +| `atUserId` | 两者 | 指定消息中 @ 的用户 ID;仅群聊 | | `forward` | 仅 `run_core_command` | 是否把捕获到的核心发送消息继续转发给协议端,默认 `true`(转发) | | `captureMode` | 仅 `run_core_command` | `reply_only` / `lane`;`forward=true` 且要捕获协议端产生的 bot 回复时建议用 `lane` | | `maxMessages` | 两者 | 最多收集消息数(1–50;ext 默认 20,core 默认 50) | @@ -186,7 +192,7 @@ ws://127.0.0.1:46880/core ## 多核心 -多个海豹核心可同时连接中间件:核心连接后上报的 `self_id` 用于路由,`run_core_command` 的调用与事件按 `self_id` 区分,插件端从当前消息上下文自动取 `selfId` 填入 `target`。`run_ext_command` 在插件内本地直调当前实例的扩展 `solve`,不涉及多核心路由。 +核心桥收到 `triggerUserId` 后会同时写入假消息的 `user_id` 与 `sender.user_id`;`atUserId` 会写入 OB11 at 消息段及 raw_message。多个海豹核心可同时连接中间件:核心连接后上报的 `self_id` 用于路由,`run_core_command` 的调用与事件按 `self_id` 区分,插件端从当前消息上下文自动取 `selfId` 填入 `target`。`run_ext_command` 在插件内本地直调当前实例的扩展 `solve`,不涉及多核心路由。 ## 排障 diff --git a/skills/aiplugin4-test-suite/references/tool.md b/skills/aiplugin4-test-suite/references/tool.md index 26a0027..21fe6b2 100644 --- a/skills/aiplugin4-test-suite/references/tool.md +++ b/skills/aiplugin4-test-suite/references/tool.md @@ -14,9 +14,10 @@ | TOOL-08 | `.ai tool call show_timer_list` | M | `返回内容`、`定时器` | 需骰主;无副作用(无定时器时返回「当前对话没有定时器」) | | TOOL-09 | `.ai tool call 不存在的函数` | M | `调用函数失败:未注册的函数` | 错误路径 | | TOOL-10 | `.ai tool on <禁止调用的函数>` | I | `不被允许开启` | 仅当知道"禁止调用的函数"配置项内容时测,否则 SKIP(env) | +| TOOL-11 | `.ai tool help draw_deck` | U | `没有这个工具函数` | 回归:旧牌堆独立工具已移除,牌堆抽取改用 `run_ext_command` 的 `deck|draw` | 说明: - `tool call` 有调用副作用(如 `ban`/`whole_ban`/`record`/`text_to_sound`),只测无副作用的 `show_timer_list` 与未注册函数错误路径。 -- `run_ext_command` 不依赖会话历史中的 `Tool.cmdArgs`,也不要求先使用 `.r`;每次调用都会按工具参数现场构造 `CmdArgs`。核心指令使用 `run_core_command`,需要 OB11 核心桥。 +- `run_ext_command` 不依赖会话历史中的 `Tool.cmdArgs`,也不要求先使用 `.r`;每次调用都会按工具参数现场构造 `CmdArgs`。核心指令使用 `run_core_command`,需要 OB11 核心桥。牌堆、模组、今日人品等能力不再提供独立工具名,统一通过扩展白名单调用 `deck|draw`、`story|modu`、`fun|jrrp` 等指令。 - TOOL-04~07 结束后按 TOOL-01 快照逐工具还原(`on`/`off <工具名>`)。 diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index ab1733e..d8bd399 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -9,12 +9,11 @@ export default class ToolConfig { seal.ext.registerBoolConfig(ext, "拉黑前需要骰主确认", true, "AI 建议拉黑时需骰主确认后才生效;关闭后 AI 可直接拉黑", "工具"); seal.ext.registerIntConfig(ext, "允许连续调用函数次数", 10, "单次回复流程中允许连续调用工具的次数,防止无限循环;0 为不限制", "工具"); seal.ext.registerIntConfig(ext, "工具响应压缩触发字数", 5000, "工具返回结果超过该字数时压缩后再存入上下文;设为 0 不压缩", "工具"); - seal.ext.registerTemplateConfig(ext, "禁止调用的函数", [''], "每行一个禁止 AI 调用的函数名,示例:draw_deck;修改后自动生效", "工具"); + seal.ext.registerTemplateConfig(ext, "禁止调用的函数", [''], "每行一个禁止 AI 调用的函数名,示例:run_ext_command;扩展指令的细粒度控制请使用「可调用指令白名单」;修改后自动生效", "工具"); seal.ext.registerTemplateConfig(ext, "默认关闭的函数", [''], "每行一个默认关闭的函数名,AI 默认无法调用,示例:get_msg;修改后自动生效", "工具"); seal.ext.registerTemplateConfig(ext, "可调用指令白名单", SEALDICE_COMMAND_WHITELIST, "每行一个 AI 可调用的海豹指令;格式:扩展名|指令名/别名1/别名2,同一元素内的别名用 / 分隔;核心指令的扩展名统一写 core(如 core|roll/r/rd)。默认已包含当前 SealDice 源码中的全部核心命令、内置扩展命令及其别名;修改后自动生效", "工具"); seal.ext.registerBoolConfig(ext, "是否允许调用所有指令", false, "开启后忽略白名单,允许调用所有可解析的扩展指令;核心指令仍通过 run_core_command 调用", "工具"); seal.ext.registerStringConfig(ext, "指令前缀", ".", "注入到 SealDice 核心的指令前缀,通常为 .;如果核心改成其他前缀,请同步修改", "工具"); - seal.ext.registerTemplateConfig(ext, "提供给AI的牌堆名称", [''], "每行一个牌堆名,示例:克苏鲁的呼唤;没有的话建议把 draw_deck 加入不允许调用", "工具"); seal.ext.registerBoolConfig(ext, "是否启用MCP", false, "MCP 功能总开关;默认关闭,避免未安装 MCP 后端时启动或对话报错。开启后才会解析下方「MCP服务器配置」并连接/注册 MCP 工具", "工具"); seal.ext.registerTemplateConfig(ext, "MCP服务器配置", [ `{ @@ -93,7 +92,6 @@ export default class ToolConfig { CMD_WHITELIST: seal.ext.getTemplateConfig(ext, "可调用指令白名单"), ALLOW_ALL_CMDS: seal.ext.getBoolConfig(ext, "是否允许调用所有指令"), COMMAND_PREFIX: seal.ext.getStringConfig(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/config/static_config/sealdice_command_defaults.ts b/src/config/static_config/sealdice_command_defaults.ts index be50791..2ed40fb 100644 --- a/src/config/static_config/sealdice_command_defaults.ts +++ b/src/config/static_config/sealdice_command_defaults.ts @@ -39,6 +39,7 @@ const COMMAND_CALL_RULES = ` - 扩展指令使用 run_ext_command,参数格式:{"action":"call","extension":"扩展名","command":"指令名","args":["参数1","参数2"]};不要在 command 中带前缀点号。 - 核心指令(包括 core|team)使用 run_core_command,参数格式:{"command":"指令名","args":["参数1","参数2"]}。 - args 按 SealDice 原始指令的空格分隔顺序逐项传入;需要 @、表达式或带空格的文本时按工具 schema 传入单独字符串。 +- 如需让指令替其他用户触发,传 triggerUserId;如需模拟消息中的 @,传 atUserId。两者均为用户 ID,不填 triggerUserId 时使用当前对话用户;私聊不支持 atUserId。 - 下方同一行中的名称是同一条指令的别名,均已加入白名单;优先使用第一个主名称。 - 参数不确定时,先调用对应的 help:核心用 run_core_command(command="help", args=["指令名"],扩展可调用扩展的 help 参数或先查看 SealDice 指令帮助。 `; @@ -138,7 +139,7 @@ ${COMMAND_CALL_RULES} - ti:抽取临时性疯狂症状;args=[]。 - li:抽取总结性疯狂症状;args=[]。 - ra / rc / rch / rah / cra / crc / crch / crah:属性或技能检定,同一实现的不同别名;args 按顺序传奖励/惩罚骰、检定表达式、原因。 -- rav / rcv:对抗/竞争检定;args 按原命令帮助传入。 +- rav / rcv:对抗/竞争检定;args 按原命令帮助传入;需要替其他用户触发或 @ 对象时,额外传 triggerUserId / atUserId。 - sc:San 检定;args 按顺序传奖励/惩罚骰、成功/失败损失表达式,例如 ["0/1d6"]。 - coc:生成 COC 角色卡;args 可传数量,例如 ["3"]。 - st / cst:COC 扩展的角色属性查看/修改别名;常用 args=["show","玩家名称或QQ号"]。角色卡导入流程优先使用核心 run_core_command 的 st。`, @@ -165,9 +166,9 @@ ${COMMAND_CALL_RULES} - rc / ra / rah / rch / drc:DND 检定;args 按顺序传检定表达式、难度或原因,别名可按习惯选择。 - buff / dbuff:增益/减益效果;参数按原命令帮助传入。 - spellslots / ss / dss / 法术位:查看或管理法术位。 -- cast / dcast:施法;args 按原命令帮助传入。 +- cast / dcast:施法;args 按原命令帮助传入;需要替其他用户触发或 @ 对象时,额外传 triggerUserId / atUserId。 - 长休 / longrest / dlongrest:执行长休。 -- ds / 死亡豁免:死亡豁免检定;args 按原命令帮助传入。`, +- ds / 死亡豁免:死亡豁免检定;args 按原命令帮助传入;需要替其他用户触发或 @ 对象时,额外传 triggerUserId / atUserId。`, `--- name: SealDice log扩展调用帮助 @@ -182,5 +183,3 @@ ${COMMAND_CALL_RULES} 日志命令可能读取或导出群聊历史,调用前确认范围,避免无必要地暴露隐私。` ]; - - diff --git a/src/tool/command_target.ts b/src/tool/command_target.ts new file mode 100644 index 0000000..af7fb5d --- /dev/null +++ b/src/tool/command_target.ts @@ -0,0 +1,45 @@ +import { createCtx, createMsg } from "../utils/seal"; + +export interface CommandTargetOptions { + triggerUserId?: string; + atUserId?: string; +} + +/** 将 QQ:123 / 123 等用户标识统一为可注入 OB11/海豹消息的纯用户 ID。 */ +export function normalizeCommandUserId(value: any): string | undefined { + const text = String(value == null ? '' : value).trim().replace(/^.+:/, ''); + return text || undefined; +} + +export function qualifyCommandUserId(ctx: seal.MsgContext, userId: string): string { + const platform = String(ctx.endPoint.userId || '').split(':', 1)[0]; + return platform ? `${platform}:${userId}` : userId; +} + +export function resolveCommandTarget(ctx: seal.MsgContext, args: { [key: string]: any } | undefined): CommandTargetOptions & { triggerUserId: string } { + const currentUserId = normalizeCommandUserId(ctx.player && ctx.player.userId) || normalizeCommandUserId(ctx.endPoint.userId) || ''; + const triggerUserId = normalizeCommandUserId(args && args.triggerUserId) || currentUserId; + const atUserId = normalizeCommandUserId(args && args.atUserId); + return { triggerUserId, ...(atUserId ? { atUserId } : {}) }; +} + +export function buildCommandContext( + ctx: seal.MsgContext, + target: CommandTargetOptions & { triggerUserId: string } +): { ctx: seal.MsgContext; msg: seal.Message } { + const groupId = ctx.group && normalizeCommandUserId(ctx.group.groupId) || ''; + const msg = createMsg(ctx.isPrivate ? 'private' : 'group', target.triggerUserId, groupId); + if (target.atUserId) { + const atText = `[CQ:at,qq=${target.atUserId}]`; + msg.message = `${atText} `; + (msg as any).segment = [{ + target: target.atUserId, + isRobot: target.atUserId === normalizeCommandUserId(ctx.endPoint.userId), + type: () => 1 + }]; + } + if (msg.sender) msg.sender.nickname = ctx.player && ctx.player.name || ''; + const targetCtx = createCtx(ctx.endPoint.userId, msg); + if (!targetCtx) throw new Error(`未找到通信端点: ${ctx.endPoint.userId}`); + return { ctx: targetCtx, msg }; +} diff --git a/src/tool/extension_executor.ts b/src/tool/extension_executor.ts index 983816a..74b3844 100644 --- a/src/tool/extension_executor.ts +++ b/src/tool/extension_executor.ts @@ -3,6 +3,7 @@ import Logger from "../logger"; import { ResolvedCommand } from "./command_catalog"; +import { buildCommandContext } from "./command_target"; import { ToolListen } from "./types"; const RE_KEYWORD = /^--([^\s=]+)(?:=(\S+))?$/; @@ -101,6 +102,8 @@ export interface LocalExecutionOptions { settleMs?: number; maxMessages?: number; at?: seal.AtInfo[]; + triggerUserId?: string; + atUserId?: string; } /** 本地执行一条已解析的扩展指令,并用会话监听器收集多条 bot 回复。 */ @@ -120,7 +123,14 @@ export async function executeExtensionLocally( const timeoutMs = options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 10000; const settleMs = options.settleMs != null && options.settleMs >= 0 ? options.settleMs : 400; const maxMessages = options.maxMessages && options.maxMessages > 0 ? options.maxMessages : 20; - const cmdArgs = buildCmdArgs(ctx, rc.cmd, plainArgs, options.at || [], prefix); + const targetContext = options.triggerUserId + ? buildCommandContext(ctx, { triggerUserId: options.triggerUserId, atUserId: options.atUserId }) + : { ctx, msg }; + const executionCtx = targetContext.ctx; + const executionMsg = targetContext.msg; + const atText = (options.at || []).map(item => `[CQ:at,qq=${item.userId.replace(/^.+:/, '')}]`).join(' '); + executionMsg.message = [atText, `${prefix}${rc.cmd}`, plainArgs.join(' ')].filter(Boolean).join(' '); + const cmdArgs = buildCmdArgs(executionCtx, rc.cmd, plainArgs, options.at || [], prefix); const listen = session.tool.listen; // waitFor 是当前监听器的消息收集入口;resolve 仅作为旧会话对象的防御性兜底。 @@ -136,7 +146,7 @@ export async function executeExtensionLocally( let solved = false; try { - const result = await Promise.resolve(ext.cmdMap[rc.cmd].solve(ctx, msg, cmdArgs)) as { solved?: boolean } | undefined; + const result = await Promise.resolve(ext.cmdMap[rc.cmd].solve(executionCtx, executionMsg, cmdArgs)) as { solved?: boolean } | undefined; solved = !!(result && result.solved); } catch (e) { listen.cleanup(); diff --git a/src/tool/mcp/adapters.ts b/src/tool/mcp/adapters.ts index 0464c8c..721d95e 100644 --- a/src/tool/mcp/adapters.ts +++ b/src/tool/mcp/adapters.ts @@ -7,6 +7,7 @@ import { Session } from "../../session/session"; import { parseSpecialTokens } from "../../utils/string"; import { generateId } from "../../utils/utils"; import { isAllowedCore, splitEntry, whitelistEntries } from "../command_catalog"; +import { resolveCommandTarget } from "../command_target"; import { MCPCallResult } from "./types"; @@ -277,6 +278,8 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { if (!isAllowedCore(authorizedCommand)) return `核心指令 core|${authorizedCommand} 不在可调用指令白名单内,无法调用`; const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; + const commandTarget = resolveCommandTarget(ctx, args); + if (commandTarget.atUserId && ctx.isPrivate) return '私聊消息不支持 atUserId'; const options = captureOptions(args, 50, 500); if (authorizedCommand === 'ext' && !(args && args.captureMode)) options.capture.mode = 'lane'; @@ -288,7 +291,7 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { } = { selfId: String(ctx.endPoint.userId || '').replace(/^.+:/, ''), messageType: ctx.isPrivate ? 'private' : 'group', - userId: String(ctx.player && ctx.player.userId || '').replace(/^.+:/, '') + userId: commandTarget.triggerUserId }; if (!ctx.isPrivate) target.groupId = String(ctx.group && ctx.group.groupId || '').replace(/^.+:/, ''); @@ -297,7 +300,7 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { action: 'call', target, actor: { - userId: target.userId || target.selfId, + userId: commandTarget.triggerUserId || target.selfId, nickname: String(ctx.player && ctx.player.name || 'AI'), role: 'member' }, @@ -306,7 +309,9 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { captureMode: options.capture.mode, forward: options.capture.forward, timeoutMs: options.timeoutMs, - __commandPrefix: Config.tool.COMMAND_PREFIX + __commandPrefix: Config.tool.COMMAND_PREFIX, + triggerUserId: commandTarget.triggerUserId, + ...(commandTarget.atUserId ? { atUserId: commandTarget.atUserId } : {}) }; if (hasRawMessage) remoteArgs.raw_message = rawMessage; else { diff --git a/src/tool/tools/core/tool_cmd.ts b/src/tool/tools/core/tool_cmd.ts index bb6a0ed..aba9bfb 100644 --- a/src/tool/tools/core/tool_cmd.ts +++ b/src/tool/tools/core/tool_cmd.ts @@ -2,6 +2,7 @@ import Config from "../../../config/config"; import Logger from "../../../logger"; import { collectCommands, extensionNames, isAllowedExtension, ResolvedCommand, resolveEntry } from "../../command_catalog"; +import { qualifyCommandUserId, resolveCommandTarget } from "../../command_target"; import { executeExtensionLocally } from "../../extension_executor"; import Tool from "../../tool"; @@ -31,7 +32,7 @@ export function registerCmdTool(): Tool { type: 'function', function: { name: 'run_ext_command', - description: `在 SealDice 本地直接调用扩展指令的 solve,不依赖核心桥/中间件,无需 MCP 与协议端。扩展分为 builtin(fun/story/coc7/deck/dnd5e/exp/log/reply)与 non_builtin(第三方扩展及本插件),无需配置内置扩展列表。action=list 可按 kind 列出指令;action=call 执行指令,并收集扩展发出的多条消息作为返回。可调用指令仍受「可调用指令白名单」约束,白名单格式为扩展名|指令名/别名1/别名2,同一元素内的别名用 / 分隔。核心指令请使用 run_core_command。`, + description: `在 SealDice 本地直接调用扩展指令的 solve,不依赖核心桥/中间件,无需 MCP 与协议端;可用 triggerUserId 指定触发对象、atUserId 模拟 @ 对象。扩展分为 builtin(fun/story/coc7/deck/dnd5e/exp/log/reply)与 non_builtin(第三方扩展及本插件),无需配置内置扩展列表。action=list 可按 kind 列出指令;action=call 执行指令,并收集扩展发出的多条消息作为返回。可调用指令仍受「可调用指令白名单」约束,白名单格式为扩展名|指令名/别名1/别名2,同一元素内的别名用 / 分隔。核心指令请使用 run_core_command。`, parameters: { type: 'object', properties: { @@ -40,6 +41,8 @@ export function registerCmdTool(): Tool { extension: { type: 'string', description: '扩展名;call 时可填写,核心扩展名不是这里的 core' }, command: { type: 'string', description: '指令名;也支持直接填写 扩展名|指令名' }, args: { type: 'array', items: { type: 'string' }, description: '指令参数,按顺序填写' }, + triggerUserId: { type: 'string', description: '可选;指定这条指令消息的发送者/触发对象(用户 ID),不填则使用当前对话用户' }, + atUserId: { type: 'string', description: '可选;让注入的指令消息 @ 此用户(用户 ID);群聊可用,私聊不支持' }, maxMessages: { type: 'integer', minimum: 1, maximum: 50, description: '最多收集多少条回复消息' }, settleMs: { type: 'integer', minimum: 0, maximum: 10000, description: '收到消息后等待多久没有新消息才结束' }, timeoutMs: { type: 'integer', minimum: 100, maximum: 120000, description: '最长等待时间,单位毫秒' } @@ -66,12 +69,22 @@ export function registerCmdTool(): Tool { const requested = `${rc.extName}|${rc.cmd}`; if (!isAllowedExtension(rc)) return `扩展指令 ${requested} 不在可调用指令白名单内,无法调用`; const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; + const target = resolveCommandTarget(ctx, args); + if (target.atUserId && ctx.isPrivate) return '私聊消息不支持 atUserId'; + const at = target.atUserId ? [{ + userId: qualifyCommandUserId(ctx, target.atUserId), + isRobot: target.atUserId === String(ctx.endPoint.userId || '').replace(/^.+:/, ''), + name: '' + }] : []; try { const content = await executeExtensionLocally(ctx, msg, session, rc, cmdArgs, { prefix: Config.tool.COMMAND_PREFIX, timeoutMs: optionNumber(args && args.timeoutMs), settleMs: optionNumber(args && args.settleMs), - maxMessages: optionNumber(args && args.maxMessages) + maxMessages: optionNumber(args && args.maxMessages), + at, + triggerUserId: target.triggerUserId, + atUserId: target.atUserId }); return `扩展指令 ${requested} 返回:\n${content}`; } catch (e) { diff --git a/src/tool/tools/seal/init.ts b/src/tool/tools/seal/init.ts index aac1b47..53d808e 100644 --- a/src/tool/tools/seal/init.ts +++ b/src/tool/tools/seal/init.ts @@ -1,9 +1,7 @@ -// seal.ts 子目录工具注册统一入口 +// Seal API 工具注册统一入口 import { registerAttrSeal } from "./tool_attr"; -import { registerDeck } from "./tool_deck"; -/** 注册 seal.ts 下全部海豹牌堆相关工具 */ +/** 注册 Seal API 工具 */ export function registerSealTools() { - registerDeck(); registerAttrSeal(); } diff --git a/src/tool/tools/seal/tool_deck.ts b/src/tool/tools/seal/tool_deck.ts deleted file mode 100644 index 50d5a78..0000000 --- a/src/tool/tools/seal/tool_deck.ts +++ /dev/null @@ -1,49 +0,0 @@ -// 牌堆工具:抽取牌堆 -import Config from "../../../config/config"; -import { logger } from "../../../logger"; -import Tool from "../../tool" - -export function registerDeck() { - const toolDraw = new Tool({ - type: "function", - function: { - name: "draw_deck", - description: `用牌堆名称抽取牌堆,返回抽取结果;可用牌堆列表以调用时的返回信息为准`, - parameters: { - type: "object", - properties: { - name: { - type: 'string', - description: "牌堆名称" - }, - send: { - type: 'boolean', - description: "是否直接将抽牌结果发送给用户:true 时直接发送原始抽牌结果;false 或省略时返回结果由你转述,可结合剧情再加工" - } - }, - required: ["name"] - } - } - }); - toolDraw.solve = async (ctx, msg, _, args) => { - const { name, send = false } = args; - - const dr = seal.deck.draw(ctx, name, true); - if (!dr.exists) { - logger.error(`牌堆${name}不存在:${dr.err}`); - const decks = Config.tool.DECKS || []; - return `牌堆${name}不存在:${dr.err}${decks.length !== 0 ? `,当前可用的牌堆有:${decks.join('、')}` : ''}`; - } - - const result = dr.result; - if (result == null) { - logger.error(`牌堆${name}结果为空:${dr.err}`); - return `牌堆${name}结果为空:${dr.err}`; - } - - if (send) { - seal.replyToSender(ctx, msg, result); - } - return result; - } -} diff --git a/src/update.ts b/src/update.ts index 67ed9b8..93c5f17 100644 --- a/src/update.ts +++ b/src/update.ts @@ -14,13 +14,16 @@ export const updateInfo: { [version: string]: string } = { - 完善 Milky 消息段转换:按 msg.segments 类型展开文本、图片、at、reply、face、file、video、record、music、json、消息节点等内容,并将 reply 序号转换为可引用的消息 ID;异常段只占位,不中断整条消息 - 修复负数消息 ID、未知表情、全体 at、私聊消息和嵌套消息节点进入上下文时的标签与发送者信息 +## 工具与指令 +- run_ext_command 与 run_core_command 支持 triggerUserId 指定触发对象、atUserId 指定群聊 @ 对象;扩展本地临时构造消息上下文,核心桥注入 OB11 的 user_id/sender.user_id 与 at 段。 +- 移除已由 run_ext_command 覆盖的 draw_deck 独立工具入口,牌堆、模组、今日人品等扩展能力统一通过扩展指令调用。 ## 工具与图片 - 增加 MCP 总开关与图生图能力:text_to_image 支持图片 ID、头像、URL、data URL 和 base64,适配 aiplugin4-dependencies 新接口 - 按需工具摘要限制数量并提供参数概览,减少 system prompt 中函数列表占用的 token - 更新 SealDice 核心与内置扩展命令白名单:默认包含全部命令及别名,并补充对应的 Skills 调用帮助 ## 配置项更改 -- 无新增或删除配置项;本版本主要为消息接收、提示词缓存、工具调用和图片处理的实现修复。升级后无需恢复默认配置。`, +- 移除「提供给AI的牌堆名称」配置项;牌堆权限统一由「可调用指令白名单」控制。升级后无需恢复默认配置。`, "4.15.1": `## 修复 - 修复 MCP 服务器配置默认值 mcp-files-exec 的 url 缺少 /mcp 路径的问题:默认值修正为 http://127.0.0.1:3910/mcp,与后端 Streamable HTTP 实际路径一致(旧默认值初始化会 404,面板实测 list_dir / read_file 调用正常) - 自动改名(昵称/群名片)增加 5 秒超时保护:ob11 网络连接依赖请求异常或未响应时不再让整条消息处理链路挂起,超时记 warning 后继续处理 From 3a045db644b9fb404f2d12d36797df09508983f5 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Wed, 19 Aug 2026 05:47:20 +0800 Subject: [PATCH 17/20] feat: complete command targeting and backend tool refactor --- README.md | 26 +- ...41\345\235\227\350\257\246\350\247\243.md" | 2 +- ...45\345\205\267\347\263\273\347\273\237.md" | 20 +- ...44\344\270\216\351\205\215\347\275\256.md" | 6 +- ...77\347\224\250\346\214\207\345\215\227.md" | 12 +- package.json | 1 + scripts/ob11-runtime-test-entry.ts | 5 + scripts/test-ob11-runtime.cjs | 155 +++++++ .../aiplugin4-test-suite/references/tool.md | 2 +- src/cmd/sub_cmd/block.ts | 43 +- src/cmd/sub_cmd/ignore.ts | 35 +- src/cmd/sub_cmd/memory.ts | 28 +- src/config/configs/tool.ts | 9 +- src/config/static_config/ob11_api_skills.ts | 110 +++++ .../sealdice_command_defaults.ts | 10 +- src/context/context.ts | 126 +---- src/memory/memory.ts | 10 +- src/memory/session_memory.ts | 78 ++-- src/pipeline.ts | 8 +- src/prompt/templates.ts | 40 +- src/tool/command_target.ts | 54 ++- src/tool/extension_executor.ts | 36 +- src/tool/local_command_capture.ts | 34 ++ src/tool/mcp/adapters.ts | 45 +- src/tool/skills.ts | 5 +- src/tool/tool.ts | 1 + src/tool/tools/context/tool_context.ts | 16 +- src/tool/tools/core/tool_cmd.ts | 19 +- src/tool/tools/core/tool_trigger.ts | 12 +- src/tool/tools/image/tool_image.ts | 4 +- src/tool/tools/image/tool_meme.ts | 29 +- src/tool/tools/init.ts | 6 +- src/tool/tools/manage/tool_block.ts | 40 +- src/tool/tools/memory/tool_memory.ts | 125 +++-- src/tool/tools/message/init.ts | 7 - src/tool/tools/message/tool_message.ts | 423 ----------------- src/tool/tools/ob11/init.ts | 17 +- src/tool/tools/ob11/tool_ban.ts | 117 ----- src/tool/tools/ob11/tool_call_api.ts | 51 ++ src/tool/tools/ob11/tool_essence_msg.ts | 145 ------ src/tool/tools/ob11/tool_group_sign.ts | 33 -- src/tool/tools/ob11/tool_person_info.ts | 60 --- src/tool/tools/ob11/tool_qq_list.ts | 230 --------- src/tool/tools/ob11/tool_rename.ts | 60 --- src/tool/tools/resource/init.ts | 11 +- src/tool/tools/resource/tool_music.ts | 132 ++---- src/tool/tools/resource/tool_resource.ts | 159 +------ src/tool/tools/resource/tool_voice.ts | 162 ++----- src/tool/tools/seal/tool_attr.ts | 24 +- src/transport/ob11/capability_catalog.ts | 100 ++++ src/transport/ob11/dispatcher.ts | 73 +++ src/transport/ob11/message_segments.ts | 90 ++++ src/transport/ob11/ob11_net_backend.ts | 66 +++ src/transport/ob11/result.ts | 39 ++ src/transport/ob11/seal_native_backend.ts | 123 +++++ src/transport/ob11/types.ts | 45 ++ src/update.ts | 4 +- src/utils/ob11.ts | 437 ++---------------- src/utils/string.ts | 88 ++-- src/utils/target_id.ts | 43 ++ src/utils/utils.ts | 12 +- 61 files changed, 1517 insertions(+), 2386 deletions(-) create mode 100644 scripts/ob11-runtime-test-entry.ts create mode 100644 scripts/test-ob11-runtime.cjs create mode 100644 src/config/static_config/ob11_api_skills.ts create mode 100644 src/tool/local_command_capture.ts delete mode 100644 src/tool/tools/message/init.ts delete mode 100644 src/tool/tools/message/tool_message.ts delete mode 100644 src/tool/tools/ob11/tool_ban.ts create mode 100644 src/tool/tools/ob11/tool_call_api.ts delete mode 100644 src/tool/tools/ob11/tool_essence_msg.ts delete mode 100644 src/tool/tools/ob11/tool_group_sign.ts delete mode 100644 src/tool/tools/ob11/tool_person_info.ts delete mode 100644 src/tool/tools/ob11/tool_qq_list.ts delete mode 100644 src/tool/tools/ob11/tool_rename.ts create mode 100644 src/transport/ob11/capability_catalog.ts create mode 100644 src/transport/ob11/dispatcher.ts create mode 100644 src/transport/ob11/message_segments.ts create mode 100644 src/transport/ob11/ob11_net_backend.ts create mode 100644 src/transport/ob11/result.ts create mode 100644 src/transport/ob11/seal_native_backend.ts create mode 100644 src/transport/ob11/types.ts create mode 100644 src/utils/target_id.ts diff --git a/README.md b/README.md index 3520852..bee30ec 100644 --- a/README.md +++ b/README.md @@ -312,8 +312,8 @@ max_tokens = 2048 | `.ai priv show <指令>` | - | 检查指定指令的权限限制 | | `.ai priv reset` | - | 重置所有指令权限为默认 | | `.ai prompt` | - | 查看当前 system prompt(骰主) | -| `.ai block add <统一ID/@xxx> <原因>` | `.ai block add QQ:1234567890 乱发广告` | 拉黑用户/群(骰主),被拉黑对象无法触发 AI 对话 | -| `.ai block rm <统一ID/@xxx>` | - | 移除黑名单 | +| `.ai block add <用户ID/群ID> <原因>` | `.ai block add QQ:1234567890 乱发广告` | 拉黑用户/群(骰主),被拉黑对象无法触发 AI 对话 | +| `.ai block rm <用户ID/群ID>` | - | 移除黑名单 | | `.ai block list` | - | 查看黑名单列表 | > 权限数值:-30 黑名单 / 0 普通用户 / 40 邀请者 / 50 群管理员 / 60 群主 / 70 白名单 / 100 骰主。 @@ -340,7 +340,7 @@ max_tokens = 2048 | 命令 | 使用示例 | 说明 | |:---:|:---:|:---| -| `.ai memo status (@xxx)` | - | 查看当前(或 @ 指定用户的)长期/短期记忆状态 | +| `.ai memo status [用户ID]` | - | 查看当前(或指定用户ID的)长期/短期记忆状态 | | `.ai memo [p/g] st <内容>` | `.ai memo p st 西瓜` | 设置个人/群聊设定(个人≤20字,群聊≤30字) | | `.ai memo [p/g] st clr` | - | 清除设定 | | `.ai memo [p/g] del --关键词` | - | 按 ID 删除记忆,可附带关键词 | @@ -367,8 +367,8 @@ max_tokens = 2048 | 命令 | 使用示例 | 说明 | |:---:|:---:|:---| -| `.ai ign add @xxx` | - | 添加名单(仅群聊)。名单内的用户能正常对话,但无法被选中作为作用目标 | -| `.ai ign rm @xxx` | - | 删除名单 | +| `.ai ign add <用户ID>` | - | 添加名单(仅群聊)。名单内的用户能正常对话,但不会被选为目标用户 | +| `.ai ign rm <用户ID>` | - | 删除名单 | | `.ai ign lst` | - | 查看名单 | ### token 计数命令 @@ -407,26 +407,26 @@ max_tokens = 2048 |:---:|:---| | 记忆 | `add_memory`、`del_memory`、`search_memory`、`clear_memory` | | 知识库 | `kb_search`、`kb_read`、`kb_list`(只读检索,内容由配置维护) | -| 消息 | `send_msg`、`get_msg`、`delete_msg`、`send_forward_msg`、`get_context` | +| OB11 API | `call_ob11_api`(通过 action 调用消息、查询、管理、文件和合并转发 API) | | 定时 | `set_timer`、`show_timer_list`、`cancel_timer` | | 触发 | `set_trigger_condition` | | 指令 | `run_ext_command`(本地执行扩展指令)、`run_core_command`(经 MCP 调用核心指令) | | 工具调度 | `search_tools`(按需搜索工具)、`call_tool`(统一执行任意工具) | -| 语音 | `record`、`text_to_sound` | +| 音频资源 | `generate_audio`(生成 record 消息段,不直接发送) | | 网页 | `web_search`、`web_read` | | 属性 | `attr_get`、`attr_set` | | 图片 | `image_to_text`、`text_to_image`、`meme_list`、`get_meme_info`、`meme_generator`、`render_markdown`、`render_html` | -| QQ 管理 | `ban`、`whole_ban`、`get_ban_list`、`rename`、`group_sign` | -| 群资料 | `get_list`、`get_group_member_list`、`search_chat`、`search_common_group`、`get_person_info` | -| 精华消息 | `set_essence_msg`、`get_essence_msg_list`、`delete_essence_msg` | -| 音乐 | `music_play` | +| OB11 管理/查询 | 统一通过 `call_ob11_api` 传入 `set_group_ban`、`set_group_name`、`get_group_list` 等 action | +| 资源/群资料 | `list_resources`;群资料统一通过 `call_ob11_api` 传入对应 action | +| 精华消息 | 统一通过 `call_ob11_api` 传入 `set_essence_msg`、`get_essence_msg_list`、`delete_essence_msg` | +| 音乐资源 | `search_music`(返回 music 消息段,不直接发送) | | 黑名单 | `suggest_block`(AI 建议拉黑,带冷却;默认需骰主确认)、`unblock_user`、`get_block_list` | | 论坛 | `forum_get_posts`、`forum_get_post_detail`、`forum_search`、`forum_create_post`、`forum_manage_comment`、`forum_get_activity`、`forum_manage_post` | | MCP / 技能 | 远端工具名(MCP 工具,同名冲突时跳过)、`use_skill`(技能) | > 指令类技能(今日人品、COC 模组抽取/搜索、属性展示、属性检定、san 检定等)通过 `use_skill` 按需获取内容,内部统一使用 `run_ext_command` / `run_core_command` 调用海豹指令,对应指令需加入「可调用指令白名单」。 -> 依赖说明:ob11 相关工具需要安装 [ob11 网络连接依赖](https://raw.githubusercontent.com/error2913/sealdice-plugin-ob11-net-connection/refs/heads/main/dist/ob11%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%E4%BE%9D%E8%B5%96.js) 或 [http 依赖插件](https://github.com/error2913/sealdice-js/blob/main/HTTP%E4%BE%9D%E8%B5%96.js);`text_to_sound` 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖与 ffmpeg;`text_to_image` 需要生成图片依赖;`music_play` 需要协议端配置音卡签名;`render_markdown` / `render_html` 需要配置 md 和 html 图片渲染后端。 +> OB11 说明:AI 只调用 `call_ob11_api`。安装 ob11 网络连接依赖时,action 原样交给 `net.callApi`;未安装时仍由 SealDice 原生后端完成当前上下文可完成的发送和查询,远端 action 返回 `OB11_DEPENDENCY_REQUIRED`,不会假装成功。图片、语音、视频、文件、JSON、Markdown、音乐和合并转发均通过 message segment 保留格式;`generate_audio`、`search_music` 只生成可发送的 segment。 > 扩展/核心指令工具不再要求会话先出现 `.r`:`run_ext_command` 在插件内本地直调扩展 `solve`,无需中间件;`run_core_command` 通过 OB11 核心桥注入假消息,需启动 `ob11-core-bridge`(SealDice 的 OB11 网络依赖连接中间件 `/core`,并在「工具 → MCP服务器配置」中启用 `ob11-core-bridge`,`http://127.0.0.1:46880/mcp`)。 @@ -455,7 +455,7 @@ max_tokens = 2048 - 查看日志中的调用失败原因(未注册/未经许可/参数缺失/类型不符/会话类型不符/超时等); - 用 `.ai tool` 查看开关状态,`.ai tool help <函数名>` 查看参数,`.ai tool call <函数名> --参数=值` 手动试用; - 工具在「禁止调用的函数」列表中时无法开启;新会话中「默认关闭的函数」需要 `.ai tool on <函数名>` 手动开启; -- 扩展指令工具不需要先使用 `.r`;`run_ext_command` 每次调用现场构造 `CmdArgs`。`run_ext_command` / `run_core_command` 均支持 `triggerUserId` 指定触发对象、`atUserId` 指定群聊中的 @ 对象。核心指令工具通过 `run_core_command` 走 OB11 核心桥。 +- 扩展指令工具不需要先使用 `.r`;`run_ext_command` 每次调用现场构造 `CmdArgs`。`run_ext_command` / `run_core_command` 均支持 `trigger` 指定触发对象、`at` 指定群聊中的 @ 对象列表。核心指令工具通过 `run_core_command` 走 OB11 核心桥。 **记忆/知识库检索不到** 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 651cecf..45487de 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" @@ -104,7 +104,7 @@ - `context.ts`:`Context` 类: - 添加用户消息(自动改名、超长消息交给 `compress_agent` 压缩)、助手消息(触发记忆访问与总结检查)、系统用户消息、工具调用/回调消息。 - `limitMessages()` 按 `MAX_ROUNDS`(出现一次 user 视作一轮)裁剪;`handleMessages` 另有 token 预算裁剪。 - - `findUserId` / `findGroupId`:按 ID、名字(编辑距离容错)、上下文/记忆、群成员/好友列表解析用户与群。 + - `getUserById` / `getGroupById`:仅按用户ID或群ID获取目标,不执行名称搜索。 - `findImage`:支持 `user_avatar`、`group_avatar`、图片 ID、本地图片路径、记忆内图片。 - `setName`/`updateName`:按 `autoNameMod`(0/1/2)自动把名字改为昵称或群名片。 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 82e525c..17983f1 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" @@ -15,7 +15,7 @@ | `sensitive` | 敏感工具标记(发消息/禁言/改名等),执行日志显著记录 | | `solve(ctx, msg, session, args)` | 工具实现,返回字符串作为给 AI 的结果 | -`run_ext_command` 在插件内**本地直接调用**扩展的 `solve`(构造独立的 `CmdArgs`,不依赖最近一次真实指令,也无需 MCP/中间件);`run_core_command` 通过 `aiplugin4-backends/ob11-core-bridge`(MCP)注入标准 OB11 假消息执行核心指令。`run_ext_command` 的扩展名分为硬编码的 `builtin`(fun/story/coc7/deck/dnd5e/exp/log/reply)与 `non_builtin`;核心指令统一使用虚拟扩展名 `core`,例如 `core|ext`。指令前缀由「工具 → 指令前缀」配置,默认 `.`。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时结束)。两种工具均支持 `triggerUserId` 指定触发对象、`atUserId` 指定 @ 对象;触发对象会替换本地临时 ctx/msg 或 OB11 假消息的 sender,@ 对象会写入消息段。 +`run_ext_command` 在插件内**本地直接调用**扩展的 `solve`(构造独立的 `CmdArgs`,不依赖最近一次真实指令,也无需 MCP/中间件);`run_core_command` 通过 `aiplugin4-backends/ob11-core-bridge`(MCP)注入标准 OB11 假消息执行核心指令。`run_ext_command` 的扩展名分为硬编码的 `builtin`(fun/story/coc7/deck/dnd5e/exp/log/reply)与 `non_builtin`;核心指令统一使用虚拟扩展名 `core`,例如 `core|ext`。指令前缀由「工具 → 指令前缀」配置,默认 `.`。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时结束)。两种工具均支持 `trigger` 指定触发对象、`at` 指定 @ 对象;触发对象会替换本地临时 ctx/msg 或 OB11 假消息的 sender,@ 对象会写入消息段。 ## 调用流程与校验链 @@ -53,7 +53,7 @@ 中间件位于 `aiplugin4-backends/backends/ob11-core-bridge`,**仅用于核心指令**(插件无法在 JS 侧直接调用 `.ext`/`.help` 等核心指令)。SealDice 的 OB11 网络依赖作为 WebSocket 客户端主动连接中间件 `/core`(或 `/core/ws`);中间件再出站主动连接协议端(NapCat/LLOneBot 或测试模拟器)的 WS 地址(启动即连、带指数退避重连);插件通过 MCP(`/mcp`)调用 `run_core_command` 注入假消息,不直接连接中转 WS。扩展指令由 `run_ext_command` 在插件内本地直调扩展 `solve`,不经过中间件。 -- 中间件原样转发协议端与核心之间的 OB11 JSON;核心的 `send_msg`/`send_group_msg` 等 Action,以及 lane 模式下的 bot 消息事件,都可按当前调用的会话 lane 捕获。 +- 中间件原样转发协议端与核心之间的 OB11 JSON;核心的 `send_private_msg`/`send_group_msg` 等 action(由 `call_ob11_api` 统一调用),以及 lane 模式下的 bot 消息事件,都可按当前调用的会话 lane 捕获。 - 默认 `forward=true`:捕获后的 Action/消息事件默认继续发送到协议端;显式 `forward=false` 时拦截,但仍向核心返回成功 API 响应,避免核心阻塞。`run_core_command` 可通过 `captureMode`、`maxMessages`、`settleMs`、`timeoutMs` 调整监听策略。 - 同一群/私聊 lane 串行化,单次调用使用 `settleMs` 空闲窗口收集多条消息;结果含 `ambiguous`、`completedBy`,因为纯 OB11 没有标准 trace_id,无法保证跨同 lane 的绝对关联。 - `run_core_command` 的 `command=ext` 直接调用核心 `.ext`,用于查看所有扩展名称,不需要在配置中维护扩展列表。`run_ext_command` 的 `action=list` 仅能列出白名单 + 内置扩展 + 插件自身指令,完整扩展列表以 `run_core_command` 执行 `ext` 为准。 @@ -68,25 +68,25 @@ | --- | --- | | 记忆 | `add_memory`、`del_memory`、`search_memory`、`clear_memory` | | 知识库 | `kb_search`、`kb_read`、`kb_list`(只读检索,内容由「知识库」配置维护) | -| 消息 | `send_msg`、`get_msg`、`delete_msg`、`send_forward_msg`、`get_context` | +| OB11 API | `call_ob11_api`(通过 action 调用消息、查询、管理、文件和合并转发 API) | | 定时 | `set_timer`、`show_timer_list`、`cancel_timer` | | 触发 | `set_trigger_condition` | -| 语音 | `record`、`text_to_sound` | +| 音频资源 | `generate_audio`(生成 record 消息段,不直接发送) | | 网页 | `web_search`、`web_read` | | 指令 | `run_ext_command`、`run_core_command`(调用 SealDice 扩展与核心指令) | | 属性 | `attr_show`、`attr_get`、`attr_set` | | 图片 | `image_to_text`、`text_to_image`、`meme_list`、`get_meme_info`、`meme_generator`、`render_markdown`、`render_html` | -| QQ 管理 | `ban`、`whole_ban`、`get_ban_list`、`rename`、`group_sign` | -| 群资料 | `get_list`、`get_group_member_list`、`search_chat`、`search_common_group`、`get_person_info` | -| 精华消息 | `set_essence_msg`、`get_essence_msg_list`、`delete_essence_msg` | -| 音乐 | `music_play` | +| OB11 管理/查询 | 统一通过 `call_ob11_api` 传入对应 action | +| 资源/群资料 | `list_resources`;群资料统一通过 `call_ob11_api` 传入对应 action | +| 精华消息 | 统一通过 `call_ob11_api` 传入对应 action | +| 音乐资源 | `search_music`(返回 music 消息段,不直接发送) | | 黑名单 | `suggest_block`(AI 建议拉黑,带冷却;默认需骰主用 `.ai block` 确认,可在工具配置关闭)、`unblock_user`、`get_block_list` | | 论坛 | `forum_get_posts`、`forum_get_post_detail`、`forum_search`、`forum_create_post`、`forum_manage_comment`、`forum_get_activity`、`forum_manage_post` | > 注:`text_to_image` 支持可选 `image` 参数进行以图生图。参数可传图片 ID、`user_avatar:...`、`group_avatar:...`、图片 URL、data URL 或 base64;tti 模型配置需要在请求体中使用 `{image}`、`{image_url}` 或 `{image_base64}` 占位符。 -> 注:资源发送工具支持直接传 `path`。`send_image`、`send_file`、`send_video`、`record` 可接收本地绝对路径、`file://` URI,以及相对 SealDice 目录的路径;Windows 盘符路径(如 `C:\data\a.pdf`)和 UNC 路径会自动规范化。`send_file` 在 Milky 端点使用 `upload_group_file`/`upload_private_file`,OB11 端点继续使用 `file` 消息段。 +> 注:资源发送统一通过 `call_ob11_api` 的 image/record/video/file 消息段;文件区上传使用 `upload_group_file`/`upload_private_file`。本地绝对路径、`file://` URI、MCP `mcp://` 引用会在运行时解析,资源 ID 使用 `resource:资源名`。 > 注:MCP 文件服务器 `mcp-files-exec` 支持把沙箱文件先导出为短时下载地址再发送。可使用 `mcp://mcp-files-exec/output/example.pdf`,或显式传 `source=mcp`、`server=mcp-files-exec`、`path=output/example.pdf`;相对 MCP 路径相对第一个沙箱根目录。 外部注册: @@ -94,7 +94,7 @@ - MCP 工具:默认直接按远端 `tools/list` 返回的工具名注册;与本地/已有工具同名时自动跳过。 - 技能工具:`use_skill`(见技能节)。 -依赖说明:ob11 相关工具(收发消息、用户/群信息、禁言、打卡等)需要安装 ob11 网络连接依赖或 HTTP 依赖;`text_to_sound` 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)与 ffmpeg;`music_play` 需要协议端配置音卡签名;`text_to_image` 需要生成图片依赖(tti)。 +依赖说明:AI 只调用 `call_ob11_api`。有 ob11 网络连接依赖时 action 原样透传;没有依赖时原生可完成的发送和上下文查询继续可用,其余 action 返回 `OB11_DEPENDENCY_REQUIRED`。 ## 工具权限(按会话开关) diff --git "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" index 47a3dc2..277de1f 100644 --- "a/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" +++ "b/docs/05-\345\221\275\344\273\244\344\270\216\351\205\215\347\275\256.md" @@ -24,8 +24,8 @@ | `memory` | 记忆管理 | 见下文 | | `kb` | 知识库查询(只读) | `.ai kb list`;`.ai kb search <关键词>`;`.ai kb read ` | | `tool` | 工具管理 | 见下文 | -| `ignore` | 群内忽略名单 | `.ai ign add @xxx`;`.ai ign rm @xxx`;`.ai ign lst`(仅群聊) | -| `block` | 黑名单管理(骰主) | `.ai block add <统一ID/@xxx> <原因>`;`.ai block rm <统一ID/@xxx>`;`.ai block list`;被拉黑对象无法触发 AI 对话,指令不受影响;AI 建议拉黑默认需骰主确认,可在工具配置关闭 | +| `ignore` | 群内忽略名单 | `.ai ign add <用户ID>`;`.ai ign rm <用户ID>`;`.ai ign lst`(仅群聊) | +| `block` | 黑名单管理(骰主) | `.ai block add <用户ID/群ID> <原因>`;`.ai block rm <用户ID/群ID>`;`.ai block list`;被拉黑对象无法触发 AI 对话,指令不受影响;AI 建议拉黑默认需骰主确认,可在工具配置关闭 | | `token` | token 用量统计 | 见下文 | | `shut` | 打断当前流式对话 | `.ai shut` | | `model` | 查看/设置会话模型 | `.ai model`;`.ai model <模型名>`;`.ai model clr` | @@ -34,7 +34,7 @@ ### memory 子命令 ```text -.ai memo status (@xxx) # 记忆状态,@ 查看个人记忆状态 +.ai memo status [用户ID] # 记忆状态,传用户ID查看对应个人记忆状态 .ai memo [p/g] st <内容> # 设置个人/群聊设定(个人≤20字,群聊≤30字) .ai memo [p/g] st clr # 清除设定 .ai memo [p/g] del --关键词1 --关键词2 # 删除记忆 diff --git "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" index e8401ba..cdbc34e 100644 --- "a/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/11-ob11-core-bridge-\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -118,8 +118,8 @@ ws://127.0.0.1:46880/core - `action=call`:执行扩展指令 - `extension` + `command`:`扩展名|指令名`;`command` 也支持直接填 `扩展名|指令名` - `args`:指令参数,按顺序 -- `triggerUserId`:可选,指定注入消息的发送者/触发对象用户 ID;不填则使用当前对话用户 -- `atUserId`:可选,指定注入消息中 @ 的用户 ID;仅群聊支持 +- `trigger`:可选,指定注入消息的发送者/触发对象用户 ID;不填则使用当前对话用户 +- `at`:可选,指定注入消息中 @ 的用户 ID 列表(字符串数组);仅群聊支持 本地执行:直接调用扩展 `cmdMap[cmd].solve`(构造全新 `CmdArgs`,不要求会话先出现 `.r`),并复用多消息收集器收集扩展发出的多条回复。无需 MCP/中间件;核心内置扩展与第三方扩展均可调用,仍受「可调用指令白名单」约束。 @@ -134,14 +134,14 @@ ws://127.0.0.1:46880/core - 结构化模式:传 `command` 和可选 `args`,插件会按当前「指令前缀」组装核心原始消息 - 原始消息模式:传 `raw_message`,原样注入核心;`raw_message` 不能与 `command` 或 `args` 同时使用 -触发对象与 @ 对象:两种工具都支持 `triggerUserId` / `atUserId`。前者写入 OB11 `user_id` / `sender.user_id`,后者写入消息的 at 段和 `[CQ:at,qq=...]` 原文;省略前者时沿用当前对话用户。私聊传 `atUserId` 会返回提示,不会伪造私聊 @。 +触发对象与 @ 对象:两种工具都支持 `trigger` / `at`。前者写入 OB11 `user_id` / `sender.user_id`,后者写入消息的 at 段和 `[CQ:at,qq=...]` 原文;省略前者时沿用当前对话用户。私聊传 `at` 会返回提示,不会伪造私聊 @。 ### 参数 | 参数 | 适用 | 说明 | | --- | --- | --- | -| `triggerUserId` | 两者 | 指定指令消息发送者/触发对象用户 ID;默认当前对话用户 | -| `atUserId` | 两者 | 指定消息中 @ 的用户 ID;仅群聊 | +| `trigger` | 两者 | 指定指令消息发送者/触发对象用户 ID;默认当前对话用户 | +| `at` | 两者 | 指定消息中 @ 的用户 ID 列表(字符串数组);仅群聊 | | `forward` | 仅 `run_core_command` | 是否把捕获到的核心发送消息继续转发给协议端,默认 `true`(转发) | | `captureMode` | 仅 `run_core_command` | `reply_only` / `lane`;`forward=true` 且要捕获协议端产生的 bot 回复时建议用 `lane` | | `maxMessages` | 两者 | 最多收集消息数(1–50;ext 默认 20,core 默认 50) | @@ -192,7 +192,7 @@ ws://127.0.0.1:46880/core ## 多核心 -核心桥收到 `triggerUserId` 后会同时写入假消息的 `user_id` 与 `sender.user_id`;`atUserId` 会写入 OB11 at 消息段及 raw_message。多个海豹核心可同时连接中间件:核心连接后上报的 `self_id` 用于路由,`run_core_command` 的调用与事件按 `self_id` 区分,插件端从当前消息上下文自动取 `selfId` 填入 `target`。`run_ext_command` 在插件内本地直调当前实例的扩展 `solve`,不涉及多核心路由。 +核心桥收到 `trigger` 后会同时写入假消息的 `user_id` 与 `sender.user_id`;`at` 会写入 OB11 at 消息段及 raw_message。多个海豹核心可同时连接中间件:核心连接后上报的 `self_id` 用于路由,`run_core_command` 的调用与事件按 `self_id` 区分,插件端从当前消息上下文自动取 `selfId` 填入 `target`。`run_ext_command` 在插件内本地直调当前实例的扩展 `solve`,不涉及多核心路由。 ## 排障 diff --git a/package.json b/package.json index 121e8fd..6a2561f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build": "cross-env NODE_ENV=production node tools/build.js", "build-dev": "cross-env NODE_ENV=dev node tools/build.js", "smoke": "node scripts/smoke.js", + "test:ob11": "node scripts/test-ob11-runtime.cjs", "lint": "eslint \"src/**/*.ts\"", "package:check": "npm run build && node scripts/prepare-sealpack.js && sealpack validate sealpack && sealpack size sealpack", "pack:sealpack": "node scripts/prepare-sealpack.js && sealpack pack sealpack --out dist/aiplugin4.sealpack", diff --git a/scripts/ob11-runtime-test-entry.ts b/scripts/ob11-runtime-test-entry.ts new file mode 100644 index 0000000..ab4ece1 --- /dev/null +++ b/scripts/ob11-runtime-test-entry.ts @@ -0,0 +1,5 @@ +import Config from "../src/config/config"; +Config.registerConfig(); +export { dispatchOb11Api } from "../src/transport/ob11/dispatcher"; +export { encodeNativeMessage, normalizeFileReference, normalizeMessageSegments } from "../src/transport/ob11/message_segments"; +export { getActionCapability } from "../src/transport/ob11/capability_catalog"; diff --git a/scripts/test-ob11-runtime.cjs b/scripts/test-ob11-runtime.cjs new file mode 100644 index 0000000..c987baa --- /dev/null +++ b/scripts/test-ob11-runtime.cjs @@ -0,0 +1,155 @@ +/* Pure OB11 runtime simulation. It never starts SealDice, connects QQ, or calls a real endpoint. */ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const esbuild = require('esbuild'); + +const repo = path.resolve(__dirname, '..'); +const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aiplugin4-ob11-')); +const bundle = path.join(outDir, 'runtime.cjs'); +const sent = []; +const templateConfig = { + '本地图片路径': ['/virtual/avatar.png'], + '本地语音路径': ['voice=/virtual/voice.mp3'], + '本地文件路径': ['book=/virtual/book.pdf'], + '本地视频路径': ['clip=/virtual/clip.mp4'] +}; + +// Minimal SealDice surface used by module initialization and the native fallback. +globalThis.seal = { + ext: { + find: () => undefined, + new: () => ({}), + register: () => undefined, + registerBoolConfig: () => undefined, + registerIntConfig: () => undefined, + registerFloatConfig: () => undefined, + registerStringConfig: () => undefined, + registerOptionConfig: () => undefined, + registerTemplateConfig: () => undefined, + getTemplateConfig: (_ext, key) => { + if (['对话模型', '图片模型', '嵌入模型'].includes(key)) return []; + return templateConfig[key] || ['']; + }, + getBoolConfig: () => true, + getIntConfig: () => 10, + getFloatConfig: () => 0, + getStringConfig: () => '/virtual/sealdice', + getOptionConfig: () => '信息', + storageGet: () => '', + storageSet: () => undefined + }, + replyToSender: (ctx, msg, content) => sent.push({ ctx, msg, content }), + newMessage: () => ({ sender: {}, messageType: 'group' }), + getEndPoints: () => [], + createTempCtx: () => ({}) +}; + +(async () => { + await esbuild.build({ + entryPoints: [path.join(repo, 'scripts/ob11-runtime-test-entry.ts')], + bundle: true, + platform: 'node', + format: 'cjs', + outfile: bundle, + logLevel: 'silent' + }); + + const runtime = require(bundle); + assert.equal(runtime.getActionCapability('send_group_msg'), 'either'); + assert.equal(runtime.getActionCapability('get_group_member_list'), 'network'); + + const context = { + endpointId: '10000', + ctx: { + endPoint: { userId: '10000' }, + group: { groupId: 'QQ-Group:123456', groupName: '模拟群' }, + player: { userId: 'QQ:20000', name: '模拟用户', role: 'member' } + }, + msg: { messageType: 'group' } + }; + + // No ob11 dependency: native send preserves special segments as CQ content. + delete globalThis.net; + sent.length = 0; + const nativeSend = await runtime.dispatchOb11Api(context, 'send_group_msg', { + group_id: '123456', + message: [ + { type: 'text', data: { text: 'hello' } }, + { type: 'image', data: { file: '/virtual/avatar.png' } }, + { type: 'record', data: { file: '/virtual/voice.mp3' } }, + { type: 'video', data: { file: '/virtual/clip.mp4' } }, + { type: 'file', data: { file: '/virtual/book.pdf', name: 'book.pdf' } }, + { type: 'at', data: { qq: '20000' } }, + { type: 'reply', data: { id: '88' } }, + { type: 'json', data: { data: '{"app":"demo"}' } }, + { type: 'markdown', data: { content: '# demo' } }, + { type: 'music', data: { type: 'qq', id: '99' } }, + { type: 'poke', data: { qq: '20000' } }, + { type: 'dice', data: {} }, + { type: 'rps', data: {} } + ] + }); + assert.equal(nativeSend.ok, true); + assert.equal(nativeSend.backend, 'seal-native'); + assert.equal(sent.length, 1); + for (const marker of ['[CQ:image', '[CQ:record', '[CQ:video', '[CQ:file', '[CQ:at', '[CQ:reply', '[CQ:json', '[CQ:markdown', '[CQ:music', '[CQ:poke', '[CQ:dice]', '[CQ:rps]']) { + assert.match(sent[0].content, new RegExp(marker.replace(/[\[\]]/g, '\\$&'))); + } + + const nativeGroupInfo = await runtime.dispatchOb11Api(context, 'get_group_info', { group_id: '123456' }); + assert.equal(nativeGroupInfo.ok, true); + assert.equal(nativeGroupInfo.backend, 'seal-native'); + + for (const action of ['get_group_member_list', 'upload_group_file', 'totally_unknown_action']) { + const unavailable = await runtime.dispatchOb11Api(context, action, {}); + assert.equal(unavailable.ok, false); + assert.equal(unavailable.error.code, 'OB11_DEPENDENCY_REQUIRED'); + } + + const unsupportedNative = await runtime.dispatchOb11Api(context, 'send_group_msg', { + group_id: '123456', + message: [{ type: 'node', data: { user_id: '1', content: [{ type: 'text', data: { text: 'node' } }] } }] + }); + assert.equal(unsupportedNative.ok, false); + assert.equal(unsupportedNative.error.code, 'NATIVE_SEND_ERROR'); + + // With a mocked ob11 dependency: all actions use callApi, while file uploads use sendFile. + const calls = []; + globalThis.net = { + callApi: async (epId, action, params) => { + calls.push({ kind: 'callApi', epId, action, params }); + return { message_id: 501, action }; + }, + sendFile: async (epId, scene, peerId, file, name, folderId) => { + calls.push({ kind: 'sendFile', epId, scene, peerId, file, name, folderId }); + return { message_id: 502 }; + } + }; + const remoteSend = await runtime.dispatchOb11Api(context, 'send_private_msg', { + user_id: '20000', + message: [{ type: 'image', data: { file: 'resource:avatar' } }] + }); + assert.equal(remoteSend.ok, true); + assert.equal(remoteSend.backend, 'ob11-net'); + assert.equal(remoteSend.message_id, 501); + assert.equal(calls[0].kind, 'callApi'); + assert.equal(calls[0].params.message[0].data.file, '/virtual/avatar.png'); + + const upload = await runtime.dispatchOb11Api(context, 'upload_group_file', { + group_id: '123456', file: 'resource:book', name: 'book.pdf', folder_id: 'folder' + }); + assert.equal(upload.ok, true); + assert.equal(upload.message_id, 502); + assert.equal(calls[1].kind, 'sendFile'); + assert.equal(calls[1].scene, 'group'); + assert.equal(calls[1].peerId, '123456'); + assert.equal(calls[1].file, '/virtual/book.pdf'); + + console.log('OB11 runtime simulation passed: native fallback, dependency errors, special segments, resource references, net calls, and file uploads.'); +})().finally(() => fs.rmSync(outDir, { recursive: true, force: true })).catch(error => { + console.error(error); + process.exitCode = 1; +}); + diff --git a/skills/aiplugin4-test-suite/references/tool.md b/skills/aiplugin4-test-suite/references/tool.md index 21fe6b2..c8427d9 100644 --- a/skills/aiplugin4-test-suite/references/tool.md +++ b/skills/aiplugin4-test-suite/references/tool.md @@ -18,6 +18,6 @@ 说明: -- `tool call` 有调用副作用(如 `ban`/`whole_ban`/`record`/`text_to_sound`),只测无副作用的 `show_timer_list` 与未注册函数错误路径。 +- `call_ob11_api` 可能调用有副作用的 action(如 `set_group_ban`、`send_group_msg`);模拟测试只验证参数校验和依赖分流,不连接真实海豹。 - `run_ext_command` 不依赖会话历史中的 `Tool.cmdArgs`,也不要求先使用 `.r`;每次调用都会按工具参数现场构造 `CmdArgs`。核心指令使用 `run_core_command`,需要 OB11 核心桥。牌堆、模组、今日人品等能力不再提供独立工具名,统一通过扩展白名单调用 `deck|draw`、`story|modu`、`fun|jrrp` 等指令。 - TOOL-04~07 结束后按 TOOL-01 快照逐工具还原(`on`/`off <工具名>`)。 diff --git a/src/cmd/sub_cmd/block.ts b/src/cmd/sub_cmd/block.ts index a931482..56b204e 100644 --- a/src/cmd/sub_cmd/block.ts +++ b/src/cmd/sub_cmd/block.ts @@ -1,5 +1,6 @@ // .ai block:黑名单管理(拉黑用户/群,命中时忽略其消息/指令/戳一戳) import { BlockManager } from "../../block"; +import { normalizeTargetId } from "../../utils/target_id"; import { aliasToCmd } from "../../utils/utils"; import { M, U } from "../privilege"; import { SubCmd, SubCmdContext } from "../root_cmd"; @@ -8,8 +9,8 @@ export function registerCmdBlock() { const cmd = new SubCmd('block'); cmd.desc = '黑名单相关操作'; cmd.help = `帮助: -【.ai block add <统一ID/@xxx> <原因>】添加黑名单 -【.ai block rm <统一ID/@xxx>】移除黑名单 +【.ai block add <用户ID/群ID> <原因>】添加黑名单 +【.ai block rm <用户ID/群ID>】移除黑名单 【.ai block list】查看黑名单列表 被拉黑的对象无法触发AI对话`; @@ -22,28 +23,16 @@ export function registerCmdBlock() { } }; cmd.solve = (scc: SubCmdContext) => { - const { ctx, msg, cmdArgs, epId, ret } = scc; - - const mctx = seal.getCtxProxyFirst(ctx, cmdArgs); - const muid = cmdArgs.amIBeMentionedFirst ? epId : mctx.player!.userId; + const { ctx, msg, cmdArgs, ret } = scc; const val2 = cmdArgs.getArgN(2); switch (aliasToCmd(val2)) { case 'add': { - let targetId = ''; - let reason = ''; - const arg3 = cmdArgs.getArgN(3); - const arg4 = cmdArgs.getArgN(4); + const targetId = normalizeTargetId(cmdArgs.getArgN(3)); + let reason = cmdArgs.getRestArgsFrom(4).trim(); - if (cmdArgs.at.length > 0) { - targetId = muid; - // at 不占参数位:原因从第 3 个参数起(支持多词),如 `.ai block add @xxx 测试 原因` - reason = cmdArgs.getRestArgsFrom(3).trim(); - } else if (arg3 && (arg3.startsWith('QQ:') || arg3.startsWith('QQ-Group:'))) { - targetId = arg3; - reason = arg4; - } else { - seal.replyToSender(ctx, msg, '参数缺失,【.ai block add <统一ID/@xxx> <原因>】添加黑名单'); + if (!targetId) { + seal.replyToSender(ctx, msg, '参数无效,【.ai block add <用户ID/群ID> <原因>】添加黑名单'); return ret; } @@ -61,15 +50,9 @@ export function registerCmdBlock() { return ret; } case 'remove': { - let targetId = ''; - const arg3 = cmdArgs.getArgN(3); - - if (cmdArgs.at.length > 0) { - targetId = muid; - } else if (arg3 && (arg3.startsWith('QQ:') || arg3.startsWith('QQ-Group:'))) { - targetId = arg3; - } else { - seal.replyToSender(ctx, msg, '参数缺失,【.ai block rm <统一ID/@xxx>】移除黑名单'); + const targetId = normalizeTargetId(cmdArgs.getArgN(3)); + if (!targetId) { + seal.replyToSender(ctx, msg, '参数无效,【.ai block rm <用户ID/群ID>】移除黑名单'); return ret; } @@ -86,8 +69,8 @@ export function registerCmdBlock() { } default: { seal.replyToSender(ctx, msg, `帮助: -【.ai block add <统一ID/@xxx> <原因>】添加黑名单 -【.ai block rm <统一ID/@xxx>】移除黑名单 +【.ai block add <用户ID/群ID> <原因>】添加黑名单 +【.ai block rm <用户ID/群ID>】移除黑名单 【.ai block list】查看黑名单列表 被拉黑的对象无法触发AI对话`); diff --git a/src/cmd/sub_cmd/ignore.ts b/src/cmd/sub_cmd/ignore.ts index 000bd03..b39275f 100644 --- a/src/cmd/sub_cmd/ignore.ts +++ b/src/cmd/sub_cmd/ignore.ts @@ -1,4 +1,5 @@ // .ai ignore:群内忽略名单管理 +import { normalizeUserId } from "../../utils/target_id"; import { aliasToCmd } from "../../utils/utils"; import { U } from "../privilege"; import { SubCmd, SubCmdContext } from "../root_cmd"; @@ -7,11 +8,11 @@ export function registerCmdIgnore() { const cmd = new SubCmd('ignore'); cmd.desc = '忽略名单相关操作'; cmd.help = `帮助: - 【.ai ign add @xxx】添加忽略名单 - 【.ai ign rm @xxx】移除忽略名单 + 【.ai ign add <用户ID>】添加忽略名单 + 【.ai ign rm <用户ID>】移除忽略名单 【.ai ign lst】列出忽略名单 - 忽略名单中的对象仍能正常对话,但无法被选中QQ号`; + 忽略名单中的对象仍能正常对话,但不会被选为目标用户`; cmd.priv = { priv: U, args: { add: { priv: U }, @@ -20,42 +21,40 @@ export function registerCmdIgnore() { } }; cmd.solve = (scc: SubCmdContext) => { - const { ctx, msg, cmdArgs, epId, session, ret } = scc; + const { ctx, msg, cmdArgs, session, ret } = scc; if (ctx.isPrivate) { seal.replyToSender(ctx, msg, '忽略名单仅在群聊可用'); return ret; } - const mctx = seal.getCtxProxyFirst(ctx, cmdArgs); - const muid = cmdArgs.amIBeMentionedFirst ? epId : mctx.player!.userId; - const val2 = cmdArgs.getArgN(2); + const targetId = normalizeUserId(cmdArgs.getArgN(3)); switch (aliasToCmd(val2)) { case 'add': { - if (cmdArgs.at.length === 0) { - seal.replyToSender(ctx, msg, '参数缺失,【.ai ign add @xxx】添加忽略名单'); + if (!targetId) { + seal.replyToSender(ctx, msg, '参数无效,【.ai ign add <用户ID>】添加忽略名单'); return ret; } - if (session.context.ignoreList.includes(muid)) { + if (session.context.ignoreList.includes(targetId!)) { seal.replyToSender(ctx, msg, '已经在忽略名单中'); return ret; } - session.context.ignoreList.push(muid); + session.context.ignoreList.push(targetId!); seal.replyToSender(ctx, msg, '已添加到忽略名单'); session.save(); return ret; } case 'remove': { - if (cmdArgs.at.length === 0) { - seal.replyToSender(ctx, msg, '参数缺失,【.ai ign rm @xxx】移除忽略名单'); + if (!targetId) { + seal.replyToSender(ctx, msg, '参数无效,【.ai ign rm <用户ID>】移除忽略名单'); return ret; } - if (!session.context.ignoreList.includes(muid)) { + if (!session.context.ignoreList.includes(targetId!)) { seal.replyToSender(ctx, msg, '不在忽略名单中'); return ret; } - session.context.ignoreList = session.context.ignoreList.filter(item => item !== muid); + session.context.ignoreList = session.context.ignoreList.filter(item => item !== targetId!); seal.replyToSender(ctx, msg, '已从忽略名单中移除'); session.save(); return ret; @@ -67,11 +66,11 @@ export function registerCmdIgnore() { } default: { seal.replyToSender(ctx, msg, `帮助: - 【.ai ign add @xxx】添加忽略名单 - 【.ai ign rm @xxx】移除忽略名单 + 【.ai ign add <用户ID>】添加忽略名单 + 【.ai ign rm <用户ID>】移除忽略名单 【.ai ign lst】列出忽略名单 - 忽略名单中的对象仍能正常对话,但无法被选中QQ号`); + 忽略名单中的对象仍能正常对话,但不会被选为目标用户`); return ret; } } diff --git a/src/cmd/sub_cmd/memory.ts b/src/cmd/sub_cmd/memory.ts index 9a30154..e076fcc 100644 --- a/src/cmd/sub_cmd/memory.ts +++ b/src/cmd/sub_cmd/memory.ts @@ -3,6 +3,7 @@ import Config from "../../config/config"; import { Session } from "../../session/session"; import { getSession } from "../../session/session_service"; import { stripInternalTags } from "../../utils/string"; +import { normalizeUserId } from "../../utils/target_id"; import { aliasToCmd } from "../../utils/utils"; import { I, S, U } from "../privilege"; import { SubCmd, SubCmdContext } from "../root_cmd"; @@ -22,7 +23,7 @@ export function registerCmdMemory() { const cmd = new SubCmd('memory'); cmd.desc = '记忆相关操作'; cmd.help = `帮助: - 【.ai memo status (@xxx)】查看记忆状态,@为查看个人记忆状态 + 【.ai memo status [用户ID]】查看记忆状态,传用户ID时查看对应个人记忆 【.ai memo [p/g] st <内容>】设置个人/群聊设定 【.ai memo [p/g] st clr】清除个人/群聊设定 【.ai memo [p/g] del --关键词1 --关键词2】删除个人/群聊记忆 @@ -72,18 +73,27 @@ export function registerCmdMemory() { } }; cmd.solve = async (scc: SubCmdContext) => { - const { ctx, msg, cmdArgs, epId, session, page, ret } = scc; + const { ctx, msg, cmdArgs, session, page, ret } = scc; - const sessionCtx = seal.getCtxProxyFirst(ctx, cmdArgs); - const targetUserId = sessionCtx.player!.userId; - - const targetSession = getSession(targetUserId); + const currentUserId = normalizeUserId(ctx.player && ctx.player.userId); + if (!currentUserId) { + seal.replyToSender(ctx, msg, '当前消息缺少有效用户ID'); + return ret; + } + const sessionCtx = ctx; + const targetSession = getSession(currentUserId); const val2 = cmdArgs.getArgN(2); switch (aliasToCmd(val2)) { case 'status': { let statusSession = session; - if (cmdArgs.at.length > 0 && (cmdArgs.at.length !== 1 || cmdArgs.at[0].userId !== epId)) { - statusSession = targetSession; + const targetUserId = cmdArgs.getArgN(3); + if (targetUserId) { + const normalizedUserId = normalizeUserId(targetUserId); + if (!normalizedUserId) { + seal.replyToSender(ctx, msg, '参数无效,【.ai memo status [用户ID]】'); + return ret; + } + statusSession = getSession(normalizedUserId); } const { MEMORY: isMemory, SUMMARY: isShortMemory } = Config.memory; seal.replyToSender(ctx, msg, `${statusSession.id} @@ -351,7 +361,7 @@ export function registerCmdMemory() { } default: { seal.replyToSender(ctx, msg, `帮助: - 【.ai memo status (@xxx)】查看记忆状态,@为查看个人记忆状态 + 【.ai memo status [用户ID]】查看记忆状态,传用户ID时查看对应个人记忆 【.ai memo [p/g] st <内容>】设置个人/群聊设定 【.ai memo [p/g] st clr】清除个人/群聊设定 【.ai memo [p/g] del --关键词1 --关键词2】删除个人/群聊记忆 diff --git a/src/config/configs/tool.ts b/src/config/configs/tool.ts index d8bd399..3077bfb 100644 --- a/src/config/configs/tool.ts +++ b/src/config/configs/tool.ts @@ -1,5 +1,6 @@ // 工具配置:函数调用开关/提示词工程/上限/禁用与默认关闭 import { ext } from "../config"; +import { OB11_API_SKILLS } from "../static_config/ob11_api_skills"; import { SEALDICE_COMMAND_SKILLS, SEALDICE_COMMAND_WHITELIST } from "../static_config/sealdice_command_defaults"; export default class ToolConfig { static register() { @@ -10,7 +11,9 @@ export default class ToolConfig { seal.ext.registerIntConfig(ext, "允许连续调用函数次数", 10, "单次回复流程中允许连续调用工具的次数,防止无限循环;0 为不限制", "工具"); seal.ext.registerIntConfig(ext, "工具响应压缩触发字数", 5000, "工具返回结果超过该字数时压缩后再存入上下文;设为 0 不压缩", "工具"); seal.ext.registerTemplateConfig(ext, "禁止调用的函数", [''], "每行一个禁止 AI 调用的函数名,示例:run_ext_command;扩展指令的细粒度控制请使用「可调用指令白名单」;修改后自动生效", "工具"); - seal.ext.registerTemplateConfig(ext, "默认关闭的函数", [''], "每行一个默认关闭的函数名,AI 默认无法调用,示例:get_msg;修改后自动生效", "工具"); + seal.ext.registerTemplateConfig(ext, "默认关闭的函数", [''], "每行一个默认关闭的函数名,AI 默认无法调用;OB11 action 请使用下方 action 配置;修改后自动生效", "工具"); + seal.ext.registerTemplateConfig(ext, "禁止调用的 OB11 action", [''], "每行一个禁止 call_ob11_api 调用的原始 OB11 action,例如 set_group_ban;修改后自动生效", "工具"); + seal.ext.registerTemplateConfig(ext, "默认关闭的 OB11 action", [''], "每行一个默认关闭的原始 OB11 action,例如 get_group_member_list;关闭后 AI 不会调用,修改后自动生效", "工具"); seal.ext.registerTemplateConfig(ext, "可调用指令白名单", SEALDICE_COMMAND_WHITELIST, "每行一个 AI 可调用的海豹指令;格式:扩展名|指令名/别名1/别名2,同一元素内的别名用 / 分隔;核心指令的扩展名统一写 core(如 core|roll/r/rd)。默认已包含当前 SealDice 源码中的全部核心命令、内置扩展命令及其别名;修改后自动生效", "工具"); seal.ext.registerBoolConfig(ext, "是否允许调用所有指令", false, "开启后忽略白名单,允许调用所有可解析的扩展指令;核心指令仍通过 run_core_command 调用", "工具"); seal.ext.registerStringConfig(ext, "指令前缀", ".", "注入到 SealDice 核心的指令前缀,通常为 .;如果核心改成其他前缀,请同步修改", "工具"); @@ -40,7 +43,7 @@ export default class ToolConfig { } }` ], "仅支持标准 mcpServers JSON 格式:{\"mcpServers\":{\"服务器名\":{\"type\":\"http\",\"url\":\"...\",\"headers\":{...}}}}(Claude Desktop/Cursor/.mcp.json 可直接粘贴),一个块可包含多个服务器。工具名称、描述和参数 schema 会在连接后通过 MCP tools/list 自动发现,不需要也不支持额外的 tools 配置块。字段:type(仅支持 http,即 Streamable HTTP)、url(服务器地址)、headers(任意自定义请求头,如 Authorization)、token(自动生成 Bearer 头,与 headers 二选一)。默认包含四个:mcp-files-exec(提供 read_file、list_dir、write_file、delete_file、download_file、run_shell、export_file;默认可直接传后端绝对路径)、web-read(提供 scrape_url、screenshot_url)、md-html-render(提供 render_markdown、render_html)、ob11-core-bridge(仅提供 run_core_command)。格式定义见 https://modelcontextprotocol.io/specification/latest (MCP 官方规范,国内可访问)。说明:stdio(command)服务器需拉起子进程,海豹环境不支持会自动跳过,请改用 Streamable HTTP(type=http + url)。修改后自动生效(缓存最多 1 分钟)", "工具"); - seal.ext.registerTemplateConfig(ext, "技能配置", SEALDICE_COMMAND_SKILLS, "每条配置项一个技能,仅支持标准 SKILL.md 格式:以 --- 开头的 YAML frontmatter 里写 name(必填)/description(可选),正文为技能内容;默认包含当前 SealDice 核心命令、内置扩展命令及别名的调用帮助,统一说明 run_ext_command / run_core_command 的参数传递方式。修改后自动生效(缓存最多 1 分钟)。AI 可通过 use_skill 工具按需调用", "工具"); + seal.ext.registerTemplateConfig(ext, "技能配置", [...SEALDICE_COMMAND_SKILLS, ...OB11_API_SKILLS], "每条配置项一个技能,仅支持标准 SKILL.md 格式:以 --- 开头的 YAML frontmatter 里写 name(必填)/description(可选),正文为技能内容;默认包含当前 SealDice 核心命令、内置扩展命令及别名的调用帮助,统一说明 run_ext_command / run_core_command 的参数传递方式。修改后自动生效(缓存最多 1 分钟)。AI 可通过 use_skill 工具按需调用", "工具"); seal.ext.registerTemplateConfig(ext, "音乐服务配置", [ `{ "platform": "网易云", @@ -89,6 +92,8 @@ export default class ToolConfig { TOOL_RESPONSE_COMPRESS_MIN_LENGTH: seal.ext.getIntConfig(ext, "工具响应压缩触发字数"), BLOCKED: seal.ext.getTemplateConfig(ext, "禁止调用的函数"), DEFAULT_CLOSED: seal.ext.getTemplateConfig(ext, "默认关闭的函数"), + OB11_BLOCKED_ACTIONS: seal.ext.getTemplateConfig(ext, "禁止调用的 OB11 action"), + OB11_DEFAULT_CLOSED_ACTIONS: seal.ext.getTemplateConfig(ext, "默认关闭的 OB11 action"), CMD_WHITELIST: seal.ext.getTemplateConfig(ext, "可调用指令白名单"), ALLOW_ALL_CMDS: seal.ext.getBoolConfig(ext, "是否允许调用所有指令"), COMMAND_PREFIX: seal.ext.getStringConfig(ext, "指令前缀"), diff --git a/src/config/static_config/ob11_api_skills.ts b/src/config/static_config/ob11_api_skills.ts new file mode 100644 index 0000000..e088e36 --- /dev/null +++ b/src/config/static_config/ob11_api_skills.ts @@ -0,0 +1,110 @@ +// OB11 API 默认技能:统一指导 AI 通过唯一工具 call_ob11_api 调用协议动作。 +// 这里刻意不注册任何按功能拆分的旧工具;动作仍然以 OneBot 11 action 原名传入。 +export const OB11_API_SKILLS = [ + `--- +name: ob11-api +description: 通过唯一的 call_ob11_api 工具调用 OneBot 11/兼容协议 API,覆盖消息、媒体、文件、合并转发、查询和群管理 +--- +# OB11 API 调用规范 + +## 1. 唯一入口 +所有 OB11/协议端操作只能使用工具 **call_ob11_api**,不要寻找或调用任何已删除的按功能拆分工具。工具参数: + + +data: +{ + "action": "send_group_msg", + "params": { + "group_id": "123456", + "message": [ + {"type": "text", "data": {"text": "你好"}}, + {"type": "image", "data": {"file": "resource:图片名"}} + ] + }, + "reason": "可选:说明发送或管理操作的目的" +} + +\`action\` 是原始 OneBot 11 action 字符串,\`params\` 必须是该 action 的原始参数对象;不要把 action 改写成工具名,不要再套一层 \`params.params\`。结果是 JSON:成功为 \`{"ok":true,...}\`,失败为 \`{"ok":false,"error":{"code":...}}\`。 + +## 2. ID、目标和返回值 +- \`user_id\`、\`group_id\`、\`message_id\`、\`message_seq\` 等 ID 可传数字或字符串;优先使用当前上下文给出的 QQ号、群号或对应 ID,不要擅自把群号当用户号。 +- 私聊发送:\`action="send_private_msg"\`,参数 \`{"user_id":"用户ID","message":...}\`。 +- 群聊发送:\`action="send_group_msg"\`,参数 \`{"group_id":"群ID","message":...}\`。 +- 发送成功时通常返回 \`message_id\`;后续引用、撤回、回复应优先使用这个值。 +- \`send_msg\` 不是本工具入口。需要发送时根据场景明确选择 \`send_private_msg\` 或 \`send_group_msg\`。 +- 管理、删除、加好友等有副作用的 action 只有在用户意图明确时调用;\`reason\` 只用于记录意图,不会替代 API 参数,也不会绕过权限。 + +## 3. message 格式 +\`message\` 可以是纯文本字符串,也可以是 OneBot 11 消息段数组。消息段统一格式为 \`{"type":"类型","data":{...}}\`。可混排,数组顺序就是发送顺序。 + +常用消息段: +- 文本:\`{"type":"text","data":{"text":"文字"}}\` +- QQ 表情:\`{"type":"face","data":{"id":"123"}}\` +- 图片:\`{"type":"image","data":{"file":"绝对路径、URL、base64://... 或 resource:资源名"}}\` +- 语音:\`{"type":"record","data":{"file":"绝对路径、URL、base64://... 或 resource:资源名","magic":0}}\` +- 视频:\`{"type":"video","data":{"file":"绝对路径、URL、base64://... 或 resource:资源名"}}\` +- 文件消息:\`{"type":"file","data":{"file":"绝对路径、URL、base64://... 或 resource:资源名","name":"可选文件名"}}\` +- @:\`{"type":"at","data":{"qq":"用户ID"}}\`;全体成员可用 \`qq:"all"\`(以协议端支持为准)。 +- 回复:\`{"type":"reply","data":{"id":"消息ID"}}\` +- 戳一戳:\`{"type":"poke","data":{"qq":"用户ID"}}\` +- 骰子 / 猜拳:\`{"type":"dice","data":{}}\`、\`{"type":"rps","data":{}}\` +- 音乐:见第 5 节。 +- JSON / Markdown:见第 6 节。 +- 合并转发 node:见第 7 节。 + +不要把图片、语音、视频或文件静默改成文本路径;必须保留对应 segment type。资源路径无法解析时应让工具返回错误并修正路径,不要假装发送成功。 + +## 4. 本地资源引用 +需要发送插件资源时,先调用 \`list_resources\` 查询资源 ID(这是资源查询工具,不是发送工具),然后在消息段中写 \`resource:资源ID\`。例如: + +{"action":"send_group_msg","params":{"group_id":"123","message":[{"type":"image","data":{"file":"resource:角色头像"}}]}} + +也可以直接传本地绝对路径、\`file://\` URI、HTTP(S) URL 或 \`base64://\` 内容。MCP 导出的文件使用 \`mcp://服务器名/沙箱相对路径\` 或已解析的可访问 URL。不要只传用户可见的文件名,除非该文件名就是 \`resource:\` 引用。 + +## 5. 图片、语音、视频和文件的区别 +- 图片/语音/视频作为消息内容发送:使用 \`send_private_msg\` / \`send_group_msg\`,把对应 segment 放进 \`message\`。 +- 上传文件到群文件区或私聊文件区:使用 \`upload_group_file\` / \`upload_private_file\`,例如: + \`{"action":"upload_group_file","params":{"group_id":"123","file":"/data/a.zip","name":"a.zip"}}\`。 +- 上传 action 的 \`file\` 也支持 \`resource:资源ID\`;工具会先解析到已配置资源的实际路径,再交给文件上传 API。 +- \`file\` 消息段是消息中的文件卡片,不等同于 \`upload_group_file\`;不要用普通 \`file\` segment 冒充群文件区上传。 +- 语音生成先使用 \`generate_audio\` 得到 record segment,再把该 segment 原样交给 \`call_ob11_api\`;直接把 record segment 交给 \`call_ob11_api\`。 +- 音乐搜索先使用 \`search_music\` 得到 music segment,再把该 segment 放入发送 action;不要直接发送,继续使用 \`call_ob11_api\`。 + +## 6. JSON、Markdown、音乐 +- JSON:\`{"type":"json","data":{"data":"JSON字符串"}}\`。保留合法 JSON 字符串;协议端不支持时工具会返回失败,不要擅自改成普通文本。 +- Markdown:\`{"type":"markdown","data":{"content":"# 标题\\n正文"}}\` 或按协议端要求使用 \`data\` 字段。 +- QQ 音乐:\`{"type":"music","data":{"type":"qq","id":"歌曲ID"}}\`。 +- 网易云音乐:\`{"type":"music","data":{"type":"163","id":"歌曲ID"}}\`。 +- 自定义音乐:\`{"type":"music","data":{"type":"custom","url":"展示页","audio":"音频URL","title":"标题","content":"作者/描述","image":"封面URL"}}\`。 +- JSON/Markdown/music 是协议端特殊消息,不要在安装依赖缺失时把它们伪装成普通文字。 + +## 7. 合并转发 +- 已有转发消息 ID:可在 \`message\` 中使用 \`{"type":"forward","data":{"id":"转发ID"}}\`(原生 SealDice 仅能尽力编码该引用)。 +- 需要构造新的合并转发:使用 \`send_group_forward_msg\` 或 \`send_private_forward_msg\`,并按当前协议端的 \`messages\` / \`messages\` 节点格式提供 node;这类远端动作需要 ob11 网络连接依赖。 +- node 示例:\`{"type":"node","data":{"user_id":"10001","nickname":"昵称","content":[{"type":"text","data":{"text":"内容"}}]}}\`。不同协议端对 node 字段可能有差异,优先遵循当前端点文档。 + +## 8. 常用 action 分类 +以下是常用原始 action,不是额外工具名: +- 消息:\`send_private_msg\`、\`send_group_msg\`、\`get_msg\`、\`delete_msg\`、\`get_forward_msg\`、\`send_group_forward_msg\`、\`send_private_forward_msg\`。 +- 查询:\`get_login_info\`、\`get_status\`、\`get_version_info\`、\`get_group_info\`、\`get_group_list\`、\`get_group_member_info\`、\`get_group_member_list\`、\`get_friend_list\`、\`get_stranger_info\`、\`get_group_msg_history\`、\`get_friend_msg_history\`。 +- 群管理:\`set_group_kick\`、\`set_group_ban\`、\`set_group_whole_ban\`、\`set_group_admin\`、\`set_group_card\`、\`set_group_name\`、\`set_group_leave\`、\`set_group_special_title\`、\`send_group_sign\`。 +- 请求与好友:\`send_like\`、\`set_friend_add_request\`、\`set_group_add_request\`。 +- 群文件:\`upload_group_file\`、\`upload_private_file\`、\`get_group_file_url\`、\`get_group_root_files\`、\`get_group_files_by_folder\`、\`create_group_file_folder\`、\`delete_group_file\`、\`delete_group_folder\`、\`move_group_file\`、\`rename_group_file\`、\`get_private_file_url\`。 +- 账号/缓存:\`get_cookies\`、\`get_csrf_token\`、\`get_credentials\`、\`get_record\`、\`get_image\`、\`can_send_image\`、\`can_send_record\`、\`set_qq_avatar\`、\`set_qq_profile\`、\`set_group_portrait\`、\`set_restart\`、\`clean_cache\`。 +- 未列出的 action:如果已安装 ob11 网络连接依赖,统一入口会原样透传给协议端;没有依赖时不能执行未知 action,也不能猜测结果。 + +## 9. ob11 依赖是否安装 +运行时每次调用都会检测 \`globalThis.net\`,不需要用户手动选择后端: +- 已安装 ob11 网络连接依赖:调用 \`net.callApi\`;\`upload_*_file\` 优先使用统一的 \`net.sendFile\`,保留端点对文件上传的特殊实现;所有协议 action、特殊消息、列表、管理和远端合并转发都可按端点能力执行。 +- 未安装依赖:仍使用 SealDice 原生后端完成当前上下文可完成的 \`send_private_msg\`、\`send_group_msg\`、\`get_login_info\`、\`get_status\`、\`get_version_info\`、\`get_group_info\`、\`get_group_member_info\`、\`get_stranger_info\`。文本、图片、语音、视频、文件、at、reply、face、poke、dice、rps、music、json、markdown 等消息段会尽力转换为 SealDice/CQ 内容;不支持的 node 或远程 forward 不会静默丢弃。 +- 未安装依赖时,列表、历史、撤回、群管理、群文件上传、远程合并转发和未知 action 返回 \`error.code="OB11_DEPENDENCY_REQUIRED"\`。不要重复重试;先安装依赖,或改用原生可完成的 action。 +- 如果 native 发送返回 \`NATIVE_ACTION_UNSUPPORTED\` / \`NATIVE_CONTEXT_UNAVAILABLE\`,这是当前上下文能力不足,不代表协议端已成功。 + +## 10. 调用纪律 +1. 先确认目标场景和 ID,再组装 action/params。 +2. 发送特殊消息时使用 segment 数组,保持 type 和 data 字段,不要压扁为文本。 +3. 看到 \`OB11_DEPENDENCY_REQUIRED\` 时停止重复调用并明确告知缺少依赖。 +4. 看到 \`OB11_API_ERROR\` 时根据错误决定是否重试;不要盲目重复有副作用的 action。 +5. 旧工具已彻底移除;不要创建别名、兼容调用或猜测旧工具仍可用。` +]; + diff --git a/src/config/static_config/sealdice_command_defaults.ts b/src/config/static_config/sealdice_command_defaults.ts index 2ed40fb..472e335 100644 --- a/src/config/static_config/sealdice_command_defaults.ts +++ b/src/config/static_config/sealdice_command_defaults.ts @@ -39,7 +39,7 @@ const COMMAND_CALL_RULES = ` - 扩展指令使用 run_ext_command,参数格式:{"action":"call","extension":"扩展名","command":"指令名","args":["参数1","参数2"]};不要在 command 中带前缀点号。 - 核心指令(包括 core|team)使用 run_core_command,参数格式:{"command":"指令名","args":["参数1","参数2"]}。 - args 按 SealDice 原始指令的空格分隔顺序逐项传入;需要 @、表达式或带空格的文本时按工具 schema 传入单独字符串。 -- 如需让指令替其他用户触发,传 triggerUserId;如需模拟消息中的 @,传 atUserId。两者均为用户 ID,不填 triggerUserId 时使用当前对话用户;私聊不支持 atUserId。 +- 如需让指令替其他用户触发,传 trigger;如需模拟消息中的 @,传 at。trigger 为字符串用户 ID,at 为字符串用户 ID 数组;不填 trigger 时使用当前对话用户;私聊不支持 at。 - 下方同一行中的名称是同一条指令的别名,均已加入白名单;优先使用第一个主名称。 - 参数不确定时,先调用对应的 help:核心用 run_core_command(command="help", args=["指令名"],扩展可调用扩展的 help 参数或先查看 SealDice 指令帮助。 `; @@ -139,10 +139,10 @@ ${COMMAND_CALL_RULES} - ti:抽取临时性疯狂症状;args=[]。 - li:抽取总结性疯狂症状;args=[]。 - ra / rc / rch / rah / cra / crc / crch / crah:属性或技能检定,同一实现的不同别名;args 按顺序传奖励/惩罚骰、检定表达式、原因。 -- rav / rcv:对抗/竞争检定;args 按原命令帮助传入;需要替其他用户触发或 @ 对象时,额外传 triggerUserId / atUserId。 +- rav / rcv:对抗/竞争检定;args 按原命令帮助传入;需要替其他用户触发或 @ 对象时,额外传 trigger / at。 - sc:San 检定;args 按顺序传奖励/惩罚骰、成功/失败损失表达式,例如 ["0/1d6"]。 - coc:生成 COC 角色卡;args 可传数量,例如 ["3"]。 -- st / cst:COC 扩展的角色属性查看/修改别名;常用 args=["show","玩家名称或QQ号"]。角色卡导入流程优先使用核心 run_core_command 的 st。`, +- st / cst:COC 扩展的角色属性查看/修改别名;常用 args=["show","角色名或用户ID"]。角色名是角色卡命令自身的参数,涉及用户定位时只使用用户ID。角色卡导入流程优先使用核心 run_core_command 的 st。`, `--- name: SealDice deck扩展调用帮助 @@ -166,9 +166,9 @@ ${COMMAND_CALL_RULES} - rc / ra / rah / rch / drc:DND 检定;args 按顺序传检定表达式、难度或原因,别名可按习惯选择。 - buff / dbuff:增益/减益效果;参数按原命令帮助传入。 - spellslots / ss / dss / 法术位:查看或管理法术位。 -- cast / dcast:施法;args 按原命令帮助传入;需要替其他用户触发或 @ 对象时,额外传 triggerUserId / atUserId。 +- cast / dcast:施法;args 按原命令帮助传入;需要替其他用户触发或 @ 对象时,额外传 trigger / at。 - 长休 / longrest / dlongrest:执行长休。 -- ds / 死亡豁免:死亡豁免检定;args 按原命令帮助传入;需要替其他用户触发或 @ 对象时,额外传 triggerUserId / atUserId。`, +- ds / 死亡豁免:死亡豁免检定;args 按原命令帮助传入;需要替其他用户触发或 @ 对象时,额外传 trigger / at。`, `--- name: SealDice log扩展调用帮助 diff --git a/src/context/context.ts b/src/context/context.ts index 5762da0..4c056c9 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -1,4 +1,4 @@ -// 会话上下文:消息增删/忽略名单/压缩与总结触发/用户群查找/图片查找 +// 会话上下文:消息增删/忽略名单/压缩与总结触发/按 ID 获取目标/图片查找 import Agent from "../agent/agent"; import Config from "../config/config"; import Logger from "../logger"; @@ -8,8 +8,9 @@ import Group from "../session/group"; import { Session } from "../session/session"; import User from "../session/user"; import { ToolCall } from "../tool/types"; -import { getFriendList, getGroupList, getGroupMemberInfo, getGroupMemberList, getStrangerInfo, netExists } from "../utils/ob11"; -import { levenshteinDistance, stripInternalTags } from "../utils/string"; +import { callOb11Api } from "../utils/ob11"; +import { stripInternalTags } from "../utils/string"; +import { normalizeGroupId, normalizeUserId } from "../utils/target_id"; import { TypeDescriptor, withTimeout } from "../utils/utils"; import Message from "./message"; @@ -242,55 +243,10 @@ export class Context { } } - async findUserId(ctx: seal.MsgContext, name: string | number, findInFriendList: boolean = false): Promise { - const session = this.session; - const returnUserId = (userId: string) => session.checkIgnoredUserId(userId) ? '' : userId; - - name = String(name); - if (!name) return ''; - - if (name.length > 4 && !isNaN(parseInt(name))) return returnUserId(`QQ:${name}`); - - const match = name.match(/^<([^>]+?)>(?:[\((]\d+[\))])?$|(.+?)[\((]\d+[\))]$/); - if (match) name = match[1] || match[2]; - - if (name === ctx.player!.name) return returnUserId(ctx.player!.userId); - if (name === seal.formatTmpl(ctx, "核心:骰子名字")) return returnUserId(ctx.endPoint.userId); - - // 在上下文和记忆中查找用户 - const users = Array.from(new Set([...this.users, ...MemoryService.getItemsFromRelatedMemories(this.session, 'users')])); - for (const userId of users) { - const u = User.get(userId); - if (name === u.userName) return returnUserId(u.userId); - if (name.length > 4 && levenshteinDistance(name, u.userName) <= 2) return returnUserId(u.userId); - } - - // 在群成员列表、好友列表中查找用户 - if (netExists()) { - const epId = ctx.endPoint.userId; - - if (!ctx.isPrivate) { - const gid = ctx.group!.groupId; - const groupMemberList = await getGroupMemberList(epId, gid.replace(/^.+:/, '')); - if (groupMemberList && Array.isArray(groupMemberList)) { - const user_id = groupMemberList.find(item => item.card === name || item.nickname === name)?.user_id; - if (user_id) return returnUserId(`QQ:${user_id}`); - } - } - - if (findInFriendList) { - const friendList = await getFriendList(epId); - if (friendList && Array.isArray(friendList)) { - const user_id = friendList.find(item => item.nickname === name || item.remark === name)?.user_id; - if (user_id) return returnUserId(`QQ:${user_id}`); - } - } - } - - if (name.length > 4 && levenshteinDistance(name, ctx.player!.name) <= 2) return returnUserId(ctx.player!.userId); - - Logger.warning(`未找到用户<${name}>`); - return ''; + getUserById(userId: string | number): User | null { + const normalizedId = normalizeUserId(userId); + if (!normalizedId || this.session.checkIgnoredUserId(normalizedId)) return null; + return User.get(normalizedId); } get userInfoList(): { isPrivate: true, id: string, name: string }[] { const userMap: { [key: string]: { isPrivate: true, id: string, name: string } } = {}; @@ -315,7 +271,7 @@ export class Context { let name = ''; switch (mod) { case 'nickname': { - const strangerInfo = await getStrangerInfo(epId, uid.replace(/^.+:/, '')); + const strangerInfo = await callOb11Api(epId, "get_stranger_info", { user_id: uid.replace(/^.+:/, ""), no_cache: true }); if (!strangerInfo || !strangerInfo.nickname) { Logger.warning(`未找到用户<${uid}>的昵称`); break; @@ -325,7 +281,7 @@ export class Context { } case 'card': { if (!gid) break; - const memberInfo = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), uid.replace(/^.+:/, '')); + const memberInfo = await callOb11Api(epId, "get_group_member_info", { group_id: gid.replace(/^.+:/, ""), user_id: uid.replace(/^.+:/, ""), no_cache: true }); if (!memberInfo) { Logger.warning(`获取用户<${uid}>的群成员信息失败,尝试使用昵称`); await this.setName(epId, gid, uid, 'nickname'); @@ -347,52 +303,21 @@ export class Context { u.userName = name; User.save(u); } - async findGroupId(ctx: seal.MsgContext, groupName: string | number): Promise { - groupName = String(groupName); - if (!groupName) return ''; - - if (groupName.length > 5 && !isNaN(parseInt(groupName))) return `QQ-Group:${groupName}`; - - const match = groupName.match(/^<([^>]+?)>(?:[\((]\d+[\))])?$|(.+?)[\((]\d+[\))]$/); - if (match) groupName = match[1] || match[2]; - - if (!ctx.isPrivate && ctx.group && groupName === ctx.group.groupName) return ctx.group.groupId; - - // 在记忆中查找群聊 - for (const groupId of MemoryService.getItemsFromRelatedMemories(this.session, 'groups')) { - const g = Group.get(groupId); - if (g.groupName === groupName) return groupId; - if (g.groupName.length > 4 && levenshteinDistance(groupName, g.groupName) <= 2) return groupId; - } - - // 在群聊列表中查找用户 - if (netExists()) { - const epId = ctx.endPoint.userId; - const groupList = await getGroupList(epId); - if (groupList && Array.isArray(groupList)) { - const group_id = groupList.find(item => item.group_name === groupName)?.group_id; - if (group_id) return `QQ-Group:${group_id}`; - } - } - - if (!ctx.isPrivate && ctx.group && groupName.length > 4 && levenshteinDistance(groupName, ctx.group.groupName) <= 2) return ctx.group.groupId; - - Logger.warning(`未找到群聊<${groupName}>`); - return ''; - } - async findUser(ctx: seal.MsgContext, name: string | number, findInFriendList: boolean = false): Promise { - const userId = await this.findUserId(ctx, name, findInFriendList); - if (!userId) return null; - return User.get(userId); + getGroupById(groupId: string | number): Group | null { + const normalizedId = normalizeGroupId(groupId); + if (!normalizedId) return null; + return Group.get(normalizedId); } - async findImage(ctx: seal.MsgContext, id: string): Promise { - if (/^user_avatar[:?]/.test(id)) { - const ui = await this.findUser(ctx, id.replace(/^user_avatar[:?]/, '')); - if (ui) return Image.getUserAvatar(ui.userId); + async findImage(_ctx: seal.MsgContext, id: string): Promise { + if (/^user_avatar[::?]/.test(id)) { + const userId = normalizeUserId(id.replace(/^user_avatar[::?]/, '')); + if (userId) return Image.getUserAvatar(userId); + return null; } - if (/^group_avatar[:?]/.test(id)) { - const gi = await this.findGroup(ctx, id.replace(/^group_avatar[:?]/, '')); - if (gi) return Image.getGroupAvatar(gi.groupId); + if (/^group_avatar[::?]/.test(id)) { + const groupId = normalizeGroupId(id.replace(/^group_avatar[::?]/, '')); + if (groupId) return Image.getGroupAvatar(groupId); + return null; } const img = Image.get(id); if (img) return img; @@ -424,9 +349,4 @@ export class Context { } } - async findGroup(ctx: seal.MsgContext, groupName: string | number): Promise { - const groupId = await this.findGroupId(ctx, groupName); - if (!groupId) return null; - return Group.get(groupId); - } } diff --git a/src/memory/memory.ts b/src/memory/memory.ts index 44acff8..54b932b 100644 --- a/src/memory/memory.ts +++ b/src/memory/memory.ts @@ -1,4 +1,4 @@ -// 记忆服务:MemoryItem 存取/检索/权重/短期记忆(含旧格式迁移) +// 记忆服务:MemoryItem 存取/检索/权重/短期记忆 import Agent from "../agent/agent"; import Config from "../config/config"; import { VECTOR_SIMILARITY } from "../config/static_config"; @@ -543,9 +543,7 @@ export default class MemoryService { // 目前数量级应该没什么优化的需求 /** - * 兼容旧存储格式(src/AI/memory.ts 中的 MemoryManager)的过渡类。 - * 负责把历史 AI_* 存档里的旧 Memory 字段迁移成新 MemoryItem, - * 迁移完成后可删除。 + * 旧存档读取器:仅按当前 MemoryItem 字段复活数据,不再解析旧的名称/列表字段。 */ export class MemoryManager extends MemoryService { static validKeysMap: { [key in keyof MemoryManager]?: TypeDescriptor } = { @@ -585,8 +583,8 @@ export class MemoryManager extends MemoryService { vector: m.vector || [], tags: m.tags || m.keywords || [], relatedMemories: m.relatedMemories || [], - users: m.users || (m.userList || []).map((u: any) => u.id), - groups: m.groups || (m.groupList || []).map((g: any) => g.id) + users: Array.isArray(m.users) ? m.users : [], + groups: Array.isArray(m.groups) ? m.groups : [] }); this.memoryMap[id] = item; } diff --git a/src/memory/session_memory.ts b/src/memory/session_memory.ts index 29d7c43..2f1c88e 100644 --- a/src/memory/session_memory.ts +++ b/src/memory/session_memory.ts @@ -9,6 +9,7 @@ import { GroupInfo, UserInfo } from "../session/types"; import User from "../session/user"; import { buildContent } from "../utils/message"; import { stripInternalTags, truncateText } from "../utils/string"; +import { normalizeGroupId, normalizeUserId } from "../utils/target_id"; import { TypeDescriptor } from "../utils/utils"; import MemoryService from "./memory"; @@ -97,11 +98,11 @@ export default class SessionMemoryService extends MemoryService { content: string, memories: { memory_type: 'private' | 'group', - name: string, + target_id: string, text: string, keywords?: string[], - userList?: string[], - groupList?: string[], + related_user_ids?: string[], + related_group_ids?: string[], visibility?: 'public' | 'private', }[] }; @@ -115,14 +116,12 @@ export default class SessionMemoryService extends MemoryService { this.limitSummaries(); bumpSummaryRevision(); - // 与 add_memory 工具一致:按 memory_type/name 决定记忆归属(个人→目标用户会话,群聊→目标群会话)。 - // 模型不保证遵守模板:memories 缺失/非数组时跳过落库(摘要仍保留),逐条定位失败仅跳过该条, - // 缺失/未知 memory_type 时兜底归属当前会话(总结的就是当前对话) + // 与 add_memory 工具一致:按 memory_type/target_id 决定记忆归属。 + // 目标字段只接受 ID;缺少 target_id 时,仅允许当前会话作为摘要归属,不读取任何旧字段。 const memoryItems = memoryData.memories; if (!Array.isArray(memoryItems)) { Logger.warning('总结记忆:模型返回的 memories 不是数组,本次不落库记忆'); } else { - const lastCtx = this.session.lastCtx; const sessionIsGroup = this.session.sessionType === 'group'; let successCount = 0; let fallbackCount = 0; @@ -135,71 +134,66 @@ export default class SessionMemoryService extends MemoryService { const normalizedVisibility: 'public' | 'private' = m.visibility === 'private' ? 'private' : 'public'; let targetSession: Session | null = null; if (m.memory_type === 'private') { - // 与 add_memory 一致:name 存在时按名称定位目标用户,缺失时仅当前私聊会话可作归属 - if (m.name) { - if (!lastCtx) { - skipped.push('无可用上下文,无法定位个人记忆目标'); + if (m.target_id) { + const targetId = normalizeUserId(m.target_id); + if (!targetId) { + skipped.push(`用户目标ID格式无效<${m.target_id}>`); continue; } - const ui = await this.session.context.findUser(lastCtx, m.name, true); - if (ui === null) { - skipped.push(`未找到用户<${m.name}>`); - continue; - } - // 与 add_memory 工具(getSession → Agent.get('*'))一致,避免引入循环依赖 - targetSession = Agent.get('*').sessionService.getSession(ui.userId); + targetSession = Agent.get('*').sessionService.getSession(targetId); } else if (!sessionIsGroup) { targetSession = this.session; + fallbackCount++; } else { - skipped.push('缺少 name,无法定位个人记忆'); + skipped.push('缺少 target_id,无法定位个人记忆'); continue; } } else if (m.memory_type === 'group') { - if (m.name) { - if (!lastCtx) { - skipped.push('无可用上下文,无法定位群聊记忆目标'); - continue; - } - const gi = await this.session.context.findGroup(lastCtx, m.name); - if (gi === null) { - skipped.push(`未找到群聊<${m.name}>`); + if (m.target_id) { + const targetId = normalizeGroupId(m.target_id); + if (!targetId) { + skipped.push(`群目标ID格式无效<${m.target_id}>`); continue; } - targetSession = Agent.get('*').sessionService.getSession(gi.groupId); + targetSession = Agent.get('*').sessionService.getSession(targetId); } else if (sessionIsGroup) { targetSession = this.session; + fallbackCount++; } else { - skipped.push('缺少 name,无法定位群聊记忆'); + skipped.push('缺少 target_id,无法定位群聊记忆'); continue; } } else { - // 缺失/未知 memory_type:兜底归属当前会话 - if (m.memory_type) skipped.push(`未知 memory_type<${m.memory_type}>,按当前会话归属`); - else skipped.push('缺少 memory_type,按当前会话归属'); + skipped.push(`未知 memory_type<${m.memory_type}>,按当前会话归属`); fallbackCount++; targetSession = this.session; } - // 与 add_memory 工具一致:解析相关用户/群聊名称列表;无可用上下文时跳过解析(不影响落库) const uiList: UserInfo[] = []; - if (lastCtx) { - for (const n of (Array.isArray(m.userList) ? m.userList : [])) { - const ui = await targetSession.context.findUser(lastCtx, n, true); - if (ui !== null) uiList.push({ isPrivate: true, id: ui.userId, name: ui.userName }); + for (const userId of (Array.isArray(m.related_user_ids) ? m.related_user_ids : [])) { + const normalizedUserId = normalizeUserId(userId); + if (!normalizedUserId) { + skipped.push(`相关用户ID格式无效<${userId}>`); + continue; } + const ui = targetSession.context.getUserById(normalizedUserId); + if (ui !== null) uiList.push({ isPrivate: true, id: ui.userId, name: ui.userName }); } const giList: GroupInfo[] = []; - if (lastCtx) { - for (const n of (Array.isArray(m.groupList) ? m.groupList : [])) { - const gi = await targetSession.context.findGroup(lastCtx, n); - if (gi !== null) giList.push({ isPrivate: false, id: gi.groupId, name: gi.groupName }); + for (const groupId of (Array.isArray(m.related_group_ids) ? m.related_group_ids : [])) { + const normalizedGroupId = normalizeGroupId(groupId); + if (!normalizedGroupId) { + skipped.push(`相关群ID格式无效<${groupId}>`); + continue; } + const gi = targetSession.context.getGroupById(normalizedGroupId); + if (gi !== null) giList.push({ isPrivate: false, id: gi.groupId, name: gi.groupName }); } await targetSession.memory.addMemory(null, targetSession, uiList, giList, Array.isArray(m.keywords) ? m.keywords : [], [], m.text, normalizedVisibility); successCount++; } if (skipped.length > 0) { - Logger.warning(`总结记忆:成功写入 ${successCount} 条(含 ${fallbackCount} 条归属兜底),跳过 ${skipped.length} 条(${Array.from(new Set(skipped)).join(';')})`); + Logger.warning(`总结记忆:成功写入 ${successCount} 条(含 ${fallbackCount} 条当前会话归属),跳过 ${skipped.length} 条(${Array.from(new Set(skipped)).join(';')})`); } } } catch (e) { diff --git a/src/pipeline.ts b/src/pipeline.ts index b73ea74..d20e5c8 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -4,6 +4,7 @@ import Config, { ext } from "./config/config"; import { CQ_TYPES_ALLOW } from "./config/static_config"; import { logger } from "./logger"; import { getSession } from "./session/session_service"; +import { dispatchLocalCommandOutput } from "./tool/local_command_capture"; import { triggerConditionMap } from "./tool/tools/core/tool_trigger"; import { expandForwardMessage } from "./utils/ob11"; import { createCtx, createMsg } from "./utils/seal"; @@ -473,8 +474,11 @@ export class MessagePipeline { ? messageArray.map(item => item.type === 'text' ? ((item.data && item.data.text) || '') : `[${item.type}]`).join('') : message; - const deliver = session.tool.listen.push || session.tool.listen.resolve; - deliver?.(messageText); + const captured = dispatchLocalCommandOutput(sid, messageText); + if (!captured) { + const deliver = session.tool.listen.push || session.tool.listen.resolve; + deliver?.(messageText); + } const { RECEIVE_MSG_BY_BOT: allmsg } = Config.received; if (!allmsg) return; diff --git a/src/prompt/templates.ts b/src/prompt/templates.ts index 55902eb..31ea9cf 100644 --- a/src/prompt/templates.ts +++ b/src/prompt/templates.ts @@ -16,8 +16,8 @@ const TEMPLATES: { [key: string]: string } = { - 会话ID:{{{sessionId}}} - 当前时间:**CURRENT_TIME** -- [at:xxx]表示@某个群成员 -- [poke:xxx]表示戳一戳某个群成员 +- [at:用户ID]表示@某个群成员,用户ID使用QQ号或规范化用户ID +- [poke:用户ID]表示戳一戳某个群成员,用户ID使用QQ号或规范化用户ID - [from:xxx]表示消息来源,xxx为发送者名称,用户消息带此前缀(含QQ号);同一发送者连续发言时仅首条带 - [msg_id:xxx]表示消息ID,xxx为对应消息的ID,引用某条消息时使用[quote:xxx] - [time:xxxx-xx-xx xx:xx:xx]表示消息发送时间 @@ -30,17 +30,15 @@ const TEMPLATES: { [key: string]: string } = { {{#if RECEIVE_IMAGE}} - [img:xxxxxx:yyy]表示图片,其中xxxxxx为6位的图片id,yyy为图片描述(可能没有),如果要发送出现过的图片请使用[img:xxxxxx]的格式 {{/if}} -- 可使用[avatar:xxxxxx]发送用户头像,其中xxxxxx为用户名称或用户ID -- 可使用[group_avatar:xxxxxx]发送群聊头像,其中xxxxxx为群聊名称或群聊ID +- 可使用[avatar:用户ID]发送用户头像 +- 可使用[group_avatar:群ID]发送群聊头像 - 可使用[img:图片ID]发送本地图片,可用名称先通过 list_resources(type=image) 查询 -## 音频相关 -- 可使用[audio:音频ID]发送本地音频,可用名称先通过 list_resources(type=audio) 查询 - -## 文件与视频相关 -- 可使用send_file工具发送本地文件,可用名称先通过 list_resources(type=file) 查询;也可直接传 path -- 可使用send_video工具发送本地视频,可用名称先通过 list_resources(type=video) 查询;也可直接传 path -- send_image、send_file、send_video、record 的 path 支持本地绝对路径、file:// URI;MCP 沙箱文件使用 mcp://服务器名/沙箱相对路径,或 source=mcp + server + path +## OB11 消息与资源 +- 所有协议消息只能使用 call_ob11_api:选择 send_private_msg/send_group_msg,并在 message 中传文本或消息段数组。 +- 图片、语音、视频、文件使用 image/record/video/file 消息段;本地资源先用 list_resources 查询,再将 file 写成 resource:资源ID。 +- 文件区上传使用 upload_group_file/upload_private_file,不要把上传动作伪装成普通 file 消息段。 +- 资源路径支持本地绝对路径、file:// URI、HTTP(S) URL、base64://;MCP 沙箱文件使用 mcp://服务器名/沙箱相对路径。 **DYNAMIC_SECTIONS** @@ -106,8 +104,8 @@ const TEMPLATES: { [key: string]: string } = { - 当前私聊:<{{{用户名称}}}>({{{用户号码}}}) {{else}} - 当前群聊:<{{{群聊名称}}}>({{{群聊号码}}}) - - [at:xxx]表示@某个群成员 - - [poke:xxx]表示戳一戳某个群成员 + - [at:用户ID]表示@某个群成员,用户ID使用QQ号或规范化用户ID + - [poke:用户ID]表示戳一戳某个群成员,用户ID使用QQ号或规范化用户ID - [quote:xxx]表示引用消息,xxx为对应的消息ID - [face:xxx]表示使用某个表情,xxx为表情名称,注意与img表情包区分 {{/if}} @@ -134,9 +132,9 @@ const TEMPLATES: { [key: string]: string } = { description: "记忆归属,个人或群聊,与可见性无关。", enum: ["private", "group"] }, - "name": { + "target_id": { type: 'string', - description: '用户名称或群聊名称或纯数字QQ号、群号,实际使用时与记忆类型对应' + description: '目标用户ID或群ID,实际使用时与记忆类型对应' }, "text": { type: 'string', @@ -144,21 +142,21 @@ const TEMPLATES: { [key: string]: string } = { }, "keywords": { type: 'array', - description: '相关用户名称列表', + description: '相关关键词列表', items: { type: 'string' } }, - "userList": { + "related_user_ids": { type: 'array', - description: '相关用户名称列表', + description: '相关用户ID列表', items: { type: 'string' } }, - "groupList": { + "related_group_ids": { type: 'array', - description: '相关群聊名称列表', + description: '相关群ID列表', items: { type: 'string' } @@ -169,7 +167,7 @@ const TEMPLATES: { [key: string]: string } = { enum: ["public", "private"] } }, - "required": ['memory_type', 'name', 'text'] + "required": ['memory_type', 'target_id', 'text'] } } }` diff --git a/src/tool/command_target.ts b/src/tool/command_target.ts index af7fb5d..a9144ee 100644 --- a/src/tool/command_target.ts +++ b/src/tool/command_target.ts @@ -1,14 +1,23 @@ import { createCtx, createMsg } from "../utils/seal"; +import { getRawId, normalizeGroupId, normalizeUserId } from "../utils/target_id"; export interface CommandTargetOptions { - triggerUserId?: string; - atUserId?: string; + /** 显式指定指令消息的触发用户;未填写时由调用方使用当前会话用户。 */ + trigger?: string; + /** 指令消息中的 @ 用户列表。 */ + at: string[]; } -/** 将 QQ:123 / 123 等用户标识统一为可注入 OB11/海豹消息的纯用户 ID。 */ +/** 将用户标识严格规范化为可注入 OB11/海豹消息的原始用户 ID。 */ export function normalizeCommandUserId(value: any): string | undefined { - const text = String(value == null ? '' : value).trim().replace(/^.+:/, ''); - return text || undefined; + const normalized = normalizeUserId(String(value == null ? '' : value)); + return normalized ? getRawId(normalized) : undefined; +} + +export function currentCommandUserId(ctx: seal.MsgContext): string { + return normalizeCommandUserId(ctx.player && ctx.player.userId) + || normalizeCommandUserId(ctx.endPoint.userId) + || ''; } export function qualifyCommandUserId(ctx: seal.MsgContext, userId: string): string { @@ -16,27 +25,34 @@ export function qualifyCommandUserId(ctx: seal.MsgContext, userId: string): stri return platform ? `${platform}:${userId}` : userId; } -export function resolveCommandTarget(ctx: seal.MsgContext, args: { [key: string]: any } | undefined): CommandTargetOptions & { triggerUserId: string } { - const currentUserId = normalizeCommandUserId(ctx.player && ctx.player.userId) || normalizeCommandUserId(ctx.endPoint.userId) || ''; - const triggerUserId = normalizeCommandUserId(args && args.triggerUserId) || currentUserId; - const atUserId = normalizeCommandUserId(args && args.atUserId); - return { triggerUserId, ...(atUserId ? { atUserId } : {}) }; +/** 解析工具参数;不对旧字段做兼容,at 必须是数组。 */ +export function resolveCommandTarget(ctx: seal.MsgContext, args: { [key: string]: any } | undefined): CommandTargetOptions & { effectiveTrigger: string } { + const trigger = normalizeCommandUserId(args && args.trigger); + const at = Array.isArray(args && args.at) + ? args.at.map(normalizeCommandUserId).filter((item): item is string => !!item) + : []; + return { + ...(trigger ? { trigger } : {}), + at, + effectiveTrigger: trigger || currentCommandUserId(ctx) + }; } export function buildCommandContext( ctx: seal.MsgContext, - target: CommandTargetOptions & { triggerUserId: string } + target: { trigger: string; at: string[] } ): { ctx: seal.MsgContext; msg: seal.Message } { - const groupId = ctx.group && normalizeCommandUserId(ctx.group.groupId) || ''; - const msg = createMsg(ctx.isPrivate ? 'private' : 'group', target.triggerUserId, groupId); - if (target.atUserId) { - const atText = `[CQ:at,qq=${target.atUserId}]`; + const normalizedGroupId = ctx.group && normalizeGroupId(ctx.group.groupId); + const groupId = normalizedGroupId ? getRawId(normalizedGroupId) : ''; + const msg = createMsg(ctx.isPrivate ? 'private' : 'group', target.trigger, groupId); + if (target.at.length) { + const atText = target.at.map(userId => `[CQ:at,qq=${userId}]`).join(' '); msg.message = `${atText} `; - (msg as any).segment = [{ - target: target.atUserId, - isRobot: target.atUserId === normalizeCommandUserId(ctx.endPoint.userId), + (msg as any).segment = target.at.map(userId => ({ + target: userId, + isRobot: userId === normalizeCommandUserId(ctx.endPoint.userId), type: () => 1 - }]; + })); } if (msg.sender) msg.sender.nickname = ctx.player && ctx.player.name || ''; const targetCtx = createCtx(ctx.endPoint.userId, msg); diff --git a/src/tool/extension_executor.ts b/src/tool/extension_executor.ts index 74b3844..af89ca0 100644 --- a/src/tool/extension_executor.ts +++ b/src/tool/extension_executor.ts @@ -1,9 +1,8 @@ -// 本地扩展指令执行器:直接调用 SealDice 扩展 cmdMap 的 solve,无需依赖 OB11 核心桥。 -// 仅用于扩展指令;核心指令请走 run_core_command(经 ob11-core-bridge 中间件中转)。 import Logger from "../logger"; import { ResolvedCommand } from "./command_catalog"; -import { buildCommandContext } from "./command_target"; +import { buildCommandContext, currentCommandUserId } from "./command_target"; +import { registerLocalCommandCapture } from "./local_command_capture"; import { ToolListen } from "./types"; const RE_KEYWORD = /^--([^\s=]+)(?:=(\S+))?$/; @@ -34,10 +33,8 @@ function splitKwargs(plainArgs: string[]): { args: string[]; kwargs: seal.Kwarg[ } /** - * 依据海豹核心 CmdArgs 语义构造一个全新的指令参数对象。 - * 不污染会话最近一次真实指令的 cmdArgs,也不要求会话里先收到过指令。 - * 调用扩展 solve 时 goja 会把该对象按 jsbind 标签转换回 Go *CmdArgs, - * 因此 solve 内部拿到的是带真实方法的对象,这里的方法实现仅作为类型兜底。 + * 依据海豹 CmdArgs 语义构造一个全新的指令参数对象。 + * 不污染会话最近一次真实指令的参数,也不要求会话里先收到过指令。 */ export function buildCmdArgs(ctx: seal.MsgContext, command: string, plainArgs: string[], at: seal.AtInfo[], prefix: string): seal.CmdArgs { const { args, kwargs } = splitKwargs(plainArgs); @@ -102,8 +99,12 @@ export interface LocalExecutionOptions { settleMs?: number; maxMessages?: number; at?: seal.AtInfo[]; - triggerUserId?: string; - atUserId?: string; + /** 显式触发用户;不填写时保留原始会话 ctx/msg。 */ + trigger?: string; +} + +function sessionLane(ctx: seal.MsgContext): string { + return ctx.isPrivate ? String(ctx.player && ctx.player.userId || '') : String(ctx.group && ctx.group.groupId || ''); } /** 本地执行一条已解析的扩展指令,并用会话监听器收集多条 bot 回复。 */ @@ -123,17 +124,18 @@ export async function executeExtensionLocally( const timeoutMs = options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 10000; const settleMs = options.settleMs != null && options.settleMs >= 0 ? options.settleMs : 400; const maxMessages = options.maxMessages && options.maxMessages > 0 ? options.maxMessages : 20; - const targetContext = options.triggerUserId - ? buildCommandContext(ctx, { triggerUserId: options.triggerUserId, atUserId: options.atUserId }) + const at = options.at || []; + const useSyntheticContext = options.trigger !== undefined || at.length > 0; + const targetContext = useSyntheticContext + ? buildCommandContext(ctx, { trigger: options.trigger || currentCommandUserId(ctx), at: at.map(item => item.userId.replace(/^.+:/, '')) }) : { ctx, msg }; const executionCtx = targetContext.ctx; const executionMsg = targetContext.msg; - const atText = (options.at || []).map(item => `[CQ:at,qq=${item.userId.replace(/^.+:/, '')}]`).join(' '); + const atText = at.map(item => `[CQ:at,qq=${item.userId.replace(/^.+:/, '')}]`).join(' '); executionMsg.message = [atText, `${prefix}${rc.cmd}`, plainArgs.join(' ')].filter(Boolean).join(' '); - const cmdArgs = buildCmdArgs(executionCtx, rc.cmd, plainArgs, options.at || [], prefix); + const cmdArgs = buildCmdArgs(executionCtx, rc.cmd, plainArgs, at, prefix); const listen = session.tool.listen; - // waitFor 是当前监听器的消息收集入口;resolve 仅作为旧会话对象的防御性兜底。 const responsePromise = listen.waitFor ? listen.waitFor(timeoutMs, settleMs, maxMessages) : new Promise(resolve => { @@ -143,12 +145,17 @@ export async function executeExtensionLocally( listen.cleanup(); }; }); + const unregisterCapture = registerLocalCommandCapture( + Array.from(new Set([sessionLane(ctx), sessionLane(executionCtx)])), + content => (listen.push || listen.resolve)?.(content) + ); let solved = false; try { const result = await Promise.resolve(ext.cmdMap[rc.cmd].solve(executionCtx, executionMsg, cmdArgs)) as { solved?: boolean } | undefined; solved = !!(result && result.solved); } catch (e) { + unregisterCapture(); listen.cleanup(); await responsePromise.catch(() => []); Logger.warning(`[run_ext_command] 本地执行 ${rc.extName}|${rc.cmd} 抛异常:${e instanceof Error ? e.message : String(e)}`); @@ -156,6 +163,7 @@ export async function executeExtensionLocally( } const messages = await responsePromise; + unregisterCapture(); if (messages.length) return messages.join('\n'); return solved ? '海豹未返回文本消息' : '指令未响应(扩展返回 solved=false),海豹未返回文本消息'; } diff --git a/src/tool/local_command_capture.ts b/src/tool/local_command_capture.ts new file mode 100644 index 0000000..e26b866 --- /dev/null +++ b/src/tool/local_command_capture.ts @@ -0,0 +1,34 @@ +/** + * 本地扩展工具调用的出站消息捕获器。 + * + * SealDice 的 onMessageSend 回调只携带出站消息上下文,不能保证与工具调用使用同一个 + * 临时 ctx 对象。因此这里按会话 lane 关联调用,避免依赖对象 identity;同一 lane 的 + * 并发工具调用会像普通 session listener 一样同时收到消息。 + */ +type CaptureHandler = (content: string) => void; + +interface CaptureEntry { + lanes: Set; + handler: CaptureHandler; +} + +const captures = new Set(); + +export function registerLocalCommandCapture(lanes: string[], handler: CaptureHandler): () => void { + const entry: CaptureEntry = { + lanes: new Set(lanes.filter(Boolean)), + handler + }; + captures.add(entry); + return () => captures.delete(entry); +} + +export function dispatchLocalCommandOutput(lane: string, content: string): boolean { + let captured = false; + for (const entry of Array.from(captures)) { + if (!entry.lanes.has(lane)) continue; + captured = true; + entry.handler(content); + } + return captured; +} diff --git a/src/tool/mcp/adapters.ts b/src/tool/mcp/adapters.ts index 721d95e..5a8296c 100644 --- a/src/tool/mcp/adapters.ts +++ b/src/tool/mcp/adapters.ts @@ -5,6 +5,7 @@ import Logger from "../../logger"; import Image from "../../resource/image"; import { Session } from "../../session/session"; import { parseSpecialTokens } from "../../utils/string"; +import { getRawId, normalizeGroupId, normalizeUserId } from "../../utils/target_id"; import { generateId } from "../../utils/utils"; import { isAllowedCore, splitEntry, whitelistEntries } from "../command_catalog"; import { resolveCommandTarget } from "../command_target"; @@ -120,13 +121,11 @@ async function transformContentToUrlText(ctx: seal.MsgContext, session: Session, break; } case 'at': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) { - text += ` @${ui.userName} `; - } else { - Logger.warning(`无法找到用户:${name}`); - text += ` @${name} `; + const userId = normalizeUserId(seg.content); + if (userId) text += ` @${getRawId(userId)} `; + else { + Logger.warning(`用户ID格式无效:${seg.content}`); + text += ` @${seg.content} `; } break; } @@ -144,27 +143,21 @@ async function transformContentToUrlText(ctx: seal.MsgContext, session: Session, break; } case 'avatar': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) { - const image = Image.getUserAvatar(ui.userId); + const userId = normalizeUserId(seg.content); + if (userId) { + const image = Image.getUserAvatar(userId); images.push(image); text += image.url; - } else { - Logger.warning(`无法找到用户:${name}`); - } + } else Logger.warning(`用户ID格式无效:${seg.content}`); break; } case 'group_avatar': { - const name = seg.content; - const gi = await session.context.findGroup(ctx, name); - if (gi) { - const image = Image.getGroupAvatar(gi.groupId); + const groupId = normalizeGroupId(seg.content); + if (groupId) { + const image = Image.getGroupAvatar(groupId); images.push(image); text += image.url; - } else { - Logger.warning(`无法找到群聊:${name}`); - } + } else Logger.warning(`群ID格式无效:${seg.content}`); break; } } @@ -279,7 +272,7 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; const commandTarget = resolveCommandTarget(ctx, args); - if (commandTarget.atUserId && ctx.isPrivate) return '私聊消息不支持 atUserId'; + if (commandTarget.at.length && ctx.isPrivate) return '私聊消息不支持 at'; const options = captureOptions(args, 50, 500); if (authorizedCommand === 'ext' && !(args && args.captureMode)) options.capture.mode = 'lane'; @@ -291,7 +284,7 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { } = { selfId: String(ctx.endPoint.userId || '').replace(/^.+:/, ''), messageType: ctx.isPrivate ? 'private' : 'group', - userId: commandTarget.triggerUserId + userId: commandTarget.effectiveTrigger }; if (!ctx.isPrivate) target.groupId = String(ctx.group && ctx.group.groupId || '').replace(/^.+:/, ''); @@ -300,7 +293,7 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { action: 'call', target, actor: { - userId: commandTarget.triggerUserId || target.selfId, + userId: commandTarget.effectiveTrigger || target.selfId, nickname: String(ctx.player && ctx.player.name || 'AI'), role: 'member' }, @@ -310,8 +303,8 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { forward: options.capture.forward, timeoutMs: options.timeoutMs, __commandPrefix: Config.tool.COMMAND_PREFIX, - triggerUserId: commandTarget.triggerUserId, - ...(commandTarget.atUserId ? { atUserId: commandTarget.atUserId } : {}) + trigger: commandTarget.effectiveTrigger, + at: commandTarget.at }; if (hasRawMessage) remoteArgs.raw_message = rawMessage; else { diff --git a/src/tool/skills.ts b/src/tool/skills.ts index d01d233..f6cbf22 100644 --- a/src/tool/skills.ts +++ b/src/tool/skills.ts @@ -1,5 +1,6 @@ // Skills:配置驱动的技能,AI 通过 use_skill 工具按需获取技能内容 import { ext } from "../config/config"; +import { OB11_API_SKILLS } from "../config/static_config/ob11_api_skills"; import { SEALDICE_COMMAND_SKILLS } from "../config/static_config/sealdice_command_defaults"; import Logger from "../logger"; @@ -11,7 +12,7 @@ interface Skill { content: string; } -const MAX_SKILL_CONTENT_LENGTH = 4000; // 单次返回的技能内容上限 +const MAX_SKILL_CONTENT_LENGTH = 12000; // 单次返回的技能内容上限 const MAX_REF_DEPTH = 2; // 技能间引用的最大解析深度 /** 解析标准 SKILL.md frontmatter(--- 开头,name/description 键值对),兼容 Claude/Codex/Cursor 等 agent 的技能文件 */ @@ -59,7 +60,7 @@ function getSkills(): Skill[] { // registerTemplateConfig 不会覆盖已有安装的配置。为避免升级后新默认技能(如“录卡”) // 因旧配置持久化而不可用,补入缺失的默认技能;同名自定义技能优先保留。 const configuredNames = new Set(configured.map(skill => skill.name)); - const defaults = SEALDICE_COMMAND_SKILLS + const defaults = [...SEALDICE_COMMAND_SKILLS, ...OB11_API_SKILLS] .map(line => parseSkillEntry(line.trim())) .filter(skill => skill.name && !configuredNames.has(skill.name)); return configured.concat(defaults); diff --git a/src/tool/tool.ts b/src/tool/tool.ts index 983f448..ca06509 100644 --- a/src/tool/tool.ts +++ b/src/tool/tool.ts @@ -23,6 +23,7 @@ export const CORE_TOOL_NAMES: string[] = [ 'search_tools', // 按需发现工具(返回完整参数说明) 'call_tool', // 统一执行入口(调用任意已开启工具) 'use_skill', // 技能调用 + 'call_ob11_api', // 唯一 OB11 API 调用入口 'run_ext_command', // 扩展指令调用 'run_core_command' // 核心指令调用 ]; diff --git a/src/tool/tools/context/tool_context.ts b/src/tool/tools/context/tool_context.ts index 3836375..8b313b9 100644 --- a/src/tool/tools/context/tool_context.ts +++ b/src/tool/tools/context/tool_context.ts @@ -18,29 +18,29 @@ export function registerContext() { description: "上下文类型,私聊或群聊", enum: ["private", "group"] }, - name: { + target_id: { type: 'string', - description: '用户名称或群聊名称或纯数字QQ号、群号,实际使用时与上下文类型对应' + description: '目标用户ID或群ID,与上下文类型对应,不支持使用名称' } }, - required: ["ctx_type", "name"] + required: ["ctx_type", "target_id"] } } }); toolGet.solve = async (ctx, _, session, args) => { - const { ctx_type, name } = args; + const { ctx_type, target_id } = args; if (ctx_type === "private") { - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; + const ui = await session.context.getUserById(target_id); + if (ui === null) return `未找到目标ID<${target_id}>`; if (ui.userId === ctx.player!.userId && ctx.isPrivate) return `向当前私聊发送消息无需调用函数`; if (ui.userId === ctx.endPoint.userId) return `禁止向自己发送消息`; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, ui.userId, '')); session = getSession(ui.userId); } else if (ctx_type === "group") { - const gi = await session.context.findGroup(ctx, name); - if (gi === null) return `未找到<${name}>`; + const gi = await session.context.getGroupById(target_id); + if (gi === null) return `未找到目标ID<${target_id}>`; if (gi.groupId === ctx.group!.groupId) return `向当前群聊发送消息无需调用函数`; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, '', gi.groupId)); diff --git a/src/tool/tools/core/tool_cmd.ts b/src/tool/tools/core/tool_cmd.ts index aba9bfb..9c4b352 100644 --- a/src/tool/tools/core/tool_cmd.ts +++ b/src/tool/tools/core/tool_cmd.ts @@ -32,7 +32,7 @@ export function registerCmdTool(): Tool { type: 'function', function: { name: 'run_ext_command', - description: `在 SealDice 本地直接调用扩展指令的 solve,不依赖核心桥/中间件,无需 MCP 与协议端;可用 triggerUserId 指定触发对象、atUserId 模拟 @ 对象。扩展分为 builtin(fun/story/coc7/deck/dnd5e/exp/log/reply)与 non_builtin(第三方扩展及本插件),无需配置内置扩展列表。action=list 可按 kind 列出指令;action=call 执行指令,并收集扩展发出的多条消息作为返回。可调用指令仍受「可调用指令白名单」约束,白名单格式为扩展名|指令名/别名1/别名2,同一元素内的别名用 / 分隔。核心指令请使用 run_core_command。`, + description: `在 SealDice 本地直接调用扩展指令的 solve,不依赖核心桥/中间件,无需 MCP 与协议端;可用 trigger 指定触发对象、at 模拟 @ 对象。扩展分为 builtin(fun/story/coc7/deck/dnd5e/exp/log/reply)与 non_builtin(第三方扩展及本插件),无需配置内置扩展列表。action=list 可按 kind 列出指令;action=call 执行指令,并收集扩展发出的多条消息作为返回。可调用指令仍受「可调用指令白名单」约束,白名单格式为扩展名|指令名/别名1/别名2,同一元素内的别名用 / 分隔。核心指令请使用 run_core_command。`, parameters: { type: 'object', properties: { @@ -41,8 +41,8 @@ export function registerCmdTool(): Tool { extension: { type: 'string', description: '扩展名;call 时可填写,核心扩展名不是这里的 core' }, command: { type: 'string', description: '指令名;也支持直接填写 扩展名|指令名' }, args: { type: 'array', items: { type: 'string' }, description: '指令参数,按顺序填写' }, - triggerUserId: { type: 'string', description: '可选;指定这条指令消息的发送者/触发对象(用户 ID),不填则使用当前对话用户' }, - atUserId: { type: 'string', description: '可选;让注入的指令消息 @ 此用户(用户 ID);群聊可用,私聊不支持' }, + trigger: { type: 'string', description: '可选;指定这条指令消息的发送者/触发对象(用户 ID),不填则使用当前对话用户' }, + at: { type: 'array', items: { type: 'string' }, description: '可选;让注入的指令消息 @ 这些用户(用户 ID);群聊可用,私聊不支持' }, maxMessages: { type: 'integer', minimum: 1, maximum: 50, description: '最多收集多少条回复消息' }, settleMs: { type: 'integer', minimum: 0, maximum: 10000, description: '收到消息后等待多久没有新消息才结束' }, timeoutMs: { type: 'integer', minimum: 100, maximum: 120000, description: '最长等待时间,单位毫秒' } @@ -70,12 +70,12 @@ export function registerCmdTool(): Tool { if (!isAllowedExtension(rc)) return `扩展指令 ${requested} 不在可调用指令白名单内,无法调用`; const cmdArgs = Array.isArray(args && args.args) ? args.args.map(String) : []; const target = resolveCommandTarget(ctx, args); - if (target.atUserId && ctx.isPrivate) return '私聊消息不支持 atUserId'; - const at = target.atUserId ? [{ - userId: qualifyCommandUserId(ctx, target.atUserId), - isRobot: target.atUserId === String(ctx.endPoint.userId || '').replace(/^.+:/, ''), + if (target.at.length && ctx.isPrivate) return '私聊消息不支持 at'; + const at = target.at.map(userId => ({ + userId: qualifyCommandUserId(ctx, userId), + isRobot: userId === String(ctx.endPoint.userId || '').replace(/^.+:/, ''), name: '' - }] : []; + })); try { const content = await executeExtensionLocally(ctx, msg, session, rc, cmdArgs, { prefix: Config.tool.COMMAND_PREFIX, @@ -83,8 +83,7 @@ export function registerCmdTool(): Tool { settleMs: optionNumber(args && args.settleMs), maxMessages: optionNumber(args && args.maxMessages), at, - triggerUserId: target.triggerUserId, - atUserId: target.atUserId + trigger: target.trigger }); return `扩展指令 ${requested} 返回:\n${content}`; } catch (e) { diff --git a/src/tool/tools/core/tool_trigger.ts b/src/tool/tools/core/tool_trigger.ts index f72824e..240f32c 100644 --- a/src/tool/tools/core/tool_trigger.ts +++ b/src/tool/tools/core/tool_trigger.ts @@ -16,9 +16,9 @@ export function registerSetTrigger() { type: 'string', description: '触发关键词,可使用正则表达式,为空时任意消息都可触发' }, - name: { + user_id: { type: 'string', - description: '指定触发必须满足的用户名称或纯数字QQ号,为空时任意用户均可触发' + description: '指定触发必须满足的用户ID,为空时任意用户均可触发' }, reason: { type: 'string', @@ -30,7 +30,7 @@ export function registerSetTrigger() { } }); tool.solve = async (ctx, _, session, args) => { - const { keyword = '', name = '', reason } = args; + const { keyword = '', user_id = '', reason } = args; const condition = { keyword: '', @@ -47,9 +47,9 @@ export function registerSetTrigger() { } } - if (name) { - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; + if (user_id) { + const ui = await session.context.getUserById(user_id); + if (ui === null) return `未找到用户ID<${user_id}>`; if (ui.userId === ctx.endPoint.userId) return `禁止将自己设置为触发条件`; condition.uid = ui.userId; } diff --git a/src/tool/tools/image/tool_image.ts b/src/tool/tools/image/tool_image.ts index 3ddf9bf..1363e80 100644 --- a/src/tool/tools/image/tool_image.ts +++ b/src/tool/tools/image/tool_image.ts @@ -45,7 +45,7 @@ export function registerImage() { properties: { id: { type: "string", - description: '图片id,或user_avatar:用户名称或纯数字QQ号,或group_avatar:群聊名称或纯数字群号' + description: '图片ID,或user_avatar:用户ID,或group_avatar:群ID' }, content: { type: "string", @@ -94,7 +94,7 @@ export function registerImage() { }, image: { type: "string", - description: "可选参考图,用于以图生图。支持图片id、user_avatar:用户名称或纯数字QQ号、group_avatar:群聊名称或纯数字群号、http(s)图片URL、data:image/...;base64或base64数据" + description: "可选参考图,用于以图生图。支持图片ID、user_avatar:用户ID、group_avatar:群ID、http(s)图片URL、data:image/...;base64或base64数据" } }, required: ['prompt', 'save', 'name'] diff --git a/src/tool/tools/image/tool_meme.ts b/src/tool/tools/image/tool_meme.ts index fdd3f8f..6cd4167 100644 --- a/src/tool/tools/image/tool_meme.ts +++ b/src/tool/tools/image/tool_meme.ts @@ -2,6 +2,7 @@ import { logger } from "../../../logger"; import Image from "../../../resource/image"; import { GroupInfo, UserInfo } from "../../../session/types"; +import { normalizeGroupId, normalizeUserId } from "../../../utils/target_id"; import { generateId } from "../../../utils/utils"; import Tool from "../../tool"; @@ -101,7 +102,7 @@ export function registerMeme() { image_ids: { type: "array", items: { type: "string" }, - description: '图片id,或user_avatar:用户名称或纯数字QQ号,或group_avatar:群聊名称或纯数字群号' + description: '图片ID,或user_avatar:用户ID,或group_avatar:群ID' }, save: { type: "boolean", @@ -146,23 +147,21 @@ export function registerMeme() { const giList: GroupInfo[] = []; for (const id of image_ids) { if (/^user_avatar[::]/.test(id)) { - const ui = await session.context.findUser(ctx, id.replace(/^user_avatar[::]/, '')); - if (ui) { - uiList.push({ isPrivate: true, id: ui.userId, name: ui.userName }); - images.push(Image.getUserAvatar(ui.userId)); - } else { - return `用户 ${id} 不存在`; - } + const userId = normalizeUserId(id.replace(/^user_avatar[::]/, '')); + if (!userId) return `用户ID格式无效:${id}`; + const ui = session.context.getUserById(userId); + if (!ui) return `用户ID无效:${id}`; + uiList.push({ isPrivate: true, id: ui.userId, name: ui.userName }); + images.push(Image.getUserAvatar(ui.userId)); continue; } if (/^group_avatar[::]/.test(id)) { - const gi = await session.context.findGroup(ctx, id.replace(/^group_avatar[::]/, '')); - if (gi) { - giList.push({ isPrivate: false, id: gi.groupId, name: gi.groupName }); - images.push(Image.getGroupAvatar(gi.groupId)); - } else { - return `群聊 ${id} 不存在`; - } + const groupId = normalizeGroupId(id.replace(/^group_avatar[::]/, '')); + if (!groupId) return `群ID格式无效:${id}`; + const gi = session.context.getGroupById(groupId); + if (!gi) return `群ID无效:${id}`; + giList.push({ isPrivate: false, id: gi.groupId, name: gi.groupName }); + images.push(Image.getGroupAvatar(gi.groupId)); continue; } const img = await session.context.findImage(ctx, id); diff --git a/src/tool/tools/init.ts b/src/tool/tools/init.ts index e3d3152..049d63d 100644 --- a/src/tool/tools/init.ts +++ b/src/tool/tools/init.ts @@ -1,24 +1,20 @@ -// 工具注册统一入口:把 tools/ 下(含各子目录)的全部内置工具注册函数汇集于此, -// 让 tool.ts 只需引用本文件即可保持整洁 +// 工具注册统一入口:OB11 消息发送由 call_ob11_api 统一处理。 import { registerContextTools } from "./context/init"; import { registerCoreTools } from "./core/init"; import { registerImageTools } from "./image/init"; import { registerManageTools } from "./manage/init"; import { registerMemoryTools } from "./memory/init"; -import { registerMessageTools } from "./message/init"; import { registerOb11Tools } from "./ob11/init"; import { registerResourceTools } from "./resource/init"; import { registerSealTools } from "./seal/init"; import { registerWebTools } from "./web/init"; -/** 注册 tools/ 下全部内置工具(含子目录),调用顺序与原先 tool.ts 内一致 */ export function registerTools() { registerImageTools(); registerOb11Tools(); registerSealTools(); registerContextTools(); registerMemoryTools(); - registerMessageTools(); registerResourceTools(); registerCoreTools(); registerWebTools(); diff --git a/src/tool/tools/manage/tool_block.ts b/src/tool/tools/manage/tool_block.ts index 431a3eb..f242927 100644 --- a/src/tool/tools/manage/tool_block.ts +++ b/src/tool/tools/manage/tool_block.ts @@ -16,42 +16,42 @@ export function registerBlockTool() { parameters: { type: 'object', properties: { - name: { + user_id: { type: 'string', - description: '用户名称或纯数字QQ号' + description: '用户ID' }, reason: { type: 'string', description: '建议拉黑原因' } }, - required: ['name', 'reason'] + required: ['user_id', 'reason'] } } }); toolSuggestBlock.solve = async (ctx, _msg, session, args) => { - const { name, reason } = args; + const { user_id, reason } = args; - const ui = await session.context.findUser(ctx, name); - if (!ui) return `未找到<${name}>`; + const ui = await session.context.getUserById(user_id); + if (!ui) return `未找到用户ID<${user_id}>`; if (BlockManager.checkBlock(ui.userId)) { - return `用户<${name}>已经在黑名单中`; + return `用户ID<${user_id}>已经在黑名单中`; } const last = suggestCooldown[ui.userId]; if (last && Date.now() - last < SUGGEST_COOLDOWN_MS) { const remain = Math.ceil((SUGGEST_COOLDOWN_MS - (Date.now() - last)) / 1000); - return `已建议过拉黑<${name}>,冷却中(剩余约 ${remain} 秒),等待骰主处理`; + return `已建议过拉黑用户ID<${user_id}>,冷却中(剩余约 ${remain} 秒),等待骰主处理`; } suggestCooldown[ui.userId] = Date.now(); if (Config.tool.BLOCK_REQUIRE_OWNER_CONFIRM) { - ctx.notice(`AI 建议拉黑用户<${name}>(${ui.userId}),原因: ${reason}。骰主可执行 .ai block add ${ui.userId} <原因> 确认拉黑`); - return `已向骰主建议拉黑<${name}>,原因: ${reason},等待骰主确认`; + ctx.notice(`AI 建议拉黑用户ID<${ui.userId}>,原因: ${reason}。骰主可执行 .ai block add ${ui.userId} <原因> 确认拉黑`); + return `已向骰主建议拉黑用户ID<${ui.userId}>,原因: ${reason},等待骰主确认`; } BlockManager.addBlock(ui.userId, `${reason}(AI 自动拉黑,已关闭骰主确认)`); - return `已直接拉黑<${name}>(原因: ${reason}),其消息将不再触发 AI`; + return `已直接拉黑用户ID<${ui.userId}>(原因: ${reason}),其消息将不再触发 AI`; } // 移除黑名单(骰主指令之外的补充入口) @@ -63,25 +63,25 @@ export function registerBlockTool() { parameters: { type: 'object', properties: { - name: { + user_id: { type: 'string', - description: '用户名称或纯数字QQ号' + description: '用户ID' } }, - required: ['name'] + required: ['user_id'] } } }); - toolUnblock.solve = async (ctx, _msg, session, args) => { - const { name } = args; + toolUnblock.solve = async (_ctx, _msg, session, args) => { + const { user_id } = args; - const ui = await session.context.findUser(ctx, name); - if (!ui) return `未找到<${name}>`; + const ui = await session.context.getUserById(user_id); + if (!ui) return `未找到用户ID<${user_id}>`; if (BlockManager.removeBlock(ui.userId)) { - return `已将<${name}>移出黑名单`; + return `已将用户ID<${user_id}>移出黑名单`; } - return `用户<${name}>不在黑名单中`; + return `用户ID<${user_id}>不在黑名单中`; } // 查看黑名单列表 diff --git a/src/tool/tools/memory/tool_memory.ts b/src/tool/tools/memory/tool_memory.ts index 0be9a81..89fc43a 100644 --- a/src/tool/tools/memory/tool_memory.ts +++ b/src/tool/tools/memory/tool_memory.ts @@ -4,6 +4,7 @@ import { searchOptions as SearchOptions } from "../../../memory/types"; import { getSession, SessionService } from "../../../session/session_service"; import { GroupInfo, SessionInfo, UserInfo } from "../../../session/types"; import { getCtxAndMsg } from "../../../utils/seal"; +import { normalizeGroupId, normalizeUserId } from "../../../utils/target_id"; import Tool from "../../tool"; export function registerMemory() { @@ -20,9 +21,9 @@ export function registerMemory() { description: "记忆归属,个人或群聊(决定存到哪个会话),与可见性无关。", enum: ["private", "group"] }, - name: { + target_id: { type: 'string', - description: '目标用户名称或群聊名称或纯数字QQ号、群号,实际使用时与记忆类型对应' + description: '目标用户ID或群ID,实际使用时与记忆类型对应' }, text: { type: 'string', @@ -30,21 +31,21 @@ export function registerMemory() { }, keywords: { type: 'array', - description: '相关用户名称列表', + description: '相关用户ID列表', items: { type: 'string' } }, - userList: { + related_user_ids: { type: 'array', - description: '相关用户名称列表', + description: '相关用户ID列表', items: { type: 'string' } }, - groupList: { + related_group_ids: { type: 'array', - description: '相关群聊名称列表', + description: '相关群ID列表', items: { type: 'string' } @@ -55,24 +56,28 @@ export function registerMemory() { enum: ['public', 'private'] } }, - required: ['memory_type', 'name', 'text'] + required: ['memory_type', 'target_id', 'text'] } } }); toolAdd.solve = async (ctx, _, session, args) => { - const { memory_type, name, text, keywords = [], userList = [], groupList = [], visibility = 'public' } = args; + const { memory_type, target_id, text, keywords = [], related_user_ids = [], related_group_ids = [], visibility = 'public' } = args; // 规范化可见性,避免模型传入非法枚举值 const normalizedVisibility: 'public' | 'private' = visibility === 'private' ? 'private' : 'public'; if (memory_type === "private") { - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; + const normalizedTargetId = normalizeUserId(target_id); + if (normalizedTargetId === null) return `目标ID格式无效<${target_id}>`; + const ui = session.context.getUserById(normalizedTargetId); + if (ui === null) return `未找到目标ID<${target_id}>`; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, ui.userId, '')); session = getSession(ui.userId); } else if (memory_type === "group") { - const gi = await session.context.findGroup(ctx, name); - if (gi === null) return `未找到<${name}>`; + const normalizedTargetId = normalizeGroupId(target_id); + if (normalizedTargetId === null) return `目标ID格式无效<${target_id}>`; + const gi = session.context.getGroupById(normalizedTargetId); + if (gi === null) return `未找到目标ID<${target_id}>`; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, '', gi.groupId)); session = getSession(gi.groupId); @@ -81,13 +86,17 @@ export function registerMemory() { } const uiList: UserInfo[] = []; - for (const n of userList) { - const ui = await session.context.findUser(ctx, n, true); + for (const n of related_user_ids) { + const normalizedUserId = normalizeUserId(n); + if (normalizedUserId === null) return `相关用户ID格式无效<${n}>`; + const ui = session.context.getUserById(normalizedUserId); if (ui !== null) uiList.push({ isPrivate: true, id: ui.userId, name: ui.userName }); } const giList: GroupInfo[] = []; - for (const n of groupList) { - const gi = await session.context.findGroup(ctx, n); + for (const n of related_group_ids) { + const normalizedGroupId = normalizeGroupId(n); + if (normalizedGroupId === null) return `相关群ID格式无效<${n}>`; + const gi = session.context.getGroupById(normalizedGroupId); if (gi !== null) giList.push({ isPrivate: false, id: gi.groupId, name: gi.groupName }); } @@ -111,9 +120,9 @@ export function registerMemory() { description: "记忆类型,个人或群聊。", enum: ["private", "group"] }, - name: { + target_id: { type: 'string', - description: '用户名称或群聊名称或纯数字QQ号、群号,实际使用时与记忆类型对应' + description: '目标用户ID或群ID,实际使用时与记忆类型对应' }, id_list: { type: 'array', @@ -130,24 +139,28 @@ export function registerMemory() { } } }, - required: ['memory_type', 'name', 'id_list', 'keywords'] + required: ['memory_type', 'target_id', 'id_list', 'keywords'] } } }); toolDel.solve = async (ctx, _, session, args) => { - const { memory_type, name, id_list, keywords } = args; + const { memory_type, target_id, id_list, keywords } = args; // 记录调用方会话:与 search_memory 一致,其他会话创建的私有记忆不可删除 const callerSessionId = session.sessionId; if (memory_type === "private") { - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; + const normalizedTargetId = normalizeUserId(target_id); + if (normalizedTargetId === null) return `目标ID格式无效<${target_id}>`; + const ui = session.context.getUserById(normalizedTargetId); + if (ui === null) return `未找到目标ID<${target_id}>`; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, ui.userId, '')); session = getSession(ui.userId); } else if (memory_type === "group") { - const gi = await session.context.findGroup(ctx, name); - if (gi === null) return `未找到<${name}>`; + const normalizedTargetId = normalizeGroupId(target_id); + if (normalizedTargetId === null) return `目标ID格式无效<${target_id}>`; + const gi = session.context.getGroupById(normalizedTargetId); + if (gi === null) return `未找到目标ID<${target_id}>`; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, '', gi.groupId)); session = getSession(gi.groupId); @@ -196,9 +209,9 @@ export function registerMemory() { description: "记忆类型,个人或群聊", enum: ["private", "group"] }, - name: { + target_id: { type: 'string', - description: '用户名称或群聊名称或纯数字QQ号、群号,实际使用时与记忆类型对应' + description: '目标用户ID或群ID,实际使用时与记忆类型对应' }, query: { type: 'string', @@ -210,21 +223,21 @@ export function registerMemory() { }, keywords: { type: 'array', - description: '相关用户名称列表', + description: '相关用户ID列表', items: { type: 'string' } }, - userList: { + related_user_ids: { type: 'array', - description: '相关用户名称列表', + description: '相关用户ID列表', items: { type: 'string' } }, - groupList: { + related_group_ids: { type: 'array', - description: '相关群聊名称列表', + description: '相关群ID列表', items: { type: 'string' } @@ -239,26 +252,30 @@ export function registerMemory() { enum: ['similarity', 'score', 'early', 'late', 'recent'] } }, - required: ['memory_type'] + required: ['memory_type', 'target_id'] } } }); toolSearch.solve = async (ctx, _, session, args) => { - const { memory_type, name = '', query = '', topK = 5, keywords = [], userList = [], groupList = [], method = 'similarity' } = args; + const { memory_type, target_id, query = '', topK = 5, keywords = [], related_user_ids = [], related_group_ids = [], method = 'similarity' } = args; // 记录调用方会话:私有记忆只对创建它的会话可见,搜索其他会话时按调用方会话过滤 const callerSessionId = session.sessionId; let si: SessionInfo; if (memory_type === "private") { - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; + const normalizedTargetId = normalizeUserId(target_id); + if (normalizedTargetId === null) return `目标ID格式无效<${target_id}>`; + const ui = session.context.getUserById(normalizedTargetId); + if (ui === null) return `未找到目标ID<${target_id}>`; si = { isPrivate: true, id: ui.userId, name: ui.userName || ui.userId }; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, ui.userId, '')); session = getSession(ui.userId); } else if (memory_type === "group") { - const gi = await session.context.findGroup(ctx, name); - if (gi === null) return `未找到<${name}>`; + const normalizedTargetId = normalizeGroupId(target_id); + if (normalizedTargetId === null) return `目标ID格式无效<${target_id}>`; + const gi = session.context.getGroupById(normalizedTargetId); + if (gi === null) return `未找到目标ID<${target_id}>`; si = { isPrivate: false, id: gi.groupId, name: gi.groupName || gi.groupId }; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, '', gi.groupId)); @@ -270,13 +287,17 @@ export function registerMemory() { if (session.memory.memoryIds.length === 0) return `暂无记忆`; const uiList: UserInfo[] = []; - for (const n of userList) { - const ui = await session.context.findUser(ctx, n, true); + for (const n of related_user_ids) { + const normalizedUserId = normalizeUserId(n); + if (normalizedUserId === null) return `相关用户ID格式无效<${n}>`; + const ui = session.context.getUserById(normalizedUserId); if (ui !== null) uiList.push({ isPrivate: true, id: ui.userId, name: ui.userName }); } const giList: GroupInfo[] = []; - for (const n of groupList) { - const gi = await session.context.findGroup(ctx, n); + for (const n of related_group_ids) { + const normalizedGroupId = normalizeGroupId(n); + if (normalizedGroupId === null) return `相关群ID格式无效<${n}>`; + const gi = session.context.getGroupById(normalizedGroupId); if (gi !== null) giList.push({ isPrivate: false, id: gi.groupId, name: gi.groupName }); } @@ -307,29 +328,33 @@ export function registerMemory() { description: "记忆类型,个人或群聊", enum: ["private", "group"] }, - name: { + target_id: { type: 'string', - description: '用户名称或群聊名称或纯数字QQ号、群号,实际使用时与记忆类型对应' + description: '目标用户ID或群ID,实际使用时与记忆类型对应' } }, - required: ['memory_type', 'name'] + required: ['memory_type', 'target_id'] } } }); toolClear.solve = async (ctx, _, session, args) => { - const { memory_type, name } = args; + const { memory_type, target_id } = args; // 记录调用方会话:与 search_memory 一致,其他会话创建的私有记忆不可清除 const callerSessionId = session.sessionId; if (memory_type === "private") { - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; + const normalizedTargetId = normalizeUserId(target_id); + if (normalizedTargetId === null) return `目标ID格式无效<${target_id}>`; + const ui = session.context.getUserById(normalizedTargetId); + if (ui === null) return `未找到目标ID<${target_id}>`; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, ui.userId, '')); session = getSession(ui.userId); } else if (memory_type === "group") { - const gi = await session.context.findGroup(ctx, name); - if (gi === null) return `未找到<${name}>`; + const normalizedTargetId = normalizeGroupId(target_id); + if (normalizedTargetId === null) return `目标ID格式无效<${target_id}>`; + const gi = session.context.getGroupById(normalizedTargetId); + if (gi === null) return `未找到目标ID<${target_id}>`; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, '', gi.groupId)); session = getSession(gi.groupId); diff --git a/src/tool/tools/message/init.ts b/src/tool/tools/message/init.ts deleted file mode 100644 index 080d239..0000000 --- a/src/tool/tools/message/init.ts +++ /dev/null @@ -1,7 +0,0 @@ -// message 子目录工具注册统一入口 -import { registerMessage } from "./tool_message"; - -/** 注册 message 下全部消息工具 */ -export function registerMessageTools() { - registerMessage(); -} diff --git a/src/tool/tools/message/tool_message.ts b/src/tool/tools/message/tool_message.ts deleted file mode 100644 index fb02071..0000000 --- a/src/tool/tools/message/tool_message.ts +++ /dev/null @@ -1,423 +0,0 @@ -// 消息工具:发送消息/取消息/撤回/合并转发 -import { CQ_TYPES_ALLOW as CQTYPESALLOW, FACE_MAP as faceMap } from "../../../config/static_config"; -import { logger } from "../../../logger"; -import Image from "../../../resource/image"; -import { getSession, SessionService } from "../../../session/session_service"; -import { deleteMsg, getGroupMemberInfo, getMsg, netExists, sendGroupForwardMsg, sendPrivateForwardMsg } from "../../../utils/ob11"; -import { getCtxAndMsg } from "../../../utils/seal"; -import { handleReply, MessageSegment, parseSpecialTokens, transformArrayToContent } from "../../../utils/string"; -import { replyToSender, transformMsgIdBack } from "../../../utils/utils"; -import Tool from "../../tool"; - -export function registerMessage() { - const toolSend = new Tool({ - type: "function", - function: { - name: "send_msg", - description: `向当前聊天以外的指定私聊或群聊发送消息或调用函数`, - parameters: { - type: "object", - properties: { - msg_type: { - type: "string", - description: "消息类型,私聊或群聊", - enum: ["private", "group"] - }, - name: { - type: 'string', - description: '用户名称或群聊名称或纯数字QQ号、群号,实际使用时与消息类型对应' - }, - content: { - type: 'string', - description: '消息内容' - }, - function: { - type: "string", - description: '函数调用,纯JSON字符串,格式为:{"name": "函数名称", "arguments": {"参数1": "值1", "参数2": "值2"}}' - }, - reason: { - type: 'string', - description: '发送原因' - } - }, - required: ["msg_type", "name", "content"] - } - } - }); - toolSend.sensitive = true; // 发送消息属敏感操作 - toolSend.solve = async (ctx, msg, session, args) => { - const { msg_type, name, content, function: tool_call, reason = '' } = args; - - const source = ctx.isPrivate ? - `来自<${ctx.player!.name}>(${ctx.player!.userId.replace(/^.+:/, '')})` : - `来自群聊<${ctx.group!.groupName}>(${ctx.group!.groupId.replace(/^.+:/, '')})`; - - const segs = parseSpecialTokens(content); - const originalImages: Image[] = []; - for (const seg of segs) { - switch (seg.type) { - case 'img': { - const id = seg.content; - // 兼容 [img:imageId:描述]:整体找不到时取首个冒号前作为图片 id - const image = await session.context.findImage(ctx, id) || (id.includes(':') ? await session.context.findImage(ctx, id.split(':')[0]) : null); - if (image) originalImages.push(image); - else logger.warning(`无法找到图片:${id}`); - break; - } - case 'avatar': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) originalImages.push(Image.getUserAvatar(ui.userId)); - else logger.warning(`无法找到用户:${name}`); - break; - } - case 'group_avatar': { - const name = seg.content; - const gi = await session.context.findGroup(ctx, name); - if (gi) originalImages.push(Image.getGroupAvatar(gi.groupId)); - else logger.warning(`无法找到群聊:${name}`); - break; - } - } - } - - if (msg_type === "private") { - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; - if (ui.userId === ctx.player!.userId && ctx.isPrivate) return `向当前私聊发送消息无需调用函数`; - if (ui.userId === ctx.endPoint.userId) return `禁止向自己发送消息`; - - ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, ui.userId, '')); - session = getSession(ui.userId); - } else if (msg_type === "group") { - const gi = await session.context.findGroup(ctx, name); - if (gi === null) return `未找到<${name}>`; - if (gi.groupId === ctx.group!.groupId) return `向当前群聊发送消息无需调用函数`; - - ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, '', gi.groupId)); - session = getSession(gi.groupId); - } else { - return `未知的消息类型<${msg_type}>`; - } - - session.resetState(); - - await session.context.addSystemUserMessage(`${source}: 原因: ${reason || '无'}`, "来自其他对话的消息发送提示"); - - const { contextArray, replyArray } = await handleReply(ctx, msg, session, content); - - for (let i = 0; i < contextArray.length; i++) { - const content = contextArray[i]; - const reply = replyArray[i]; - const msgId = await replyToSender(ctx, msg, session, reply); - await session.context.addAssistantMessage(content, msgId); - } - - if (tool_call) await Tool.handlePromptToolCalls(ctx, msg, session, tool_call); - - SessionService.save(session); - return "消息发送成功"; - } - - const toolGet = new Tool({ - type: 'function', - function: { - name: 'get_msg', - description: '获取指定消息', - parameters: { - type: 'object', - properties: { - msg_id: { - type: 'string', - description: '消息ID' - } - }, - required: ['msg_id'] - } - } - }); - toolGet.solve = async (ctx, _, _session, args) => { - const { msg_id } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - - const backId = transformMsgIdBack(msg_id); - if (!Number.isFinite(backId)) return `消息ID无效:${msg_id}`; - - const result = await getMsg(epId, backId); - if (!result) return `获取消息 ${msg_id} 失败`; - const messageArray: MessageSegment[] = result.message.filter((item: MessageSegment) => item.type === 'text' && !CQTYPESALLOW.includes(item.type)); - - const { content } = await transformArrayToContent(ctx, messageArray); - - const gid = ctx.group!.groupId; - const uid = `QQ:${result.sender.user_id}`; - ({ ctx } = getCtxAndMsg(epId, uid, gid)); - const name = ctx.player!.name || '未知用户'; - const prefix = `[from:${name}(${uid.replace(/^.+:/, '')})]`; - - return prefix + content; - } - - const toolDel = new Tool({ - type: 'function', - function: { - name: 'delete_msg', - description: '撤回指定消息', - parameters: { - type: 'object', - properties: { - msg_id: { - type: 'string', - description: '消息ID' - } - }, - required: ['msg_id'] - } - } - }); - toolDel.solve = async (ctx, _, __, args) => { - const { msg_id } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - - const backId = transformMsgIdBack(msg_id); - if (!Number.isFinite(backId)) return `消息ID无效:${msg_id}`; - - const result = await getMsg(epId, backId); - if (!result) return `获取消息 ${msg_id} 失败`; - if (result.sender.user_id != epId.replace(/^.+:/, '')) { - if (result.sender.role == 'owner' || result.sender.role == 'admin') { - return `你没有权限撤回该消息`; - } - - const memberInfo = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), epId.replace(/^.+:/, '')); - if (!memberInfo) return `获取权限信息失败`; - if (memberInfo.role !== 'owner' && memberInfo.role !== 'admin') return `你没有管理员权限`; - } - - await deleteMsg(epId, backId); - return `已撤回消息${msg_id}`; - } - - const toolMerge = new Tool({ - type: 'function', - function: { - name: 'send_forward_msg', - description: '发送合并转发消息', - parameters: { - type: 'object', - properties: { - msg_type: { - type: 'string', - description: '消息类型,私聊或群聊', - enum: ['private', 'group'] - }, - name: { - type: 'string', - description: '用户名称或群聊名称或纯数字QQ号、群号,实际使用时与消息类型对应' - }, - messages: { - type: 'array', - description: '消息节点列表,可以有多个', - items: { - type: 'object', - properties: { - name: { - type: 'string', - description: '用户名称或纯数字QQ号' - }, - nickname: { - type: 'string', - description: '发送者名称,默认与name相同' - }, - content: { - type: 'string', - description: '消息内容' - } - }, - required: ['content'] - } - } - }, - required: ['msg_type', 'name', 'messages'] - } - } - }); - toolMerge.sensitive = true; // 发送合并转发属敏感操作 - toolMerge.solve = async (ctx, _, session, args) => { - const { msg_type, name, messages } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const messagesToSend = []; - const images: Image[] = []; - const randomId = Math.floor(Math.random() * 1000000000); - const unknowUserArray: string[] = []; - for (const messageItem of messages) { - const segs = parseSpecialTokens(messageItem.content); - const content: MessageSegment[] = []; - for (const seg of segs) { - switch (seg.type) { - case 'text': { - content.push({ - type: 'text', - data: { - text: seg.content - } - }) - break; - } - case 'at': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) { - content.push({ - type: 'at', - data: { - qq: ui.userId.replace(/^.+:/, "") - } - }) - } else { - logger.warning(`无法找到用户:${name}`); - content.push({ - type: 'text', - data: { - text: ` @${name} ` - } - }) - } - break; - } - case 'quote': { - const msgId = seg.content; - // msgid 可能为负数(base36 保留符号),仅当可解析时才生成引用,避免 id=NaN - const backId = transformMsgIdBack(msgId); - if (Number.isFinite(backId)) { - content.push({ - type: 'reply', - data: { id: String(backId) } - }); - } - break; - } - case 'img': { - const id = seg.content; - // 兼容 [img:imageId:描述]:整体找不到时取首个冒号前作为图片 id - const image = await session.context.findImage(ctx, id) || (id.includes(':') ? await session.context.findImage(ctx, id.split(':')[0]) : null); - - if (image) { - if (image.type === 'local') break; - images.push(image); - content.push({ - type: 'image', - data: { file: image.type === 'base64' ? seal.base64ToImage(image.base64) : (image.url || image.path) } - }) - } else { - logger.warning(`无法找到图片:${id}`); - } - break; - } - case 'avatar': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) { - const image = Image.getUserAvatar(ui.userId); - if (image.type === 'local') break; - images.push(image); - content.push({ - type: 'image', - data: { file: image.type === 'base64' ? seal.base64ToImage(image.base64) : (image.url || image.path) } - }) - } else { - logger.warning(`无法找到用户:${name}`); - } - break; - } - case 'group_avatar': { - const name = seg.content; - const gi = await session.context.findGroup(ctx, name); - if (gi) { - const image = Image.getGroupAvatar(gi.groupId); - if (image.type === 'local') break; - images.push(image); - content.push({ - type: 'image', - data: { file: image.type === 'base64' ? seal.base64ToImage(image.base64) : (image.url || image.path) } - }) - } else { - logger.warning(`无法找到群聊:${name}`); - } - break; - } - case 'face': { - const faceId = Object.keys(faceMap).find(key => faceMap[key] === seg.content) || ''; - content.push({ - type: 'face', - data: { id: faceId } - }) - break; - } - } - } - - if (content.length === 0) { - return `消息长度不能为0`; - } - - let userId = ''; - let name = ''; - const ui = await session.context.findUser(ctx, messageItem.name, true); - if (ui !== null) { - userId = ui.userId.replace(/^.+:/, ""); - name = ui.userName; - } else { - let unknowUserIndex = unknowUserArray.indexOf(messageItem.name); - if (unknowUserIndex === -1) { - unknowUserIndex = unknowUserArray.length; - unknowUserArray.push(messageItem.name); - } - userId = String(unknowUserIndex + randomId); - name = `未知用户${unknowUserIndex + 1}`; - } - - messagesToSend.push({ - type: 'node', - data: { - user_id: userId, - nickname: messageItem.nickname || name, - content: content - } - }); - } - - const news: string[] = []; - const prompt = ""; - const summary = ""; - const source = ""; - - if (msg_type === "private") { - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; - if (ui.userId === ctx.endPoint.userId) return `禁止向自己发送消息`; - - await sendPrivateForwardMsg(ctx.endPoint.userId, ui.userId.replace(/^.+:/, ""), messagesToSend, news, prompt, summary, source); - } else if (msg_type === "group") { - const gi = await session.context.findGroup(ctx, name); - if (gi === null) return `未找到<${name}>`; - - await sendGroupForwardMsg(ctx.endPoint.userId, gi.groupId.replace(/^.+:/, ""), messagesToSend, news, prompt, summary, source); - } else { - return `未知的消息类型<${msg_type}>`; - } - - return `发送合并消息成功`; - } -} - -// TODO: 合并消息嵌套 diff --git a/src/tool/tools/ob11/init.ts b/src/tool/tools/ob11/init.ts index e47d152..ba17dd4 100644 --- a/src/tool/tools/ob11/init.ts +++ b/src/tool/tools/ob11/init.ts @@ -1,17 +1,6 @@ -// ob11.ts 子目录工具注册统一入口 -import { registerBan } from "./tool_ban"; -import { registerEssenceMsg } from "./tool_essence_msg"; -import { registerGroupSign } from "./tool_group_sign"; -import { registerGetPersonInfo } from "./tool_person_info"; -import { registerQQList } from "./tool_qq_list"; -import { registerRename } from "./tool_rename"; +import { registerCallOb11Api } from "./tool_call_api"; -/** 注册 ob11.ts 下全部 ob11 相关工具 */ +/** 仅注册统一的 call_ob11_api;旧的按 action 工具已经删除。 */ export function registerOb11Tools() { - registerBan(); - registerEssenceMsg(); - registerGroupSign(); - registerQQList(); - registerGetPersonInfo(); - registerRename(); + registerCallOb11Api(); } diff --git a/src/tool/tools/ob11/tool_ban.ts b/src/tool/tools/ob11/tool_ban.ts deleted file mode 100644 index 5c6b0a7..0000000 --- a/src/tool/tools/ob11/tool_ban.ts +++ /dev/null @@ -1,117 +0,0 @@ -// 禁言工具:禁言/全员禁言/禁言列表 -import { getGroupMemberInfo, getGroupShutList, netExists, setGroupBan, setGroupWholeBan } from "../../../utils/ob11"; -import { fmtDate } from "../../../utils/string"; -import Tool from "../../tool"; - -export function registerBan() { - const toolBan = new Tool({ - type: 'function', - function: { - name: 'ban', - description: '禁言指定用户', - parameters: { - type: 'object', - properties: { - name: { - type: 'string', - description: '用户名称或纯数字QQ号' - }, - duration: { - type: 'integer', - description: '禁言时长,单位为秒,最大为2591940' - } - }, - required: ['name', 'duration'] - } - } - }); - toolBan.sessionType = 'group'; - toolBan.sensitive = true; // 禁言属敏感操作 - toolBan.solve = async (ctx, _, session, args) => { - const { name, duration } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - const ui = await session.context.findUser(ctx, name); - - if (ui === null) return `未找到<${name}>`; - const memberInfo = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), epId.replace(/^.+:/, '')); - if (!memberInfo) return `获取权限信息失败`; - if (memberInfo.role !== 'owner' && memberInfo.role !== 'admin') return `你没有管理员权限`; - - const memberInfo2 = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), ui.userId.replace(/^.+:/, '')); - if (!memberInfo2) return `获取用户 ${ui.userId} 信息失败`; - if (memberInfo2.role === 'owner') return `你无法禁言群主`; - if (memberInfo2.role === 'admin' && memberInfo.role !== 'owner') return `你无法禁言管理员,因为你不是群主`; - - await setGroupBan(epId, gid.replace(/^.+:/, ''), ui.userId.replace(/^.+:/, ''), duration); - return `已禁言<${name}> ${duration}秒`; - } - - const toolWhole = new Tool({ - type: 'function', - function: { - name: 'whole_ban', - description: '全员禁言', - parameters: { - type: 'object', - properties: { - enable: { - type: 'boolean', - description: '开启还是关闭全员禁言' - } - }, - required: ['enable'] - } - } - }); - toolWhole.sessionType = 'group'; - toolWhole.sensitive = true; // 全员禁言属敏感操作 - toolWhole.solve = async (ctx, _, __, args) => { - const { enable } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - const memberInfo = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), epId.replace(/^.+:/, '')); - if (!memberInfo) return `获取权限信息失败`; - if (memberInfo.role !== 'owner' && memberInfo.role !== 'admin') return `你没有管理员权限`; - - await setGroupWholeBan(epId, gid.replace(/^.+:/, ''), enable); - return `已${enable ? '开启' : '关闭'}全员禁言`; - } - - const toolList = new Tool({ - type: 'function', - function: { - name: 'get_ban_list', - description: '获取群内禁言列表', - parameters: { - type: 'object', - properties: { - }, - required: [] - } - } - }); - toolList.sessionType = 'group'; - toolList.solve = async (ctx, _, __, ___) => { - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - - const groupShutList = await getGroupShutList(epId, gid.replace(/^.+:/, '')); - if (!groupShutList || !Array.isArray(groupShutList)) return `获取禁言列表失败`; - - const s = `被禁言成员数量: ${groupShutList.length}\n` + - groupShutList.slice(0, 50) - .map((item: any, index: number) => `${index + 1}. ${item.nick}(${item.uin}) ${item.cardName && item.cardName !== item.nick ? `群名片: ${item.cardName}` : ''} 禁言结束时间: ${fmtDate(item.shutUpTime)}`) - .join('\n'); - - return s; - } -} diff --git a/src/tool/tools/ob11/tool_call_api.ts b/src/tool/tools/ob11/tool_call_api.ts new file mode 100644 index 0000000..5c9352f --- /dev/null +++ b/src/tool/tools/ob11/tool_call_api.ts @@ -0,0 +1,51 @@ +import Config from "../../../config/config"; +import { callOb11ApiForContext, formatOb11Result } from "../../../transport/ob11/dispatcher"; +import Tool from "../../tool"; + +export function registerCallOb11Api() { + const tool = new Tool({ + type: "function", + function: { + name: "call_ob11_api", + description: "调用 OneBot 11 API 或已支持的 NapCat/Milky API。运行时会自动选择 ob11-net 后端或 SealDice 原生后端;不要调用旧的按功能拆分工具。", + parameters: { + type: "object", + properties: { + action: { + type: "string", + description: "API action,例如 send_group_msg、get_group_member_list、set_group_ban" + }, + params: { + type: "object", + description: "该 API 的原始参数对象", + additionalProperties: true + }, + reason: { + type: "string", + description: "发送消息、群管理、撤回等敏感操作的原因" + } + }, + required: ["action", "params"] + } + } + }); + tool.sensitive = true; + tool.solve = async (ctx, msg, _session, args) => { + const action = args && typeof args.action === "string" ? args.action.trim() : ""; + const params = args && args.params && typeof args.params === "object" && !Array.isArray(args.params) ? args.params : null; + if (!action) return JSON.stringify({ ok: false, backend: "seal-native", action: "", error: { code: "INVALID_PARAMS", message: "action 不能为空" } }); + if (!params) return JSON.stringify({ ok: false, backend: "seal-native", action, error: { code: "INVALID_PARAMS", message: "params 必须是对象" } }); + + const blockedActions = (Config.tool.OB11_BLOCKED_ACTIONS || []).filter(Boolean); + if (blockedActions.includes(action)) { + return JSON.stringify({ ok: false, backend: "seal-native", action, error: { code: "ACTION_BLOCKED", message: `OB11 action 已被配置禁止:${action}` } }); + } + const defaultClosedActions = (Config.tool.OB11_DEFAULT_CLOSED_ACTIONS || []).filter(Boolean); + if (defaultClosedActions.includes(action)) { + return JSON.stringify({ ok: false, backend: "seal-native", action, error: { code: "ACTION_DISABLED", message: `OB11 action 默认关闭:${action}` } }); + } + + const result = await callOb11ApiForContext(ctx, msg, action, params); + return formatOb11Result(result); + }; +} diff --git a/src/tool/tools/ob11/tool_essence_msg.ts b/src/tool/tools/ob11/tool_essence_msg.ts deleted file mode 100644 index 754cc9f..0000000 --- a/src/tool/tools/ob11/tool_essence_msg.ts +++ /dev/null @@ -1,145 +0,0 @@ -// 精华消息工具:设置/获取/删除 -import Image from "../../../resource/image"; -import { deleteEssenceMsg, getEssenceMsgList, getGroupMemberInfo, netExists, setEssenceMsg } from "../../../utils/ob11"; -import { transformArrayToContent } from "../../../utils/string"; -import { transformMsgId, transformMsgIdBack } from "../../../utils/utils"; -import Tool from "../../tool"; - -export function registerEssenceMsg() { - const toolSet = new Tool({ - type: 'function', - function: { - name: 'set_essence_msg', - description: '设置指定消息为精华消息', - parameters: { - type: 'object', - properties: { - msg_id: { - type: 'string', - description: '消息ID' - } - }, - required: ['msg_id'] - } - } - }); - toolSet.solve = async (ctx, _, __, args) => { - const { msg_id } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - - const memberInfo = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), epId.replace(/^.+:/, '')); - if (!memberInfo) return `获取权限信息失败`; - if (memberInfo.role !== 'owner' && memberInfo.role !== 'admin') return `你没有管理员权限`; - - const backId = transformMsgIdBack(msg_id); - if (!Number.isFinite(backId)) return `消息ID无效:${msg_id}`; - - await setEssenceMsg(epId, backId); - return `已将消息${msg_id}设置为精华消息`; - } - - const toolGet = new Tool({ - type: 'function', - function: { - name: 'get_essence_msg_list', - description: '获取群精华消息列表', - parameters: { - type: 'object', - properties: { - }, - required: [] - } - } - }); - toolGet.solve = async (ctx, _, _session, __) => { - if (ctx.isPrivate) { - return `精华消息功能仅在群聊中可用`; - } - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - - const essenceMsgList = await getEssenceMsgList(epId, gid.replace(/^.+:/, '')); - if (!essenceMsgList || !Array.isArray(essenceMsgList)) return `获取群 ${gid} 精华消息列表失败`; - - if (essenceMsgList.length === 0) return `该群暂无精华消息`; - - let s = `群精华消息列表 (${essenceMsgList.length}条):\n`; - const images: Image[] = []; - - for (let i = 0; i < essenceMsgList.length; i++) { - const essence = essenceMsgList[i]; - const addTime = new Date(essence.operator_time * 1000).toLocaleString(); - const operatorName = essence.operator_nick || `用户${essence.operator_id}`; - const senderName = essence.sender_nick || `用户${essence.sender_id}`; - const msgId = transformMsgId(essence.message_id); - - if (essence.content) { - let content = ''; - if (Array.isArray(essence.content)) { - const result = await transformArrayToContent(ctx, essence.content); - content = result.content; - images.push(...result.images); - } else if (typeof essence.content === 'string') { - content = essence.content; - } - - if (content.length > 50) content = content.substring(0, 100) + '...'; - - s += `${i + 1}. 发送者: ${senderName} - 操作者: ${operatorName} - 设置时间: ${addTime} - 消息ID: ${msgId} - 内容: ${content}\n`; - } - } - - return s.trim(); - }; - - const toolDel = new Tool({ - type: 'function', - function: { - name: 'delete_essence_msg', - description: '删除群精华消息', - parameters: { - type: 'object', - properties: { - msg_id: { - type: 'string', - description: '要删除的精华消息ID' - } - }, - required: ['msg_id'] - } - } - }); - toolDel.solve = async (ctx, _, __, args) => { - const { msg_id } = args; - - if (ctx.isPrivate) { - return `精华消息功能仅在群聊中可用`; - } - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - - const memberInfo = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), epId.replace(/^.+:/, '')); - if (!memberInfo) return `获取权限信息失败`; - if (memberInfo.role !== 'owner' && memberInfo.role !== 'admin') return `你没有管理员权限`; - - const backId = transformMsgIdBack(msg_id); - if (!Number.isFinite(backId)) return `消息ID无效:${msg_id}`; - - await deleteEssenceMsg(epId, backId); - return `已删除精华消息 ${msg_id}`; - }; -} diff --git a/src/tool/tools/ob11/tool_group_sign.ts b/src/tool/tools/ob11/tool_group_sign.ts deleted file mode 100644 index baea5b9..0000000 --- a/src/tool/tools/ob11/tool_group_sign.ts +++ /dev/null @@ -1,33 +0,0 @@ -// 群打卡工具 -import { netExists, sendGroupSign } from "../../../utils/ob11"; -import Tool from "../../tool"; - -export function registerGroupSign() { - const tool = new Tool({ - type: 'function', - function: { - name: 'group_sign', - description: '发送群打卡', - parameters: { - type: 'object', - properties: { - }, - required: [] - } - } - }); - tool.sessionType = 'group'; - tool.solve = async (ctx, _, __, ___) => { - if (ctx.isPrivate) { - return `群打卡只能在群聊中使用`; - } - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - - await sendGroupSign(epId, gid.replace(/^.+:/, '')); - return `已发送群打卡`; - } -} \ No newline at end of file diff --git a/src/tool/tools/ob11/tool_person_info.ts b/src/tool/tools/ob11/tool_person_info.ts deleted file mode 100644 index 565f8ec..0000000 --- a/src/tool/tools/ob11/tool_person_info.ts +++ /dev/null @@ -1,60 +0,0 @@ -// 个人信息工具:资料/生日/星座等 -import { getStrangerInfo, netExists } from "../../../utils/ob11"; -import Tool from "../../tool"; - -const constellations = ["水瓶座", "双鱼座", "白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "摩羯座"]; -const shengXiao = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"]; - -export function registerGetPersonInfo() { - const tool = new Tool({ - type: 'function', - function: { - name: 'get_person_info', - description: '获取用户信息', - parameters: { - type: 'object', - properties: { - name: { - type: 'string', - description: '用户名称或纯数字QQ号' - } - }, - required: ['name'] - } - } - }); - tool.solve = async (ctx, _, session, args) => { - const { name } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; - - const epId = ctx.endPoint.userId; - - const strangerInfo = await getStrangerInfo(epId, ui.userId.replace(/^.+:/, '')); - if (!strangerInfo) return `获取用户${ui.userId}信息失败`; - - let s = `昵称: ${strangerInfo.nickname} -QQ号: ${strangerInfo.user_id} -性别: ${strangerInfo.sex} -QQ等级: ${strangerInfo.qqLevel} -是否为VIP: ${strangerInfo.is_vip} -是否为年费会员: ${strangerInfo.is_years_vip}`; - if (strangerInfo.remark) s += `\n备注: ${strangerInfo.remark}`; - if (strangerInfo.birthday_year && strangerInfo.birthday_year !== 0) s += `\n年龄: ${strangerInfo.age} -生日: ${strangerInfo.birthday_year}-${strangerInfo.birthday_month}-${strangerInfo.birthday_day} -星座: ${constellations[strangerInfo.constellation - 1]} -生肖: ${shengXiao[strangerInfo.shengXiao - 1]}`; - if (strangerInfo.pos) s += `\n位置: ${strangerInfo.pos}`; - if (strangerInfo.country) s += `\n所在地: ${strangerInfo.country} ${strangerInfo.province} ${strangerInfo.city}`; - if (strangerInfo.address) s += `\n地址: ${strangerInfo.address}`; - if (strangerInfo.eMail) s += `\n邮箱: ${strangerInfo.eMail}`; - if (strangerInfo.interest) s += `\n兴趣: ${strangerInfo.interest}`; - if (strangerInfo.labels && strangerInfo.labels.length > 0) s += `\n标签: ${strangerInfo.labels.join(',')}`; - if (strangerInfo.long_nick) s += `\n个性签名: ${strangerInfo.long_nick}`; - - return s; - } -} diff --git a/src/tool/tools/ob11/tool_qq_list.ts b/src/tool/tools/ob11/tool_qq_list.ts deleted file mode 100644 index 70e8c80..0000000 --- a/src/tool/tools/ob11/tool_qq_list.ts +++ /dev/null @@ -1,230 +0,0 @@ -// 列表工具:好友/群/成员/共同群搜索 -import { getFriendList, getGroupList, getGroupMemberList, netExists } from "../../../utils/ob11"; -import Tool from "../../tool"; - -export function registerQQList() { - const toolList = new Tool({ - type: "function", - function: { - name: "get_list", - description: `查看当前好友列表或群聊列表`, - parameters: { - type: "object", - properties: { - msg_type: { - type: "string", - description: "消息类型,私聊或群聊", - enum: ["private", "group"] - } - }, - required: ["msg_type"] - } - } - }); - toolList.solve = async (ctx, _, __, args) => { - const { msg_type } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - - if (msg_type === "private") { - const friendList = await getFriendList(epId); - if (!friendList || !Array.isArray(friendList)) return `获取好友列表失败`; - - const s = `好友数量: ${friendList.length}\n` + friendList.slice(0, 50).map((item: any, index: number) => { - return `${index + 1}. ${item.nickname}(${item.user_id}) ${item.remark && item.remark !== item.nickname ? `备注: ${item.remark}` : ''}`; - }).join('\n'); - - return s; - } else if (msg_type === "group") { - const groupList = await getGroupList(epId); - if (!groupList || !Array.isArray(groupList)) return `获取群聊列表失败`; - - const s = `群聊数量: ${groupList.length}\n` + groupList.slice(0, 50).map((item: any, index: number) => { - return `${index + 1}. ${item.group_name}(${item.group_id}) 人数: ${item.member_count}/${item.max_member_count}`; - }).join('\n'); - - return s; - } else { - return `未知的消息类型<${msg_type}>`; - } - } - - const toolMember = new Tool({ - type: "function", - function: { - name: "get_group_member_list", - description: `查看群聊成员列表`, - parameters: { - type: "object", - properties: { - role: { - type: "string", - description: "成员角色,群主或管理员", - enum: ["owner", "admin", "robot"] - } - }, - required: [] - } - } - }); - toolMember.solve = async (ctx, _, __, args) => { - const { role = '' } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - - const groupMemberList = await getGroupMemberList(epId, gid.replace(/^.+:/, '')); - if (!groupMemberList || !Array.isArray(groupMemberList)) return `获取群聊成员列表失败`; - - switch (role) { - case 'owner': { - const owner = groupMemberList.find((item: any) => item.role === role); - if (!owner) return `未找到群主`; - return `群主: ${owner.nickname}(${owner.user_id}) ${owner.card && owner.card !== owner.nickname ? `群名片: ${owner.card}` : ''}`; - } - case 'admin': { - const admins = groupMemberList.filter((item: any) => item.role === role); - if (admins.length === 0) return `未找到管理员`; - const s = `管理员数量: ${admins.length}\n` + - admins.slice(0, 50) - .map((item: any, index: number) => `${index + 1}. ${item.nickname}(${item.user_id}) ${item.card && item.card !== item.nickname ? `群名片: ${item.card}` : ''}`) - .join('\n'); - return s; - } - case 'robot': { - const robots = groupMemberList.filter((item: any) => item.is_robot); - if (robots.length === 0) return `未找到机器人`; - const s = `机器人数量: ${robots.length}\n` + - robots.slice(0, 50) - .map((item: any, index: number) => `${index + 1}. ${item.nickname}(${item.user_id}) ${item.card && item.card !== item.nickname ? `群名片: ${item.card}` : ''}`) - .join('\n'); - return s; - } - default: { - const s = `群成员数量: ${groupMemberList.length}\n` + - groupMemberList.slice(0, 50) - .map((item: any, index: number) => `${index + 1}. ${item.nickname}(${item.user_id}) ${item.card && item.card !== item.nickname ? `群名片: ${item.card}` : ''} ${item.title ? `头衔: ${item.title}` : ''} ${item.role === 'owner' ? '【群主】' : item.role === 'admin' ? '【管理员】' : item.is_robot ? '【机器人】' : ''}`) - .join('\n'); - return s; - } - } - } - - const toolChat = new Tool({ - type: "function", - function: { - name: "search_chat", - description: `搜索好友或群聊`, - parameters: { - type: "object", - properties: { - msg_type: { - type: "string", - description: "消息类型,私聊或群聊", - enum: ["private", "group"] - }, - q: { - type: 'string', - description: '搜索关键字' - } - }, - required: ["q"] - } - } - }); - toolChat.solve = async (ctx, _, __, args) => { - const { msg_type, q } = args; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - - if (msg_type === "private") { - const friendList = await getFriendList(epId); - if (!friendList || !Array.isArray(friendList)) return `获取好友列表失败`; - const arr = friendList.filter((item: any) => (item.nickname || '').includes(q) || (item.remark || '').includes(q)); - - const s = `搜索结果好友数量: ${arr.length}\n` + arr.slice(0, 50).map((item: any, index: number) => { - return `${index + 1}. ${item.nickname}(${item.user_id}) ${item.remark && item.remark !== item.nickname ? `备注: ${item.remark}` : ''}`; - }).join('\n'); - - return s; - } else if (msg_type === "group") { - const groupList = await getGroupList(epId); - if (!groupList || !Array.isArray(groupList)) return `获取群聊列表失败`; - const arr = groupList.filter((item: any) => item.group_name.includes(q)); - - const s = `搜索结果群聊数量: ${arr.length}\n` + arr.slice(0, 50).map((item: any, index: number) => { - return `${index + 1}. ${item.group_name}(${item.group_id}) 人数: ${item.member_count}/${item.max_member_count}`; - }).join('\n'); - - return s; - } else { - const friendList = await getFriendList(epId); - if (!friendList || !Array.isArray(friendList)) return `获取好友列表失败`; - const arr1 = friendList.filter((item: any) => (item.nickname || '').includes(q) || (item.remark || '').includes(q)); - - const groupList = await getGroupList(epId); - if (!groupList || !Array.isArray(groupList)) return `获取群聊列表失败`; - const arr2 = groupList.filter((item: any) => item.group_name.includes(q)); - - const s = `搜索结果好友数量: ${arr1.length}\n` + arr1.slice(0, 50).map((item: any, index: number) => { - return `${index + 1}. ${item.nickname}(${item.user_id}) ${item.remark && item.remark !== item.nickname ? `备注: ${item.remark}` : ''}`; - }).join('\n') + `\n搜索结果群聊数量: ${arr2.length}\n` + arr2.slice(0, 50).map((item: any, index: number) => { - return `${index + 1}. ${item.group_name}(${item.group_id}) 人数: ${item.member_count}/${item.max_member_count}`; - }).join('\n'); - - return s; - } - } - - const toolCommon = new Tool({ - type: "function", - function: { - name: "search_common_group", - description: `搜索共同群聊`, - parameters: { - type: "object", - properties: { - name: { - type: 'string', - description: '用户名称或纯数字QQ号' - } - }, - required: ["name"] - } - } - }); - toolCommon.solve = async (ctx, _, session, args) => { - const { name } = args; - - const ui = await session.context.findUser(ctx, name, true); - if (ui === null) return `未找到<${name}>`; - if (ui.userId === ctx.endPoint.userId) return `禁止搜索自己`; - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - - const groupList = await getGroupList(epId); - if (!groupList || !Array.isArray(groupList)) return `获取群聊列表失败`; - - const arr = []; - for (const group_info of groupList) { - const groupMemberList = await getGroupMemberList(epId, group_info.group_id); - if (!groupMemberList || !Array.isArray(groupMemberList)) continue; - const user_info = groupMemberList.find((user_info: any) => user_info.user_id.toString() === ui.userId.replace(/^.+:/, '')); - if (user_info) arr.push({ group_info, user_info }); - } - - const s = `共群数量: ${arr.length}\n` + arr.slice(0, 50).map((item: any, index: number) => { - return `${index + 1}. ${item.group_info.group_name}(${item.group_info.group_id}) 人数: ${item.group_info.member_count}/${item.group_info.max_member_count} ${item.user_info.card && item.user_info.card !== item.user_info.nickname ? `群名片: ${item.user_info.card}` : ''}`; - }).join('\n'); - - return s; - } -} diff --git a/src/tool/tools/ob11/tool_rename.ts b/src/tool/tools/ob11/tool_rename.ts deleted file mode 100644 index 37e6280..0000000 --- a/src/tool/tools/ob11/tool_rename.ts +++ /dev/null @@ -1,60 +0,0 @@ -// 改名工具:设置群名片 -import { logger } from "../../../logger"; -import { getGroupMemberInfo, netExists } from "../../../utils/ob11"; -import { getCtxAndMsg } from "../../../utils/seal"; -import Tool from "../../tool"; - -export function registerRename() { - const tool = new Tool({ - type: "function", - function: { - name: "rename", - description: `设置群名片`, - parameters: { - type: "object", - properties: { - name: { - type: 'string', - description: '用户名称或纯数字QQ号' - }, - new_name: { - type: 'string', - description: "新的名字" - } - }, - required: ['name', 'new_name'] - } - } - }); - tool.sessionType = 'group'; - tool.sensitive = true; // 改名属敏感操作 - tool.solve = async (ctx, msg, session, args) => { - const { name, new_name } = args; - - if (netExists()) { - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - - const memberInfo = await getGroupMemberInfo(epId, gid.replace(/^.+:/, ''), epId.replace(/^.+:/, '')); - if (!memberInfo) return `获取权限信息失败`; - if (memberInfo.role !== 'owner' && memberInfo.role !== 'admin') return `你没有管理员权限`; - } - - const ui = await session.context.findUser(ctx, name); - if (ui === null) return `未找到<${name}>`; - - ({ ctx, msg } = getCtxAndMsg(ctx.endPoint.userId, ui.userId, ctx.group!.groupId)); - - try { - seal.setPlayerGroupCard(ctx, new_name); - if (session.context.autoNameMod === 2) { - ctx.player!.name = new_name; - } - seal.replyToSender(ctx, msg, `已将<${ctx.player!.name}>的群名片设置为<${new_name}>`); - return '设置成功'; - } catch (e) { - logger.error(e); - return '设置失败'; - } - } -} diff --git a/src/tool/tools/resource/init.ts b/src/tool/tools/resource/init.ts index 01d9228..2ed2980 100644 --- a/src/tool/tools/resource/init.ts +++ b/src/tool/tools/resource/init.ts @@ -1,11 +1,10 @@ -// resource 子目录工具注册统一入口(本地文件/视频/点歌/语音) +// resource 子目录工具注册统一入口:资源查询与资源生产,不直接注册旧的发送工具。 import { registerMusicPlay } from "./tool_music"; -import { registerResourceTools as registerResourceSend } from "./tool_resource"; -import { registerRecord } from "./tool_voice"; +import { registerResourceTools as registerResourceList } from "./tool_resource"; +import { registerAudioTools } from "./tool_voice"; -/** 注册 resource 下全部资源与媒体工具 */ export function registerResourceTools() { - registerResourceSend(); + registerResourceList(); + registerAudioTools(); registerMusicPlay(); - registerRecord(); } diff --git a/src/tool/tools/resource/tool_music.ts b/src/tool/tools/resource/tool_music.ts index 3b38dbc..20800b8 100644 --- a/src/tool/tools/resource/tool_music.ts +++ b/src/tool/tools/resource/tool_music.ts @@ -1,26 +1,19 @@ -// 点歌工具 +// 音乐资源工具:搜索并返回 music 消息段,不直接发送。 import Config from "../../../config/config"; import { logger } from "../../../logger"; import Tool from "../../tool"; -interface MusicServiceConfig { - api: string; - cookie: string; -} +interface MusicServiceConfig { api: string; cookie: string; } -/** 从「音乐服务配置」模板配置中解析指定平台的 域名/Cookie(仅支持 JSON 格式);未配置时返回 null */ function getMusicConfig(platform: string): MusicServiceConfig | null { - const list = Config.tool.MUSIC || []; - for (const line of list) { + for (const line of Config.tool.MUSIC || []) { try { - const j = JSON.parse(line || ''); - if (!j || typeof j !== 'object') continue; - if (String(j.platform || '').trim() !== platform) continue; - const api = String(j.api || '').trim(); - if (!api) continue; - return { api, cookie: String(j.cookie || '').trim() }; - } catch (e) { - logger.error(`音乐服务配置解析失败(仅支持 JSON 格式): ${e instanceof Error ? e.message : String(e)},内容: ${line}`); + const item = JSON.parse(line || ""); + if (item && String(item.platform || "").trim() === platform && String(item.api || "").trim()) { + return { api: String(item.api).trim(), cookie: String(item.cookie || "").trim() }; + } + } catch (error) { + logger.error(`音乐服务配置解析失败:${error instanceof Error ? error.message : String(error)}`); } } return null; @@ -30,98 +23,51 @@ export function registerMusicPlay() { const tool = new Tool({ type: "function", function: { - name: "music_play", - description: `搜索并播放音乐`, + name: "search_music", + description: "搜索音乐并返回可交给 call_ob11_api 的 music 消息段,不会自动发送。", parameters: { type: "object", properties: { - platform: { - type: "string", - description: "音乐平台", - enum: ["网易云", "qq"] - }, - song_name: { - type: "string", - description: "歌曲名称" - } + platform: { type: "string", enum: ["网易云", "qq"], description: "音乐平台" }, + song_name: { type: "string", description: "歌曲名称" } }, required: ["platform", "song_name"] } } }); - tool.sensitive = true; // 发送点歌属敏感操作 - tool.solve = async (ctx, msg, _, args) => { - const { platform, song_name } = args; - - if (platform !== '网易云' && platform !== 'qq') { - return `不支持的平台: ${platform}`; - } + tool.solve = async (_ctx, _msg, _session, args) => { + const platform = String(args.platform || ""); + const songName = String(args.song_name || ""); + if (!songName) return "歌曲名称不能为空"; const config = getMusicConfig(platform); - if (!config) { - return `未配置 ${platform} 的音乐服务:请在「工具」配置的「音乐服务配置」中添加 {"platform":"${platform}","api":"域名","cookie":""} 格式的 JSON 条目`; - } - - const api = platform === '网易云' - ? `${config.api}/search?keywords=${encodeURIComponent(song_name)}` - : `${config.api}/search?key=${encodeURIComponent(song_name)}`; + if (!config) return `未配置 ${platform} 的音乐服务`; try { - logger.info(`搜索音乐: ${api}`); - const response = await fetch(api, { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }); + const api = platform === "网易云" + ? `${config.api}/search?keywords=${encodeURIComponent(songName)}` + : `${config.api}/search?key=${encodeURIComponent(songName)}`; + const response = await fetch(api, { headers: { "Content-Type": "application/json" } }); + if (!response.ok) throw new Error(`${platform} API 失效`); + const data: any = await response.json(); - if (!response.ok) { - throw new Error(`${platform}API失效`); + if (platform === "网易云") { + const song = data.result?.songs?.[0]; + if (!song) return "网易云没找到这首歌"; + const id = song.id; + const detail = await (await fetch(`${config.api}/song/detail?ids=${id}`)).json() as any; + const headers: Record = { "User-Agent": "aiplugin4" }; + if (config.cookie) headers.Cookie = config.cookie; + const download = await (await fetch(`${config.api}/song/download/url?id=${id}`, { headers })).json() as any; + const audio = download.data?.url || ""; + return JSON.stringify({ kind: "message_segment", segment: { type: "music", data: { type: "custom", url: audio, audio, title: song.name, content: song.artists?.[0]?.name || "", image: detail.songs?.[0]?.al?.picUrl || "" } } }); } - const data = await response.json(); - - switch (platform) { - case '网易云': { - const song = data.result.songs[0]; - if (!song) { - return "网易云没找到这首歌"; - } - - const id = song.id; - const name = song.name; - const artist = song.artists[0].name; - - const imgResponse = await fetch(`${config.api}/song/detail?ids=${id}`); - const imgData = await imgResponse.json(); - const img = imgData.songs[0].al.picUrl; - - const headers: { [key: string]: string } = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' - }; - if (config.cookie) headers['Cookie'] = config.cookie; - const downloadResponse = await fetch(`${config.api}/song/download/url?id=${id}`, { headers }); - const downloadData = await downloadResponse.json(); - const url = downloadData.data.url; - - seal.replyToSender(ctx, msg, `[CQ:music,type=163,url=${url},audio=${url},title=${name},content=${artist},image=${img}]`); - return `发送成功,歌名:${name},歌手:${artist}`; - } - case 'qq': { - const song = data.data.list[0]; - if (!song) { - return "QQ音乐没找到这首歌..."; - } - - seal.replyToSender(ctx, msg, `[CQ:music,type=qq,id=${song.songid}]`); - return '发送成功'; - } - default: { - return "不支持的平台"; - } - } + const song = data.data?.list?.[0]; + if (!song) return "QQ音乐没找到这首歌"; + return JSON.stringify({ kind: "message_segment", segment: { type: "music", data: { type: "qq", id: String(song.songid) } } }); } catch (error) { - logger.warning(`音乐搜索请求错误: ${error}`); - return `音乐搜索请求错误: ${error}`; + logger.warning(`音乐搜索请求错误:${error instanceof Error ? error.message : String(error)}`); + return `音乐搜索请求错误:${error instanceof Error ? error.message : String(error)}`; } }; } diff --git a/src/tool/tools/resource/tool_resource.ts b/src/tool/tools/resource/tool_resource.ts index bbad413..65abbba 100644 --- a/src/tool/tools/resource/tool_resource.ts +++ b/src/tool/tools/resource/tool_resource.ts @@ -1,156 +1,29 @@ -// 资源工具:本地文件/视频发送(走 ob11 网络连接依赖) +// 资源查询工具:只负责列出可用资源,不直接发送消息。 import Config from "../../../config/config"; -import { logger } from "../../../logger"; -import { netExists, sendGroupFile, sendGroupMsg, sendPrivateFile, sendPrivateMsg } from "../../../utils/ob11"; -import { resolveResourceReference } from "../../../utils/resource"; import Tool from "../../tool"; -type ResourceEntry = { [key: string]: string | undefined }; - -/** 按“资源名=路径”映射构建 名称->路径 表,key 为 id 字段名(imageId/fileId/videoId) */ -function buildPathMap(items: ResourceEntry[], key: string): { [key: string]: string } { - const pathMap: { [key: string]: string } = {}; - for (const item of items || []) { - const id = item[key]; - if (id && item.path) pathMap[id] = item.path; - } - return pathMap; -} - function buildResourceList(type: string): string { const sections: Array<[string, string[]]> = []; - if (type === 'all' || type === 'image') { - sections.push(['本地图片', (Config.resource.LOCAL_IMAGES || []).map(img => img.imageId)]); - } - if (type === 'all' || type === 'audio') { - sections.push(['本地音频', (Config.resource.LOCAL_AUDIOS || []).map(a => a.audioId)]); - } - if (type === 'all' || type === 'file') { - sections.push(['本地文件', (Config.resource.LOCAL_FILES || []).map(f => f.fileId)]); - } - if (type === 'all' || type === 'video') { - sections.push(['本地视频', (Config.resource.LOCAL_VIDEOS || []).map(v => v.videoId)]); - } - return sections.map(([label, names]) => `${label}:${names.length > 0 ? names.join('、') : '暂无'}`).join('\n'); -} - -function getConfiguredResources(key: 'imageId' | 'fileId' | 'videoId'): ResourceEntry[] { - if (key === 'imageId') { - return (Config.resource.LOCAL_IMAGES || []).map(item => ({ [key]: item.imageId, path: item.path })); - } - if (key === 'fileId') { - return (Config.resource.LOCAL_FILES || []).map(item => ({ [key]: item.fileId, path: item.path })); - } - return (Config.resource.LOCAL_VIDEOS || []).map(item => ({ [key]: item.videoId, path: item.path })); -} - -function registerResourceTool( - name: string, - desc: string, - key: 'imageId' | 'fileId' | 'videoId', - segmentType: 'image' | 'file' | 'video' -) { - const tool = new Tool({ - type: 'function', - function: { - name, - description: `${desc}。可传 name(已登记资源,先通过 list_resources 查询可用名称)或 path(本地绝对路径、相对 SealDice 目录的路径、file:// URI,或 mcp://服务器名/沙箱路径);也可用 source=mcp、server、path 调用 MCP 文件服务器`, - parameters: { - type: 'object', - properties: { - name: { - type: 'string', - description: '资源名称(已登记资源)' - }, - path: { - type: 'string', - description: '文件路径:本地绝对路径、相对 SealDice 目录的路径、file:// URI,或 mcp://服务器名/沙箱相对路径' - }, - source: { - type: 'string', - enum: ['local', 'mcp'], - description: '资源来源;使用 MCP 文件服务器时填 mcp' - }, - server: { - type: 'string', - description: 'MCP 服务器名称,source=mcp 时使用,默认 mcp-files-exec' - } - }, - required: [] - } - } - }); - tool.sensitive = true; // 发送文件/视频属敏感操作 - tool.solve = async (ctx, _, __, args) => { - const { name, path, source, server } = args; - - // 每次调用实时读取配置,保证修改配置后无需重载即可生效 - const items = getConfiguredResources(key); - const pathMap = buildPathMap(items, key); - const nameList = Object.keys(pathMap); - - let rawPath = ''; - if (name && path) { - return `name 与 path 不能同时提供,请二选一:name(已登记资源)或 path(直接传路径)`; - } - if (name) { - if (!Object.prototype.hasOwnProperty.call(pathMap, name)) { - logger.error(`${desc}${name}不存在`); - return `${desc}${name}不存在${nameList.length !== 0 ? `,可发送的资源名有:${nameList.join('、')}` : ''}`; - } - rawPath = pathMap[name]; - } else if (path) { - rawPath = path; - } else { - return `必须提供 name(已登记资源)或 path(直接传路径)中的一项`; - } - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - try { - const resource = await resolveResourceReference(rawPath, source, server); - const epId = ctx.endPoint.userId; - let result = null; - const peerId = ctx.isPrivate - ? ctx.player!.userId.replace(/^.+:/, '') - : ctx.group!.groupId.replace(/^.+:/, ''); - if (segmentType === 'file') { - result = ctx.isPrivate - ? await sendPrivateFile(epId, peerId, resource.path, resource.name) - : await sendGroupFile(epId, peerId, resource.path, resource.name); - } else { - const segment = { type: segmentType, data: { file: resource.path } }; - result = ctx.isPrivate - ? await sendPrivateMsg(epId, peerId, [segment]) - : await sendGroupMsg(epId, peerId, [segment]); - } - - if (result === null || result === undefined) return `${desc}发送失败,请查看ob11网络连接依赖日志`; - return `${desc}发送成功`; - } catch (e) { - logger.error(`${desc}失败:${e instanceof Error ? e.message : String(e)}`); - return `${desc}失败:${e instanceof Error ? e.message : String(e)}`; - } - }; + if (type === "all" || type === "image") sections.push(["本地图片", (Config.resource.LOCAL_IMAGES || []).map(item => item.imageId)]); + if (type === "all" || type === "audio") sections.push(["本地音频", (Config.resource.LOCAL_AUDIOS || []).map(item => item.audioId)]); + if (type === "all" || type === "file") sections.push(["本地文件", (Config.resource.LOCAL_FILES || []).map(item => item.fileId)]); + if (type === "all" || type === "video") sections.push(["本地视频", (Config.resource.LOCAL_VIDEOS || []).map(item => item.videoId)]); + return sections.map(([label, names]) => `${label}:${names.length > 0 ? names.join("、") : "暂无"}`).join("\n"); } export function registerResourceTools() { - registerResourceTool('send_image', '发送本地图片', 'imageId', 'image'); - registerResourceTool('send_file', '发送本地文件', 'fileId', 'file'); - registerResourceTool('send_video', '发送本地视频', 'videoId', 'video'); - const tool = new Tool({ - type: 'function', + type: "function", function: { - name: 'list_resources', - description: '查询当前已配置的本地资源名称。type 可选 image/audio/file/video/all。图片和音频分别使用 [img:名称] 和 [audio:名称];文件和视频使用 send_file/send_video 的 name 参数。', + name: "list_resources", + description: "查询当前已配置的本地资源名称。发送资源请使用 call_ob11_api 的 image/record/video/file 消息段,并将 file 写成 resource:资源名。", parameters: { - type: 'object', + type: "object", properties: { type: { - type: 'string', - enum: ['all', 'image', 'audio', 'file', 'video'], - description: '资源类型,默认 all' + type: "string", + enum: ["all", "image", "audio", "file", "video"], + description: "资源类型,默认 all" } }, required: [] @@ -158,10 +31,10 @@ export function registerResourceTools() { } }); tool.solve = async (_, __, ___, args) => { - const type = args && typeof args.type === 'string' ? args.type : 'all'; - if (!['image', 'audio', 'file', 'video', 'all'].includes(type)) { + const type = args && typeof args.type === "string" ? args.type : "all"; + if (!["image", "audio", "file", "video", "all"].includes(type)) { return `未知资源类型:${type},可选值为 image/audio/file/video/all`; } return buildResourceList(type); }; -} \ No newline at end of file +} diff --git a/src/tool/tools/resource/tool_voice.ts b/src/tool/tools/resource/tool_voice.ts index e20868c..c072e45 100644 --- a/src/tool/tools/resource/tool_voice.ts +++ b/src/tool/tools/resource/tool_voice.ts @@ -1,158 +1,48 @@ -// 语音工具:本地语音/文字转语音(tts) +// 音频资源工具:生成音频资源,不直接发送消息。 import Config from "../../../config/config"; import { logger } from "../../../logger"; -import { netExists, sendGroupAISound } from "../../../utils/ob11"; import { resolveResourceReference } from "../../../utils/resource"; import Tool from "../../tool"; -const characterMap: { [key: string]: string } = { - "小新": "lucy-voice-laibixiaoxin", - "猴哥": "lucy-voice-houge", - "四郎": "lucy-voice-silang", - "东北老妹儿": "lucy-voice-guangdong-f1", - "广西大表哥": "lucy-voice-guangxi-m1", - "妲己": "lucy-voice-daji", - "霸道总裁": "lucy-voice-lizeyan", - "酥心御姐": "lucy-voice-suxinjiejie", - "说书先生": "lucy-voice-m8", - "憨憨小弟": "lucy-voice-male1", - "憨厚老哥": "lucy-voice-male3", - "吕布": "lucy-voice-lvbu", - "元气少女": "lucy-voice-xueling", - "文艺少女": "lucy-voice-f37", - "磁性大叔": "lucy-voice-male2", - "邻家小妹": "lucy-voice-female1", - "低沉男声": "lucy-voice-m14", - "傲娇少女": "lucy-voice-f38", - "爹系男友": "lucy-voice-m101", - "暖心姐姐": "lucy-voice-female2", - "温柔妹妹": "lucy-voice-f36", - "书香少女": "lucy-voice-f34" -}; - -export function registerRecord() { - // 本地语音统一走“资源”配置(支持 语音名=路径 或纯路径) - const toolRecord = new Tool({ +export function registerAudioTools() { + const tool = new Tool({ type: "function", function: { - name: "record", - description: `发送本地语音。可传 name(先通过 list_resources(type=audio) 查询)或 path(本地绝对路径、相对 SealDice 目录的路径、file:// URI、mcp://服务器名/沙箱相对路径)`, + name: "generate_audio", + description: "生成或解析语音资源,返回可交给 call_ob11_api 的 record 消息段;不会自动发送。自定义音色需要 tts 生成音频依赖。", parameters: { type: "object", properties: { - name: { - type: "string", - description: "语音名称" - }, - path: { - type: "string", - description: "本地语音路径、file:// URI,或 mcp://服务器名/沙箱相对路径" - }, - source: { - type: "string", - enum: ["local", "mcp"], - description: "资源来源;使用 MCP 文件服务器时填 mcp" - }, - server: { - type: "string", - description: "MCP 服务器名称,source=mcp 时使用,默认 mcp-files-exec" - } + text: { type: "string", description: "要合成的文本" }, + path: { type: "string", description: "已有本地语音路径、file:// URI 或 mcp:// 资源" }, + source: { type: "string", enum: ["local", "mcp"], description: "资源来源" }, + server: { type: "string", description: "MCP 文件服务器名称" } }, required: [] } } }); - toolRecord.sensitive = true; // 发送语音属敏感操作 - toolRecord.solve = async (ctx, msg, _, args) => { - const { name, path, source, server } = args; - - if (name && path) return 'name 与 path 不能同时提供,请二选一:name 或 path'; - if (!name && !path) return '必须提供 name(已登记资源)或 path(直接传路径)中的一项'; - - // 每次调用实时读取配置,保证修改配置后无需重载即可生效 - const recordPathMap: { [key: string]: string } = {}; - for (const audio of Config.resource.LOCAL_AUDIOS || []) { - recordPathMap[audio.audioId] = audio.path; - } + tool.solve = async (_ctx, _msg, _session, args) => { + const text = args && typeof args.text === "string" ? args.text : ""; + const path = args && typeof args.path === "string" ? args.path : ""; + if (!text && !path) return "必须提供 text 或 path"; + if (text && path) return "text 与 path 不能同时提供"; try { if (path) { - const resource = await resolveResourceReference(path, source, server); - seal.replyToSender(ctx, msg, `[语音:${resource.path}]`); - return '发送成功'; - } - if (Object.prototype.hasOwnProperty.call(recordPathMap, name)) { - const resource = await resolveResourceReference(recordPathMap[name]); - seal.replyToSender(ctx, msg, `[语音:${resource.path}]`); - return '发送成功'; - } else { - const nameList = Object.keys(recordPathMap); - logger.error(`本地语音${name}不存在`); - return `本地语音${name}不存在${nameList.length !== 0 ? `,可发送的语音名有:${nameList.join('、')}` : ''}`; + const resource = await resolveResourceReference(path, args.source, args.server); + return JSON.stringify({ kind: "resource", type: "record", name: resource.name, segment: { type: "record", data: { file: resource.path } } }); } - } catch (e) { - logger.error(`发送语音失败:${e instanceof Error ? e.message : String(e)}`); - return `发送语音失败:${e instanceof Error ? e.message : String(e)}`; - } - } - - const toolTTS = new Tool({ - type: 'function', - function: { - name: 'text_to_sound', - description: '发送AI声聊合成语音', - parameters: { - type: 'object', - properties: { - text: { - type: 'string', - description: '要合成的文本' - } - }, - required: ['text'] + if (!globalThis.tts) { + return JSON.stringify({ ok: false, code: "TTS_DEPENDENCY_REQUIRED", message: "文字转语音需要安装 tts 生成音频依赖" }); } + const result = await globalThis.tts.generate({ text, model: String(Config.tool.TTS_CHARACTER || "") }); + if (!result.success) return JSON.stringify({ ok: false, code: "TTS_ERROR", message: result.error || "生成音频失败" }); + const file = /^https?:\/\//i.test(result.data) ? result.data : seal.base64ToImage(result.data); + return JSON.stringify({ kind: "resource", type: "record", segment: { type: "record", data: { file } } }); + } catch (error) { + logger.error(`生成音频失败:${error instanceof Error ? error.message : String(error)}`); + return JSON.stringify({ ok: false, code: "AUDIO_GENERATION_ERROR", message: error instanceof Error ? error.message : String(error) }); } - }); - toolTTS.sensitive = true; // AI 声聊合成语音属敏感操作 - toolTTS.solve = async (ctx, msg, _, args) => { - const { text } = args; - - const { TTS_CHARACTER: character } = Config.tool; - if (character === '自定义') { - const ttsExt = seal.ext.find('tts'); - if (!ttsExt) { - logger.error(`未找到生成音频依赖(tts)`); - return `未找到生成音频依赖(tts),请提示用户安装生成音频依赖`; - } - try { - if (!globalThis.tts) return `未找到生成音频依赖(tts),请提示用户安装生成音频依赖`; - const result = await globalThis.tts.generate({ text }); - if (!result.success) throw new Error(result.error || '生成音频失败'); - let file = result.data; - if (!/^https?:\/\//i.test(file)) { - try { - file = seal.base64ToImage(file); - } catch (e) { - throw new Error(`已获取音频数据,但保存为本地文件失败:${e}`); - } - } - seal.replyToSender(ctx, msg, `[CQ:record,file=${file}]`); - } catch (e) { - logger.error(e); - return `发送语音失败`; - } - - return `发送语音成功`; - } - - if (!netExists()) return `未找到ob11网络连接依赖,请提示用户安装`; - - const epId = ctx.endPoint.userId; - const gid = ctx.group!.groupId; - - const characterId = characterMap[character]; - await sendGroupAISound(epId, characterId, gid.replace(/^.+:/, ''), text); - - return `发送语音成功`; - } + }; } diff --git a/src/tool/tools/seal/tool_attr.ts b/src/tool/tools/seal/tool_attr.ts index 72f412f..4136fd8 100644 --- a/src/tool/tools/seal/tool_attr.ts +++ b/src/tool/tools/seal/tool_attr.ts @@ -11,24 +11,24 @@ export function registerAttrSeal() { parameters: { type: 'object', properties: { - name: { + user_id: { type: 'string', - description: '用户名称或纯数字QQ号' + description: '用户ID' }, attr: { type: 'string', description: '属性名称' } }, - required: ['name', 'attr'] + required: ['user_id', 'attr'] } } }); toolGet.solve = async (ctx, _, session, args) => { - const { name, attr } = args; + const { user_id, attr } = args; - const ui = await session.context.findUser(ctx, name); - if (ui === null) return `未找到<${name}>`; + const ui = await session.context.getUserById(user_id); + if (ui === null) return `未找到用户ID<${user_id}>`; ({ ctx } = getCtxAndMsg(ctx.endPoint.userId, ui.userId, ctx.group!.groupId)); @@ -44,24 +44,24 @@ export function registerAttrSeal() { parameters: { type: 'object', properties: { - name: { + user_id: { type: 'string', - description: '用户名称或纯数字QQ号' + description: '用户ID' }, expression: { type: 'string', description: '修改表达式,例如`hp=hp+1d6`就是将hp的值修改为hp+1d6' } }, - required: ['name', 'expression'] + required: ['user_id', 'expression'] } } }); toolSet.solve = async (ctx, msg, session, args) => { - const { name, expression } = args; + const { user_id, expression } = args; - const ui = await session.context.findUser(ctx, name); - if (ui === null) return `未找到<${name}>`; + const ui = await session.context.getUserById(user_id); + if (ui === null) return `未找到用户ID<${user_id}>`; ({ ctx, msg } = getCtxAndMsg(ctx.endPoint.userId, ui.userId, ctx.group!.groupId)); diff --git a/src/transport/ob11/capability_catalog.ts b/src/transport/ob11/capability_catalog.ts new file mode 100644 index 0000000..3f6c460 --- /dev/null +++ b/src/transport/ob11/capability_catalog.ts @@ -0,0 +1,100 @@ +/** + * OB11 action 能力目录。 + * + * native 表示没有 ob11 网络依赖时仍可由 SealDice 原生输出或上下文完成; + * network 表示必须通过 ob11-net 执行; + * either 表示优先走 ob11-net,没有依赖时交给 native handler。 + */ +export type Ob11ActionCapability = "native" | "network" | "either"; + +const NATIVE_ACTIONS = new Set([ + "send_private_msg", + "send_group_msg" +]); + +const CONTEXT_ACTIONS = new Set([ + "get_login_info", + "get_status", + "get_version_info", + "get_group_info", + "get_group_member_info", + "get_stranger_info" +]); + +/** 这些动作需要通过远端协议读取历史、列表或管理状态。 */ +const NETWORK_ACTIONS = new Set([ + "get_msg", + "get_forward_msg", + "get_friend_list", + "get_group_list", + "get_group_member_list", + "get_group_msg_history", + "get_friend_msg_history", + "set_group_kick", + "set_group_ban", + "set_group_anonymous_ban", + "set_group_whole_ban", + "set_group_admin", + "set_group_anonymous", + "set_group_card", + "set_group_name", + "set_group_leave", + "set_group_special_title", + "set_friend_add_request", + "set_group_add_request", + "send_like", + "set_essence_msg", + "delete_essence_msg", + "get_essence_msg_list", + "send_group_sign", + "set_group_sign", + "get_group_shut_list", + "upload_group_file", + "upload_private_file", + "get_group_file_url", + "get_group_root_files", + "get_group_files_by_folder", + "create_group_file_folder", + "delete_group_file", + "delete_group_folder", + "move_group_file", + "rename_group_file", + "get_private_file_url", + "send_group_forward_msg", + "send_private_forward_msg", + "send_forward_msg", + "delete_msg", + "set_group_portrait", + "set_qq_avatar", + "set_qq_profile", + "get_cookies", + "get_csrf_token", + "get_credentials", + "get_record", + "get_image", + "can_send_image", + "can_send_record", + "set_restart", + "clean_cache" +]); + +export function getActionCapability(action: string): Ob11ActionCapability { + if (NATIVE_ACTIONS.has(action)) return "either"; + if (CONTEXT_ACTIONS.has(action)) return "either"; + // 未知动作仍允许 ob11-net 原样透传,未安装依赖时不能伪造。 + return NETWORK_ACTIONS.has(action) ? "network" : "network"; +} + +export function isNativeAction(action: string): boolean { + return NATIVE_ACTIONS.has(action) || CONTEXT_ACTIONS.has(action); +} + +export function isNetworkAction(action: string): boolean { + return getActionCapability(action) === "network"; +} + +export const OB11_CORE_ACTIONS = [ + ...Array.from(NATIVE_ACTIONS), + ...Array.from(CONTEXT_ACTIONS), + ...Array.from(NETWORK_ACTIONS) +]; diff --git a/src/transport/ob11/dispatcher.ts b/src/transport/ob11/dispatcher.ts new file mode 100644 index 0000000..5f62c94 --- /dev/null +++ b/src/transport/ob11/dispatcher.ts @@ -0,0 +1,73 @@ +import Logger from "../../logger"; + +import { getActionCapability } from "./capability_catalog"; +import { Ob11NetBackend } from "./ob11_net_backend"; +import { failure, serializeResult } from "./result"; +import { SealNativeBackend } from "./seal_native_backend"; +import { Ob11CallContext, Ob11Result } from "./types"; + +const ob11NetBackend = new Ob11NetBackend(); +const sealNativeBackend = new SealNativeBackend(); + +export function hasOb11Network(): boolean { + return ob11NetBackend.canHandle(""); +} + +export function dependencyRequired(action: string): Ob11Result { + return failure( + "seal-native", + action, + "OB11_DEPENDENCY_REQUIRED", + `${action} 需要 ob11 网络连接依赖,当前未检测到 globalThis.net`, + { + install_hint: "请安装 ob11 网络连接依赖或 http 依赖插件", + retryable: false + } + ); +} + +/** 面向工具和内部模块的统一调用入口。每次调用实时检测依赖,支持热加载/卸载。 */ +export async function dispatchOb11Api( + context: Ob11CallContext, + action: string, + params: Record +): Promise { + const normalizedAction = String(action || "").trim(); + if (!normalizedAction) { + return failure("seal-native", "", "INVALID_PARAMS", "action 不能为空"); + } + + if (ob11NetBackend.canHandle(normalizedAction)) { + return ob11NetBackend.call(context, normalizedAction, params || {}); + } + + if (getActionCapability(normalizedAction) === "network" || !sealNativeBackend.canHandle(normalizedAction)) { + return dependencyRequired(normalizedAction); + } + + return sealNativeBackend.call(context, normalizedAction, params || {}); +} + +/** 只在已有 ob11 网络依赖时使用的无消息上下文调用,供上下文查询和事件展开使用。 */ +export async function callOb11ApiDirect(endpointId: string, action: string, params: Record = {}): Promise { + if (!ob11NetBackend.canHandle(action)) return null; + const result = await ob11NetBackend.call({ endpointId }, action, params); + if (result.ok === false) { + Logger.warning(`OB11 API ${action} 调用失败:${result.error.message}`); + return null; + } + return result.data; +} + +export async function callOb11ApiForContext( + ctx: seal.MsgContext, + msg: seal.Message, + action: string, + params: Record = {} +): Promise { + return dispatchOb11Api({ ctx, msg, endpointId: ctx.endPoint.userId }, action, params); +} + +export function formatOb11Result(result: Ob11Result): string { + return serializeResult(result); +} diff --git a/src/transport/ob11/message_segments.ts b/src/transport/ob11/message_segments.ts new file mode 100644 index 0000000..cd86f80 --- /dev/null +++ b/src/transport/ob11/message_segments.ts @@ -0,0 +1,90 @@ +import Config from "../../config/config"; +import { resolveResourceReference } from "../../utils/resource"; +import { MessageSegment } from "../../utils/string"; + +function getResourcePath(id: string): string | null { + const rawId = id.replace(/^resource:/i, ""); + const groups = [ + ...(Config.resource.LOCAL_IMAGES || []).map((item: any) => ({ id: item.imageId, path: item.path })), + ...(Config.resource.LOCAL_AUDIOS || []).map((item: any) => ({ id: item.audioId, path: item.path })), + ...(Config.resource.LOCAL_FILES || []).map((item: any) => ({ id: item.fileId, path: item.path })), + ...(Config.resource.LOCAL_VIDEOS || []).map((item: any) => ({ id: item.videoId, path: item.path })) + ]; + return groups.find(item => item.id === rawId)?.path || null; +} + +/** 将消息/文件上传参数中的资源值解析成协议端可读取的路径或 URI。 */ +export async function normalizeFileReference(value: any): Promise { + if (typeof value !== "string") return value; + const configured = /^resource:/i.test(value) ? getResourcePath(value) : value; + if (!configured) throw new Error(`未找到资源引用:${value}`); + const resource = await resolveResourceReference(configured); + return resource.path; +} + +/** 解析资源引用,但不把消息段降级成普通文本。 */ +export async function normalizeMessageSegments(message: MessageSegment[] | string): Promise { + if (typeof message === "string") return message; + if (!Array.isArray(message)) throw new Error("message 必须是字符串或消息段数组"); + + const result: MessageSegment[] = []; + for (const segment of message) { + if (!segment || typeof segment !== "object" || typeof segment.type !== "string") { + throw new Error("消息段格式无效"); + } + const data: Record = { ...(segment.data || {}) }; + if (["image", "record", "video", "file"].includes(segment.type) && data.file) { + data.file = await normalizeFileReference(data.file); + } + if (segment.type === "node" && Array.isArray(data.content)) { + data.content = await normalizeMessageSegments(data.content as MessageSegment[]); + } + result.push({ type: segment.type, data }); + } + return result; +} + +function escapeCQ(value: any): string { + return String(value ?? "") + .replace(/&/g, "&") + .replace(/\[/g, "[") + .replace(/\]/g, "]") + .replace(/,/g, ","); +} + +function musicToCQ(data: Record): string { + const type = data.type || "custom"; + if (type === "qq" || type === "163") return `[CQ:music,type=${escapeCQ(type)},id=${escapeCQ(data.id)}]`; + return `[CQ:music,type=custom,url=${escapeCQ(data.url)},audio=${escapeCQ(data.audio)},title=${escapeCQ(data.title)},content=${escapeCQ(data.content || "")},image=${escapeCQ(data.image || "")}]`; +} + +/** 将 OB11 消息段编码成 SealDice 原生可以接收的消息字符串。 */ +export function encodeNativeMessage(message: MessageSegment[] | string): string { + if (typeof message === "string") return message; + return message.map(segment => { + const data = segment.data || {}; + switch (segment.type) { + case "text": return String(data.text || ""); + case "face": return `[CQ:face,id=${escapeCQ(data.id)}]`; + case "image": return `[CQ:image,file=${escapeCQ(data.file || data.url)}]`; + case "record": return `[CQ:record,file=${escapeCQ(data.file || data.url)}${data.magic ? `,magic=${escapeCQ(data.magic)}` : ""}]`; + case "video": return `[CQ:video,file=${escapeCQ(data.file || data.url)}]`; + case "at": return `[CQ:at,qq=${escapeCQ(data.qq || data.user_id)}]`; + case "reply": return `[CQ:reply,id=${escapeCQ(data.id)}]`; + case "json": return `[CQ:json,data=${escapeCQ(data.data || data.content || "")}]`; + case "markdown": return `[CQ:markdown,content=${escapeCQ(data.content || data.data || "")}]`; + case "file": return `[CQ:file,file=${escapeCQ(data.file || data.url)}${data.name ? `,name=${escapeCQ(data.name)}` : ""}]`; + case "music": return musicToCQ(data); + case "poke": return `[CQ:poke,qq=${escapeCQ(data.qq || data.user_id)}]`; + case "dice": return "[CQ:dice]"; + case "rps": return "[CQ:rps]"; + case "forward": + if (data.id) return `[CQ:forward,id=${escapeCQ(data.id)}]`; + throw new Error("native 后端无法凭空构造远程 forward,请使用 send_*_forward_msg 并安装 ob11 网络连接依赖"); + case "node": + throw new Error("native 后端不支持直接发送 node 合并转发,请安装 ob11 网络连接依赖"); + default: + throw new Error(`native 后端不支持消息段:${segment.type}`); + } + }).join(""); +} diff --git a/src/transport/ob11/ob11_net_backend.ts b/src/transport/ob11/ob11_net_backend.ts new file mode 100644 index 0000000..119d1cf --- /dev/null +++ b/src/transport/ob11/ob11_net_backend.ts @@ -0,0 +1,66 @@ +import Logger from "../../logger"; + +import { normalizeFileReference, normalizeMessageSegments } from "./message_segments"; +import { failure, success } from "./result"; +import { Ob11Backend, Ob11CallContext, Ob11Result } from "./types"; + +function getNet(): NetApi | null { + const net = globalThis.net; + return net && typeof net.callApi === "function" ? net : null; +} + +function getMessageId(data: any): string | number | null { + return data && (data.message_id ?? data.msgid ?? data.message_seq ?? null); +} + +export class Ob11NetBackend implements Ob11Backend { + readonly name = "ob11-net" as const; + + canHandle(_action: string): boolean { + return getNet() !== null; + } + + async call(context: Ob11CallContext, action: string, params: Record): Promise { + const net = getNet(); + if (!net) { + return failure(this.name, action, "OB11_DEPENDENCY_REQUIRED", "未检测到 ob11 网络连接依赖"); + } + + try { + const normalizedParams = { ...params }; + if (normalizedParams.message !== undefined) { + normalizedParams.message = await normalizeMessageSegments(normalizedParams.message); + } + if ((action === "upload_group_file" || action === "upload_private_file") && normalizedParams.file !== undefined) { + normalizedParams.file = await normalizeFileReference(normalizedParams.file); + } + + let data: any; + if (action === "upload_group_file" || action === "upload_private_file") { + const scene = action === "upload_group_file" ? "group" : "private"; + const peerId = scene === "group" ? normalizedParams.group_id : normalizedParams.user_id; + const file = normalizedParams.file; + if (typeof net.sendFile === "function" && peerId !== undefined && file) { + data = await net.sendFile( + context.endpointId, + scene, + peerId, + String(file), + String(normalizedParams.name || ""), + normalizedParams.folder_id + ); + } else { + data = await net.callApi(context.endpointId, action, normalizedParams); + } + } else { + data = await net.callApi(context.endpointId, action, normalizedParams); + } + + return success(this.name, action, data, getMessageId(data)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + Logger.error(`OB11 API ${action} 调用失败:${message}`); + return failure(this.name, action, "OB11_API_ERROR", message, { retryable: true }); + } + } +} diff --git a/src/transport/ob11/result.ts b/src/transport/ob11/result.ts new file mode 100644 index 0000000..b36f13b --- /dev/null +++ b/src/transport/ob11/result.ts @@ -0,0 +1,39 @@ +import { Ob11BackendName, Ob11Failure, Ob11Result, Ob11Success } from "./types"; + +export function success( + backend: Ob11BackendName, + action: string, + data?: any, + messageId: string | number | null = null +): Ob11Success { + return { + ok: true, + backend, + action, + data, + message_id: messageId + }; +} + +export function failure( + backend: Ob11BackendName, + action: string, + code: string, + message: string, + extra: Partial = {} +): Ob11Failure { + return { + ok: false, + backend, + action, + error: { + code, + message, + ...extra + } + }; +} + +export function serializeResult(result: Ob11Result): string { + return JSON.stringify(result); +} diff --git a/src/transport/ob11/seal_native_backend.ts b/src/transport/ob11/seal_native_backend.ts new file mode 100644 index 0000000..54d7779 --- /dev/null +++ b/src/transport/ob11/seal_native_backend.ts @@ -0,0 +1,123 @@ +import { getCtxAndMsg } from "../../utils/seal"; + +import { encodeNativeMessage, normalizeMessageSegments } from "./message_segments"; +import { failure, success } from "./result"; +import { Ob11Backend, Ob11CallContext, Ob11Result } from "./types"; + +function stripPrefix(value: any): string { + return String(value ?? "").replace(/^.+:/, ""); +} + +function sameId(left: any, right: any): boolean { + return stripPrefix(left) === stripPrefix(right); +} + +function contextSnapshot(context: Ob11CallContext, action: string, params: Record): Ob11Result { + const ctx: any = context.ctx; + if (!ctx) { + return failure("seal-native", action, "NATIVE_CONTEXT_UNAVAILABLE", "没有可用于读取原生上下文的 SealDice 消息上下文"); + } + + const endpointId = stripPrefix(context.endpointId); + const player = ctx.player || {}; + const group = ctx.group || {}; + const requestedUser = params.user_id === undefined ? "" : stripPrefix(params.user_id); + const requestedGroup = params.group_id === undefined ? "" : stripPrefix(params.group_id); + + switch (action) { + case "get_login_info": + return success("seal-native", action, { user_id: endpointId, nickname: ctx.endPoint?.userId === context.endpointId ? player.name || "" : "" }); + case "get_status": + return success("seal-native", action, { online: true, good: true, backend: "seal-native" }); + case "get_version_info": + return success("seal-native", action, { app_name: "SealDice", app_version: "unknown", protocol: "native" }); + case "get_group_info": + if (!group.groupId || (requestedGroup && !sameId(group.groupId, requestedGroup))) { + return failure("seal-native", action, "NATIVE_CONTEXT_UNAVAILABLE", "目标群不在当前 SealDice 上下文或本地缓存中"); + } + return success("seal-native", action, { + group_id: Number(stripPrefix(group.groupId)) || stripPrefix(group.groupId), + group_name: group.groupName || "" + }); + case "get_group_member_info": + if (!group.groupId || !player.userId || (requestedGroup && !sameId(group.groupId, requestedGroup)) || (requestedUser && !sameId(player.userId, requestedUser))) { + return failure("seal-native", action, "NATIVE_CONTEXT_UNAVAILABLE", "目标用户或群不在当前 SealDice 上下文或本地缓存中"); + } + return success("seal-native", action, { + group_id: Number(stripPrefix(group.groupId)) || stripPrefix(group.groupId), + user_id: Number(stripPrefix(player.userId)) || stripPrefix(player.userId), + nickname: player.name || "", + card: player.name || "", + role: player.role || "member" + }); + case "get_stranger_info": + if (!player.userId || (requestedUser && !sameId(player.userId, requestedUser))) { + return failure("seal-native", action, "NATIVE_CONTEXT_UNAVAILABLE", "目标用户不在当前 SealDice 上下文或本地缓存中"); + } + return success("seal-native", action, { + user_id: Number(stripPrefix(player.userId)) || stripPrefix(player.userId), + nickname: player.name || "", + sex: "unknown", + age: 0 + }); + default: + return failure("seal-native", action, "NATIVE_ACTION_UNSUPPORTED", `native 后端未实现 ${action}`); + } +} + +export class SealNativeBackend implements Ob11Backend { + readonly name = "seal-native" as const; + + canHandle(action: string): boolean { + return action === "send_private_msg" || action === "send_group_msg" || [ + "get_login_info", + "get_status", + "get_version_info", + "get_group_info", + "get_group_member_info", + "get_stranger_info" + ].includes(action); + } + + async call(context: Ob11CallContext, action: string, params: Record): Promise { + if (action !== "send_private_msg" && action !== "send_group_msg") { + return contextSnapshot(context, action, params); + } + + if (!context.ctx || !context.msg) { + return failure(this.name, action, "NATIVE_CONTEXT_UNAVAILABLE", "原生消息发送需要 SealDice 上下文"); + } + if (params.message === undefined) { + return failure(this.name, action, "INVALID_PARAMS", "send_*_msg 必须提供 message"); + } + + try { + const normalized = await normalizeMessageSegments(params.message); + const content = encodeNativeMessage(normalized); + if (!content) return failure(this.name, action, "INVALID_PARAMS", "message 为空"); + + const targetId = action === "send_group_msg" ? params.group_id : params.user_id; + if (targetId === undefined || targetId === null || targetId === "") { + return failure(this.name, action, "INVALID_PARAMS", `${action} 缺少目标 ID`); + } + + let targetCtx = context.ctx; + let targetMsg = context.msg; + const currentTarget = action === "send_group_msg" + ? (context.ctx.group && context.ctx.group.groupId) + : (context.ctx.player && context.ctx.player.userId); + if (!sameId(currentTarget, targetId)) { + const target = action === "send_group_msg" + ? getCtxAndMsg(context.endpointId, "", `QQ-Group:${stripPrefix(targetId)}`) + : getCtxAndMsg(context.endpointId, `QQ:${stripPrefix(targetId)}`, ""); + targetCtx = target.ctx; + targetMsg = target.msg; + } + + seal.replyToSender(targetCtx, targetMsg, content); + return success(this.name, action, { sent: true, content }, null); + } catch (error) { + return failure(this.name, action, "NATIVE_SEND_ERROR", error instanceof Error ? error.message : String(error), { retryable: false }); + } + } +} diff --git a/src/transport/ob11/types.ts b/src/transport/ob11/types.ts new file mode 100644 index 0000000..7c071de --- /dev/null +++ b/src/transport/ob11/types.ts @@ -0,0 +1,45 @@ +import { MessageSegment } from "../../utils/string"; + +export type Ob11BackendName = "ob11-net" | "seal-native"; + +export interface Ob11CallContext { + ctx?: seal.MsgContext; + msg?: seal.Message; + endpointId: string; +} + +export interface Ob11Success { + ok: true; + backend: Ob11BackendName; + action: string; + data?: any; + message_id?: string | number | null; +} + +export interface Ob11Failure { + ok: false; + backend: Ob11BackendName; + action: string; + error: { + code: string; + message: string; + retryable?: boolean; + install_hint?: string; + segment_type?: string; + }; +} + +export type Ob11Result = Ob11Success | Ob11Failure; + +export interface Ob11Backend { + readonly name: Ob11BackendName; + canHandle(action: string): boolean; + call(context: Ob11CallContext, action: string, params: Record): Promise; +} + +export interface Ob11MessageParams { + user_id?: string | number; + group_id?: string | number; + message?: MessageSegment[] | string; + auto_escape?: boolean; +} diff --git a/src/update.ts b/src/update.ts index 93c5f17..69a29e4 100644 --- a/src/update.ts +++ b/src/update.ts @@ -15,7 +15,7 @@ export const updateInfo: { [version: string]: string } = { - 修复负数消息 ID、未知表情、全体 at、私聊消息和嵌套消息节点进入上下文时的标签与发送者信息 ## 工具与指令 -- run_ext_command 与 run_core_command 支持 triggerUserId 指定触发对象、atUserId 指定群聊 @ 对象;扩展本地临时构造消息上下文,核心桥注入 OB11 的 user_id/sender.user_id 与 at 段。 +- run_ext_command 与 run_core_command 支持 trigger 指定触发对象、at 指定群聊 @ 对象;扩展本地临时构造消息上下文,核心桥注入 OB11 的 user_id/sender.user_id 与 at 段。 - 移除已由 run_ext_command 覆盖的 draw_deck 独立工具入口,牌堆、模组、今日人品等扩展能力统一通过扩展指令调用。 ## 工具与图片 - 增加 MCP 总开关与图生图能力:text_to_image 支持图片 ID、头像、URL、data URL 和 base64,适配 aiplugin4-dependencies 新接口 @@ -72,7 +72,7 @@ export const updateInfo: { [version: string]: string } = { - 修复「允许连续调用函数次数」在重构后未真正累计生效的问题:同一触发流程内的多轮工具调用现在按配置累计(设为 0 时不再限制连续调用次数),每次触发开始时自动清零 - 修复 kb_search 检索条数无上限的问题:单次返回条数限制在 1~50 之间,防止模型请求超大 topK 造成上下文与 API 浪费 - 修复私有记忆权限遗漏:del_memory / clear_memory 现在与 search_memory 一致,其他会话创建的私有记忆不可删除或清空(跨会话删除时自动保留并提示) -- 修复摘要智能体总结出的记忆归属:按模型返回的 memory_type / name 定位目标用户或群聊会话后写入(与 add_memory 工具一致),不再全部落进当前会话;找不到目标时跳过该条并记录日志 +- 修复摘要智能体总结出的记忆归属:按模型返回的 memory_type / target_id 定位目标用户或群聊会话后写入(与 add_memory 工具一致),不再全部落进当前会话;找不到目标时跳过该条并记录日志 - 修复 search_memory 私有记忆展示标头:个人记忆不再误标为群聊(显示私聊会话与用户名) - 修复摘要记忆对模型输出的容错:memories 非数组/条目缺 text 时跳过落库,缺失或未知 memory_type 兜底归属当前会话并计数,不再抛错导致「摘要已存、记忆未落库」的半成功状态 - 修复知识库分块 ID 跨条目碰撞:相同内容的不同条目通过条目序号区分,ID 保持稳定且全局唯一 diff --git a/src/utils/ob11.ts b/src/utils/ob11.ts index 7e71e37..60e6f17 100644 --- a/src/utils/ob11.ts +++ b/src/utils/ob11.ts @@ -1,156 +1,54 @@ -// ob11 API 封装:消息/群/好友/禁言等 -import { logger } from "../logger"; +// OB11 运行时工具:只保留统一调用与转发文本解析,不保留按 action 的旧包装函数。 +import Logger from "../logger"; +import { callOb11ApiDirect, hasOb11Network } from "../transport/ob11/dispatcher"; -import { MessageSegment, parseCardToText, parseMusicToText } from "./string"; +import { parseCardToText, parseMusicToText } from "./string"; -/** 合并转发/消息节点展开的最大嵌套深度,防止恶意或异常嵌套导致无限递归 */ const MAX_FORWARD_DEPTH = 5; -export function getNet() { - const net = globalThis.net; - if (!net) { - logger.warning(`未找到ob11网络连接依赖`); - return null; - } - return net; -} - export function netExists(): boolean { - const net = globalThis.net; - return net !== null && net !== undefined; -} - -export async function sendPrivateMsg(epId: string, user_id: string, message: MessageSegment[]): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'send_private_msg', { - user_id, - message - }) - return data; - } catch (_e) { - logger.error(`发送私聊消息失败`); - return null; - } -} - -/** - * 统一发送文件。新版 ob11 网络依赖提供 sendFile,可针对 Milky 正确调用 - * upload_group_file/upload_private_file;旧版依赖回退到 OB11 file 消息段。 - */ -export async function sendPrivateFile(epId: string, user_id: string, file: string, name: string): Promise { - const net = getNet(); - if (!net) return null; - try { - if (typeof net.sendFile === 'function') return await net.sendFile(epId, 'private', user_id, file, name); - return await net.callApi(epId, 'send_private_msg', { - user_id, - message: [{ type: 'file', data: { file, ...(name ? { name } : {}) } }] - }); - } catch (_e) { - logger.error(`发送私聊文件失败`); - return null; - } + return hasOb11Network(); } -export async function sendGroupFile(epId: string, group_id: string, file: string, name: string): Promise { - const net = getNet(); - if (!net) return null; - try { - if (typeof net.sendFile === 'function') return await net.sendFile(epId, 'group', group_id, file, name); - return await net.callApi(epId, 'send_group_msg', { - group_id, - message: [{ type: 'file', data: { file, ...(name ? { name } : {}) } }] - }); - } catch (_e) { - logger.error(`发送群文件失败`); - return null; - } -} -export async function sendGroupMsg(epId: string, group_id: string, message: MessageSegment[]): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'send_group_msg', { - group_id, - message - }) - return data; - } catch (_e) { - logger.error(`发送群聊消息失败`); - return null; - } +export async function callOb11Api(epId: string, action: string, params: Record = {}): Promise { + return callOb11ApiDirect(epId, action, params); } -export async function getStrangerInfo(epId: string, user_id: string): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'get_stranger_info', { - user_id, - no_cache: true - }) - return data; - } catch (e) { - logger.error(`获取用户 ${user_id} 信息失败:${e}`); - return null; - } -} - -/** 获取合并转发消息(OneBot get_forward_msg),返回消息数组。 - * 兼容 ob11 网络连接依赖 milky 转接的两种返回形态(均为 message 数组): - * - 平铺: [{ user_id, nickname, time, message: [...] }] - * - node: [{ type: 'node', data: { user_id, nickname, time, content: [...] } }] */ -export async function getForwardMessage(epId: string, id: string): Promise { - const net = getNet(); - if (!net) return []; - try { - const data = await net.callApi(epId, 'get_forward_msg', { id }); - if (!data) return []; - if (Array.isArray(data.message)) return data.message; - if (Array.isArray(data.messages)) return data.messages; - return []; - } catch (e) { - logger.error(`获取合并转发消息 ${id} 失败:${e}`); - return []; - } +async function getForwardMessage(epId: string, id: string): Promise { + const data = await callOb11Api(epId, "get_forward_msg", { id }); + if (!data) return []; + if (Array.isArray(data.message)) return data.message; + if (Array.isArray(data.messages)) return data.messages; + return []; } -/** 合并转发单条消息内容转可读文本(支持嵌套 forward) */ async function forwardSegmentsToText(epId: string, message: any, depth: number, visited: Set): Promise { - if (depth > MAX_FORWARD_DEPTH) return '[消息嵌套过深,已截断]'; - if (typeof message === 'string') return message; - if (!Array.isArray(message)) return ''; + if (depth > MAX_FORWARD_DEPTH) return "[消息嵌套过深,已截断]"; + if (typeof message === "string") return message; + if (!Array.isArray(message)) return ""; - let text = ''; + let text = ""; for (const seg of message) { - if (!seg || typeof seg !== 'object') continue; + if (!seg || typeof seg !== "object") continue; switch (seg.type) { - case 'text': text += (seg.data && seg.data.text) || ''; break; - case 'at': text += `@${(seg.data && seg.data.qq) || ''} `; break; - case 'face': text += `[表情${(seg.data && seg.data.id) || ''}]`; break; - case 'image': text += '[图片]'; break; - case 'record': text += '【语音】'; break; - case 'video': text += `【视频】${(seg.data && (seg.data.file || seg.data.url)) || ''}`; break; - case 'file': text += `【文件】${(seg.data && (seg.data.name || seg.data.file || seg.data.file_id)) || ''}`; break; - case 'json': text += parseCardToText(seg.data && seg.data.data); break; - case 'music': text += parseMusicToText(seg.data || {}); break; - case 'node': { - // 嵌套消息节点:复用整条消息的渲染逻辑(含发送者名) - text += await forwardMessagesToText(epId, [seg], depth + 1, visited); - break; - } - case 'forward': { - const fid = (seg.data && seg.data.id) || ''; - // 已展开过的转发 id 直接截断,防止循环引用或反复拉取同一转发 + case "text": text += (seg.data && seg.data.text) || ""; break; + case "at": text += `@${(seg.data && (seg.data.qq || seg.data.user_id)) || ""} `; break; + case "face": text += `[表情${(seg.data && seg.data.id) || ""}]`; break; + case "image": text += "[图片]"; break; + case "record": text += "【语音】"; break; + case "video": text += `【视频】${(seg.data && (seg.data.file || seg.data.url)) || ""}`; break; + case "file": text += `【文件】${(seg.data && (seg.data.name || seg.data.file || seg.data.file_id)) || ""}`; break; + case "json": text += parseCardToText(seg.data && (seg.data.data || seg.data.content)); break; + case "music": text += parseMusicToText(seg.data || {}); break; + case "node": text += await forwardMessagesToText(epId, [seg], depth + 1, visited); break; + case "forward": { + const fid = (seg.data && seg.data.id) || ""; if (!fid || visited.has(String(fid))) { - text += '[合并转发循环引用,已截断]'; + text += "[合并转发循环引用,已截断]"; break; } visited.add(String(fid)); - const sub = await getForwardMessage(epId, String(fid)); - text += await forwardMessagesToText(epId, sub, depth + 1, visited); + text += await forwardMessagesToText(epId, await getForwardMessage(epId, String(fid)), depth + 1, visited); break; } default: text += `[${seg.type}]`; @@ -159,267 +57,26 @@ async function forwardSegmentsToText(epId: string, message: any, depth: number, return text; } -/** 合并转发消息数组转可读文本(发送者 + 内容) */ -async function forwardMessagesToText(epId: string, messages: any[], depth: number = 0, visited: Set = new Set()): Promise { - if (depth > MAX_FORWARD_DEPTH) return '[消息嵌套过深,已截断]'; +async function forwardMessagesToText(epId: string, messages: any[], depth = 0, visited: Set = new Set()): Promise { + if (depth > MAX_FORWARD_DEPTH) return "[消息嵌套过深,已截断]"; const lines: string[] = []; - for (const m of messages) { - if (!m || typeof m !== 'object') continue; - // milky 转接的 node 形态:内容在 data.content;平铺形态:内容在 message - const nodeData = m.type === 'node' ? (m.data || {}) : null; - const sender = nodeData || m.sender || {}; - const name = sender.card || sender.nickname || `用户${sender.user_id || ''}`; - const content = await forwardSegmentsToText(epId, nodeData ? nodeData.content : m.message, depth + 1, visited); + for (const message of messages) { + if (!message || typeof message !== "object") continue; + const nodeData = message.type === "node" ? message.data || {} : null; + const sender = nodeData || message.sender || {}; + const name = sender.card || sender.nickname || `用户${sender.user_id || ""}`; + const content = await forwardSegmentsToText(epId, nodeData ? nodeData.content : message.message, depth + 1, visited); lines.push(`${name}: ${content}`); } - return lines.join('\n'); -} - -/** 展开合并转发消息 id,返回可读文本(含嵌套) */ -export async function expandForwardMessage(epId: string, id: string, depth: number = 0): Promise { - const messages = await getForwardMessage(epId, id); - return forwardMessagesToText(epId, messages, depth + 1); -} - -export async function getGroupMemberInfo(epId: string, group_id: string, user_id: string): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'get_group_member_info', { - group_id, - user_id, - no_cache: true - }) - return data; - } catch (e) { - logger.error(`获取群 ${group_id} 用户 ${user_id} 信息失败:${e}`); - return null; - } -} - -export async function getGroupMemberList(epId: string, group_id: string): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'get_group_member_list', { - group_id, - no_cache: true - }) - return data; - } catch (e) { - logger.error(`获取群 ${group_id} 成员列表失败:${e}`); - return null; - } -} - -export async function getFriendList(epId: string): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'get_friend_list'); - return data; - } catch (e) { - logger.error(`获取好友列表失败:${e}`); - return null; - } -} - -export async function getGroupList(epId: string): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'get_group_list'); - return data; - } catch (e) { - logger.error(`获取群列表失败:${e}`); - return null; - } -} - -export async function setGroupBan(epId: string, group_id: string, user_id: string, duration: number = 0): Promise { - const net = getNet(); - if (!net) return; - try { - await net.callApi(epId, 'set_group_ban', { - group_id, - user_id, - duration - }) - } catch (e) { - logger.error(`设置群 ${group_id} 用户 ${user_id} 禁言失败:${e}`); - return; - } -} - -export async function setGroupWholeBan(epId: string, group_id: string, enable: boolean): Promise { - const net = getNet(); - if (!net) return; - try { - await net.callApi(epId, 'set_group_whole_ban', { - group_id, - enable - }) - } catch (e) { - logger.error(`设置群 ${group_id} 全员禁言失败:${e}`); - return; - } -} - -export async function getGroupShutList(epId: string, group_id: string): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'get_group_shut_list', { - group_id, - no_cache: true - }) - return data; - } catch (e) { - logger.error(`获取群 ${group_id} 关闭列表失败:${e}`); - return null; - } -} - -export async function setEssenceMsg(epId: string, message_id: number): Promise { - const net = getNet(); - if (!net) return; - try { - await net.callApi(epId, 'set_essence_msg', { - message_id - }) - } catch (e) { - logger.error(`设置消息 ${message_id} 精华消息失败:${e}`); - return; - } -} - -export async function getEssenceMsgList(epId: string, group_id: string): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'get_essence_msg_list', { - group_id, - no_cache: true - }) - return data; - } catch (e) { - logger.error(`获取群 ${group_id} 精华消息列表失败:${e}`); - return null; - } -} - -export async function deleteEssenceMsg(epId: string, message_id: number): Promise { - const net = getNet(); - if (!net) return; - try { - await net.callApi(epId, 'delete_essence_msg', { - message_id - }) - } catch (e) { - logger.error(`删除消息 ${message_id} 精华消息失败:${e}`); - return; - } -} - -export async function sendGroupSign(epId: string, group_id: string): Promise { - const net = getNet(); - if (!net) return; - try { - await net.callApi(epId, 'send_group_sign', { - group_id - }); - } catch (e) { - logger.error(`发送群 ${group_id} 签名失败:${e}`); - return; - } -} - -export async function getMsg(epId: string, message_id: number): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'get_msg', { - message_id - }) - return data; - } catch (e) { - logger.error(`获取消息 ${message_id} 失败:${e}`); - return null; - } -} - -export async function deleteMsg(epId: string, message_id: number): Promise { - const net = getNet(); - if (!net) return; - try { - await net.callApi(epId, 'delete_msg', { - message_id - }) - } catch (e) { - logger.error(`删除消息 ${message_id} 失败:${e}`); - return; - } -} - -export async function sendGroupAISound(epId: string, characterId: string, group_id: string, text: string): Promise { - const net = getNet(); - if (!net) return; - try { - await net.callApi(epId, 'send_group_ai_record', { - character: characterId, - group_id, - text - }); - } catch (e) { - logger.error(`发送群 ${group_id} AI 声聊合成语音失败:${e}`); - return; - } -} - -export async function sendPrivateForwardMsg(epId: string, user_id: string, - messages: any[], - news: string[], - prompt: string, - summary: string, - source: string): Promise { - const net = getNet(); - if (!net) return null; - try { - const data = await net.callApi(epId, 'send_private_forward_msg', { - user_id, - messages, - news, - prompt, - summary, - source - }) - return data; - } catch (e) { - logger.error(`发送用户 ${user_id} 转发消息失败:${e}`); - return null; - } + return lines.join("\n"); } -export async function sendGroupForwardMsg(epId: string, group_id: string, - messages: any[], - news: string[], - prompt: string, - summary: string, - source: string): Promise { - const net = getNet(); - if (!net) return null; +export async function expandForwardMessage(epId: string, id: string, depth = 0): Promise { + if (!netExists()) return "[未安装 ob11 网络连接依赖,无法展开合并转发]"; try { - const data = await net.callApi(epId, 'send_group_forward_msg', { - group_id, - messages, - news, - prompt, - summary, - source - }) - return data; - } catch (e) { - logger.error(`发送群 ${group_id} 转发消息失败:${e}`); - return null; + return await forwardMessagesToText(epId, await getForwardMessage(epId, id), depth + 1); + } catch (error) { + Logger.error(`展开合并转发 ${id} 失败:${error instanceof Error ? error.message : String(error)}`); + return "[合并转发展开失败]"; } } diff --git a/src/utils/string.ts b/src/utils/string.ts index 626211c..d934fbb 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -7,7 +7,7 @@ import { Context } from "../context/context"; import { logger } from "../logger"; import Image from "../resource/image"; -import { getCtxAndMsg } from "./seal"; +import { getRawId, normalizeGroupId, normalizeUserId } from "./target_id"; import { getMilkyReplyQuoteId, resolveLocalPath, transformMsgId, transformMsgIdBack } from "./utils"; export function truncateText(text: string, maxLength: number): string { @@ -351,40 +351,17 @@ export async function transformArrayToContent(ctx: seal.MsgContext, messageArray break; } case 'at': { - const epId = ctx.endPoint.userId; - const gid = ctx.group ? ctx.group.groupId : ''; - const prefix = epId.includes(':') ? epId.slice(0, epId.indexOf(':')) : 'QQ'; - const uid = `${prefix}:${seg.data.qq || ''}`; if (seg.data.qq === 'all') { - content += '[at:全体成员]'; + content += '[at:all]'; break; } - // OB11 段通常直接带 name;优先使用它,避免为每个 at 额外请求群成员信息。 - let name = seg.data.name || '未知用户'; - try { - if (!seg.data.name) { - const targetCtx = getCtxAndMsg(epId, uid, gid).ctx; - name = targetCtx.player?.name || name; - } - } catch (_e) { - // 目标用户上下文创建失败(如目标不在当前会话)时保留 at 标签,不中断整条消息转换 - } - content += `[at:${name}]`; + const userId = normalizeUserId(seg.data.qq || ''); + content += `[at:${userId ? getRawId(userId) : String(seg.data.qq || '')}]`; break; } case 'poke': { - const epId = ctx.endPoint.userId; - const gid = ctx.group ? ctx.group.groupId : ''; - const prefix = epId.includes(':') ? epId.slice(0, epId.indexOf(':')) : 'QQ'; - const uid = `${prefix}:${seg.data.qq || ''}`; - let name = '未知用户'; - try { - const targetCtx = getCtxAndMsg(epId, uid, gid).ctx; - name = targetCtx.player?.name || name; - } catch (_e) { - // 目标用户上下文创建失败时保留 poke 标签,不中断整条消息转换 - } - content += `[poke:${name}]`; + const userId = normalizeUserId(seg.data.qq || ''); + content += `[poke:${userId ? getRawId(userId) : String(seg.data.qq || '')}]`; break; } case 'reply': { @@ -427,24 +404,19 @@ async function transformContentToText(ctx: seal.MsgContext, session: { context: break; } case 'at': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) { - text += `[CQ:at,qq=${ui.userId.replace(/^.+:/, "")}]`; - } else { - logger.warning(`无法找到用户:${name}`); - text += ` @${name} `; + if (seg.content.trim().toLowerCase() === 'all') { + text += '[CQ:at,qq=all]'; + break; } + const userId = normalizeUserId(seg.content); + if (userId) text += `[CQ:at,qq=${getRawId(userId)}]`; + else logger.warning(`用户ID格式无效:${seg.content}`); break; } case 'poke': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) { - text += `[CQ:poke,qq=${ui.userId.replace(/^.+:/, "")}]`; - } else { - logger.warning(`无法找到用户:${name}`); - } + const userId = normalizeUserId(seg.content); + if (userId) text += `[CQ:poke,qq=${getRawId(userId)}]`; + else logger.warning(`用户ID格式无效:${seg.content}`); break; } case 'quote': { @@ -470,27 +442,21 @@ async function transformContentToText(ctx: seal.MsgContext, session: { context: break; } case 'avatar': { - const name = seg.content; - const ui = await session.context.findUser(ctx, name); - if (ui !== null) { - const image = Image.getUserAvatar(ui.userId); + const userId = normalizeUserId(seg.content); + if (userId) { + const image = Image.getUserAvatar(userId); images.push(image); text += image.CQCode; - } else { - logger.warning(`无法找到用户:${name}`); - } + } else logger.warning(`用户ID格式无效:${seg.content}`); break; } case 'group_avatar': { - const name = seg.content; - const gi = await session.context.findGroup(ctx, name); - if (gi) { - const image = Image.getGroupAvatar(gi.groupId); + const groupId = normalizeGroupId(seg.content); + if (groupId) { + const image = Image.getGroupAvatar(groupId); images.push(image); text += image.CQCode; - } else { - logger.warning(`无法找到群聊:${name}`); - } + } else logger.warning(`群ID格式无效:${seg.content}`); break; } case 'audio': { @@ -534,9 +500,11 @@ export async function handleReply(ctx: seal.MsgContext, msg: seal.Message, sessi const segment = segments[i]; const match = segment.match(/^[[[]from[::]?\s?(.+?)[\]]]$/); if (match) { - // 如果臆想对象是自己,那么将下一条消息添加到s中 - const ui = await session.context.findUser(ctx, match[1]); - if (ui && ui.userId === ctx.endPoint.userId && i < segments.length - 1) s += segments[i + 1]; + // 如果臆想对象是自己,那么将下一条消息添加到s中;只读取 [from] 中的显式 QQ 号。 + const numberMatch = match[1].match(/(?:^|\()([0-9]+)(?:\))?$/); + const fromUserId = numberMatch ? normalizeUserId(numberMatch[1]) : null; + const currentUserId = normalizeUserId(ctx.endPoint.userId); + if (fromUserId && currentUserId && fromUserId === currentUserId && i < segments.length - 1) s += segments[i + 1]; } else if (i === 0) { s = segment; } diff --git a/src/utils/target_id.ts b/src/utils/target_id.ts new file mode 100644 index 0000000..4e4657c --- /dev/null +++ b/src/utils/target_id.ts @@ -0,0 +1,43 @@ +/** 目标用户/群 ID 规范化:只接受 QQ 号、群号或统一会话 ID,不接受名称。 */ + +function normalizeRawId(value: string | number): string { + return String(value ?? '').trim(); +} + +function normalizeNumericId(value: string): string | null { + return /^\d+$/.test(value) ? value : null; +} + +export function normalizeUserId(value: string | number): string | null { + const id = normalizeRawId(value); + if (!id) return null; + if (id.startsWith('QQ:')) { + const rawId = normalizeNumericId(id.slice(3)); + return rawId ? `QQ:${rawId}` : null; + } + const rawId = normalizeNumericId(id); + return rawId ? `QQ:${rawId}` : null; +} + +export function normalizeGroupId(value: string | number): string | null { + const id = normalizeRawId(value); + if (!id) return null; + if (id.startsWith('QQ-Group:')) { + const rawId = normalizeNumericId(id.slice('QQ-Group:'.length)); + return rawId ? `QQ-Group:${rawId}` : null; + } + const rawId = normalizeNumericId(id); + return rawId ? `QQ-Group:${rawId}` : null; +} + +/** 规范化需要同时支持用户和群的目标 ID;裸数字不接受,因为无法判断目标类型。 */ +export function normalizeTargetId(value: string | number): string | null { + const id = normalizeRawId(value); + if (id.startsWith('QQ-Group:')) return normalizeGroupId(id); + if (id.startsWith('QQ:')) return normalizeUserId(id); + return null; +} + +export function getRawId(id: string): string { + return id.replace(/^.+:/, ''); +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f644d8c..fdcbd5a 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -2,8 +2,9 @@ import Config from "../config/config"; import { ALIAS_MAP } from "../config/static_config"; import { logger } from "../logger"; +import { callOb11ApiForContext } from "../transport/ob11/dispatcher"; -import { netExists, sendGroupMsg, sendPrivateMsg } from "./ob11"; +import { netExists } from "./ob11"; import { transformTextToArray } from "./string"; export function transformMsgId(msgId: string | number | null): string { @@ -107,18 +108,17 @@ export async function replyToSender(ctx: seal.MsgContext, msg: seal.Message, ses if (messageArray.length === 0) return ''; - const epId = ctx.endPoint.userId; const uid = ctx.player!.userId; if (msg.messageType === 'private') { - const result = await sendPrivateMsg(epId, uid.replace(/^.+:/, ''), messageArray); - if (result?.message_id) { + const result = await callOb11ApiForContext(ctx, msg, "send_private_msg", { user_id: uid.replace(/^.+:/, ""), message: messageArray }); + if (result.ok && result.message_id) { logger.info(`(${result.message_id})发送给${uid}:${s}`); return transformMsgId(result.message_id); } } else if (msg.messageType === 'group') { const gid = ctx.group ? ctx.group.groupId : ''; - const result = await sendGroupMsg(epId, gid.replace(/^.+:/, ''), messageArray); - if (result?.message_id) { + const result = await callOb11ApiForContext(ctx, msg, "send_group_msg", { group_id: gid.replace(/^.+:/, ""), message: messageArray }); + if (result.ok && result.message_id) { logger.info(`(${result.message_id})发送给${gid}:${s}`); return transformMsgId(result.message_id); } From 526de1e3adbaece17901a1842ab576e0aeca0c50 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Wed, 19 Aug 2026 06:36:54 +0800 Subject: [PATCH 18/20] fix: preserve group identity for local command capture --- src/tool/command_target.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tool/command_target.ts b/src/tool/command_target.ts index a9144ee..8108472 100644 --- a/src/tool/command_target.ts +++ b/src/tool/command_target.ts @@ -42,8 +42,10 @@ export function buildCommandContext( ctx: seal.MsgContext, target: { trigger: string; at: string[] } ): { ctx: seal.MsgContext; msg: seal.Message } { + // Message.groupId 必须保留 SealDice 的平台前缀(如 QQ-Group:123), + // 否则 OB11 核心在 SendToGroup 时无法命中群会话,也不会触发 onMessageSend。 const normalizedGroupId = ctx.group && normalizeGroupId(ctx.group.groupId); - const groupId = normalizedGroupId ? getRawId(normalizedGroupId) : ''; + const groupId = normalizedGroupId || ''; const msg = createMsg(ctx.isPrivate ? 'private' : 'group', target.trigger, groupId); if (target.at.length) { const atText = target.at.map(userId => `[CQ:at,qq=${userId}]`).join(' '); From 51f3efdaa333e45a55746d389f7dbfa304cd9264 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Wed, 19 Aug 2026 06:38:53 +0800 Subject: [PATCH 19/20] test: record command target e2e verification --- test-reports/aiplugin4-e2e-20260818-0638.md | 77 +++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test-reports/aiplugin4-e2e-20260818-0638.md diff --git a/test-reports/aiplugin4-e2e-20260818-0638.md b/test-reports/aiplugin4-e2e-20260818-0638.md new file mode 100644 index 0000000..6c7ccdc --- /dev/null +++ b/test-reports/aiplugin4-e2e-20260818-0638.md @@ -0,0 +1,77 @@ +# aiplugin4 端到端测试报告(command target / core bridge) + +- 测试日期:2026-08-18 +- 测试群:1064487252(呱呱群) +- 测试机器人:3893625976 +- 测试方式:QQ-MCP 发送 `.ai tool call ...`,未直接调用 AI + +## 代码与单元测试 + +- 主仓库修复:`src/tool/command_target.ts` 保留 `QQ-Group:` 平台前缀,避免 OB11 核心无法命中群会话,导致本地捕获器收不到发送消息。 +- `npm run build`:PASS +- `npm run lint`:PASS +- `git diff --check`:PASS +- `aiplugin4-backends-shop/backends/ob11-core-bridge` `npm test`:20 PASS / 0 FAIL + +## 真实群测试 + +### ext:显式 trigger + 多项 at + +```text +.ai tool call run_ext_command --action=call --extension=coc7 --command=rav --args=["1d100","50"] --trigger="3893625976" --at=["2484294762","2913949387"] +``` + +PASS。群内收到 `对抗检定`,工具返回实际检定文本,不再出现「海豹未返回文本消息」。日志显示: + +```text +发给(群QQ-Group:1064487252): 对抗检定: ... +扩展指令 coc7|rav 返回:... +``` + +### ext:省略 trigger,验证默认当前会话用户 + +```text +.ai tool call run_ext_command --action=call --extension=coc7 --command=rav --args=["1d100","50"] --at=["2484294762"] +``` + +PASS。返回结果以当前会话用户 `Fairy` 为触发者,并正确执行 `rav`。 + +### core:查询白名单 + +```text +.ai tool call run_core_command --action=list +``` + +PASS。返回 33 个可调用核心指令,包含 `core|help`、`core|r`、`core|st` 等。 + +### core:显式 trigger + 多项 at + +```text +.ai tool call run_core_command --action=call --command=help --args=["ext"] --trigger="3893625976" --at=["2484294762","2913949387"] +``` + +PASS。群内收到核心 `.help ext` 的真实回复,工具返回: + +```text +核心指令:ext +群扩展模块管理: +.ext // 查看扩展列表 +(结束方式:idle) +``` + +期间出现一次「MCP 会话失效,重新初始化后重试」警告,随后重试成功;不是功能失败。 + +## 后端部署与重连 + +- 后端仓库元数据已提交并推送:`bfd1940 release: bump ob11 core bridge to 1.0.7` +- 后端 shop 分支提交:`7a8888f`、`6c23140` +- 面板 `http://45.152.65.217:56210/` 已执行 `POST /api/update-backend/ob11-core-bridge`,返回 `updated=true`,进程已重启并重新连接协议端。 +- 面板卡片显示版本仍为 `1.0.6`,原因是服务器 launcher 本地注册表尚未刷新;实际更新接口已成功执行,后端日志显示新进程启动并连接 `ws://localhost:8083`。 +- 后端更新后按要求操作海豹账号设置第四个卡片:禁用,等待约 5 秒,再启用;最终状态「已连接」。 +- 海豹日志确认 `OneBot 连接成功,账号<正确确>(3893625976)`。 + +## 主仓库提交 + +- `526de1e fix: preserve group identity for local command capture` +- 当前工作区无未提交改动。 +- 推送到 GitHub 的两次尝试均因连接 GitHub 443 失败;本地提交已完成。 From e909dbc115223d8af5460ace8668395a9a9125ac Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Wed, 19 Aug 2026 13:29:21 +0800 Subject: [PATCH 20/20] chore: prepare 4.16.0 release --- README.md | 1041 +- ...70\350\247\201\351\227\256\351\242\230.md" | 148 +- header.txt | 22 +- package-lock.json | 10494 ++++++++-------- package.json | 124 +- scripts/ob11-runtime-test-entry.ts | 11 +- scripts/test-ob11-runtime.cjs | 315 +- sealpack/info.toml | 92 +- src/cmd/sub_cmd/memory.ts | 754 +- src/config/static_config/meta.ts | 16 +- src/tool/command_target.ts | 132 +- src/tool/mcp/adapters.ts | 29 +- src/update.ts | 552 +- src/utils/string.ts | 1861 +-- 14 files changed, 7839 insertions(+), 7752 deletions(-) diff --git a/README.md b/README.md index bee30ec..975c393 100644 --- a/README.md +++ b/README.md @@ -1,520 +1,521 @@ -# 🎲 AI骰娘4 - SealDice AI插件 - -- 让你的骰娘活起来 - -![License](https://img.shields.io/badge/License-MIT-blue) -![Version](https://img.shields.io/badge/Version-4.15.2-green) - -## 快速开始 - -### 1. 下载插件 - -- 通过 GitHub 下载最新稳定版:[下载链接](https://github.com/error2913/aiplugin4/releases/latest); -- 在 QQ 交流群(143412516)中获取; -- 想体验最新开发版,可以自行编译,见[下载](#下载)。 - -### 2. 安装插件 - -- 参考[海豹手册](https://docs.sealdice.com/config/jsscript.html),在 SealDice WebUI →「JS插件」中上传 `aiplugin4.js`; -- 点击重载,刷新浏览器页面。 - -### 3. 配置大模型 - -- 在 WebUI →「JS插件」→「插件设置」中找到 `aiplugin4`,点击展开; -- 进入「模型」分组,在「对话模型」中用 **TOML 格式**填写你的模型,例如: - -```toml -name = "deepseek-chat" # 模型名,查看你所用大模型平台的文档 -api_key = "sk-xxxx" # 你的 API Key -use = ["chat"] # 用途,对话模型填 chat - -[body] # 可选:覆盖请求参数 -temperature = 1 -max_tokens = 2048 -``` - -- `provider` / `base_url` 可以省略(deepseek / openai / google / zhipu / alibaba / anthropic / moonshot / xai / mistral / siliconflow 会自动识别),也可以显式填写 `base_url`; -- `anthropic`(Claude)已适配请求/响应格式(system 拆出、tool_result 合并、响应归一化);其流式暂不支持,配置 `stream = true` 时会自动回退为非流式; -- 图片识别需要配置「图片模型」(`use = ["image-understanding"]`);如果模型支持视觉,可在「图片模型」的 `use` 里加 `chat` 把它当多模态对话模型用(上下文中的图片直接传给模型);向量记忆需要配置「嵌入模型」(`use = ["text-embedding"]`,输出维度从嵌入模型的 `[body] dimensions` 读取,默认 1024); -- 默认对话模型取列表第一项,可在群里用 `.ai model <模型名>` 切换,`.ai model clr` 恢复默认。 - -### 4. 设置触发与角色 - -- 在「消息触发」分组中修改「触发正则表达式」,改为你希望的触发方式(默认示例为 @ 骰娘,可直接替换); -- 在「对话」分组中修改「角色扮演设定」,决定 AI 扮演的角色;每条设定第一行为角色设定名称(可通过 `.ai role <名称>` 切换),其余为设定内容。 - -### 5. 开始对话 - -- 对着骰娘输入你设定的触发方式(默认是 @ 骰娘),即可看到回复;也可以使用 `.ai on` 开启计数 / 计时 / 概率等自动触发模式; -- 如果没有回复,把「基础 → 日志级别」改为「调试」查看触发日志,并对照[常见问题处理](#常见问题处理)排查。 - -更多设置见[⚙️ 配置手册](#️-配置手册),全部指令见[💻 完整命令手册](#-完整命令手册)。 - ---- - -## 目录 - -- [🎲 AI骰娘4 - SealDice AI插件](#-ai骰娘4---sealdice-ai插件) - - [快速开始](#快速开始) - - [目录](#目录) - - [🌟 核心特性](#-核心特性) - - [🛠️ 完整安装指南](#️-完整安装指南) - - [环境要求](#环境要求) - - [下载](#下载) - - [依赖下载](#依赖下载) - - [安装](#安装) - - [⚙️ 配置手册](#️-配置手册) - - [模型](#模型) - - [基础](#基础) - - [对话](#对话) - - [消息接收](#消息接收) - - [消息触发](#消息触发) - - [图片](#图片) - - [工具](#工具) - - [记忆](#记忆) - - [回复](#回复) - - [后端](#后端) - - [资源](#资源) - - [prompt 模板](#prompt-模板) - - [💻 完整命令手册](#-完整命令手册) - - [管理员命令](#管理员命令) - - [基础控制命令](#基础控制命令) - - [记忆管理命令](#记忆管理命令) - - [工具管理命令](#工具管理命令) - - [忽略名单相关命令](#忽略名单相关命令) - - [token 计数命令](#token-计数命令) - - [图片相关命令](#图片相关命令) - - [定时器相关命令](#定时器相关命令) - - [🧰 可用工具函数](#-可用工具函数) - - [🚨 注意事项](#-注意事项) - - [常见问题处理](#常见问题处理) - - [可用AI大模型开放平台列表](#可用ai大模型开放平台列表) - - [版权信息](#版权信息) - - [致谢](#致谢) - - [📞 技术支持](#-技术支持) - -## 🌟 核心特性 - -AI骰娘4 是一款运行在 [SealDice](https://docs.sealdice.com/) 上的智能对话插件,基于 OpenAI 兼容 API 开发,深度整合海豹骰子生态的 TRPG 功能: - -- **智能对话**:支持上下文感知的 AI 对话、角色设定与示例对话; -- **记忆体系**:长期记忆(向量检索 + 标签/用户/群组过滤)、短期总结记忆、配置驱动的知识库(Markdown 模板 + 自动分块); -- **工具系统**:内置 40+ 工具函数(TRPG 检定、牌堆抽取、消息、图片、禁言、定时器等),支持函数调用与提示词工程两种模式,可接入外部 MCP 服务器与可配置技能; -- **图片处理**:图片识别、表情包管理、Markdown/HTML 渲染为图片与本地图片资源; -- **权限体系**:命令权限(会话/用户/强触三维)与工具权限(禁止/默认关闭/按会话开关); -- **可观测性**:结构化日志(级别控制、密钥脱敏)、token 用量统计与图表、工具调用审计。 - ---- - -## 🛠️ 完整安装指南 - -### 环境要求 - -- SealDice v1.4.6+; - - v1.4.6 分离部署(napcat / llonebot 协议)下图片相关功能存在问题,建议 SealDice v1.5.0+; -- 大模型 API:OpenAI 兼容格式; -- 本地开发:Node.js + npm(构建使用 esbuild,建议使用较新 Node 版本)。 - -### 下载 - -- 通过 GitHub 下载最新稳定版:[下载链接](https://github.com/error2913/aiplugin4/releases/latest) - -- 通过 GitHub 下载后自编译最新开发版: - - - 安装 Node.js 和 npm - - ```bash - git clone https://github.com/error2913/aiplugin4 # 克隆仓库 - npm install # 安装依赖 - npm run build # 编译 - ``` - - 在 `dist/` 文件夹中可找到编译好的 `aiplugin4.js` 文件 - -- 在 QQ 群中获取 - -### 依赖下载 - -以下依赖按需安装,均可通过 GitHub 下载或在 QQ 群中获取: - -- [生成音频依赖插件](https://github.com/error2913/aiplugin4-dependencies/tree/main/tts):自定义音色的 AI 语音; -- [ob11 网络连接依赖.js](https://raw.githubusercontent.com/error2913/sealdice-plugin-ob11-net-connection/refs/heads/main/dist/ob11%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%E4%BE%9D%E8%B5%96.js)(推荐)或 [http 依赖插件](https://github.com/error2913/sealdice-js/blob/main/HTTP%E4%BE%9D%E8%B5%96.js):ob11 相关工具(发消息、用户/群信息、禁言、打卡、AI 语音等); -- [生成图片依赖插件](https://github.com/error2913/aiplugin4-dependencies/tree/main/tti):AI 生图工具; -- ffmpeg:发送本地语音需要配置到环境变量中。 - -### 安装 - -- 参考[海豹手册](https://docs.sealdice.com/config/jsscript.html)进行插件上传安装; -- 配置修改后自动生效(缓存最多 1 分钟),无需重载 JS。 - ---- - -## ⚙️ 配置手册 - -配置项在 SealDice WebUI 的插件设置中按分组展示,以下键名与当前代码注册的配置一致。 - -### 模型 - -| 设置项 | 说明 | -|:---:|:---| -| 对话模型 | TOML 格式,每行一个模型;`name` / `api_key` / `use` 必填,`use` 可选项:`chat`(普通对话)/ `compression`(消息压缩)/ `summarization`(记忆总结);`provider` / `base_url` 可省略自动识别;默认对话模型取列表第一项;可选 `[body]` 覆盖请求参数 | -| 图片模型 | TOML 格式,`use` 可选 `["image-understanding"]`(图片理解/图片转文字)或 `["chat"]`(多模态对话:作为对话模型使用,上下文中的图片直接传给模型),也可两者并存 | -| 嵌入模型 | TOML 格式,`use` 填 `["text-embedding"]`(文本嵌入),输出维度在 `[body] dimensions` 配置(默认 1024),未配置时自动降级为关键词/分数检索 | - -```toml -# 对话模型示例 -name = "deepseek-chat" -api_key = "sk-xxxx" -use = ["chat"] -provider = "deepseek" # 可省略,按名称自动识别 - -[body] # 可选:覆盖请求参数 -temperature = 0.8 -max_tokens = 2048 -``` - -> 会话内可用 `.ai model` 查看、`.ai model <模型名>` 切换、`.ai model clr` 恢复默认。 - -### 基础 - -| 设置项 | 说明 | -|:---:|:---| -| 日志级别 | 从不 / 错误 / 警告 / 信息 / 调试,反馈问题建议设为「调试」 | -| 日志简短打印 | 日志超长时只保留首尾各 500 字 | -| 日志记录消息内容 | 关闭后请求上下文日志只记录角色与长度,不打印消息正文 | -| 请求超时时限 | 单位毫秒,同时约束模型请求与工具调用,过小会导致长回复/慢工具超时 | -| 请求并发上限 | 同时进行中的请求数量上限,0 表示不限制;超出后进入等待队列 | -| 请求队列上限 | 排队等待的请求数量上限,超过后直接丢弃;0 表示超出并发后不排队 | -| 海豹核心全局路径 | 本地资源相对路径拼接用的 SealDice 核心目录 | -| 是否开启全局待机 | 开启后 AI 不主动回复,收到的所有消息会录入上下文(会话的计数器/计时器/概率仍可触发);长时间开启可能占用较多上下文 | - -### 对话 - -| 设置项 | 说明 | -|:---:|:---| -| 角色扮演设定 | 每行一个角色的扮演设定:第一行为角色设定名称(超过 20 字符自动截断,可通过 `.ai role <名称>` 或豹语变量 `$gSYSPROMPT` 切换),其余为设定内容 | -| 示例对话 | role 顺序为 user 和 assistant 轮流出现,位于上下文最前面,不会被上下文机制删除 | -| 对话保存轮数 | 出现一次 user 视作一轮,超过轮数会遗忘除示例对话外最早的对话,越长消耗 token 越多 | -| 上下文最大token | 0 为不限制;超过后从最早的消息开始丢弃 | -| 插入system message间隔轮数 | 需小于限制轮数的二分之一才能生效,为 0 时不生效,示例对话不计入轮数 | -| 消息压缩阈值 | 用户消息(含连续多条合并后)超过该字符数时,使用压缩智能体压缩后存入上下文(默认 2000) | - -### 消息接收 - -| 设置项 | 说明 | -|:---:|:---| -| 接收图片 | 是否接收图片消息并将图片 URL 记录到上下文;是否自动识别由图片识别条件和图片模型配置决定 | -| 接收指令消息 | 是否将指令消息计入上下文(指令仍会执行) | -| 接收骰子发送的消息 | 是否处理机器人自己发送的消息 | -| 忽略私聊消息 | 开启后私聊消息不触发 AI | -| 忽略消息豹语条件 | 命中为 1 时忽略,可填豹语表达式限制忽略范围 | -| 忽略消息正则表达式 | 匹配的消息不会被接收录入上下文 | -| ob11 额外消息接收 | 安装 ob11 网络连接依赖后,卡片/视频/音乐/文件/语音/合并转发消息经其事件分发接入(核心 milky 原生路径会过滤这些段),合并转发自动展开为可读文本 | - -### 消息触发 - -| 设置项 | 说明 | -|:---:|:---| -| 默认计数器 / 默认计时器 / 默认概率 / 默认触发活跃时间 | `.ai on` 不带参数时使用的默认值;活跃时间格式 `HH:mm-HH:mm-次数` | -| 触发正则表达式 | 匹配符合正则的消息用于强制触发 AI 回复,[正则表达式教程](https://www.runoob.com/regexp/regexp-syntax.html) | -| 触发需要满足的条件 | 豹语表达式,例如 `$t群号_RAW=='2001'` 表示仅允许群 2001 触发;填 1 为无限制 | -| 触发次数上限 | 群内共用令牌桶容量,触发一次减少一计数,计数为 0 时无法触发 | -| 触发次数补充间隔 | 单位秒,按该间隔补充触发次数 | - -### 图片 - -| 设置项 | 说明 | -|:---:|:---| -| 图片全局识别豹语条件 | 填 `'1'` 开启所有图片自动识别转文字;或填豹语表达式限制群/用户范围 | -| 识别图片时将url转换为base64 | 永不 / 自动 / 总是,解决大模型无法正常获取 QQ 图床图片的问题 | - -### 工具 - -| 设置项 | 说明 | -|:---:|:---| -| 开启调用函数功能 | 开启后 AI 可使用各种工具 | -| 切换为提示词工程 | 当 API 不支持 function calling 时开启 | -| 允许连续调用函数次数 | 单次触发内允许连续调用函数的次数,防止 AI 陷入调用函数死循环(默认 10,设为 0 不限制) | -| 工具响应压缩触发字数 | 工具返回结果超过该字数时压缩后再存入上下文,设为 0 不压缩(默认 5000);web_search 压缩时附带搜索目标 | -| 禁止调用的函数 | 每行一个,设置后将不被允许开启 | -| 默认关闭的函数 | 每行一个,AI 在新会话中默认无法调用,需 `.ai tool on <函数名>` 开启 | -| 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | -| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。工具名称、描述和参数均从远端 `tools/list` 自动发现,同名冲突自动跳过;`run_ext_command` 仅由插件本地实现,不由 MCP 提供。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | -| 可调用指令白名单 | 每行一个 `扩展名|指令名/别名1/别名2`;同一元素内的别名用 `/` 分隔。默认已包含当前 SealDice 核心命令、内置扩展命令及全部别名,核心扩展名统一写 `core`(如 `core|roll/r/rd`) | -| 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件;默认包含核心命令、内置扩展命令及别名的 `run_ext_command` / `run_core_command` 调用帮助。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | -| ai语音使用的音色 | 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)和 ffmpeg | - -### 记忆 - -| 设置项 | 说明 | -|:---:|:---| -| 启用长期记忆 | 开启后对话内容会沉淀为长期记忆 | -| 长期记忆上限 | 长期记忆条数上限,超出后按分数淘汰 | -| 长期记忆展示数量 | 构造记忆 prompt 时展示的长期记忆条数 | -| 启用总结记忆 | 开启后定期对对话进行总结记忆 | -| 总结记忆上限 / 总结记忆间隔轮数 / 总结记忆参与轮数 | 总结记忆条数上限、自动总结间隔、每次总结纳入的对话轮数 | -| 短期记忆上限 | 短期记忆条数上限,超出后从最旧的开始淘汰(默认 10) | -| 启用知识库记忆 | 开启后把知识库内容注入 system prompt,供对话参考 | -| 知识库注入阈值(字符) | 知识库总内容不超过该值时全量注入;超过时只注入条目索引,模型用 kb_read 工具读取详情(默认 5000) | -| 知识库 | Markdown 模板,每条一份完整文档(# 条目标题、##/### 小节,超长自动分块);只读,内容由管理员维护,AI 通过 kb 工具/指令检索。语法定义见 [CommonMark 规范](https://commonmark.org/help/) | - -### 回复 - -| 设置项 | 说明 | -|:---:|:---| -| 回复引用 | AI 回复时是否引用触发的消息;回复含戳戳(poke)时不引用,避免消息无法显示 | -| 回复最大字数 | 防止最大 tokens 限制不起效导致回复过长 | -| 回复文本去除首尾空白字符 | 发送前去除回复首尾空白 | -| 分段发送延时 | 流式/非流式输出共用,消息间隔是否开启延时防止乱序 | -| 分段发送基础延时/ms | 流式/非流式输出共用,从第二条消息开始每条发送前等待的毫秒数(默认 350) | -| 分段发送含图额外延时/ms | 流式/非流式输出共用,当消息包含图片时额外增加的等待毫秒数(默认 250) | -| 禁止回复复读 | 检测到与上一条回复相似度过高时停止回复 | -| 视作复读的最低相似度 | 与上一条回复的相似度达到该值视为复读(默认 0.8) | - -### 后端 - -| 设置项 | 说明 | -|:---:|:---| -| 流式输出 | [后端源码](https://github.com/error2913/aiplugin4-backends/tree/main/backends/stream-output),`body.stream = true` 的模型才会走流式 | -| 图片转base64 | [后端源码](https://github.com/error2913/aiplugin4-backends/tree/main/backends/image-url-to-base64),解决 QQ 图床图片无法被大模型访问的问题 | -| 联网搜索 | [searxng](https://github.com/searxng/searxng),有能力建议自己搭建,为 AI 提供联网搜索功能 | -| 用量图表 | [后端源码](https://github.com/error2913/aiplugin4-backends/tree/main/backends/usage-chart),token 使用情况图表生成 | -| 论坛地址 | 默认:`https://aiplugin-forum.fishwhite.top`,aiplugin4 专用论坛地址 | -| 论坛API Token | 论坛注册后获取的 api_token,用于发帖等写操作的鉴权 | -| 论坛签名密钥 | 论坛注册后获取的 secret_key,用于请求签名验证 | - -> 各后端服务相互独立,可按需自建;除流式输出外,其余服务并非核心功能所必需。 - -后端服务已迁移到独立仓库 [aiplugin4-backends](https://github.com/error2913/aiplugin4-backends):自带 `launcher.py` 一键管理(Windows / Linux 通用,默认不启动任何后端,首次启动某后端时才自动创建 venv 并安装依赖,异常退出自动拉起;`webui` 提供管理界面,可改端口/看日志,主题跟随系统)。后端清单与接口详见其仓库的 [docs/后端.md](https://github.com/error2913/aiplugin4-backends/blob/main/docs/后端.md),插件侧配置文档见 [docs/08-相关后端项目](docs/08-相关后端项目.md)。 - -### 资源 - -| 设置项 | 说明 | -|:---:|:---| -| 本地图片路径 | 每行一个本地图片路径,供 system prompt 列出可发送资源;修改后自动生效(缓存最多 1 分钟) | -| 本地语音路径 | 每行一个本地语音:`语音名=路径`(省略语音名时默认用文件名),供 system prompt 列出;发送语音需要配置 ffmpeg 到环境变量;修改后自动生效(缓存最多 1 分钟) | - -### prompt 模板 - -6 个 Handlebars 模板(system prompt / 长期记忆 / 总结记忆 / 工具函数 / 图片识别 / 记忆总结)已内置在插件中,不再作为配置项展示,避免误改导致渲染损坏。 - ---- - -## 💻 完整命令手册 - -根命令为 `.ai`(`AI` 大写同样注册),子命令支持别名(如 `priv→privilege`、`ses→session`、`st→set`、`ck→check`、`clr→clear`、`sb→standby`、`fgt→forget`、`memo→memory`、`tk→token`、`img→image`、`ign→ignore`),列表类命令支持 `--page=<数字>`(别名 `--p`)翻页。 - -### 管理员命令 - -| 命令 | 使用示例 | 说明 | -|:---:|:---:|:---| -| `.ai priv ses st <会话权限>` | `.ai priv ses st QQ-Group:1234 50` | 修改指定会话的权限等级,ID 可为 `now` 表示当前窗口 | -| `.ai priv ses ck ` | `.ai priv ses ck QQ-Group:1234` | 检查指定会话的权限等级 | -| `.ai priv st <指令> <权限限制>` | `.ai priv st ai-sb 0-0-0` | 修改具体命令的权限限制,指令用 `-` 连接,权限限制格式为「会话-用户-强触」 | -| `.ai priv show <指令>` | - | 检查指定指令的权限限制 | -| `.ai priv reset` | - | 重置所有指令权限为默认 | -| `.ai prompt` | - | 查看当前 system prompt(骰主) | -| `.ai block add <用户ID/群ID> <原因>` | `.ai block add QQ:1234567890 乱发广告` | 拉黑用户/群(骰主),被拉黑对象无法触发 AI 对话 | -| `.ai block rm <用户ID/群ID>` | - | 移除黑名单 | -| `.ai block list` | - | 查看黑名单列表 | - -> 权限数值:-30 黑名单 / 0 普通用户 / 40 邀请者 / 50 群管理员 / 60 群主 / 70 白名单 / 100 骰主。 - -### 基础控制命令 - -| 命令 | 使用示例 | 说明 | -|:---:|:---:|:---| -| `.ai status` | - | 查看当前会话设置 | -| `.ai help <子指令>` / `.ai <子指令> help` | `.ai help tool` | 查看子命令帮助;`.ai <子指令> help` 带更多参数时交由子命令自身处理(如 `.ai tool help <函数名>`) | -| `.ai ctxn status` | - | 查看上下文中的名字与自动修改状态 | -| `.ai ctxn set [nick/card]` | - | 将上下文中的名字设置为昵称/群名片 | -| `.ai ctxn mod <0\|1\|2>` | - | 自动修改上下文中的名字:0 不修改,1 昵称,2 群名片 | -| `.ai on` / `.ai on --r` | - | 开启 AI:非指令正则触发默认随之开启;`.ai off --r` 仅关闭正则触发,`.ai on` 重新开启 | -| `.ai on [--r --c=<条> --t=<秒> --p=<%> --a=<开始-结束-次数>]` | `.ai on --c=10 --t=60` | 开启 AI:--r 开启非指令正则触发,--c/--t/--p/--a 开启计数器/计时器/概率/活跃时间段模式,不带参数仅开启非指令正则触发 | -| `.ai standby` | - | 待机模式:仅录入上下文不主动发言,非指令关键词触发才发言 | -| `.ai off [--r/--c/--t/--p/--a]` | `.ai off --t` | 关闭 AI(含非指令正则触发),加参数只关闭对应模式 | -| `.ai fgt [assistant/user]` | - | 遗忘当前上下文;assistant 为遗忘 AI 发言与函数调用,user 为遗忘用户发言与函数返回 | -| `.ai role [<名称>]` | - | 查看 / 切换角色设定 | -| `.ai model [<模型名>]` | `.ai model deepseek-chat` | 查看 / 设置当前会话模型,`clr` 清除设置恢复默认 | -| `.ai shut` | - | 中断当前流式输出 | - -### 记忆管理命令 - -| 命令 | 使用示例 | 说明 | -|:---:|:---:|:---| -| `.ai memo status [用户ID]` | - | 查看当前(或指定用户ID的)长期/短期记忆状态 | -| `.ai memo [p/g] st <内容>` | `.ai memo p st 西瓜` | 设置个人/群聊设定(个人≤20字,群聊≤30字) | -| `.ai memo [p/g] st clr` | - | 清除设定 | -| `.ai memo [p/g] del --关键词` | - | 按 ID 删除记忆,可附带关键词 | -| `.ai memo [p/g/short] list` | - | 展示长期/短期记忆列表,支持 `--page` 翻页 | -| `.ai memo [p/g/short] clr` | - | 清除长期/短期记忆 | -| `.ai memo short [on/off]` | - | 开启/关闭短期记忆 | -| `.ai memo sum` | - | 立即总结一次短期记忆 | -| `.ai memo sum clr` | - | 清除总结记忆 | -| `.ai kb [list/search <关键词>/read ]` | `.ai kb search 触发方式` | 知识库只读查询:列出条目索引 / 按关键词搜索 / 按 ID 读取详情 | - -> 个人记忆跨群、群聊记忆仅限本群;group 分支需邀请者以上,short 分支需会话权限 1。 - -### 工具管理命令 - -| 命令 | 使用示例 | 说明 | -|:---:|:---:|:---| -| `.ai tool` | - | 列出所有工具及开关状态 | -| `.ai tool help <函数名>` | `.ai tool help set_timer` | 查看指定工具的详细说明和参数需求 | -| `.ai tool [on/off]` | - | 开启/关闭全部工具函数 | -| `.ai tool [on/off] <函数名>` | `.ai tool on run_ext_command` | 开启/关闭指定工具函数 | -| `.ai tool call <函数名> --参数=值` | `.ai tool call run_ext_command --action=call --extension=fun --command=jrrp` | 试用指定工具函数,输出调用返回信息;参数可尝试 JSON 解析,数字需要引号包裹 | - -### 忽略名单相关命令 - -| 命令 | 使用示例 | 说明 | -|:---:|:---:|:---| -| `.ai ign add <用户ID>` | - | 添加名单(仅群聊)。名单内的用户能正常对话,但不会被选为目标用户 | -| `.ai ign rm <用户ID>` | - | 删除名单 | -| `.ai ign lst` | - | 查看名单 | - -### token 计数命令 - -| 命令 | 使用示例 | 说明 | -|:---:|:---:|:---| -| `.ai tk lst` | - | 查看有使用记录的模型 | -| `.ai tk sum` | - | 查看所有模型的 token 使用记录总和 | -| `.ai tk all` | - | 查看所有模型的 token 使用记录,分别列出 | -| `.ai tk [y/m] (chart)` | `.ai tk y chart` | 查看最近 12 个月 / 31 天的 token 用量,`chart` 生成用量图片 | -| `.ai tk <模型名> [y/m] (chart)` | - | 查看指定模型的用量记录 | -| `.ai tk clr [<模型名>]` | - | 清除全部或指定模型的用量记录 | - -### 图片相关命令 - -| 命令 | 使用示例 | 说明 | -|:---:|:---:|:---| -| `.ai img list [lcl]` | `.ai img list lcl` | 展示本地图片列表 | -| `.ai img itt [图片] (附加提示词)` | `.ai img itt ran 看看这图里人物是什么` | 使用视觉大模型对图片进行图片转文字 | -| `.ai img find <图片ID>` | - | 查找图片并发送 | - -### 定时器相关命令 - -| 命令 | 使用示例 | 说明 | -|:---:|:---:|:---| -| `.ai timer lst` | - | 查看当前会话所有定时任务 | -| `.ai timer clr` | - | 清除所有定时任务 | - ---- - -## 🧰 可用工具函数 - -以下为内置工具函数(基于当前源码),可通过 `.ai tool help ` 查看详细用法,也可按分类在群里使用: - -| 分类 | 工具函数 | -|:---:|:---| -| 记忆 | `add_memory`、`del_memory`、`search_memory`、`clear_memory` | -| 知识库 | `kb_search`、`kb_read`、`kb_list`(只读检索,内容由配置维护) | -| OB11 API | `call_ob11_api`(通过 action 调用消息、查询、管理、文件和合并转发 API) | -| 定时 | `set_timer`、`show_timer_list`、`cancel_timer` | -| 触发 | `set_trigger_condition` | -| 指令 | `run_ext_command`(本地执行扩展指令)、`run_core_command`(经 MCP 调用核心指令) | -| 工具调度 | `search_tools`(按需搜索工具)、`call_tool`(统一执行任意工具) | -| 音频资源 | `generate_audio`(生成 record 消息段,不直接发送) | -| 网页 | `web_search`、`web_read` | -| 属性 | `attr_get`、`attr_set` | -| 图片 | `image_to_text`、`text_to_image`、`meme_list`、`get_meme_info`、`meme_generator`、`render_markdown`、`render_html` | -| OB11 管理/查询 | 统一通过 `call_ob11_api` 传入 `set_group_ban`、`set_group_name`、`get_group_list` 等 action | -| 资源/群资料 | `list_resources`;群资料统一通过 `call_ob11_api` 传入对应 action | -| 精华消息 | 统一通过 `call_ob11_api` 传入 `set_essence_msg`、`get_essence_msg_list`、`delete_essence_msg` | -| 音乐资源 | `search_music`(返回 music 消息段,不直接发送) | -| 黑名单 | `suggest_block`(AI 建议拉黑,带冷却;默认需骰主确认)、`unblock_user`、`get_block_list` | -| 论坛 | `forum_get_posts`、`forum_get_post_detail`、`forum_search`、`forum_create_post`、`forum_manage_comment`、`forum_get_activity`、`forum_manage_post` | -| MCP / 技能 | 远端工具名(MCP 工具,同名冲突时跳过)、`use_skill`(技能) | - -> 指令类技能(今日人品、COC 模组抽取/搜索、属性展示、属性检定、san 检定等)通过 `use_skill` 按需获取内容,内部统一使用 `run_ext_command` / `run_core_command` 调用海豹指令,对应指令需加入「可调用指令白名单」。 - -> OB11 说明:AI 只调用 `call_ob11_api`。安装 ob11 网络连接依赖时,action 原样交给 `net.callApi`;未安装时仍由 SealDice 原生后端完成当前上下文可完成的发送和查询,远端 action 返回 `OB11_DEPENDENCY_REQUIRED`,不会假装成功。图片、语音、视频、文件、JSON、Markdown、音乐和合并转发均通过 message segment 保留格式;`generate_audio`、`search_music` 只生成可发送的 segment。 - -> 扩展/核心指令工具不再要求会话先出现 `.r`:`run_ext_command` 在插件内本地直调扩展 `solve`,无需中间件;`run_core_command` 通过 OB11 核心桥注入假消息,需启动 `ob11-core-bridge`(SealDice 的 OB11 网络依赖连接中间件 `/core`,并在「工具 → MCP服务器配置」中启用 `ob11-core-bridge`,`http://127.0.0.1:46880/mcp`)。 - ---- - -## 🚨 注意事项 - -- 配置修改后自动生效(缓存最多 1 分钟),无需重载 JS; -- 嵌入模型输出维度从 `[body] dimensions` 读取(默认 1024),未配置时记忆检索自动降级为关键词/分数检索、知识库检索自动降级为关键词匹配(嵌入仅作候选重排,加载知识库本身不请求嵌入); -- 流式输出需要自建或使用公共后端,并在「后端 → 流式输出」配置 URL;`body.stream = true` 的模型才会走流式; -- 「请求超时时限」同时约束模型请求与工具调用,过小会导致长回复/慢工具超时; -- CQ 码白名单之外的图片类型消息不处理(当前允许 at/image/reply/face/poke);卡片/视频/文件/语音/合并转发等段经 ob11 事件分发接收,不受该白名单限制。 - -### 常见问题处理 - -**不回复 / 触发不生效** - -- 日志级别改为「调试」,观察触发日志与请求日志; -- 检查「触发正则表达式」与「触发需要满足的条件」(豹语表达式)是否命中,触发条件命中为 1 才触发; -- 检查「触发次数上限/补充间隔」(令牌桶),桶为空会跳过回复; -- 检查消息是否被「忽略正则」或「忽略消息豹语条件」拦下; -- 确认模型配置可访问:url/API Key/模型名正确、余额充足、支持工具调用。 - -**工具调用失败** - -- 查看日志中的调用失败原因(未注册/未经许可/参数缺失/类型不符/会话类型不符/超时等); -- 用 `.ai tool` 查看开关状态,`.ai tool help <函数名>` 查看参数,`.ai tool call <函数名> --参数=值` 手动试用; -- 工具在「禁止调用的函数」列表中时无法开启;新会话中「默认关闭的函数」需要 `.ai tool on <函数名>` 手动开启; -- 扩展指令工具不需要先使用 `.r`;`run_ext_command` 每次调用现场构造 `CmdArgs`。`run_ext_command` / `run_core_command` 均支持 `trigger` 指定触发对象、`at` 指定群聊中的 @ 对象列表。核心指令工具通过 `run_core_command` 走 OB11 核心桥。 - -**记忆/知识库检索不到** - -- 记忆检索确认「嵌入模型」已配置且「是否开启嵌入模型」开关打开,「启用长期记忆」开关打开; -- 知识库为配置驱动(Markdown 模板),不按角色加载;「启用知识库记忆」开关打开后修改「知识库」配置即可,自动生效(缓存最多 1 分钟),用 `.ai kb search <关键词>` / `.ai kb list` 验证; -- 记忆检索有相似度下限过滤,条目太旧(衰减)或相似度过低不会展示。 - -**图片识别异常** - -- 确认图片 URL 可以在浏览器访问(过期或 QQ 图床 bug 时更换协议端版本); -- 模型不支持 QQ 图床时,把「识别图片时将url转换为base64」设为「总是」或「自动」; -- 图片转文字依赖视觉模型配置(「图片模型」TOML,`use = ["image-understanding"]`)。 - -**HTTP 请求出错** - -- 日志中的错误码对照 HTTP 错误码与大模型文档排查;常见原因:url 填错、API Key 错、模型名错、不支持工具调用、余额不足、请求频繁。 - ---- - -## 可用AI大模型开放平台列表 - -| 大模型平台 | 调用url | 文档地址 | 支持语言大模型 | 支持视觉大模型 | -|:---:|:---:|:---:|:---:|:---:| -| [deepseek](https://platform.deepseek.com) | `https://api.deepseek.com/chat/completions` | [deepseek API文档](https://api-docs.deepseek.com/zh-cn) | `deepseek-chat`,`deepseek-reasoner`×▲ | - | -| [kimi](https://platform.moonshot.cn/console) | `https://api.moonshot.cn/v1/chat/completions` | [Moonshot AI 使用手册](https://platform.moonshot.cn/docs) | `moonshot-v1-8k`,`moonshot-v1-32k`,`moonshot-v1-128k`,`moonshot-v1-auto` | - | -| [百炼大模型](https://www.aliyun.com/product/bailian/getting-started) | `https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions` | [大模型服务平台百炼产品文档](https://help.aliyun.com/zh/model-studio/getting-started/what-is-model-studio) | `qwen-max`,`qwen-plus`,`qwen-turbo`,`qwen-long`,`deepseek-r1`×▲,`deepseek-v3`× | `qwen-vl-max`,`qwen-vl-plus` | -| [智谱AI](https://www.bigmodel.cn/console/overview) | `https://open.bigmodel.cn/api/paas/v4/chat/completions` | [BigModel 接口文档](https://www.bigmodel.cn/dev/api) | `glm-4-plus`,`glm-4-air`,`glm-4-air-0111`,`glm-4-airx`,`glm-4-long`,`glm-4-flashx`,`glm-4-flash`,`glm-zero-preview`×,`charglm-4`× | `glm-4v-plus-0111`,`glm-4v-plus`,`glm-4v`,`glm-4v-flash` | -| [百度千帆大模型平台](https://console.bce.baidu.com/qianfan/overview) | `https://qianfan.baidubce.com/v2/chat/completions` | [千帆大模型服务与开发平台ModelBuilder文档](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Zm2ycv77m) | `ernie-4.0-8k`▲,`ernie-4.0-turbo-8k`▲,`ernie-3.5-8k`▲,`deepseek-v3`×▲,`deepseek-r1`×▲ | `deepseek-vl2` | -| [讯飞星火大模型](https://console.xfyun.cn/services) | `https://spark-api-open.xf-yun.com/v1/chat/completions` | [讯飞开放平台文档中心](https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E) | `lite`×,`generalv3`×,`pro-128k`×,`generalv3.5`×,`max-32k`,`4.0Ultra` | | -| [google AI](https://ai.google.dev/) | `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` | [gemini API 文档](https://ai.google.dev/api) | `gemini-2.0-flash`,`gemini-1.5-flash`,`gemini-1.5-pro` | | -| [openAI](https://openai.com/api/) | `https://api.openai.com/v1/chat/completions` | [openAI API 文档](https://platform.openai.com/docs/quickstart) | `gpt-4o`,`gpt-4o-mini`,`o1`,`o3-mini`,`gpt-4-turbo`,`gpt-3.5-turbo` | `gpt-4-turbo`,`gpt-4o`,`o1`,`gpt-4o-mini` | - -> 注:× 为不支持 function call;▲ 为需要开启合并 user 消息开关。视觉模型不一定支持 QQ 图床识别,可使用中转插件。 - -> 在「模型」配置中,上表平台的 `provider` / `base_url` 大多可省略(自动识别),未列出的平台填写 `base_url` 即可使用。 - -> 仅列出部分官方的本插件支持的模型,部分大模型平台同一模型有多个版本并未在上表写出,且更新不及时,存在过期可能,未列出的不一定不能使用,最好到文档自己查看。国外大模型网络问题请自行解决。 - ---- - - -## 版权信息 - -本项目采用 MIT 开源协议,欢迎二次开发。原创作者保留署名权。 - -```text -Copyright 2026 error2913 and baiyu-yu - -Permission is hereby granted... -``` - -## 致谢 - -- 海豹骰子开发团队 -- 开源社区贡献者 - -## 📞 技术支持 - -- GitHub Issues: [问题提交](https://github.com/error2913/aiplugin4/issues) -- QQ交流群: 143412516 - -> "才、才不是专门给你写的文档呢!只是...只是顺便而已!(///ω///)" —— 正确·改 +# 🎲 AI骰娘4 - SealDice AI插件 + +- 让你的骰娘活起来 + +![License](https://img.shields.io/badge/License-MIT-blue) +![Version](https://img.shields.io/badge/Version-4.15.2-green) + +## 快速开始 + +### 1. 下载插件 + +- 通过 GitHub 下载最新稳定版:[下载链接](https://github.com/error2913/aiplugin4/releases/latest); +- 在 QQ 交流群(143412516)中获取; +- 想体验最新开发版,可以自行编译,见[下载](#下载)。 + +### 2. 安装插件 + +- 参考[海豹手册](https://docs.sealdice.com/config/jsscript.html),在 SealDice WebUI →「JS插件」中上传 `aiplugin4.js`; +- 点击重载,刷新浏览器页面。 + +### 3. 配置大模型 + +- 在 WebUI →「JS插件」→「插件设置」中找到 `aiplugin4`,点击展开; +- 进入「模型」分组,在「对话模型」中用 **TOML 格式**填写你的模型,例如: + +```toml +name = "deepseek-chat" # 模型名,查看你所用大模型平台的文档 +api_key = "sk-xxxx" # 你的 API Key +use = ["chat"] # 用途,对话模型填 chat + +[body] # 可选:覆盖请求参数 +temperature = 1 +max_tokens = 2048 +``` + +- `provider` / `base_url` 可以省略(deepseek / openai / google / zhipu / alibaba / anthropic / moonshot / xai / mistral / siliconflow 会自动识别),也可以显式填写 `base_url`; +- `anthropic`(Claude)已适配请求/响应格式(system 拆出、tool_result 合并、响应归一化);其流式暂不支持,配置 `stream = true` 时会自动回退为非流式; +- 图片识别需要配置「图片模型」(`use = ["image-understanding"]`);如果模型支持视觉,可在「图片模型」的 `use` 里加 `chat` 把它当多模态对话模型用(上下文中的图片直接传给模型);向量记忆需要配置「嵌入模型」(`use = ["text-embedding"]`,输出维度从嵌入模型的 `[body] dimensions` 读取,默认 1024); +- 默认对话模型取列表第一项,可在群里用 `.ai model <模型名>` 切换,`.ai model clr` 恢复默认。 + +### 4. 设置触发与角色 + +- 在「消息触发」分组中修改「触发正则表达式」,改为你希望的触发方式(默认示例为 @ 骰娘,可直接替换); +- 在「对话」分组中修改「角色扮演设定」,决定 AI 扮演的角色;每条设定第一行为角色设定名称(可通过 `.ai role <名称>` 切换),其余为设定内容。 + +### 5. 开始对话 + +- 对着骰娘输入你设定的触发方式(默认是 @ 骰娘),即可看到回复;也可以使用 `.ai on` 开启计数 / 计时 / 概率等自动触发模式; +- 如果没有回复,把「基础 → 日志级别」改为「调试」查看触发日志,并对照[常见问题处理](#常见问题处理)排查。 + +更多设置见[⚙️ 配置手册](#️-配置手册),全部指令见[💻 完整命令手册](#-完整命令手册)。 + +--- + +## 目录 + +- [🎲 AI骰娘4 - SealDice AI插件](#-ai骰娘4---sealdice-ai插件) + - [快速开始](#快速开始) + - [目录](#目录) + - [🌟 核心特性](#-核心特性) + - [🛠️ 完整安装指南](#️-完整安装指南) + - [环境要求](#环境要求) + - [下载](#下载) + - [依赖下载](#依赖下载) + - [安装](#安装) + - [⚙️ 配置手册](#️-配置手册) + - [模型](#模型) + - [基础](#基础) + - [对话](#对话) + - [消息接收](#消息接收) + - [消息触发](#消息触发) + - [图片](#图片) + - [工具](#工具) + - [记忆](#记忆) + - [回复](#回复) + - [后端](#后端) + - [资源](#资源) + - [prompt 模板](#prompt-模板) + - [💻 完整命令手册](#-完整命令手册) + - [管理员命令](#管理员命令) + - [基础控制命令](#基础控制命令) + - [记忆管理命令](#记忆管理命令) + - [工具管理命令](#工具管理命令) + - [忽略名单相关命令](#忽略名单相关命令) + - [token 计数命令](#token-计数命令) + - [图片相关命令](#图片相关命令) + - [定时器相关命令](#定时器相关命令) + - [🧰 可用工具函数](#-可用工具函数) + - [🚨 注意事项](#-注意事项) + - [常见问题处理](#常见问题处理) + - [可用AI大模型开放平台列表](#可用ai大模型开放平台列表) + - [版权信息](#版权信息) + - [致谢](#致谢) + - [📞 技术支持](#-技术支持) + +## 🌟 核心特性 + +AI骰娘4 是一款运行在 [SealDice](https://docs.sealdice.com/) 上的智能对话插件,基于 OpenAI 兼容 API 开发,深度整合海豹骰子生态的 TRPG 功能: + +- **智能对话**:支持上下文感知的 AI 对话、角色设定与示例对话; +- **记忆体系**:长期记忆(向量检索 + 标签/用户/群组过滤)、短期总结记忆、配置驱动的知识库(Markdown 模板 + 自动分块); +- **工具系统**:内置 40+ 工具函数(TRPG 检定、牌堆抽取、消息、图片、禁言、定时器等),支持函数调用与提示词工程两种模式,可接入外部 MCP 服务器与可配置技能; +- **图片处理**:图片识别、表情包管理、Markdown/HTML 渲染为图片与本地图片资源; +- **权限体系**:命令权限(会话/用户/强触三维)与工具权限(禁止/默认关闭/按会话开关); +- **可观测性**:结构化日志(级别控制、密钥脱敏)、token 用量统计与图表、工具调用审计。 + +--- + +## 🛠️ 完整安装指南 + +### 环境要求 + +- SealDice v1.4.6+; + - v1.4.6 分离部署(napcat / llonebot 协议)下图片相关功能存在问题,建议 SealDice v1.5.0+; +- 大模型 API:OpenAI 兼容格式; +- 本地开发:Node.js + npm(构建使用 esbuild,建议使用较新 Node 版本)。 + +### 下载 + +- 通过 GitHub 下载最新稳定版:[下载链接](https://github.com/error2913/aiplugin4/releases/latest) + +- 通过 GitHub 下载后自编译最新开发版: + + - 安装 Node.js 和 npm + - ```bash + git clone https://github.com/error2913/aiplugin4 # 克隆仓库 + npm install # 安装依赖 + npm run build # 编译 + ``` + - 在 `dist/` 文件夹中可找到编译好的 `aiplugin4.js` 文件 + +- 在 QQ 群中获取 + +### 依赖下载 + +以下依赖按需安装,均可通过 GitHub 下载或在 QQ 群中获取: + +- [生成音频依赖插件](https://github.com/error2913/aiplugin4-dependencies/tree/main/tts):自定义音色的 AI 语音; +- [ob11 网络连接依赖.js](https://raw.githubusercontent.com/error2913/sealdice-plugin-ob11-net-connection/refs/heads/main/dist/ob11%E7%BD%91%E7%BB%9C%E8%BF%9E%E6%8E%A5%E4%BE%9D%E8%B5%96.js)(推荐)或 [http 依赖插件](https://github.com/error2913/sealdice-js/blob/main/HTTP%E4%BE%9D%E8%B5%96.js):ob11 相关工具(发消息、用户/群信息、禁言、打卡、AI 语音等); +- [生成图片依赖插件](https://github.com/error2913/aiplugin4-dependencies/tree/main/tti):AI 生图工具; +- ffmpeg:发送本地语音需要配置到环境变量中。 + +### 安装 + +- 参考[海豹手册](https://docs.sealdice.com/config/jsscript.html)进行插件上传安装; +- 配置修改后自动生效(缓存最多 1 分钟),无需重载 JS。 + +--- + +## ⚙️ 配置手册 + +配置项在 SealDice WebUI 的插件设置中按分组展示,以下键名与当前代码注册的配置一致。 + +### 模型 + +| 设置项 | 说明 | +|:---:|:---| +| 对话模型 | TOML 格式,每行一个模型;`name` / `api_key` / `use` 必填,`use` 可选项:`chat`(普通对话)/ `compression`(消息压缩)/ `summarization`(记忆总结);`provider` / `base_url` 可省略自动识别;默认对话模型取列表第一项;可选 `[body]` 覆盖请求参数 | +| 图片模型 | TOML 格式,`use` 可选 `["image-understanding"]`(图片理解/图片转文字)或 `["chat"]`(多模态对话:作为对话模型使用,上下文中的图片直接传给模型),也可两者并存 | +| 嵌入模型 | TOML 格式,`use` 填 `["text-embedding"]`(文本嵌入),输出维度在 `[body] dimensions` 配置(默认 1024),未配置时自动降级为关键词/分数检索 | + +```toml +# 对话模型示例 +name = "deepseek-chat" +api_key = "sk-xxxx" +use = ["chat"] +provider = "deepseek" # 可省略,按名称自动识别 + +[body] # 可选:覆盖请求参数 +temperature = 0.8 +max_tokens = 2048 +``` + +> 会话内可用 `.ai model` 查看、`.ai model <模型名>` 切换、`.ai model clr` 恢复默认。 + +### 基础 + +| 设置项 | 说明 | +|:---:|:---| +| 日志级别 | 从不 / 错误 / 警告 / 信息 / 调试,反馈问题建议设为「调试」 | +| 日志简短打印 | 日志超长时只保留首尾各 500 字 | +| 日志记录消息内容 | 关闭后请求上下文日志只记录角色与长度,不打印消息正文 | +| 请求超时时限 | 单位毫秒,同时约束模型请求与工具调用,过小会导致长回复/慢工具超时 | +| 请求并发上限 | 同时进行中的请求数量上限,0 表示不限制;超出后进入等待队列 | +| 请求队列上限 | 排队等待的请求数量上限,超过后直接丢弃;0 表示超出并发后不排队 | +| 海豹核心全局路径 | 本地资源相对路径拼接用的 SealDice 核心目录 | +| 是否开启全局待机 | 开启后 AI 不主动回复,收到的所有消息会录入上下文(会话的计数器/计时器/概率仍可触发);长时间开启可能占用较多上下文 | + +### 对话 + +| 设置项 | 说明 | +|:---:|:---| +| 角色扮演设定 | 每行一个角色的扮演设定:第一行为角色设定名称(超过 20 字符自动截断,可通过 `.ai role <名称>` 或豹语变量 `$gSYSPROMPT` 切换),其余为设定内容 | +| 示例对话 | role 顺序为 user 和 assistant 轮流出现,位于上下文最前面,不会被上下文机制删除 | +| 对话保存轮数 | 出现一次 user 视作一轮,超过轮数会遗忘除示例对话外最早的对话,越长消耗 token 越多 | +| 上下文最大token | 0 为不限制;超过后从最早的消息开始丢弃 | +| 插入system message间隔轮数 | 需小于限制轮数的二分之一才能生效,为 0 时不生效,示例对话不计入轮数 | +| 消息压缩阈值 | 用户消息(含连续多条合并后)超过该字符数时,使用压缩智能体压缩后存入上下文(默认 2000) | + +### 消息接收 + +| 设置项 | 说明 | +|:---:|:---| +| 接收图片 | 是否接收图片消息并将图片 URL 记录到上下文;是否自动识别由图片识别条件和图片模型配置决定 | +| 接收指令消息 | 是否将指令消息计入上下文(指令仍会执行) | +| 接收骰子发送的消息 | 是否处理机器人自己发送的消息 | +| 忽略私聊消息 | 开启后私聊消息不触发 AI | +| 忽略消息豹语条件 | 命中为 1 时忽略,可填豹语表达式限制忽略范围 | +| 忽略消息正则表达式 | 匹配的消息不会被接收录入上下文 | +| ob11 额外消息接收 | 安装 ob11 网络连接依赖后,卡片/视频/音乐/文件/语音/合并转发消息经其事件分发接入(核心 milky 原生路径会过滤这些段),合并转发自动展开为可读文本 | + +### 消息触发 + +| 设置项 | 说明 | +|:---:|:---| +| 默认计数器 / 默认计时器 / 默认概率 / 默认触发活跃时间 | `.ai on` 不带参数时使用的默认值;活跃时间格式 `HH:mm-HH:mm-次数` | +| 触发正则表达式 | 匹配符合正则的消息用于强制触发 AI 回复,[正则表达式教程](https://www.runoob.com/regexp/regexp-syntax.html) | +| 触发需要满足的条件 | 豹语表达式,例如 `$t群号_RAW=='2001'` 表示仅允许群 2001 触发;填 1 为无限制 | +| 触发次数上限 | 群内共用令牌桶容量,触发一次减少一计数,计数为 0 时无法触发 | +| 触发次数补充间隔 | 单位秒,按该间隔补充触发次数 | + +### 图片 + +| 设置项 | 说明 | +|:---:|:---| +| 图片全局识别豹语条件 | 填 `'1'` 开启所有图片自动识别转文字;或填豹语表达式限制群/用户范围 | +| 识别图片时将url转换为base64 | 永不 / 自动 / 总是,解决大模型无法正常获取 QQ 图床图片的问题 | + +### 工具 + +| 设置项 | 说明 | +|:---:|:---| +| 开启调用函数功能 | 开启后 AI 可使用各种工具 | +| 切换为提示词工程 | 当 API 不支持 function calling 时开启 | +| 允许连续调用函数次数 | 单次触发内允许连续调用函数的次数,防止 AI 陷入调用函数死循环(默认 10,设为 0 不限制) | +| 工具响应压缩触发字数 | 工具返回结果超过该字数时压缩后再存入上下文,设为 0 不压缩(默认 5000);web_search 压缩时附带搜索目标 | +| 禁止调用的函数 | 每行一个,设置后将不被允许开启 | +| 默认关闭的函数 | 每行一个,AI 在新会话中默认无法调用,需 `.ai tool on <函数名>` 开启 | +| 是否启用MCP | MCP 功能总开关,默认关闭;开启后才会解析并连接下方的 MCP 服务器,未安装对应 MCP 后端时建议保持关闭 | +| MCP服务器配置 | 仅支持标准 `mcpServers` JSON 格式(Claude/Cursor/.mcp.json 可直接粘贴);stdio 服务器会跳过;配置增删自动生效。默认包含四个服务器:mcp-files-exec(文件执行)、web-read(网页读取)、md-html-render(Markdown/HTML 渲染)、ob11-core-bridge(SealDice 核心指令中转)。工具名称、描述和参数均从远端 `tools/list` 自动发现,同名冲突自动跳过;`run_ext_command` 仅由插件本地实现,不由 MCP 提供。格式定义见 [MCP 官方规范](https://modelcontextprotocol.io/specification/latest) | +| 可调用指令白名单 | 每行一个 `扩展名|指令名/别名1/别名2`;同一元素内的别名用 `/` 分隔。默认已包含当前 SealDice 核心命令、内置扩展命令及全部别名,核心扩展名统一写 `core`(如 `core|roll/r/rd`) | +| 技能配置 | 仅支持标准 SKILL.md 格式(frontmatter 的 name/description 自动解析,正文为技能内容),可直接粘贴其他 agent 的技能文件;默认包含核心命令、内置扩展命令及别名的 `run_ext_command` / `run_core_command` 调用帮助。格式定义见 [agentskills.io 规范](https://agentskills.io/specification) | +| ai语音使用的音色 | 预设音色需要支持 AI 语音的协议端,自定义音色需要生成音频依赖(tts)和 ffmpeg | + +### 记忆 + +| 设置项 | 说明 | +|:---:|:---| +| 启用长期记忆 | 开启后对话内容会沉淀为长期记忆 | +| 长期记忆上限 | 长期记忆条数上限,超出后按分数淘汰 | +| 长期记忆展示数量 | 构造记忆 prompt 时展示的长期记忆条数 | +| 启用总结记忆 | 开启后定期对对话进行总结记忆 | +| 总结记忆上限 / 总结记忆间隔轮数 / 总结记忆参与轮数 | 总结记忆条数上限、自动总结间隔、每次总结纳入的对话轮数 | +| 短期记忆上限 | 短期记忆条数上限,超出后从最旧的开始淘汰(默认 10) | +| 启用知识库记忆 | 开启后把知识库内容注入 system prompt,供对话参考 | +| 知识库注入阈值(字符) | 知识库总内容不超过该值时全量注入;超过时只注入条目索引,模型用 kb_read 工具读取详情(默认 5000) | +| 知识库 | Markdown 模板,每条一份完整文档(# 条目标题、##/### 小节,超长自动分块);只读,内容由管理员维护,AI 通过 kb 工具/指令检索。语法定义见 [CommonMark 规范](https://commonmark.org/help/) | + +### 回复 + +| 设置项 | 说明 | +|:---:|:---| +| 回复引用 | AI 回复时是否引用触发的消息;回复含戳戳(poke)时不引用,避免消息无法显示 | +| 回复最大字数 | 防止最大 tokens 限制不起效导致回复过长 | +| 回复文本去除首尾空白字符 | 发送前去除回复首尾空白 | +| 回复中的换行转义 | AI 回复里写出的字面量 `\n` / `\r\n` 会在发送时转换为真实换行;多条消息仍使用 `\f` 分隔 | +| 分段发送延时 | 流式/非流式输出共用,消息间隔是否开启延时防止乱序 | +| 分段发送基础延时/ms | 流式/非流式输出共用,从第二条消息开始每条发送前等待的毫秒数(默认 350) | +| 分段发送含图额外延时/ms | 流式/非流式输出共用,当消息包含图片时额外增加的等待毫秒数(默认 250) | +| 禁止回复复读 | 检测到与上一条回复相似度过高时停止回复 | +| 视作复读的最低相似度 | 与上一条回复的相似度达到该值视为复读(默认 0.8) | + +### 后端 + +| 设置项 | 说明 | +|:---:|:---| +| 流式输出 | [后端源码](https://github.com/error2913/aiplugin4-backends/tree/main/backends/stream-output),`body.stream = true` 的模型才会走流式 | +| 图片转base64 | [后端源码](https://github.com/error2913/aiplugin4-backends/tree/main/backends/image-url-to-base64),解决 QQ 图床图片无法被大模型访问的问题 | +| 联网搜索 | [searxng](https://github.com/searxng/searxng),有能力建议自己搭建,为 AI 提供联网搜索功能 | +| 用量图表 | [后端源码](https://github.com/error2913/aiplugin4-backends/tree/main/backends/usage-chart),token 使用情况图表生成 | +| 论坛地址 | 默认:`https://aiplugin-forum.fishwhite.top`,aiplugin4 专用论坛地址 | +| 论坛API Token | 论坛注册后获取的 api_token,用于发帖等写操作的鉴权 | +| 论坛签名密钥 | 论坛注册后获取的 secret_key,用于请求签名验证 | + +> 各后端服务相互独立,可按需自建;除流式输出外,其余服务并非核心功能所必需。 + +后端服务已迁移到独立仓库 [aiplugin4-backends](https://github.com/error2913/aiplugin4-backends):自带 `launcher.py` 一键管理(Windows / Linux 通用,默认不启动任何后端,首次启动某后端时才自动创建 venv 并安装依赖,异常退出自动拉起;`webui` 提供管理界面,可改端口/看日志,主题跟随系统)。后端清单与接口详见其仓库的 [docs/后端.md](https://github.com/error2913/aiplugin4-backends/blob/main/docs/后端.md),插件侧配置文档见 [docs/08-相关后端项目](docs/08-相关后端项目.md)。 + +### 资源 + +| 设置项 | 说明 | +|:---:|:---| +| 本地图片路径 | 每行一个本地图片路径,供 system prompt 列出可发送资源;修改后自动生效(缓存最多 1 分钟) | +| 本地语音路径 | 每行一个本地语音:`语音名=路径`(省略语音名时默认用文件名),供 system prompt 列出;发送语音需要配置 ffmpeg 到环境变量;修改后自动生效(缓存最多 1 分钟) | + +### prompt 模板 + +6 个 Handlebars 模板(system prompt / 长期记忆 / 总结记忆 / 工具函数 / 图片识别 / 记忆总结)已内置在插件中,不再作为配置项展示,避免误改导致渲染损坏。 + +--- + +## 💻 完整命令手册 + +根命令为 `.ai`(`AI` 大写同样注册),子命令支持别名(如 `priv→privilege`、`ses→session`、`st→set`、`ck→check`、`clr→clear`、`sb→standby`、`fgt→forget`、`memo→memory`、`tk→token`、`img→image`、`ign→ignore`),列表类命令支持 `--page=<数字>`(别名 `--p`)翻页。 + +### 管理员命令 + +| 命令 | 使用示例 | 说明 | +|:---:|:---:|:---| +| `.ai priv ses st <会话权限>` | `.ai priv ses st QQ-Group:1234 50` | 修改指定会话的权限等级,ID 可为 `now` 表示当前窗口 | +| `.ai priv ses ck ` | `.ai priv ses ck QQ-Group:1234` | 检查指定会话的权限等级 | +| `.ai priv st <指令> <权限限制>` | `.ai priv st ai-sb 0-0-0` | 修改具体命令的权限限制,指令用 `-` 连接,权限限制格式为「会话-用户-强触」 | +| `.ai priv show <指令>` | - | 检查指定指令的权限限制 | +| `.ai priv reset` | - | 重置所有指令权限为默认 | +| `.ai prompt` | - | 查看当前 system prompt(骰主) | +| `.ai block add <用户ID/群ID> <原因>` | `.ai block add QQ:1234567890 乱发广告` | 拉黑用户/群(骰主),被拉黑对象无法触发 AI 对话 | +| `.ai block rm <用户ID/群ID>` | - | 移除黑名单 | +| `.ai block list` | - | 查看黑名单列表 | + +> 权限数值:-30 黑名单 / 0 普通用户 / 40 邀请者 / 50 群管理员 / 60 群主 / 70 白名单 / 100 骰主。 + +### 基础控制命令 + +| 命令 | 使用示例 | 说明 | +|:---:|:---:|:---| +| `.ai status` | - | 查看当前会话设置 | +| `.ai help <子指令>` / `.ai <子指令> help` | `.ai help tool` | 查看子命令帮助;`.ai <子指令> help` 带更多参数时交由子命令自身处理(如 `.ai tool help <函数名>`) | +| `.ai ctxn status` | - | 查看上下文中的名字与自动修改状态 | +| `.ai ctxn set [nick/card]` | - | 将上下文中的名字设置为昵称/群名片 | +| `.ai ctxn mod <0\|1\|2>` | - | 自动修改上下文中的名字:0 不修改,1 昵称,2 群名片 | +| `.ai on` / `.ai on --r` | - | 开启 AI:非指令正则触发默认随之开启;`.ai off --r` 仅关闭正则触发,`.ai on` 重新开启 | +| `.ai on [--r --c=<条> --t=<秒> --p=<%> --a=<开始-结束-次数>]` | `.ai on --c=10 --t=60` | 开启 AI:--r 开启非指令正则触发,--c/--t/--p/--a 开启计数器/计时器/概率/活跃时间段模式,不带参数仅开启非指令正则触发 | +| `.ai standby` | - | 待机模式:仅录入上下文不主动发言,非指令关键词触发才发言 | +| `.ai off [--r/--c/--t/--p/--a]` | `.ai off --t` | 关闭 AI(含非指令正则触发),加参数只关闭对应模式 | +| `.ai fgt [assistant/user]` | - | 遗忘当前上下文;assistant 为遗忘 AI 发言与函数调用,user 为遗忘用户发言与函数返回 | +| `.ai role [<名称>]` | - | 查看 / 切换角色设定 | +| `.ai model [<模型名>]` | `.ai model deepseek-chat` | 查看 / 设置当前会话模型,`clr` 清除设置恢复默认 | +| `.ai shut` | - | 中断当前流式输出 | + +### 记忆管理命令 + +| 命令 | 使用示例 | 说明 | +|:---:|:---:|:---| +| `.ai memo status [用户ID]` | - | 查看当前(或指定用户ID的)长期/短期记忆状态 | +| `.ai memo [p/g] st <内容>` | `.ai memo p st 西瓜` | 设置个人/群聊设定(个人≤20字,群聊≤30字) | +| `.ai memo [p/g] st clr` | - | 清除设定 | +| `.ai memo [p/g] del --关键词` | - | 按 ID 删除记忆,可附带关键词 | +| `.ai memo [p/g/short] list` | - | 展示长期/短期记忆列表,支持 `--page` 翻页 | +| `.ai memo [p/g/short] clr` | - | 清除长期/短期记忆 | +| `.ai memo short [on/off]` | - | 开启/关闭短期记忆 | +| `.ai memo sum` | - | 立即总结一次短期记忆 | +| `.ai memo sum clr` | - | 清除总结记忆 | +| `.ai kb [list/search <关键词>/read ]` | `.ai kb search 触发方式` | 知识库只读查询:列出条目索引 / 按关键词搜索 / 按 ID 读取详情 | + +> 个人记忆跨群、群聊记忆仅限本群;group 分支需邀请者以上,short 分支需会话权限 1。 + +### 工具管理命令 + +| 命令 | 使用示例 | 说明 | +|:---:|:---:|:---| +| `.ai tool` | - | 列出所有工具及开关状态 | +| `.ai tool help <函数名>` | `.ai tool help set_timer` | 查看指定工具的详细说明和参数需求 | +| `.ai tool [on/off]` | - | 开启/关闭全部工具函数 | +| `.ai tool [on/off] <函数名>` | `.ai tool on run_ext_command` | 开启/关闭指定工具函数 | +| `.ai tool call <函数名> --参数=值` | `.ai tool call run_ext_command --action=call --extension=fun --command=jrrp` | 试用指定工具函数,输出调用返回信息;参数可尝试 JSON 解析,数字需要引号包裹 | + +### 忽略名单相关命令 + +| 命令 | 使用示例 | 说明 | +|:---:|:---:|:---| +| `.ai ign add <用户ID>` | - | 添加名单(仅群聊)。名单内的用户能正常对话,但不会被选为目标用户 | +| `.ai ign rm <用户ID>` | - | 删除名单 | +| `.ai ign lst` | - | 查看名单 | + +### token 计数命令 + +| 命令 | 使用示例 | 说明 | +|:---:|:---:|:---| +| `.ai tk lst` | - | 查看有使用记录的模型 | +| `.ai tk sum` | - | 查看所有模型的 token 使用记录总和 | +| `.ai tk all` | - | 查看所有模型的 token 使用记录,分别列出 | +| `.ai tk [y/m] (chart)` | `.ai tk y chart` | 查看最近 12 个月 / 31 天的 token 用量,`chart` 生成用量图片 | +| `.ai tk <模型名> [y/m] (chart)` | - | 查看指定模型的用量记录 | +| `.ai tk clr [<模型名>]` | - | 清除全部或指定模型的用量记录 | + +### 图片相关命令 + +| 命令 | 使用示例 | 说明 | +|:---:|:---:|:---| +| `.ai img list [lcl]` | `.ai img list lcl` | 展示本地图片列表 | +| `.ai img itt [图片] (附加提示词)` | `.ai img itt ran 看看这图里人物是什么` | 使用视觉大模型对图片进行图片转文字 | +| `.ai img find <图片ID>` | - | 查找图片并发送 | + +### 定时器相关命令 + +| 命令 | 使用示例 | 说明 | +|:---:|:---:|:---| +| `.ai timer lst` | - | 查看当前会话所有定时任务 | +| `.ai timer clr` | - | 清除所有定时任务 | + +--- + +## 🧰 可用工具函数 + +以下为内置工具函数(基于当前源码),可通过 `.ai tool help ` 查看详细用法,也可按分类在群里使用: + +| 分类 | 工具函数 | +|:---:|:---| +| 记忆 | `add_memory`、`del_memory`、`search_memory`、`clear_memory` | +| 知识库 | `kb_search`、`kb_read`、`kb_list`(只读检索,内容由配置维护) | +| OB11 API | `call_ob11_api`(通过 action 调用消息、查询、管理、文件和合并转发 API) | +| 定时 | `set_timer`、`show_timer_list`、`cancel_timer` | +| 触发 | `set_trigger_condition` | +| 指令 | `run_ext_command`(本地执行扩展指令)、`run_core_command`(经 MCP 调用核心指令) | +| 工具调度 | `search_tools`(按需搜索工具)、`call_tool`(统一执行任意工具) | +| 音频资源 | `generate_audio`(生成 record 消息段,不直接发送) | +| 网页 | `web_search`、`web_read` | +| 属性 | `attr_get`、`attr_set` | +| 图片 | `image_to_text`、`text_to_image`、`meme_list`、`get_meme_info`、`meme_generator`、`render_markdown`、`render_html` | +| OB11 管理/查询 | 统一通过 `call_ob11_api` 传入 `set_group_ban`、`set_group_name`、`get_group_list` 等 action | +| 资源/群资料 | `list_resources`;群资料统一通过 `call_ob11_api` 传入对应 action | +| 精华消息 | 统一通过 `call_ob11_api` 传入 `set_essence_msg`、`get_essence_msg_list`、`delete_essence_msg` | +| 音乐资源 | `search_music`(返回 music 消息段,不直接发送) | +| 黑名单 | `suggest_block`(AI 建议拉黑,带冷却;默认需骰主确认)、`unblock_user`、`get_block_list` | +| 论坛 | `forum_get_posts`、`forum_get_post_detail`、`forum_search`、`forum_create_post`、`forum_manage_comment`、`forum_get_activity`、`forum_manage_post` | +| MCP / 技能 | 远端工具名(MCP 工具,同名冲突时跳过)、`use_skill`(技能) | + +> 指令类技能(今日人品、COC 模组抽取/搜索、属性展示、属性检定、san 检定等)通过 `use_skill` 按需获取内容,内部统一使用 `run_ext_command` / `run_core_command` 调用海豹指令,对应指令需加入「可调用指令白名单」。 + +> OB11 说明:AI 只调用 `call_ob11_api`。安装 ob11 网络连接依赖时,action 原样交给 `net.callApi`;未安装时仍由 SealDice 原生后端完成当前上下文可完成的发送和查询,远端 action 返回 `OB11_DEPENDENCY_REQUIRED`,不会假装成功。图片、语音、视频、文件、JSON、Markdown、音乐和合并转发均通过 message segment 保留格式;`generate_audio`、`search_music` 只生成可发送的 segment。 + +> 扩展/核心指令工具不再要求会话先出现 `.r`:`run_ext_command` 在插件内本地直调扩展 `solve`,无需中间件;`run_core_command` 通过 OB11 核心桥注入假消息,需启动 `ob11-core-bridge`(SealDice 的 OB11 网络依赖连接中间件 `/core`,并在「工具 → MCP服务器配置」中启用 `ob11-core-bridge`,`http://127.0.0.1:46880/mcp`)。 + +--- + +## 🚨 注意事项 + +- 配置修改后自动生效(缓存最多 1 分钟),无需重载 JS; +- 嵌入模型输出维度从 `[body] dimensions` 读取(默认 1024),未配置时记忆检索自动降级为关键词/分数检索、知识库检索自动降级为关键词匹配(嵌入仅作候选重排,加载知识库本身不请求嵌入); +- 流式输出需要自建或使用公共后端,并在「后端 → 流式输出」配置 URL;`body.stream = true` 的模型才会走流式; +- 「请求超时时限」同时约束模型请求与工具调用,过小会导致长回复/慢工具超时; +- CQ 码白名单之外的图片类型消息不处理(当前允许 at/image/reply/face/poke);卡片/视频/文件/语音/合并转发等段经 ob11 事件分发接收,不受该白名单限制。 + +### 常见问题处理 + +**不回复 / 触发不生效** + +- 日志级别改为「调试」,观察触发日志与请求日志; +- 检查「触发正则表达式」与「触发需要满足的条件」(豹语表达式)是否命中,触发条件命中为 1 才触发; +- 检查「触发次数上限/补充间隔」(令牌桶),桶为空会跳过回复; +- 检查消息是否被「忽略正则」或「忽略消息豹语条件」拦下; +- 确认模型配置可访问:url/API Key/模型名正确、余额充足、支持工具调用。 + +**工具调用失败** + +- 查看日志中的调用失败原因(未注册/未经许可/参数缺失/类型不符/会话类型不符/超时等); +- 用 `.ai tool` 查看开关状态,`.ai tool help <函数名>` 查看参数,`.ai tool call <函数名> --参数=值` 手动试用; +- 工具在「禁止调用的函数」列表中时无法开启;新会话中「默认关闭的函数」需要 `.ai tool on <函数名>` 手动开启; +- 扩展指令工具不需要先使用 `.r`;`run_ext_command` 每次调用现场构造 `CmdArgs`。`run_ext_command` / `run_core_command` 均支持 `trigger` 指定触发对象、`at` 指定群聊中的 @ 对象列表。核心指令工具通过 `run_core_command` 走 OB11 核心桥。 + +**记忆/知识库检索不到** + +- 记忆检索确认「嵌入模型」已配置且「是否开启嵌入模型」开关打开,「启用长期记忆」开关打开; +- 知识库为配置驱动(Markdown 模板),不按角色加载;「启用知识库记忆」开关打开后修改「知识库」配置即可,自动生效(缓存最多 1 分钟),用 `.ai kb search <关键词>` / `.ai kb list` 验证; +- 记忆检索有相似度下限过滤,条目太旧(衰减)或相似度过低不会展示。 + +**图片识别异常** + +- 确认图片 URL 可以在浏览器访问(过期或 QQ 图床 bug 时更换协议端版本); +- 模型不支持 QQ 图床时,把「识别图片时将url转换为base64」设为「总是」或「自动」; +- 图片转文字依赖视觉模型配置(「图片模型」TOML,`use = ["image-understanding"]`)。 + +**HTTP 请求出错** + +- 日志中的错误码对照 HTTP 错误码与大模型文档排查;常见原因:url 填错、API Key 错、模型名错、不支持工具调用、余额不足、请求频繁。 + +--- + +## 可用AI大模型开放平台列表 + +| 大模型平台 | 调用url | 文档地址 | 支持语言大模型 | 支持视觉大模型 | +|:---:|:---:|:---:|:---:|:---:| +| [deepseek](https://platform.deepseek.com) | `https://api.deepseek.com/chat/completions` | [deepseek API文档](https://api-docs.deepseek.com/zh-cn) | `deepseek-chat`,`deepseek-reasoner`×▲ | - | +| [kimi](https://platform.moonshot.cn/console) | `https://api.moonshot.cn/v1/chat/completions` | [Moonshot AI 使用手册](https://platform.moonshot.cn/docs) | `moonshot-v1-8k`,`moonshot-v1-32k`,`moonshot-v1-128k`,`moonshot-v1-auto` | - | +| [百炼大模型](https://www.aliyun.com/product/bailian/getting-started) | `https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions` | [大模型服务平台百炼产品文档](https://help.aliyun.com/zh/model-studio/getting-started/what-is-model-studio) | `qwen-max`,`qwen-plus`,`qwen-turbo`,`qwen-long`,`deepseek-r1`×▲,`deepseek-v3`× | `qwen-vl-max`,`qwen-vl-plus` | +| [智谱AI](https://www.bigmodel.cn/console/overview) | `https://open.bigmodel.cn/api/paas/v4/chat/completions` | [BigModel 接口文档](https://www.bigmodel.cn/dev/api) | `glm-4-plus`,`glm-4-air`,`glm-4-air-0111`,`glm-4-airx`,`glm-4-long`,`glm-4-flashx`,`glm-4-flash`,`glm-zero-preview`×,`charglm-4`× | `glm-4v-plus-0111`,`glm-4v-plus`,`glm-4v`,`glm-4v-flash` | +| [百度千帆大模型平台](https://console.bce.baidu.com/qianfan/overview) | `https://qianfan.baidubce.com/v2/chat/completions` | [千帆大模型服务与开发平台ModelBuilder文档](https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Zm2ycv77m) | `ernie-4.0-8k`▲,`ernie-4.0-turbo-8k`▲,`ernie-3.5-8k`▲,`deepseek-v3`×▲,`deepseek-r1`×▲ | `deepseek-vl2` | +| [讯飞星火大模型](https://console.xfyun.cn/services) | `https://spark-api-open.xf-yun.com/v1/chat/completions` | [讯飞开放平台文档中心](https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E) | `lite`×,`generalv3`×,`pro-128k`×,`generalv3.5`×,`max-32k`,`4.0Ultra` | | +| [google AI](https://ai.google.dev/) | `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` | [gemini API 文档](https://ai.google.dev/api) | `gemini-2.0-flash`,`gemini-1.5-flash`,`gemini-1.5-pro` | | +| [openAI](https://openai.com/api/) | `https://api.openai.com/v1/chat/completions` | [openAI API 文档](https://platform.openai.com/docs/quickstart) | `gpt-4o`,`gpt-4o-mini`,`o1`,`o3-mini`,`gpt-4-turbo`,`gpt-3.5-turbo` | `gpt-4-turbo`,`gpt-4o`,`o1`,`gpt-4o-mini` | + +> 注:× 为不支持 function call;▲ 为需要开启合并 user 消息开关。视觉模型不一定支持 QQ 图床识别,可使用中转插件。 + +> 在「模型」配置中,上表平台的 `provider` / `base_url` 大多可省略(自动识别),未列出的平台填写 `base_url` 即可使用。 + +> 仅列出部分官方的本插件支持的模型,部分大模型平台同一模型有多个版本并未在上表写出,且更新不及时,存在过期可能,未列出的不一定不能使用,最好到文档自己查看。国外大模型网络问题请自行解决。 + +--- + + +## 版权信息 + +本项目采用 MIT 开源协议,欢迎二次开发。原创作者保留署名权。 + +```text +Copyright 2026 error2913 and baiyu-yu + +Permission is hereby granted... +``` + +## 致谢 + +- 海豹骰子开发团队 +- 开源社区贡献者 + +## 📞 技术支持 + +- GitHub Issues: [问题提交](https://github.com/error2913/aiplugin4/issues) +- QQ交流群: 143412516 + +> "才、才不是专门给你写的文档呢!只是...只是顺便而已!(///ω///)" —— 正确·改 diff --git "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" index 8399e21..d87f2e9 100644 --- "a/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" +++ "b/docs/09-\346\263\250\346\204\217\344\272\213\351\241\271\344\270\216\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -1,72 +1,76 @@ -# 09 - 注意事项与常见问题 - -## README 与代码的差异(重要) - -项目根目录的 README 维护滞后,存在多处与当前代码不符的内容,**排障与开发请以本知识库和源码为准**: - -- README 面向商店用户,已移除"开发文档"章节;开发知识库见 [docs/01-项目概览](01-项目概览.md) 与 [docs/07-开发指南](07-开发指南.md)。旧版 README"开发文档"中的目录结构(`src/tools/`、`src/AI/`)、`ConfigManager`、`ToolManager`、`tool_choice` 字段、`cmdInfo` 等均为旧版架构,已不存在。 -- README 命令手册中的部分语法已变更,例如: - - 工具试用为 `.ai tool call <函数名> --参数=值`(README 写的 `.ai tool <函数名> --参数=值` 无 `call`)。 - - 权限命令为 `.ai priv ses st/ck`、`.ai priv st/show/reset`(README 示例 `.ai priv ck ...` 不完整)。 - - 图片命令已并入 `.ai image`(README 中为独立的 `.img` 命令)。 - - 记忆命令新增 `status`/`short`/`sum` 等分支。 -- README 配置手册的模型配置为 TOML 格式,与代码一致;发布流程已改为推送 `v*` 标签自动发版(见 [docs/07-开发指南](07-开发指南.md)「发布流程」)。 - -## 代码中的已知不一致 - -- `src/config/static_config/meta.ts` 的 `VERSION` 与 `src/update.ts` 的 `updateInfo` 最新条目需保持一致(当前 4.15.2);`checkUpdate` 按 `VERSION` 比较输出对应更新日志。 -- `package.json` 的 `main`/`typings` 指向 `build/index.js`/`build/index.d.ts`,但实际构建产物是 `dist/aiplugin4.js`(esbuild),`build/` 目录是陈旧的 tsc 输出,与构建流程无关。 -- `engines.node >= 10` 是模板遗留,实际构建依赖 esbuild,建议使用较新 Node。 -- `sub_cmd/sample.ts` 与 `agent/agents/samples.ts` 是开发示例,未注册到运行时。 -- `src/note.txt` 是作者备忘(TODO 与重构思路),不是运行代码,其中的方案可能与现状不符。 - -## 运行与部署注意事项 - -- SealDice v1.4.6 分离部署(napcat/llonebot)下图片相关功能存在问题,建议 v1.5.0+。 -- `run_core_command` 需要启动 `ob11-core-bridge` 并通过 MCP 接入(SealDice 正向连接 `/core`);`run_ext_command` 在插件内**本地直调扩展 `solve`**,无需中间件/MCP。核心指令白名单使用 `core|指令名`,但 `core|ext` 无需加入白名单即可查看扩展。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时)。 -- ob11 相关工具(发消息、取用户/群信息、禁言、打卡、AI 语音等)需要安装 ob11 网络连接依赖或 HTTP 依赖;未安装时相关工具返回"未找到ob11网络连接依赖"。 -- 中间件捕获消息默认转发到协议端;若希望 AI 内部执行且不打扰真实 QQ,需在工具参数中设置 `capture.forward=false`。海豹核心连接 `/core`,协议端由中间件出站主动连接(WebUI「⚙ 配置」填地址,重启生效),插件通过 MCP `/mcp` 调用。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 -- 所有配置修改后自动生效(缓存最多 1 分钟,无需重载 JS);MCP 服务器增删、技能配置、音乐服务等由缓存 TTL 兜底。 -- 嵌入模型输出维度取自嵌入模型配置 `[body] dimensions`(默认 1024,须与后端一致,如 text-embedding-v4 为 1024);未配置维度时记忆检索自动降级为关键词/分数检索、知识库检索自动降级为关键词匹配(嵌入仅作候选重排,加载知识库本身不请求嵌入)。 -- 流式输出需要自建或使用公共后端,并在"后端 → 流式输出"配置 URL;`body.stream = true` 的模型才会走流式。 -- `请求超时时限` 同时约束模型请求与工具调用,过小会导致长回复/慢工具超时。 - -## 常见问题排查 - -### 不回复 / 触发不生效 - -按以下顺序检查: - -1. 日志级别设为"调试",观察触发日志(`trigger reply: ...`)与请求日志。 -2. 检查"触发正则表达式"与"触发需要满足的条件"(豹语表达式)是否命中;触发条件命中为 1 才触发。 -3. 检查"触发次数上限/补充间隔"(令牌桶),桶为空会跳过回复(`bucket empty, skip reply`)。 -4. 检查消息是否被"忽略正则"或"忽略消息豹语条件"拦下(日志有 `忽略` 字样)。 -5. 检查消息是否含白名单之外的 CQ 码类型(`CQ_TYPES_ALLOW` 之外的 type 不处理)。 -6. 确认模型配置可访问:url/API Key/模型名正确、余额充足、支持工具调用。 - -### 工具调用失败 - -- 查看日志中的 `调用函数失败:` 原因:未注册/未经许可/参数缺失/类型不符/会话类型不符/超时等。 -- 用 `.ai tool` 查看开关状态,`.ai tool help <函数名>` 查看参数,`.ai tool call <函数名> --参数=值` 手动试用。 -- 工具在"禁止调用的函数"列表中时无法开启;新会话中"默认关闭的函数"需要 `.ai tool on <函数名>` 手动开启。 - -### 记忆/知识库检索不到 - -- 记忆检索:确认嵌入模型 `[body] dimensions` 与后端输出一致(默认 1024),且"启用长期记忆"开关打开;检索有内置相似度下限(0.8)过滤,条目太旧(衰减)或相似度过低不会展示。 -- 知识库:"启用知识库记忆"开关打开后,内容来自"知识库"配置(Markdown 模板,每条一份完整文档,`#` 条目标题、`##`/`###` 小节,超长自动分块);不按角色加载,修改配置自动生效(缓存最多 1 分钟),用 `.ai kb search <关键词>` / `.ai kb list` 验证;嵌入未配置时自动降级为关键词匹配,不影响基本检索。 - -### 图片识别异常 - -- 确认图片 URL 可在浏览器访问(过期或 QQ 图床 bug 时更换协议端版本)。 -- 模型不支持 QQ 图床时,把"识别图片时将url转换为base64"设为"总是"或"自动"。 -- 图片转文字依赖视觉模型配置("图片模型" TOML,`use = ["image-understanding"]`)。 - -### HTTP 请求出错 - -- 日志中的 `请求失败! 状态码: xxx` 对照 HTTP 错误码与大模型文档排查;常见原因:url 填错、API Key 错、模型名错、不支持工具调用、余额不足、请求频繁。 - -## 维护建议 - -- README 与本知识库互补:README 面向普通用户(安装/配置/命令),`docs/` 面向开发者(架构/源码/扩展)。代码结构变化时优先同步 `docs/`。 -- 发版前统一 `VERSION`、`header.txt @version`、`updateInfo` 三处版本号。 -- 清理模板遗留:`package.json` 的 `main`/`typings`/`engines` 与 `build/` 目录,避免误导后续开发者。 +# 09 - 注意事项与常见问题 + +## README 与代码的差异(重要) + +项目根目录的 README 维护滞后,存在多处与当前代码不符的内容,**排障与开发请以本知识库和源码为准**: + +- README 面向商店用户,已移除"开发文档"章节;开发知识库见 [docs/01-项目概览](01-项目概览.md) 与 [docs/07-开发指南](07-开发指南.md)。旧版 README"开发文档"中的目录结构(`src/tools/`、`src/AI/`)、`ConfigManager`、`ToolManager`、`tool_choice` 字段、`cmdInfo` 等均为旧版架构,已不存在。 +- README 命令手册中的部分语法已变更,例如: + - 工具试用为 `.ai tool call <函数名> --参数=值`(README 写的 `.ai tool <函数名> --参数=值` 无 `call`)。 + - 权限命令为 `.ai priv ses st/ck`、`.ai priv st/show/reset`(README 示例 `.ai priv ck ...` 不完整)。 + - 图片命令已并入 `.ai image`(README 中为独立的 `.img` 命令)。 + - 记忆命令新增 `status`/`short`/`sum` 等分支。 +- README 配置手册的模型配置为 TOML 格式,与代码一致;发布流程已改为推送 `v*` 标签自动发版(见 [docs/07-开发指南](07-开发指南.md)「发布流程」)。 + +## 代码中的已知不一致 + +- `src/config/static_config/meta.ts` 的 `VERSION` 与 `src/update.ts` 的 `updateInfo` 最新条目需保持一致(当前 4.15.2);`checkUpdate` 按 `VERSION` 比较输出对应更新日志。 +- `package.json` 的 `main`/`typings` 指向 `build/index.js`/`build/index.d.ts`,但实际构建产物是 `dist/aiplugin4.js`(esbuild),`build/` 目录是陈旧的 tsc 输出,与构建流程无关。 +- `engines.node >= 10` 是模板遗留,实际构建依赖 esbuild,建议使用较新 Node。 +- `sub_cmd/sample.ts` 与 `agent/agents/samples.ts` 是开发示例,未注册到运行时。 +- `src/note.txt` 是作者备忘(TODO 与重构思路),不是运行代码,其中的方案可能与现状不符。 + +## 运行与部署注意事项 + +- SealDice v1.4.6 分离部署(napcat/llonebot)下图片相关功能存在问题,建议 v1.5.0+。 +- `run_core_command` 需要启动 `ob11-core-bridge` 并通过 MCP 接入(SealDice 正向连接 `/core`);`run_ext_command` 在插件内**本地直调扩展 `solve`**,无需中间件/MCP。核心指令白名单使用 `core|指令名`,但 `core|ext` 无需加入白名单即可查看扩展。两条路径都复用多消息收集器(空闲窗口 + 最大消息数 + 超时)。 +- ob11 相关工具(发消息、取用户/群信息、禁言、打卡、AI 语音等)需要安装 ob11 网络连接依赖或 HTTP 依赖;未安装时相关工具返回"未找到ob11网络连接依赖"。 +- 中间件捕获消息默认转发到协议端;若希望 AI 内部执行且不打扰真实 QQ,需在工具参数中设置 `capture.forward=false`。海豹核心连接 `/core`,协议端由中间件出站主动连接(WebUI「⚙ 配置」填地址,重启生效),插件通过 MCP `/mcp` 调用。纯 OB11 没有 trace_id,同一群并发动作可能返回 `ambiguous=true`;中间件已按群/私聊 lane 串行化,并以空闲窗口、最大消息数和超时结束收集。 +- 所有配置修改后自动生效(缓存最多 1 分钟,无需重载 JS);MCP 服务器增删、技能配置、音乐服务等由缓存 TTL 兜底。 +- 嵌入模型输出维度取自嵌入模型配置 `[body] dimensions`(默认 1024,须与后端一致,如 text-embedding-v4 为 1024);未配置维度时记忆检索自动降级为关键词/分数检索、知识库检索自动降级为关键词匹配(嵌入仅作候选重排,加载知识库本身不请求嵌入)。 +- 流式输出需要自建或使用公共后端,并在"后端 → 流式输出"配置 URL;`body.stream = true` 的模型才会走流式。 +- `请求超时时限` 同时约束模型请求与工具调用,过小会导致长回复/慢工具超时。 + +## 常见问题排查 + +### 不回复 / 触发不生效 + +按以下顺序检查: + +1. 日志级别设为"调试",观察触发日志(`trigger reply: ...`)与请求日志。 +2. 检查"触发正则表达式"与"触发需要满足的条件"(豹语表达式)是否命中;触发条件命中为 1 才触发。 +3. 检查"触发次数上限/补充间隔"(令牌桶),桶为空会跳过回复(`bucket empty, skip reply`)。 +4. 检查消息是否被"忽略正则"或"忽略消息豹语条件"拦下(日志有 `忽略` 字样)。 +5. 检查消息是否含白名单之外的 CQ 码类型(`CQ_TYPES_ALLOW` 之外的 type 不处理)。 +6. 确认模型配置可访问:url/API Key/模型名正确、余额充足、支持工具调用。 + +### 回复中的字面量 \n 没有换行 + +AI 回复中的字面量 `\n` 或 `\r\n` 会在发送前转换为真实换行;如果需要发送多条独立消息,使用 `\f` 作为分隔符。代码块或示例中的 `\n` 也按此规则转换。 + +### 工具调用失败 + +- 查看日志中的 `调用函数失败:` 原因:未注册/未经许可/参数缺失/类型不符/会话类型不符/超时等。 +- 用 `.ai tool` 查看开关状态,`.ai tool help <函数名>` 查看参数,`.ai tool call <函数名> --参数=值` 手动试用。 +- 工具在"禁止调用的函数"列表中时无法开启;新会话中"默认关闭的函数"需要 `.ai tool on <函数名>` 手动开启。 + +### 记忆/知识库检索不到 + +- 记忆检索:确认嵌入模型 `[body] dimensions` 与后端输出一致(默认 1024),且"启用长期记忆"开关打开;检索有内置相似度下限(0.8)过滤,条目太旧(衰减)或相似度过低不会展示。 +- 知识库:"启用知识库记忆"开关打开后,内容来自"知识库"配置(Markdown 模板,每条一份完整文档,`#` 条目标题、`##`/`###` 小节,超长自动分块);不按角色加载,修改配置自动生效(缓存最多 1 分钟),用 `.ai kb search <关键词>` / `.ai kb list` 验证;嵌入未配置时自动降级为关键词匹配,不影响基本检索。 + +### 图片识别异常 + +- 确认图片 URL 可在浏览器访问(过期或 QQ 图床 bug 时更换协议端版本)。 +- 模型不支持 QQ 图床时,把"识别图片时将url转换为base64"设为"总是"或"自动"。 +- 图片转文字依赖视觉模型配置("图片模型" TOML,`use = ["image-understanding"]`)。 + +### HTTP 请求出错 + +- 日志中的 `请求失败! 状态码: xxx` 对照 HTTP 错误码与大模型文档排查;常见原因:url 填错、API Key 错、模型名错、不支持工具调用、余额不足、请求频繁。 + +## 维护建议 + +- README 与本知识库互补:README 面向普通用户(安装/配置/命令),`docs/` 面向开发者(架构/源码/扩展)。代码结构变化时优先同步 `docs/`。 +- 发版前统一 `VERSION`、`header.txt @version`、`updateInfo` 三处版本号。 +- 清理模板遗留:`package.json` 的 `main`/`typings`/`engines` 与 `build/` 目录,避免误导后续开发者。 diff --git a/header.txt b/header.txt index c1e6ad7..863e787 100644 --- a/header.txt +++ b/header.txt @@ -1,11 +1,11 @@ -// ==UserScript== -// @name AI骰娘4 -// @author 错误、白鱼 -// @version 4.15.2 -// @description 适用于大部分OpenAI API兼容格式AI的模型插件,测试环境为 Deepseek AI (https://platform.deepseek.com/),用于与 AI 进行对话,并根据特定关键词触发回复。使用.ai help查看使用方法。具体配置查看插件配置项。\nopenai标准下的function calling功能已进行适配,选用模型若不支持该功能,可以开启迁移到提示词工程的开关,即可使用调用函数功能。\n交流答疑QQ群:143412516 -// @timestamp 1733387279 -// 2024-12-05 16:27:59 -// @license MIT -// @homepageURL https://github.com/error2913/aiplugin4/ -// @updateUrl https://github.com/error2913/aiplugin4/releases/latest/download/aiplugin4.js -// ==/UserScript== +// ==UserScript== +// @name AI骰娘4 +// @author 错误、白鱼 +// @version 4.16.0 +// @description 适用于大部分OpenAI API兼容格式AI的模型插件,测试环境为 Deepseek AI (https://platform.deepseek.com/),用于与 AI 进行对话,并根据特定关键词触发回复。使用.ai help查看使用方法。具体配置查看插件配置项。\nopenai标准下的function calling功能已进行适配,选用模型若不支持该功能,可以开启迁移到提示词工程的开关,即可使用调用函数功能。\n交流答疑QQ群:143412516 +// @timestamp 1733387279 +// 2024-12-05 16:27:59 +// @license MIT +// @homepageURL https://github.com/error2913/aiplugin4/ +// @updateUrl https://github.com/error2913/aiplugin4/releases/latest/download/aiplugin4.js +// ==/UserScript== diff --git a/package-lock.json b/package-lock.json index 3726e5d..1d59f3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5247 +1,5247 @@ -{ - "name": "aiplugin4", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "aiplugin4", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@types/handlebars": "^4.0.40", - "handlebars": "^4.7.8", - "js-toml": "^1.0.3", - "lodash-es": "^4.17.21" - }, - "devDependencies": { - "@types/lodash-es": "^4.17.12", - "@typescript-eslint/eslint-plugin": "^8.66.0", - "@typescript-eslint/parser": "^8.66.0", - "cross-env": "^7.0.3", - "esbuild": "^0.21.4", - "eslint": "^8.57.1", - "eslint-config-prettier": "^8.10.2", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-functional": "^6.6.3", - "eslint-plugin-import": "^2.32.0", - "fs-extra": "^11.2.0", - "sealpack": "^0.1.2", - "source-map-support": "^0.5.21", - "typescript": "^5.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.29.0", - "resolved": "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", - "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.48.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.2.0", - "resolved": "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.2.0.tgz", - "integrity": "sha512-ssJFvn/UXhQQeICw3SR/fZPmYVj+JM2mP+Lx7bZ51cOeHaMWOKp3AUMuyM3QR82aFFXTfcAp67P5GpPjGmbZWQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "11.2.0", - "@chevrotain/types": "11.2.0", - "lodash-es": "4.17.23" - } - }, - "node_modules/@chevrotain/gast": { - "version": "11.2.0", - "resolved": "https://registry.npmmirror.com/@chevrotain/gast/-/gast-11.2.0.tgz", - "integrity": "sha512-c+KoD6eSI1xjAZZoNUW+V0l13UEn+a4ShmUrjIKs1BeEWCji0Kwhmqn5FSx1K4BhWL7IQKlV7wLR4r8lLArORQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "11.2.0", - "lodash-es": "4.17.23" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.2.0", - "resolved": "https://registry.npmmirror.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.2.0.tgz", - "integrity": "sha512-lG73pBFqbXODTbXhdZwv0oyUaI+3Irm+uOv5/W79lI3g5hasYaJnVJOm3H2NkhA0Ef4XLBU4Scr7TJDJwgFkAw==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/types": { - "version": "11.2.0", - "resolved": "https://registry.npmmirror.com/@chevrotain/types/-/types-11.2.0.tgz", - "integrity": "sha512-vBMSj/lz/LqolbGQEHB0tlpW5BnljHVtp+kzjQfQU+5BtGMTuZCPVgaAjtKvQYXnHb/8i/02Kii00y0tsuwfsw==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "11.2.0", - "resolved": "https://registry.npmmirror.com/@chevrotain/utils/-/utils-11.2.0.tgz", - "integrity": "sha512-+7whECg4yNWHottjvr2To2BRxL4XJVjIyyv5J4+bJ0iMOVU8j/8n1qPDLZS/90W/BObDR8VNL46lFbzY/Hosmw==", - "license": "Apache-2.0" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@jsquash/jpeg": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/@jsquash/jpeg/-/jpeg-1.6.0.tgz", - "integrity": "sha512-zwN46Awh1VM6gXlIcALwb5WzqK5H2e6+Awcs1QP8AvS8ohsK/sbE4esvmH4jhlhW7+CgiUUww66vg0aTnlSIMA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@jsquash/oxipng": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/@jsquash/oxipng/-/oxipng-2.3.0.tgz", - "integrity": "sha512-aQ8wiEp6ztlTMXc+RMt/CG8crU3mEHDU+h+JYkIi6ctMhlh8+Ltj5XwQFfBuyzKYrp8NxaFW80Dp824bqjr+zA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "wasm-feature-detect": "^1.2.11" - } - }, - "node_modules/@jsquash/webp": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/@jsquash/webp/-/webp-1.5.0.tgz", - "integrity": "sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "wasm-feature-detect": "^1.2.11" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/handlebars": { - "version": "4.0.40", - "resolved": "https://registry.npmmirror.com/@types/handlebars/-/handlebars-4.0.40.tgz", - "integrity": "sha512-sGWNtsjNrLOdKha2RV1UeF8+UbQnPSG7qbe5wwbni0mw4h2gHXyPFUMOC+xwGirIiiydM/HSqjDO4rk6NFB18w==", - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.16", - "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.16.tgz", - "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", - "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/type-utils": "8.66.0", - "@typescript-eslint/utils": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.66.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.66.0.tgz", - "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", - "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.66.0", - "@typescript-eslint/types": "^8.66.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", - "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", - "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", - "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.66.0.tgz", - "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", - "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.66.0", - "@typescript-eslint/tsconfig-utils": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.66.0.tgz", - "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.66.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", - "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.66.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", - "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", - "dev": true, - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chevrotain": { - "version": "11.2.0", - "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.2.0.tgz", - "integrity": "sha512-mHCHTxM51nCklUw9RzRVc0DLjAh/SAUPM4k/zMInlTIo25ldWXOZoPt7XEIk/LwoT4lFVmJcu9g5MHtx371x3A==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "11.2.0", - "@chevrotain/gast": "11.2.0", - "@chevrotain/regexp-to-ast": "11.2.0", - "@chevrotain/types": "11.2.0", - "@chevrotain/utils": "11.2.0", - "lodash-es": "4.17.23" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-js-pure": { - "version": "3.48.0", - "resolved": "https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.48.0.tgz", - "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge-ts": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", - "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-abstract": { - "version": "1.24.2", - "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", - "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract-get": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/es-abstract-get/-/es-abstract-get-1.0.0.tgz", - "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.2", - "is-callable": "^1.2.7", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.4", - "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.4.tgz", - "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-abstract-get": "^1.0.0", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "is-callable": "^1.2.7", - "is-date-object": "^1.1.0", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-prettier": { - "version": "8.10.2", - "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", - "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.10", - "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", - "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.16.1", - "resolve": "^2.0.0-next.6" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.14.0", - "resolved": "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", - "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-eslint-comments": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", - "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5", - "ignore": "^5.0.5" - }, - "engines": { - "node": ">=6.5.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-plugin-eslint-comments/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint-plugin-functional": { - "version": "6.6.3", - "resolved": "https://registry.npmmirror.com/eslint-plugin-functional/-/eslint-plugin-functional-6.6.3.tgz", - "integrity": "sha512-sVbbvNvwX3HVkXAykKyoNLv57r4DPF7f1sy+/8j4YtzLYVQPGljMUWv3T6Kd4lwnnjmcKuj0EkIbS+knL6P5jw==", - "dev": true, - "funding": [ - { - "type": "ko-fi", - "url": "https://ko-fi.com/rebeccastevens" - } - ], - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^7.3.1", - "deepmerge-ts": "^5.1.0", - "escape-string-regexp": "^4.0.0", - "is-immutable-type": "^4.0.0", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": ">=16.10.0" - }, - "peerDependencies": { - "eslint": "^8.0.0 || ^9.0.0", - "typescript": ">=4.3.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-functional/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-functional/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/eslint-plugin-functional/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint-plugin-functional/node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.2.0.tgz", - "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2", - "hasown": "^2.0.4", - "is-callable": "^1.2.7", - "is-document.all": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-document.all": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/is-document.all/-/is-document.all-1.0.0.tgz", - "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-immutable-type": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/is-immutable-type/-/is-immutable-type-4.0.0.tgz", - "integrity": "sha512-gyFBCXv+NikTs8/PGZhgjbMmFZQ5jvHGZIsVu6+/9Bk4K7imlWBIDN7hTr9fNioGzFg71I4YM3z8f0aKXarTAw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@typescript-eslint/type-utils": "^7.2.0", - "ts-api-utils": "^1.3.0", - "ts-declaration-location": "^1.0.0" - }, - "peerDependencies": { - "eslint": "*", - "typescript": ">=4.7.4" - } - }, - "node_modules/is-immutable-type/node_modules/@typescript-eslint/scope-manager": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/is-immutable-type/node_modules/@typescript-eslint/type-utils": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/is-immutable-type/node_modules/@typescript-eslint/types": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-7.18.0.tgz", - "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/is-immutable-type/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", - "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/is-immutable-type/node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/is-immutable-type/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.18.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", - "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.18.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/is-immutable-type/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-immutable-type/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/is-immutable-type/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/is-immutable-type/node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-toml": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/js-toml/-/js-toml-1.0.3.tgz", - "integrity": "sha512-sgyRKshBUSPIlUrbVXYQHReVZUXKHTldaW+Fj7KSan21vgnmMpuAAo00rBvm7W4HQrvZSvv186wNHlIjMPYC/A==", - "license": "MIT", - "dependencies": { - "chevrotain": "^11.1.1", - "xregexp": "^5.1.2" - } - }, - "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/node-exports-info": { - "version": "1.6.2", - "resolved": "https://registry.npmmirror.com/node-exports-info/-/node-exports-info-1.6.2.tgz", - "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-exports-info/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.2.tgz", - "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "get-intrinsic": "^1.3.0", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "2.0.0-next.7", - "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.7.tgz", - "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.2", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", - "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "get-intrinsic": "^1.3.0", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sealpack": { - "version": "0.1.2", - "resolved": "https://registry.npmmirror.com/sealpack/-/sealpack-0.1.2.tgz", - "integrity": "sha512-iCHHhZhaeHg1cgKtgTpKBS3IsmshWmeJD3tOo20Dbzy3qxQkl80nDjTbr8vxzR+keyHe+eDO3R2IdsLvUi5Thg==", - "dev": true, - "dependencies": { - "@jsquash/jpeg": "^1.6.0", - "@jsquash/oxipng": "^2.3.0", - "@jsquash/webp": "^1.5.0", - "commander": "^14.0.3", - "fast-glob": "^3.3.3", - "prompts": "^2.4.2", - "semver": "^7.7.4", - "smol-toml": "^1.6.1", - "yazl": "^3.3.1" - }, - "bin": { - "sealpack": "dist/index.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/smol-toml": { - "version": "1.7.1", - "resolved": "https://registry.npmmirror.com/smol-toml/-/smol-toml-1.7.1.tgz", - "integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 18" - }, - "funding": { - "url": "https://github.com/sponsors/cyyynthia" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.11", - "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", - "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.2", - "es-object-atoms": "^1.1.2", - "has-property-descriptors": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.10", - "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", - "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-declaration-location": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", - "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", - "dev": true, - "funding": [ - { - "type": "ko-fi", - "url": "https://ko-fi.com/rebeccastevens" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" - } - ], - "license": "BSD-3-Clause", - "dependencies": { - "picomatch": "^4.0.2" - }, - "peerDependencies": { - "typescript": ">=4.0.0" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.8.tgz", - "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "for-each": "^0.3.5", - "gopd": "^1.2.0", - "is-typed-array": "^1.1.15", - "possible-typed-array-names": "^1.1.0", - "reflect.getprototypeof": "^1.0.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/wasm-feature-detect": { - "version": "1.8.0", - "resolved": "https://registry.npmmirror.com/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", - "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.22", - "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.22.tgz", - "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xregexp": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/xregexp/-/xregexp-5.1.2.tgz", - "integrity": "sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==", - "license": "MIT", - "dependencies": { - "@babel/runtime-corejs3": "^7.26.9" - } - }, - "node_modules/yazl": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/yazl/-/yazl-3.3.1.tgz", - "integrity": "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "^1.0.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} +{ + "name": "aiplugin4", + "version": "4.16.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aiplugin4", + "version": "4.16.0", + "license": "MIT", + "dependencies": { + "@types/handlebars": "^4.0.40", + "handlebars": "^4.7.8", + "js-toml": "^1.0.3", + "lodash-es": "^4.17.21" + }, + "devDependencies": { + "@types/lodash-es": "^4.17.12", + "@typescript-eslint/eslint-plugin": "^8.66.0", + "@typescript-eslint/parser": "^8.66.0", + "cross-env": "^7.0.3", + "esbuild": "^0.21.4", + "eslint": "^8.57.1", + "eslint-config-prettier": "^8.10.2", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^6.6.3", + "eslint-plugin-import": "^2.32.0", + "fs-extra": "^11.2.0", + "sealpack": "^0.1.2", + "source-map-support": "^0.5.21", + "typescript": "^5.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", + "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.48.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.2.0.tgz", + "integrity": "sha512-ssJFvn/UXhQQeICw3SR/fZPmYVj+JM2mP+Lx7bZ51cOeHaMWOKp3AUMuyM3QR82aFFXTfcAp67P5GpPjGmbZWQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.2.0", + "@chevrotain/types": "11.2.0", + "lodash-es": "4.17.23" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/gast/-/gast-11.2.0.tgz", + "integrity": "sha512-c+KoD6eSI1xjAZZoNUW+V0l13UEn+a4ShmUrjIKs1BeEWCji0Kwhmqn5FSx1K4BhWL7IQKlV7wLR4r8lLArORQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.2.0", + "lodash-es": "4.17.23" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.2.0.tgz", + "integrity": "sha512-lG73pBFqbXODTbXhdZwv0oyUaI+3Irm+uOv5/W79lI3g5hasYaJnVJOm3H2NkhA0Ef4XLBU4Scr7TJDJwgFkAw==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/types/-/types-11.2.0.tgz", + "integrity": "sha512-vBMSj/lz/LqolbGQEHB0tlpW5BnljHVtp+kzjQfQU+5BtGMTuZCPVgaAjtKvQYXnHb/8i/02Kii00y0tsuwfsw==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@chevrotain/utils/-/utils-11.2.0.tgz", + "integrity": "sha512-+7whECg4yNWHottjvr2To2BRxL4XJVjIyyv5J4+bJ0iMOVU8j/8n1qPDLZS/90W/BObDR8VNL46lFbzY/Hosmw==", + "license": "Apache-2.0" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jsquash/jpeg": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jsquash/jpeg/-/jpeg-1.6.0.tgz", + "integrity": "sha512-zwN46Awh1VM6gXlIcALwb5WzqK5H2e6+Awcs1QP8AvS8ohsK/sbE4esvmH4jhlhW7+CgiUUww66vg0aTnlSIMA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@jsquash/oxipng": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/@jsquash/oxipng/-/oxipng-2.3.0.tgz", + "integrity": "sha512-aQ8wiEp6ztlTMXc+RMt/CG8crU3mEHDU+h+JYkIi6ctMhlh8+Ltj5XwQFfBuyzKYrp8NxaFW80Dp824bqjr+zA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "wasm-feature-detect": "^1.2.11" + } + }, + "node_modules/@jsquash/webp": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@jsquash/webp/-/webp-1.5.0.tgz", + "integrity": "sha512-KggLoj2MnRSfIqTeKe1EmbljTX2vuV7mh79k89PCL1pyqiDULcPM1L47twxXt0hkb68F70bXiL31MxsuoZtKFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "wasm-feature-detect": "^1.2.11" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/handlebars": { + "version": "4.0.40", + "resolved": "https://registry.npmmirror.com/@types/handlebars/-/handlebars-4.0.40.tgz", + "integrity": "sha512-sGWNtsjNrLOdKha2RV1UeF8+UbQnPSG7qbe5wwbni0mw4h2gHXyPFUMOC+xwGirIiiydM/HSqjDO4rk6NFB18w==", + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.66.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chevrotain": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.2.0.tgz", + "integrity": "sha512-mHCHTxM51nCklUw9RzRVc0DLjAh/SAUPM4k/zMInlTIo25ldWXOZoPt7XEIk/LwoT4lFVmJcu9g5MHtx371x3A==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.2.0", + "@chevrotain/gast": "11.2.0", + "@chevrotain/regexp-to-ast": "11.2.0", + "@chevrotain/types": "11.2.0", + "@chevrotain/utils": "11.2.0", + "lodash-es": "4.17.23" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-pure": { + "version": "3.48.0", + "resolved": "https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.48.0.tgz", + "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge-ts": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", + "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-eslint-comments": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", + "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5", + "ignore": "^5.0.5" + }, + "engines": { + "node": ">=6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-eslint-comments/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-plugin-eslint-comments/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint-plugin-functional": { + "version": "6.6.3", + "resolved": "https://registry.npmmirror.com/eslint-plugin-functional/-/eslint-plugin-functional-6.6.3.tgz", + "integrity": "sha512-sVbbvNvwX3HVkXAykKyoNLv57r4DPF7f1sy+/8j4YtzLYVQPGljMUWv3T6Kd4lwnnjmcKuj0EkIbS+knL6P5jw==", + "dev": true, + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + } + ], + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^7.3.1", + "deepmerge-ts": "^5.1.0", + "escape-string-regexp": "^4.0.0", + "is-immutable-type": "^4.0.0", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": ">=16.10.0" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0", + "typescript": ">=4.3.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/eslint-plugin-functional/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-functional/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-functional/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-functional/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-functional/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-immutable-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/is-immutable-type/-/is-immutable-type-4.0.0.tgz", + "integrity": "sha512-gyFBCXv+NikTs8/PGZhgjbMmFZQ5jvHGZIsVu6+/9Bk4K7imlWBIDN7hTr9fNioGzFg71I4YM3z8f0aKXarTAw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@typescript-eslint/type-utils": "^7.2.0", + "ts-api-utils": "^1.3.0", + "ts-declaration-location": "^1.0.0" + }, + "peerDependencies": { + "eslint": "*", + "typescript": ">=4.7.4" + } + }, + "node_modules/is-immutable-type/node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/is-immutable-type/node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/is-immutable-type/node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/is-immutable-type/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/is-immutable-type/node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/is-immutable-type/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/is-immutable-type/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-immutable-type/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/is-immutable-type/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-immutable-type/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-toml": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/js-toml/-/js-toml-1.0.3.tgz", + "integrity": "sha512-sgyRKshBUSPIlUrbVXYQHReVZUXKHTldaW+Fj7KSan21vgnmMpuAAo00rBvm7W4HQrvZSvv186wNHlIjMPYC/A==", + "license": "MIT", + "dependencies": { + "chevrotain": "^11.1.1", + "xregexp": "^5.1.2" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sealpack": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/sealpack/-/sealpack-0.1.2.tgz", + "integrity": "sha512-iCHHhZhaeHg1cgKtgTpKBS3IsmshWmeJD3tOo20Dbzy3qxQkl80nDjTbr8vxzR+keyHe+eDO3R2IdsLvUi5Thg==", + "dev": true, + "dependencies": { + "@jsquash/jpeg": "^1.6.0", + "@jsquash/oxipng": "^2.3.0", + "@jsquash/webp": "^1.5.0", + "commander": "^14.0.3", + "fast-glob": "^3.3.3", + "prompts": "^2.4.2", + "semver": "^7.7.4", + "smol-toml": "^1.6.1", + "yazl": "^3.3.1" + }, + "bin": { + "sealpack": "dist/index.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/smol-toml": { + "version": "1.7.1", + "resolved": "https://registry.npmmirror.com/smol-toml/-/smol-toml-1.7.1.tgz", + "integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-declaration-location": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", + "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", + "dev": true, + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": ">=4.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xregexp": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/xregexp/-/xregexp-5.1.2.tgz", + "integrity": "sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.9" + } + }, + "node_modules/yazl": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/yazl/-/yazl-3.3.1.tgz", + "integrity": "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^1.0.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index 6a2561f..8477515 100644 --- a/package.json +++ b/package.json @@ -1,62 +1,62 @@ -{ - "name": "aiplugin4", - "version": "4.15.2", - "description": "ai plugin for sealdice", - "main": "build/index.js", - "typings": "build/index.d.ts", - "repository": "https://github.com/error2913/aiplugin4", - "license": "MIT", - "keywords": [], - "scripts": { - "build": "cross-env NODE_ENV=production node tools/build.js", - "build-dev": "cross-env NODE_ENV=dev node tools/build.js", - "smoke": "node scripts/smoke.js", - "test:ob11": "node scripts/test-ob11-runtime.cjs", - "lint": "eslint \"src/**/*.ts\"", - "package:check": "npm run build && node scripts/prepare-sealpack.js && sealpack validate sealpack && sealpack size sealpack", - "pack:sealpack": "node scripts/prepare-sealpack.js && sealpack pack sealpack --out dist/aiplugin4.sealpack", - "pack:release": "node scripts/build-release.js" - }, - "engines": { - "node": ">=10" - }, - "dependencies": { - "@types/handlebars": "^4.0.40", - "handlebars": "^4.7.8", - "js-toml": "^1.0.3", - "lodash-es": "^4.17.21" - }, - "devDependencies": { - "@types/lodash-es": "^4.17.12", - "@typescript-eslint/eslint-plugin": "^8.66.0", - "@typescript-eslint/parser": "^8.66.0", - "cross-env": "^7.0.3", - "esbuild": "^0.21.4", - "eslint": "^8.57.1", - "eslint-config-prettier": "^8.10.2", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-functional": "^6.6.3", - "eslint-plugin-import": "^2.32.0", - "fs-extra": "^11.2.0", - "sealpack": "^0.1.2", - "source-map-support": "^0.5.21", - "typescript": "^5.5.4" - }, - "files": [ - "build/main", - "build/module", - "!**/*.spec.*", - "!**/*.json", - "CHANGELOG.md", - "LICENSE", - "README.md" - ], - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } - }, - "prettier": { - "singleQuote": true - } -} +{ + "name": "aiplugin4", + "version": "4.16.0", + "description": "ai plugin for sealdice", + "main": "build/index.js", + "typings": "build/index.d.ts", + "repository": "https://github.com/error2913/aiplugin4", + "license": "MIT", + "keywords": [], + "scripts": { + "build": "cross-env NODE_ENV=production node tools/build.js", + "build-dev": "cross-env NODE_ENV=dev node tools/build.js", + "smoke": "node scripts/smoke.js", + "test:ob11": "node scripts/test-ob11-runtime.cjs", + "lint": "eslint \"src/**/*.ts\"", + "package:check": "npm run build && node scripts/prepare-sealpack.js && sealpack validate sealpack && sealpack size sealpack", + "pack:sealpack": "node scripts/prepare-sealpack.js && sealpack pack sealpack --out dist/aiplugin4.sealpack", + "pack:release": "node scripts/build-release.js" + }, + "engines": { + "node": ">=10" + }, + "dependencies": { + "@types/handlebars": "^4.0.40", + "handlebars": "^4.7.8", + "js-toml": "^1.0.3", + "lodash-es": "^4.17.21" + }, + "devDependencies": { + "@types/lodash-es": "^4.17.12", + "@typescript-eslint/eslint-plugin": "^8.66.0", + "@typescript-eslint/parser": "^8.66.0", + "cross-env": "^7.0.3", + "esbuild": "^0.21.4", + "eslint": "^8.57.1", + "eslint-config-prettier": "^8.10.2", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^6.6.3", + "eslint-plugin-import": "^2.32.0", + "fs-extra": "^11.2.0", + "sealpack": "^0.1.2", + "source-map-support": "^0.5.21", + "typescript": "^5.5.4" + }, + "files": [ + "build/main", + "build/module", + "!**/*.spec.*", + "!**/*.json", + "CHANGELOG.md", + "LICENSE", + "README.md" + ], + "config": { + "commitizen": { + "path": "cz-conventional-changelog" + } + }, + "prettier": { + "singleQuote": true + } +} diff --git a/scripts/ob11-runtime-test-entry.ts b/scripts/ob11-runtime-test-entry.ts index ab4ece1..43e76bb 100644 --- a/scripts/ob11-runtime-test-entry.ts +++ b/scripts/ob11-runtime-test-entry.ts @@ -1,5 +1,6 @@ -import Config from "../src/config/config"; -Config.registerConfig(); -export { dispatchOb11Api } from "../src/transport/ob11/dispatcher"; -export { encodeNativeMessage, normalizeFileReference, normalizeMessageSegments } from "../src/transport/ob11/message_segments"; -export { getActionCapability } from "../src/transport/ob11/capability_catalog"; +import Config from "../src/config/config"; +Config.registerConfig(); +export { dispatchOb11Api } from "../src/transport/ob11/dispatcher"; +export { encodeNativeMessage, normalizeFileReference, normalizeMessageSegments } from "../src/transport/ob11/message_segments"; +export { getActionCapability } from "../src/transport/ob11/capability_catalog"; +export { decodeEscapedNewlines } from "../src/utils/string"; diff --git a/scripts/test-ob11-runtime.cjs b/scripts/test-ob11-runtime.cjs index c987baa..616bf28 100644 --- a/scripts/test-ob11-runtime.cjs +++ b/scripts/test-ob11-runtime.cjs @@ -1,155 +1,160 @@ -/* Pure OB11 runtime simulation. It never starts SealDice, connects QQ, or calls a real endpoint. */ -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); -const esbuild = require('esbuild'); - -const repo = path.resolve(__dirname, '..'); -const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aiplugin4-ob11-')); -const bundle = path.join(outDir, 'runtime.cjs'); -const sent = []; -const templateConfig = { - '本地图片路径': ['/virtual/avatar.png'], - '本地语音路径': ['voice=/virtual/voice.mp3'], - '本地文件路径': ['book=/virtual/book.pdf'], - '本地视频路径': ['clip=/virtual/clip.mp4'] -}; - -// Minimal SealDice surface used by module initialization and the native fallback. -globalThis.seal = { - ext: { - find: () => undefined, - new: () => ({}), - register: () => undefined, - registerBoolConfig: () => undefined, - registerIntConfig: () => undefined, - registerFloatConfig: () => undefined, - registerStringConfig: () => undefined, - registerOptionConfig: () => undefined, - registerTemplateConfig: () => undefined, - getTemplateConfig: (_ext, key) => { - if (['对话模型', '图片模型', '嵌入模型'].includes(key)) return []; - return templateConfig[key] || ['']; - }, - getBoolConfig: () => true, - getIntConfig: () => 10, - getFloatConfig: () => 0, - getStringConfig: () => '/virtual/sealdice', - getOptionConfig: () => '信息', - storageGet: () => '', - storageSet: () => undefined - }, - replyToSender: (ctx, msg, content) => sent.push({ ctx, msg, content }), - newMessage: () => ({ sender: {}, messageType: 'group' }), - getEndPoints: () => [], - createTempCtx: () => ({}) -}; - -(async () => { - await esbuild.build({ - entryPoints: [path.join(repo, 'scripts/ob11-runtime-test-entry.ts')], - bundle: true, - platform: 'node', - format: 'cjs', - outfile: bundle, - logLevel: 'silent' - }); - - const runtime = require(bundle); - assert.equal(runtime.getActionCapability('send_group_msg'), 'either'); - assert.equal(runtime.getActionCapability('get_group_member_list'), 'network'); - - const context = { - endpointId: '10000', - ctx: { - endPoint: { userId: '10000' }, - group: { groupId: 'QQ-Group:123456', groupName: '模拟群' }, - player: { userId: 'QQ:20000', name: '模拟用户', role: 'member' } - }, - msg: { messageType: 'group' } - }; - - // No ob11 dependency: native send preserves special segments as CQ content. - delete globalThis.net; - sent.length = 0; - const nativeSend = await runtime.dispatchOb11Api(context, 'send_group_msg', { - group_id: '123456', - message: [ - { type: 'text', data: { text: 'hello' } }, - { type: 'image', data: { file: '/virtual/avatar.png' } }, - { type: 'record', data: { file: '/virtual/voice.mp3' } }, - { type: 'video', data: { file: '/virtual/clip.mp4' } }, - { type: 'file', data: { file: '/virtual/book.pdf', name: 'book.pdf' } }, - { type: 'at', data: { qq: '20000' } }, - { type: 'reply', data: { id: '88' } }, - { type: 'json', data: { data: '{"app":"demo"}' } }, - { type: 'markdown', data: { content: '# demo' } }, - { type: 'music', data: { type: 'qq', id: '99' } }, - { type: 'poke', data: { qq: '20000' } }, - { type: 'dice', data: {} }, - { type: 'rps', data: {} } - ] - }); - assert.equal(nativeSend.ok, true); - assert.equal(nativeSend.backend, 'seal-native'); - assert.equal(sent.length, 1); - for (const marker of ['[CQ:image', '[CQ:record', '[CQ:video', '[CQ:file', '[CQ:at', '[CQ:reply', '[CQ:json', '[CQ:markdown', '[CQ:music', '[CQ:poke', '[CQ:dice]', '[CQ:rps]']) { - assert.match(sent[0].content, new RegExp(marker.replace(/[\[\]]/g, '\\$&'))); - } - - const nativeGroupInfo = await runtime.dispatchOb11Api(context, 'get_group_info', { group_id: '123456' }); - assert.equal(nativeGroupInfo.ok, true); - assert.equal(nativeGroupInfo.backend, 'seal-native'); - - for (const action of ['get_group_member_list', 'upload_group_file', 'totally_unknown_action']) { - const unavailable = await runtime.dispatchOb11Api(context, action, {}); - assert.equal(unavailable.ok, false); - assert.equal(unavailable.error.code, 'OB11_DEPENDENCY_REQUIRED'); - } - - const unsupportedNative = await runtime.dispatchOb11Api(context, 'send_group_msg', { - group_id: '123456', - message: [{ type: 'node', data: { user_id: '1', content: [{ type: 'text', data: { text: 'node' } }] } }] - }); - assert.equal(unsupportedNative.ok, false); - assert.equal(unsupportedNative.error.code, 'NATIVE_SEND_ERROR'); - - // With a mocked ob11 dependency: all actions use callApi, while file uploads use sendFile. - const calls = []; - globalThis.net = { - callApi: async (epId, action, params) => { - calls.push({ kind: 'callApi', epId, action, params }); - return { message_id: 501, action }; - }, - sendFile: async (epId, scene, peerId, file, name, folderId) => { - calls.push({ kind: 'sendFile', epId, scene, peerId, file, name, folderId }); - return { message_id: 502 }; - } - }; - const remoteSend = await runtime.dispatchOb11Api(context, 'send_private_msg', { - user_id: '20000', - message: [{ type: 'image', data: { file: 'resource:avatar' } }] - }); - assert.equal(remoteSend.ok, true); - assert.equal(remoteSend.backend, 'ob11-net'); - assert.equal(remoteSend.message_id, 501); - assert.equal(calls[0].kind, 'callApi'); - assert.equal(calls[0].params.message[0].data.file, '/virtual/avatar.png'); - - const upload = await runtime.dispatchOb11Api(context, 'upload_group_file', { - group_id: '123456', file: 'resource:book', name: 'book.pdf', folder_id: 'folder' - }); - assert.equal(upload.ok, true); - assert.equal(upload.message_id, 502); - assert.equal(calls[1].kind, 'sendFile'); - assert.equal(calls[1].scene, 'group'); - assert.equal(calls[1].peerId, '123456'); - assert.equal(calls[1].file, '/virtual/book.pdf'); - - console.log('OB11 runtime simulation passed: native fallback, dependency errors, special segments, resource references, net calls, and file uploads.'); -})().finally(() => fs.rmSync(outDir, { recursive: true, force: true })).catch(error => { - console.error(error); - process.exitCode = 1; -}); - +/* Pure OB11 runtime simulation. It never starts SealDice, connects QQ, or calls a real endpoint. */ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const esbuild = require('esbuild'); + +const repo = path.resolve(__dirname, '..'); +const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aiplugin4-ob11-')); +const bundle = path.join(outDir, 'runtime.cjs'); +const sent = []; +const templateConfig = { + '本地图片路径': ['/virtual/avatar.png'], + '本地语音路径': ['voice=/virtual/voice.mp3'], + '本地文件路径': ['book=/virtual/book.pdf'], + '本地视频路径': ['clip=/virtual/clip.mp4'] +}; + +// Minimal SealDice surface used by module initialization and the native fallback. +globalThis.seal = { + ext: { + find: () => undefined, + new: () => ({}), + register: () => undefined, + registerBoolConfig: () => undefined, + registerIntConfig: () => undefined, + registerFloatConfig: () => undefined, + registerStringConfig: () => undefined, + registerOptionConfig: () => undefined, + registerTemplateConfig: () => undefined, + getTemplateConfig: (_ext, key) => { + if (['对话模型', '图片模型', '嵌入模型'].includes(key)) return []; + return templateConfig[key] || ['']; + }, + getBoolConfig: () => true, + getIntConfig: () => 10, + getFloatConfig: () => 0, + getStringConfig: () => '/virtual/sealdice', + getOptionConfig: () => '信息', + storageGet: () => '', + storageSet: () => undefined + }, + replyToSender: (ctx, msg, content) => sent.push({ ctx, msg, content }), + newMessage: () => ({ sender: {}, messageType: 'group' }), + getEndPoints: () => [], + createTempCtx: () => ({}) +}; + +(async () => { + await esbuild.build({ + entryPoints: [path.join(repo, 'scripts/ob11-runtime-test-entry.ts')], + bundle: true, + platform: 'node', + format: 'cjs', + outfile: bundle, + logLevel: 'silent' + }); + + const runtime = require(bundle); + assert.equal(runtime.getActionCapability('send_group_msg'), 'either'); + assert.equal(runtime.getActionCapability('get_group_member_list'), 'network'); + + // Model literal \n escapes are rendered as real line breaks; \f remains untouched for message splitting. + assert.equal(runtime.decodeEscapedNewlines('第一行\\n第二行'), '第一行\n第二行'); + assert.equal(runtime.decodeEscapedNewlines('第一行\\r\\n第二行'), '第一行\n第二行'); + assert.equal(runtime.decodeEscapedNewlines('第一条\\f第二条'), '第一条\\f第二条'); + + const context = { + endpointId: '10000', + ctx: { + endPoint: { userId: '10000' }, + group: { groupId: 'QQ-Group:123456', groupName: '模拟群' }, + player: { userId: 'QQ:20000', name: '模拟用户', role: 'member' } + }, + msg: { messageType: 'group' } + }; + + // No ob11 dependency: native send preserves special segments as CQ content. + delete globalThis.net; + sent.length = 0; + const nativeSend = await runtime.dispatchOb11Api(context, 'send_group_msg', { + group_id: '123456', + message: [ + { type: 'text', data: { text: 'hello' } }, + { type: 'image', data: { file: '/virtual/avatar.png' } }, + { type: 'record', data: { file: '/virtual/voice.mp3' } }, + { type: 'video', data: { file: '/virtual/clip.mp4' } }, + { type: 'file', data: { file: '/virtual/book.pdf', name: 'book.pdf' } }, + { type: 'at', data: { qq: '20000' } }, + { type: 'reply', data: { id: '88' } }, + { type: 'json', data: { data: '{"app":"demo"}' } }, + { type: 'markdown', data: { content: '# demo' } }, + { type: 'music', data: { type: 'qq', id: '99' } }, + { type: 'poke', data: { qq: '20000' } }, + { type: 'dice', data: {} }, + { type: 'rps', data: {} } + ] + }); + assert.equal(nativeSend.ok, true); + assert.equal(nativeSend.backend, 'seal-native'); + assert.equal(sent.length, 1); + for (const marker of ['[CQ:image', '[CQ:record', '[CQ:video', '[CQ:file', '[CQ:at', '[CQ:reply', '[CQ:json', '[CQ:markdown', '[CQ:music', '[CQ:poke', '[CQ:dice]', '[CQ:rps]']) { + assert.match(sent[0].content, new RegExp(marker.replace(/[\[\]]/g, '\\$&'))); + } + + const nativeGroupInfo = await runtime.dispatchOb11Api(context, 'get_group_info', { group_id: '123456' }); + assert.equal(nativeGroupInfo.ok, true); + assert.equal(nativeGroupInfo.backend, 'seal-native'); + + for (const action of ['get_group_member_list', 'upload_group_file', 'totally_unknown_action']) { + const unavailable = await runtime.dispatchOb11Api(context, action, {}); + assert.equal(unavailable.ok, false); + assert.equal(unavailable.error.code, 'OB11_DEPENDENCY_REQUIRED'); + } + + const unsupportedNative = await runtime.dispatchOb11Api(context, 'send_group_msg', { + group_id: '123456', + message: [{ type: 'node', data: { user_id: '1', content: [{ type: 'text', data: { text: 'node' } }] } }] + }); + assert.equal(unsupportedNative.ok, false); + assert.equal(unsupportedNative.error.code, 'NATIVE_SEND_ERROR'); + + // With a mocked ob11 dependency: all actions use callApi, while file uploads use sendFile. + const calls = []; + globalThis.net = { + callApi: async (epId, action, params) => { + calls.push({ kind: 'callApi', epId, action, params }); + return { message_id: 501, action }; + }, + sendFile: async (epId, scene, peerId, file, name, folderId) => { + calls.push({ kind: 'sendFile', epId, scene, peerId, file, name, folderId }); + return { message_id: 502 }; + } + }; + const remoteSend = await runtime.dispatchOb11Api(context, 'send_private_msg', { + user_id: '20000', + message: [{ type: 'image', data: { file: 'resource:avatar' } }] + }); + assert.equal(remoteSend.ok, true); + assert.equal(remoteSend.backend, 'ob11-net'); + assert.equal(remoteSend.message_id, 501); + assert.equal(calls[0].kind, 'callApi'); + assert.equal(calls[0].params.message[0].data.file, '/virtual/avatar.png'); + + const upload = await runtime.dispatchOb11Api(context, 'upload_group_file', { + group_id: '123456', file: 'resource:book', name: 'book.pdf', folder_id: 'folder' + }); + assert.equal(upload.ok, true); + assert.equal(upload.message_id, 502); + assert.equal(calls[1].kind, 'sendFile'); + assert.equal(calls[1].scene, 'group'); + assert.equal(calls[1].peerId, '123456'); + assert.equal(calls[1].file, '/virtual/book.pdf'); + + console.log('OB11 runtime simulation passed: native fallback, dependency errors, special segments, resource references, net calls, and file uploads.'); +})().finally(() => fs.rmSync(outDir, { recursive: true, force: true })).catch(error => { + console.error(error); + process.exitCode = 1; +}); + diff --git a/sealpack/info.toml b/sealpack/info.toml index 8cd4377..82c6094 100644 --- a/sealpack/info.toml +++ b/sealpack/info.toml @@ -1,46 +1,46 @@ -# 包格式版本,当前 CLI 支持 1.0.0。 -format_version = "1.0.0" - -# 包基础信息。id 必须使用 namespace/package 格式;version 必须是 semver, -# 与插件版本(src/config/static_config/meta.ts 的 VERSION)保持一致, -# 构建时会由 scripts/prepare-sealpack.js 自动同步。 -[package] -id = "error2913/aiplugin4" -name = "AI骰娘4" -version = "4.15.2" -authors = ["错误、白鱼"] -license = "MIT" -description = "适用于大部分 OpenAI API 兼容格式 AI 的 Sealdice 模型插件,支持对话、记忆、工具调用、MCP/Skills 等。" -keywords = ["ai", "chatgpt", "sealdice", "插件"] - -# SealDice 兼容范围。min_version 表示安装此包所需的最低 SealDice 版本。 -[package.seal] -min_version = "1.6.0" - -# 依赖的其他 sealpack 包。 -[dependencies] - -# 权限声明。插件需要联网调用模型 API,因此 network 必须为 true。 -[permissions] -network = true -file_read = [] -file_write = [] - -# 内容清单。scripts/main.js 为构建产物,由 scripts/prepare-sealpack.js 自动生成。 -[contents] -scripts = ["scripts/*.js"] -decks = [] -reply = [] -helpdoc = [] -templates = [] - -# 商店展示信息。icon、banner、screenshots 填写 assets/ 下的图片路径。 -[store] -readme = "README.md" -icon = "assets/icon.png" -banner = "" -screenshots = [] -category = "tool" - -# 包配置项,当前模板不预置配置。 -[config] +# 包格式版本,当前 CLI 支持 1.0.0。 +format_version = "1.0.0" + +# 包基础信息。id 必须使用 namespace/package 格式;version 必须是 semver, +# 与插件版本(src/config/static_config/meta.ts 的 VERSION)保持一致, +# 构建时会由 scripts/prepare-sealpack.js 自动同步。 +[package] +id = "error2913/aiplugin4" +name = "AI骰娘4" +version = "4.16.0" +authors = ["错误、白鱼"] +license = "MIT" +description = "适用于大部分 OpenAI API 兼容格式 AI 的 Sealdice 模型插件,支持对话、记忆、工具调用、MCP/Skills 等。" +keywords = ["ai", "chatgpt", "sealdice", "插件"] + +# SealDice 兼容范围。min_version 表示安装此包所需的最低 SealDice 版本。 +[package.seal] +min_version = "1.6.0" + +# 依赖的其他 sealpack 包。 +[dependencies] + +# 权限声明。插件需要联网调用模型 API,因此 network 必须为 true。 +[permissions] +network = true +file_read = [] +file_write = [] + +# 内容清单。scripts/main.js 为构建产物,由 scripts/prepare-sealpack.js 自动生成。 +[contents] +scripts = ["scripts/*.js"] +decks = [] +reply = [] +helpdoc = [] +templates = [] + +# 商店展示信息。icon、banner、screenshots 填写 assets/ 下的图片路径。 +[store] +readme = "README.md" +icon = "assets/icon.png" +banner = "" +screenshots = [] +category = "tool" + +# 包配置项,当前模板不预置配置。 +[config] diff --git a/src/cmd/sub_cmd/memory.ts b/src/cmd/sub_cmd/memory.ts index e076fcc..e5112a6 100644 --- a/src/cmd/sub_cmd/memory.ts +++ b/src/cmd/sub_cmd/memory.ts @@ -1,377 +1,377 @@ -// .ai memo:个人/群聊/短期记忆与设定管理 -import Config from "../../config/config"; -import { Session } from "../../session/session"; -import { getSession } from "../../session/session_service"; -import { stripInternalTags } from "../../utils/string"; -import { normalizeUserId } from "../../utils/target_id"; -import { aliasToCmd } from "../../utils/utils"; -import { I, S, U } from "../privilege"; -import { SubCmd, SubCmdContext } from "../root_cmd"; - -/** 受保护记忆:其他会话创建的私有记忆仅创建会话可删(与工具层 del_memory / clear_memory 一致) */ -function protectedMemoryIds(target: Session, callerSessionId: string): Set { - const ids = new Set(); - if (target.sessionId !== callerSessionId) { - for (const m of target.memory.memories) { - if (m.visibility === 'private' && m.sessionId !== callerSessionId) ids.add(m.id); - } - } - return ids; -} - -export function registerCmdMemory() { - const cmd = new SubCmd('memory'); - cmd.desc = '记忆相关操作'; - cmd.help = `帮助: - 【.ai memo status [用户ID]】查看记忆状态,传用户ID时查看对应个人记忆 - 【.ai memo [p/g] st <内容>】设置个人/群聊设定 - 【.ai memo [p/g] st clr】清除个人/群聊设定 - 【.ai memo [p/g] del --关键词1 --关键词2】删除个人/群聊记忆 - 【.ai memo [p/g/short] list】展示个人/群聊/短期记忆 - 【.ai memo [p/g/short] clr】清除个人/群聊/短期记忆 - 【.ai memo short [on/off]】开启/关闭短期记忆 - 【.ai memo sum】立即总结一次短期记忆 - 【.ai memo sum clr】清除总结记忆`; - cmd.priv = { - priv: U, args: { - status: { priv: U }, - private: { - priv: U, args: { - set: { - priv: U, args: { - clear: { priv: U }, - "*": { priv: U } - } - }, - delete: { priv: U }, - list: { priv: U }, - clear: { priv: U } - } - }, - group: { - priv: I, args: { - set: { - priv: U, args: { - clear: { priv: U }, - "*": { priv: U } - } - }, - delete: { priv: U }, - list: { priv: U }, - clear: { priv: U } - } - }, - short: { - priv: S, args: { - list: { priv: U }, - clear: { priv: U }, - on: { priv: U }, - off: { priv: U } - } - }, - sum: { priv: U } - } - }; - cmd.solve = async (scc: SubCmdContext) => { - const { ctx, msg, cmdArgs, session, page, ret } = scc; - - const currentUserId = normalizeUserId(ctx.player && ctx.player.userId); - if (!currentUserId) { - seal.replyToSender(ctx, msg, '当前消息缺少有效用户ID'); - return ret; - } - const sessionCtx = ctx; - const targetSession = getSession(currentUserId); - const val2 = cmdArgs.getArgN(2); - switch (aliasToCmd(val2)) { - case 'status': { - let statusSession = session; - const targetUserId = cmdArgs.getArgN(3); - if (targetUserId) { - const normalizedUserId = normalizeUserId(targetUserId); - if (!normalizedUserId) { - seal.replyToSender(ctx, msg, '参数无效,【.ai memo status [用户ID]】'); - return ret; - } - statusSession = getSession(normalizedUserId); - } - const { MEMORY: isMemory, SUMMARY: isShortMemory } = Config.memory; - seal.replyToSender(ctx, msg, `${statusSession.id} - 长期记忆开启状态: ${isMemory ? '是' : '否'} - 长期记忆条数: ${statusSession.memory.memoryIds.length} - 关键词库: ${statusSession.memory.keywords.join('、') || '无'} - 短期记忆开启状态: ${(isShortMemory && statusSession.memory.useShortMemory) ? '是' : '否'} - 短期记忆条数: ${statusSession.memory.shortMemoryList.length}`); - return ret; - } - case 'private': { - const val3 = cmdArgs.getArgN(3); - switch (aliasToCmd(val3)) { - case 'set': { - const s = cmdArgs.getRestArgsFrom(4); - switch (aliasToCmd(s)) { - case '': { - seal.replyToSender(ctx, msg, '参数缺失,【.ai memo p st <内容>】设置个人设定,【.ai memo p st clr】清除个人设定'); - return ret; - } - case 'clear': { - targetSession.memory.persona = '无'; - seal.replyToSender(ctx, msg, '设定已清除'); - targetSession.save(); - return ret; - } - default: { - if (s.length > 20) { - seal.replyToSender(ctx, msg, '设定过长,请控制在20字以内'); - return ret; - } - targetSession.memory.persona = stripInternalTags(s); - seal.replyToSender(ctx, msg, '设定已修改'); - targetSession.save(); - return ret; - } - } - } - case 'delete': { - const idList = cmdArgs.args.slice(3); - const kw = cmdArgs.kwargs.map(item => item.name); - if (idList.length === 0 && kw.length === 0) { - seal.replyToSender(ctx, msg, '参数缺失,【.ai memo p del --关键词1 --关键词2】删除个人记忆'); - return ret; - } - // 与工具层一致:其他会话创建的私有记忆不可删除 - const protectedIds = protectedMemoryIds(targetSession, session.sessionId); - const deleteIds = new Set(); - for (const id of idList) { - if (!protectedIds.has(id)) deleteIds.add(id); - } - if (kw.length > 0) { - for (const m of targetSession.memory.memories) { - if (!protectedIds.has(m.id) && kw.some(k => m.tags.includes(k))) deleteIds.add(m.id); - } - } - if (deleteIds.size === 0) { - seal.replyToSender(ctx, msg, '没有可删除的记忆(其他会话创建的私有记忆仅创建会话可删除)'); - return ret; - } - targetSession.memory.deleteMemory(Array.from(deleteIds)); - seal.replyToSender(ctx, msg, targetSession.memory.getLatestMemoryListText({ - isPrivate: true, - id: sessionCtx.player!.userId, - name: sessionCtx.player!.name - }, page) || '记忆已全部清除'); - targetSession.save(); - return ret; - } - case 'list': { - seal.replyToSender(ctx, msg, targetSession.memory.getLatestMemoryListText({ - isPrivate: true, - id: sessionCtx.player!.userId, - name: sessionCtx.player!.name - }, page) || '无记忆'); - return ret; - } - case 'clear': { - // 与工具层一致:保留其他会话创建的私有记忆 - const protectedIds = protectedMemoryIds(targetSession, session.sessionId); - if (protectedIds.size > 0) { - const deleteIds = targetSession.memory.memoryIds.filter(id => !protectedIds.has(id)); - if (deleteIds.length === 0) { - seal.replyToSender(ctx, msg, '无可清除的记忆(存在其他会话创建的私有记忆)'); - return ret; - } - targetSession.memory.deleteMemory(deleteIds); - seal.replyToSender(ctx, msg, `个人记忆已清除(保留 ${protectedIds.size} 条其他会话的私有记忆)`); - targetSession.save(); - return ret; - } - targetSession.memory.clearMemory(); - seal.replyToSender(ctx, msg, '个人记忆已清除'); - targetSession.save(); - return ret; - } - default: { - seal.replyToSender(ctx, msg, `参数缺失: - 【.ai memo p st <内容>】设置个人设定 - 【.ai memo p st clr】清除个人设定 - 【.ai memo p del --关键词1 --关键词2】删除个人记忆 - 【.ai memo p list】展示个人记忆 - 【.ai memo p clr】清除个人记忆`); - return ret; - } - } - } - case 'group': { - if (ctx.isPrivate) { - seal.replyToSender(ctx, msg, '群聊记忆仅在群聊可用'); - return ret; - } - - const val3 = cmdArgs.getArgN(3); - switch (aliasToCmd(val3)) { - case 'set': { - const s = cmdArgs.getRestArgsFrom(4); - switch (aliasToCmd(s)) { - case '': { - seal.replyToSender(ctx, msg, '参数缺失,【.ai memo g st <内容>】设置群聊设定,【.ai memo g st clr】清除群聊设定'); - return ret; - } - case 'clear': { - session.memory.persona = '无'; - seal.replyToSender(ctx, msg, '设定已清除'); - session.save(); - return ret; - } - default: { - if (s.length > 30) { - seal.replyToSender(ctx, msg, '设定过长,请控制在30字以内'); - return ret; - } - session.memory.persona = stripInternalTags(s); - seal.replyToSender(ctx, msg, '设定已修改'); - session.save(); - return ret; - } - } - } - case 'delete': { - const idList = cmdArgs.args.slice(3); - const kw = cmdArgs.kwargs.map(item => item.name); - if (idList.length === 0 && kw.length === 0) { - seal.replyToSender(ctx, msg, '参数缺失,【.ai memo g del 】删除群聊记忆'); - return ret; - } - // 与工具层一致:其他会话创建的私有记忆不可删除(群聊指令目标即当前会话,通常无受保护条目) - const protectedIds = protectedMemoryIds(session, session.sessionId); - const deleteIds = new Set(); - for (const id of idList) { - if (!protectedIds.has(id)) deleteIds.add(id); - } - if (kw.length > 0) { - for (const m of session.memory.memories) { - if (!protectedIds.has(m.id) && kw.some(k => m.tags.includes(k))) deleteIds.add(m.id); - } - } - if (deleteIds.size === 0) { - seal.replyToSender(ctx, msg, '没有可删除的记忆(其他会话创建的私有记忆仅创建会话可删除)'); - return ret; - } - session.memory.deleteMemory(Array.from(deleteIds)); - seal.replyToSender(ctx, msg, session.memory.getLatestMemoryListText({ - isPrivate: false, - id: ctx.group!.groupId, - name: ctx.group!.groupName - }, page) || '记忆已全部清除'); - session.save(); - return ret; - } - case 'list': { - seal.replyToSender(ctx, msg, session.memory.getLatestMemoryListText({ - isPrivate: false, - id: ctx.group!.groupId, - name: ctx.group!.groupName - }, page) || '无记忆'); - return ret; - } - case 'clear': { - const protectedIds = protectedMemoryIds(session, session.sessionId); - if (protectedIds.size > 0) { - const deleteIds = session.memory.memoryIds.filter(id => !protectedIds.has(id)); - if (deleteIds.length === 0) { - seal.replyToSender(ctx, msg, '无可清除的记忆(存在其他会话创建的私有记忆)'); - return ret; - } - session.memory.deleteMemory(deleteIds); - seal.replyToSender(ctx, msg, `群聊记忆已清除(保留 ${protectedIds.size} 条其他会话的私有记忆)`); - session.save(); - return ret; - } - session.memory.clearMemory(); - seal.replyToSender(ctx, msg, '群聊记忆已清除'); - session.save(); - return ret; - } - default: { - seal.replyToSender(ctx, msg, `参数缺失: - 【.ai memo g st <内容>】设置群聊设定 - 【.ai memo g st clr】清除群聊设定 - 【.ai memo g del --关键词1 --关键词2】删除群聊记忆 - 【.ai memo g list】展示群聊记忆 - 【.ai memo g clr】清除群聊记忆`); - return ret; - } - } - } - case 'short': { - const val3 = cmdArgs.getArgN(3); - switch (aliasToCmd(val3)) { - case 'on': { - session.memory.useShortMemory = true; - seal.replyToSender(ctx, msg, '短期记忆已开启'); - session.save(); - return ret; - } - case 'off': { - session.memory.useShortMemory = false; - seal.replyToSender(ctx, msg, '短期记忆已关闭'); - session.save(); - return ret; - } - case 'list': { - if (session.memory.shortMemoryList.length === 0) { - seal.replyToSender(ctx, msg, '短期记忆为空'); - return ret; - } - seal.replyToSender(ctx, msg, session.memory.shortMemoryList - .map((item, index) => `${index + 1}. ${item}`) - .slice((page - 1) * 10, page * 10) - .join('\n') + `\n当前页码: ${page}/${Math.ceil(session.memory.shortMemoryList.length / 10)}`); - return ret; - } - case 'clear': { - session.memory.clearShortMemory(); - seal.replyToSender(ctx, msg, '短期记忆已清除'); - session.save(); - return ret; - } - default: { - seal.replyToSender(ctx, msg, `参数缺失 - 【.ai memo short list】展示短期记忆 - 【.ai memo short clr】清除短期记忆 - 【.ai memo short [on/off]】开启/关闭短期记忆`); - return ret; - } - } - } - case 'sum': { - const val3 = cmdArgs.getArgN(3); - if (aliasToCmd(val3) === 'clear') { - session.memory.clearSummaries(); - session.save(); - seal.replyToSender(ctx, msg, '总结记忆已清除'); - return ret; - } - session.context.summaryCounter = 0; - await session.memory.updateShortMemory(ctx, msg, session) - seal.replyToSender(ctx, msg, session.memory.shortMemoryList - .map((item, index) => `${index + 1}. ${item}`) - .slice((page - 1) * 10, page * 10) - .join('\n') + `\n当前页码: ${page}/${Math.ceil(session.memory.shortMemoryList.length / 10)}`); - return ret; - } - default: { - seal.replyToSender(ctx, msg, `帮助: - 【.ai memo status [用户ID]】查看记忆状态,传用户ID时查看对应个人记忆 - 【.ai memo [p/g] st <内容>】设置个人/群聊设定 - 【.ai memo [p/g] st clr】清除个人/群聊设定 - 【.ai memo [p/g] del --关键词1 --关键词2】删除个人/群聊记忆 - 【.ai memo [p/g/short] list】展示个人/群聊/短期记忆 - 【.ai memo [p/g/short] clr】清除个人/群聊/短期记忆 - 【.ai memo short [on/off]】开启/关闭短期记忆 - 【.ai memo sum】立即总结一次短期记忆 - 【.ai memo sum clr】清除总结记忆`); - return ret; - } - } - } -} +// .ai memo:个人/群聊/短期记忆与设定管理 +import Config from "../../config/config"; +import { Session } from "../../session/session"; +import { getSession } from "../../session/session_service"; +import { stripInternalTags } from "../../utils/string"; +import { normalizeUserId } from "../../utils/target_id"; +import { aliasToCmd } from "../../utils/utils"; +import { I, S, U } from "../privilege"; +import { SubCmd, SubCmdContext } from "../root_cmd"; + +/** 受保护记忆:其他会话创建的私有记忆仅创建会话可删(与工具层 del_memory / clear_memory 一致) */ +function protectedMemoryIds(target: Session, callerSessionId: string): Set { + const ids = new Set(); + if (target.sessionId !== callerSessionId) { + for (const m of target.memory.memories) { + if (m.visibility === 'private' && m.sessionId !== callerSessionId) ids.add(m.id); + } + } + return ids; +} + +export function registerCmdMemory() { + const cmd = new SubCmd('memory'); + cmd.desc = '记忆相关操作'; + cmd.help = `帮助: + 【.ai memo status [用户ID]】查看记忆状态,传用户ID时查看对应个人记忆 + 【.ai memo [p/g] st <内容>】设置个人/群聊设定 + 【.ai memo [p/g] st clr】清除个人/群聊设定 + 【.ai memo [p/g] del --关键词1 --关键词2】删除个人/群聊记忆 + 【.ai memo [p/g/short] list】展示个人/群聊/短期记忆 + 【.ai memo [p/g/short] clr】清除个人/群聊/短期记忆 + 【.ai memo short [on/off]】开启/关闭短期记忆 + 【.ai memo sum】立即总结一次短期记忆 + 【.ai memo sum clr】清除总结记忆`; + cmd.priv = { + priv: U, args: { + status: { priv: U }, + private: { + priv: U, args: { + set: { + priv: U, args: { + clear: { priv: U }, + "*": { priv: U } + } + }, + delete: { priv: U }, + list: { priv: U }, + clear: { priv: U } + } + }, + group: { + priv: I, args: { + set: { + priv: U, args: { + clear: { priv: U }, + "*": { priv: U } + } + }, + delete: { priv: U }, + list: { priv: U }, + clear: { priv: U } + } + }, + short: { + priv: S, args: { + list: { priv: U }, + clear: { priv: U }, + on: { priv: U }, + off: { priv: U } + } + }, + sum: { priv: U } + } + }; + cmd.solve = async (scc: SubCmdContext) => { + const { ctx, msg, cmdArgs, session, page, ret } = scc; + + const currentUserId = normalizeUserId(ctx.player && ctx.player.userId || ''); + if (!currentUserId) { + seal.replyToSender(ctx, msg, '当前消息缺少有效用户ID'); + return ret; + } + const sessionCtx = ctx; + const targetSession = getSession(currentUserId); + const val2 = cmdArgs.getArgN(2); + switch (aliasToCmd(val2)) { + case 'status': { + let statusSession = session; + const targetUserId = cmdArgs.getArgN(3); + if (targetUserId) { + const normalizedUserId = normalizeUserId(targetUserId); + if (!normalizedUserId) { + seal.replyToSender(ctx, msg, '参数无效,【.ai memo status [用户ID]】'); + return ret; + } + statusSession = getSession(normalizedUserId); + } + const { MEMORY: isMemory, SUMMARY: isShortMemory } = Config.memory; + seal.replyToSender(ctx, msg, `${statusSession.id} + 长期记忆开启状态: ${isMemory ? '是' : '否'} + 长期记忆条数: ${statusSession.memory.memoryIds.length} + 关键词库: ${statusSession.memory.keywords.join('、') || '无'} + 短期记忆开启状态: ${(isShortMemory && statusSession.memory.useShortMemory) ? '是' : '否'} + 短期记忆条数: ${statusSession.memory.shortMemoryList.length}`); + return ret; + } + case 'private': { + const val3 = cmdArgs.getArgN(3); + switch (aliasToCmd(val3)) { + case 'set': { + const s = cmdArgs.getRestArgsFrom(4); + switch (aliasToCmd(s)) { + case '': { + seal.replyToSender(ctx, msg, '参数缺失,【.ai memo p st <内容>】设置个人设定,【.ai memo p st clr】清除个人设定'); + return ret; + } + case 'clear': { + targetSession.memory.persona = '无'; + seal.replyToSender(ctx, msg, '设定已清除'); + targetSession.save(); + return ret; + } + default: { + if (s.length > 20) { + seal.replyToSender(ctx, msg, '设定过长,请控制在20字以内'); + return ret; + } + targetSession.memory.persona = stripInternalTags(s); + seal.replyToSender(ctx, msg, '设定已修改'); + targetSession.save(); + return ret; + } + } + } + case 'delete': { + const idList = cmdArgs.args.slice(3); + const kw = cmdArgs.kwargs.map(item => item.name); + if (idList.length === 0 && kw.length === 0) { + seal.replyToSender(ctx, msg, '参数缺失,【.ai memo p del --关键词1 --关键词2】删除个人记忆'); + return ret; + } + // 与工具层一致:其他会话创建的私有记忆不可删除 + const protectedIds = protectedMemoryIds(targetSession, session.sessionId); + const deleteIds = new Set(); + for (const id of idList) { + if (!protectedIds.has(id)) deleteIds.add(id); + } + if (kw.length > 0) { + for (const m of targetSession.memory.memories) { + if (!protectedIds.has(m.id) && kw.some(k => m.tags.includes(k))) deleteIds.add(m.id); + } + } + if (deleteIds.size === 0) { + seal.replyToSender(ctx, msg, '没有可删除的记忆(其他会话创建的私有记忆仅创建会话可删除)'); + return ret; + } + targetSession.memory.deleteMemory(Array.from(deleteIds)); + seal.replyToSender(ctx, msg, targetSession.memory.getLatestMemoryListText({ + isPrivate: true, + id: sessionCtx.player!.userId, + name: sessionCtx.player!.name + }, page) || '记忆已全部清除'); + targetSession.save(); + return ret; + } + case 'list': { + seal.replyToSender(ctx, msg, targetSession.memory.getLatestMemoryListText({ + isPrivate: true, + id: sessionCtx.player!.userId, + name: sessionCtx.player!.name + }, page) || '无记忆'); + return ret; + } + case 'clear': { + // 与工具层一致:保留其他会话创建的私有记忆 + const protectedIds = protectedMemoryIds(targetSession, session.sessionId); + if (protectedIds.size > 0) { + const deleteIds = targetSession.memory.memoryIds.filter(id => !protectedIds.has(id)); + if (deleteIds.length === 0) { + seal.replyToSender(ctx, msg, '无可清除的记忆(存在其他会话创建的私有记忆)'); + return ret; + } + targetSession.memory.deleteMemory(deleteIds); + seal.replyToSender(ctx, msg, `个人记忆已清除(保留 ${protectedIds.size} 条其他会话的私有记忆)`); + targetSession.save(); + return ret; + } + targetSession.memory.clearMemory(); + seal.replyToSender(ctx, msg, '个人记忆已清除'); + targetSession.save(); + return ret; + } + default: { + seal.replyToSender(ctx, msg, `参数缺失: + 【.ai memo p st <内容>】设置个人设定 + 【.ai memo p st clr】清除个人设定 + 【.ai memo p del --关键词1 --关键词2】删除个人记忆 + 【.ai memo p list】展示个人记忆 + 【.ai memo p clr】清除个人记忆`); + return ret; + } + } + } + case 'group': { + if (ctx.isPrivate) { + seal.replyToSender(ctx, msg, '群聊记忆仅在群聊可用'); + return ret; + } + + const val3 = cmdArgs.getArgN(3); + switch (aliasToCmd(val3)) { + case 'set': { + const s = cmdArgs.getRestArgsFrom(4); + switch (aliasToCmd(s)) { + case '': { + seal.replyToSender(ctx, msg, '参数缺失,【.ai memo g st <内容>】设置群聊设定,【.ai memo g st clr】清除群聊设定'); + return ret; + } + case 'clear': { + session.memory.persona = '无'; + seal.replyToSender(ctx, msg, '设定已清除'); + session.save(); + return ret; + } + default: { + if (s.length > 30) { + seal.replyToSender(ctx, msg, '设定过长,请控制在30字以内'); + return ret; + } + session.memory.persona = stripInternalTags(s); + seal.replyToSender(ctx, msg, '设定已修改'); + session.save(); + return ret; + } + } + } + case 'delete': { + const idList = cmdArgs.args.slice(3); + const kw = cmdArgs.kwargs.map(item => item.name); + if (idList.length === 0 && kw.length === 0) { + seal.replyToSender(ctx, msg, '参数缺失,【.ai memo g del 】删除群聊记忆'); + return ret; + } + // 与工具层一致:其他会话创建的私有记忆不可删除(群聊指令目标即当前会话,通常无受保护条目) + const protectedIds = protectedMemoryIds(session, session.sessionId); + const deleteIds = new Set(); + for (const id of idList) { + if (!protectedIds.has(id)) deleteIds.add(id); + } + if (kw.length > 0) { + for (const m of session.memory.memories) { + if (!protectedIds.has(m.id) && kw.some(k => m.tags.includes(k))) deleteIds.add(m.id); + } + } + if (deleteIds.size === 0) { + seal.replyToSender(ctx, msg, '没有可删除的记忆(其他会话创建的私有记忆仅创建会话可删除)'); + return ret; + } + session.memory.deleteMemory(Array.from(deleteIds)); + seal.replyToSender(ctx, msg, session.memory.getLatestMemoryListText({ + isPrivate: false, + id: ctx.group!.groupId, + name: ctx.group!.groupName + }, page) || '记忆已全部清除'); + session.save(); + return ret; + } + case 'list': { + seal.replyToSender(ctx, msg, session.memory.getLatestMemoryListText({ + isPrivate: false, + id: ctx.group!.groupId, + name: ctx.group!.groupName + }, page) || '无记忆'); + return ret; + } + case 'clear': { + const protectedIds = protectedMemoryIds(session, session.sessionId); + if (protectedIds.size > 0) { + const deleteIds = session.memory.memoryIds.filter(id => !protectedIds.has(id)); + if (deleteIds.length === 0) { + seal.replyToSender(ctx, msg, '无可清除的记忆(存在其他会话创建的私有记忆)'); + return ret; + } + session.memory.deleteMemory(deleteIds); + seal.replyToSender(ctx, msg, `群聊记忆已清除(保留 ${protectedIds.size} 条其他会话的私有记忆)`); + session.save(); + return ret; + } + session.memory.clearMemory(); + seal.replyToSender(ctx, msg, '群聊记忆已清除'); + session.save(); + return ret; + } + default: { + seal.replyToSender(ctx, msg, `参数缺失: + 【.ai memo g st <内容>】设置群聊设定 + 【.ai memo g st clr】清除群聊设定 + 【.ai memo g del --关键词1 --关键词2】删除群聊记忆 + 【.ai memo g list】展示群聊记忆 + 【.ai memo g clr】清除群聊记忆`); + return ret; + } + } + } + case 'short': { + const val3 = cmdArgs.getArgN(3); + switch (aliasToCmd(val3)) { + case 'on': { + session.memory.useShortMemory = true; + seal.replyToSender(ctx, msg, '短期记忆已开启'); + session.save(); + return ret; + } + case 'off': { + session.memory.useShortMemory = false; + seal.replyToSender(ctx, msg, '短期记忆已关闭'); + session.save(); + return ret; + } + case 'list': { + if (session.memory.shortMemoryList.length === 0) { + seal.replyToSender(ctx, msg, '短期记忆为空'); + return ret; + } + seal.replyToSender(ctx, msg, session.memory.shortMemoryList + .map((item, index) => `${index + 1}. ${item}`) + .slice((page - 1) * 10, page * 10) + .join('\n') + `\n当前页码: ${page}/${Math.ceil(session.memory.shortMemoryList.length / 10)}`); + return ret; + } + case 'clear': { + session.memory.clearShortMemory(); + seal.replyToSender(ctx, msg, '短期记忆已清除'); + session.save(); + return ret; + } + default: { + seal.replyToSender(ctx, msg, `参数缺失 + 【.ai memo short list】展示短期记忆 + 【.ai memo short clr】清除短期记忆 + 【.ai memo short [on/off]】开启/关闭短期记忆`); + return ret; + } + } + } + case 'sum': { + const val3 = cmdArgs.getArgN(3); + if (aliasToCmd(val3) === 'clear') { + session.memory.clearSummaries(); + session.save(); + seal.replyToSender(ctx, msg, '总结记忆已清除'); + return ret; + } + session.context.summaryCounter = 0; + await session.memory.updateShortMemory(ctx, msg, session) + seal.replyToSender(ctx, msg, session.memory.shortMemoryList + .map((item, index) => `${index + 1}. ${item}`) + .slice((page - 1) * 10, page * 10) + .join('\n') + `\n当前页码: ${page}/${Math.ceil(session.memory.shortMemoryList.length / 10)}`); + return ret; + } + default: { + seal.replyToSender(ctx, msg, `帮助: + 【.ai memo status [用户ID]】查看记忆状态,传用户ID时查看对应个人记忆 + 【.ai memo [p/g] st <内容>】设置个人/群聊设定 + 【.ai memo [p/g] st clr】清除个人/群聊设定 + 【.ai memo [p/g] del --关键词1 --关键词2】删除个人/群聊记忆 + 【.ai memo [p/g/short] list】展示个人/群聊/短期记忆 + 【.ai memo [p/g/short] clr】清除个人/群聊/短期记忆 + 【.ai memo short [on/off]】开启/关闭短期记忆 + 【.ai memo sum】立即总结一次短期记忆 + 【.ai memo sum clr】清除总结记忆`); + return ret; + } + } + } +} diff --git a/src/config/static_config/meta.ts b/src/config/static_config/meta.ts index 34fcce5..595ba9b 100644 --- a/src/config/static_config/meta.ts +++ b/src/config/static_config/meta.ts @@ -1,8 +1,8 @@ -// 静态常量:版本/作者/模型映射表/权限等级/别名/表情表等 -export const VERSION = "4.15.2"; -export const AUTHOR = "baiyu&错误"; -export const NAME = "aiplugin4"; - -export const CONFIG_CACHE_TTL = 60000; -// 存储结构版本:存档结构变更时递增,用于后续数据迁移判断 -export const STORAGE_VERSION = 1; +// 静态常量:版本/作者/模型映射表/权限等级/别名/表情表等 +export const VERSION = "4.16.0"; +export const AUTHOR = "baiyu&错误"; +export const NAME = "aiplugin4"; + +export const CONFIG_CACHE_TTL = 60000; +// 存储结构版本:存档结构变更时递增,用于后续数据迁移判断 +export const STORAGE_VERSION = 1; diff --git a/src/tool/command_target.ts b/src/tool/command_target.ts index 8108472..9baaa58 100644 --- a/src/tool/command_target.ts +++ b/src/tool/command_target.ts @@ -1,63 +1,69 @@ -import { createCtx, createMsg } from "../utils/seal"; -import { getRawId, normalizeGroupId, normalizeUserId } from "../utils/target_id"; - -export interface CommandTargetOptions { - /** 显式指定指令消息的触发用户;未填写时由调用方使用当前会话用户。 */ - trigger?: string; - /** 指令消息中的 @ 用户列表。 */ - at: string[]; -} - -/** 将用户标识严格规范化为可注入 OB11/海豹消息的原始用户 ID。 */ -export function normalizeCommandUserId(value: any): string | undefined { - const normalized = normalizeUserId(String(value == null ? '' : value)); - return normalized ? getRawId(normalized) : undefined; -} - -export function currentCommandUserId(ctx: seal.MsgContext): string { - return normalizeCommandUserId(ctx.player && ctx.player.userId) - || normalizeCommandUserId(ctx.endPoint.userId) - || ''; -} - -export function qualifyCommandUserId(ctx: seal.MsgContext, userId: string): string { - const platform = String(ctx.endPoint.userId || '').split(':', 1)[0]; - return platform ? `${platform}:${userId}` : userId; -} - -/** 解析工具参数;不对旧字段做兼容,at 必须是数组。 */ -export function resolveCommandTarget(ctx: seal.MsgContext, args: { [key: string]: any } | undefined): CommandTargetOptions & { effectiveTrigger: string } { - const trigger = normalizeCommandUserId(args && args.trigger); - const at = Array.isArray(args && args.at) - ? args.at.map(normalizeCommandUserId).filter((item): item is string => !!item) - : []; - return { - ...(trigger ? { trigger } : {}), - at, - effectiveTrigger: trigger || currentCommandUserId(ctx) - }; -} - -export function buildCommandContext( - ctx: seal.MsgContext, - target: { trigger: string; at: string[] } -): { ctx: seal.MsgContext; msg: seal.Message } { - // Message.groupId 必须保留 SealDice 的平台前缀(如 QQ-Group:123), - // 否则 OB11 核心在 SendToGroup 时无法命中群会话,也不会触发 onMessageSend。 - const normalizedGroupId = ctx.group && normalizeGroupId(ctx.group.groupId); - const groupId = normalizedGroupId || ''; - const msg = createMsg(ctx.isPrivate ? 'private' : 'group', target.trigger, groupId); - if (target.at.length) { - const atText = target.at.map(userId => `[CQ:at,qq=${userId}]`).join(' '); - msg.message = `${atText} `; - (msg as any).segment = target.at.map(userId => ({ - target: userId, - isRobot: userId === normalizeCommandUserId(ctx.endPoint.userId), - type: () => 1 - })); - } - if (msg.sender) msg.sender.nickname = ctx.player && ctx.player.name || ''; - const targetCtx = createCtx(ctx.endPoint.userId, msg); - if (!targetCtx) throw new Error(`未找到通信端点: ${ctx.endPoint.userId}`); - return { ctx: targetCtx, msg }; -} +import { createCtx, createMsg } from "../utils/seal"; +import { getRawId, normalizeGroupId, normalizeUserId } from "../utils/target_id"; + +export interface CommandTargetOptions { + /** 显式指定指令消息的触发用户;未填写时由调用方使用当前会话用户。 */ + trigger?: string; + /** 指令消息中的 @ 用户列表。 */ + at: string[]; +} + +/** 将用户标识严格规范化为可注入 OB11/海豹消息的原始用户 ID。 */ +export function normalizeCommandUserId(value: any): string | undefined { + const normalized = normalizeUserId(String(value == null ? '' : value)); + return normalized ? getRawId(normalized) : undefined; +} + +export function currentCommandUserId(ctx: seal.MsgContext): string { + return normalizeCommandUserId(ctx.player && ctx.player.userId) + || normalizeCommandUserId(ctx.endPoint.userId) + || ''; +} + +export function qualifyCommandUserId(ctx: seal.MsgContext, userId: string): string { + const platform = String(ctx.endPoint.userId || '').split(':', 1)[0]; + return platform ? `${platform}:${userId}` : userId; +} + +/** 解析工具参数;不对旧字段做兼容,at 必须是数组。 */ +export function resolveCommandTarget(ctx: seal.MsgContext, args: { [key: string]: any } | undefined): CommandTargetOptions & { effectiveTrigger: string } { + const trigger = normalizeCommandUserId(args && args.trigger); + const atValue = args && args.at; + const at = Array.isArray(atValue) + ? atValue.map((value: any) => normalizeCommandUserId(value)).filter((item: string | undefined): item is string => !!item) + : []; + return { + ...(trigger ? { trigger } : {}), + at, + effectiveTrigger: trigger || currentCommandUserId(ctx) + }; +} + +export function buildCommandContext( + ctx: seal.MsgContext, + target: { trigger: string; at: string[] } +): { ctx: seal.MsgContext; msg: seal.Message } { + // Message.groupId 必须保留 SealDice 的平台前缀(如 QQ-Group:123), + // 否则 OB11 核心在 SendToGroup 时无法命中群会话,也不会触发 onMessageSend。 + const normalizedGroupId = ctx.group && normalizeGroupId(ctx.group.groupId); + const groupId = normalizedGroupId || ''; + const triggerUserId = qualifyCommandUserId(ctx, target.trigger); + const msg = createMsg(ctx.isPrivate ? 'private' : 'group', triggerUserId, groupId); + if (target.at.length) { + const atText = target.at.map(userId => `[CQ:at,qq=${userId}]`).join(' '); + msg.message = `${atText} `; + (msg as any).segment = target.at.map(userId => ({ + target: userId, + isRobot: userId === normalizeCommandUserId(ctx.endPoint.userId), + type: () => 1 + })); + } + // 显式切换触发用户时不能沿用当前会话发送者的昵称;否则本地扩展指令(如 rav) + // 会以当前 AI 对话用户的名字执行,而不是以 target.trigger 对应的玩家执行。 + // 未切换触发者时仍保留原消息昵称,避免普通 @ 模拟丢失当前玩家显示名。 + const currentUserId = currentCommandUserId(ctx); + if (msg.sender && target.trigger === currentUserId) msg.sender.nickname = ctx.player && ctx.player.name || ''; + const targetCtx = createCtx(ctx.endPoint.userId, msg); + if (!targetCtx) throw new Error(`未找到通信端点: ${ctx.endPoint.userId}`); + return { ctx: targetCtx, msg }; +} diff --git a/src/tool/mcp/adapters.ts b/src/tool/mcp/adapters.ts index 5a8296c..e58b407 100644 --- a/src/tool/mcp/adapters.ts +++ b/src/tool/mcp/adapters.ts @@ -4,9 +4,10 @@ import { CoreBridgeResult } from "../../integration/core_bridge/types"; import Logger from "../../logger"; import Image from "../../resource/image"; import { Session } from "../../session/session"; +import { callOb11ApiDirect } from "../../transport/ob11/dispatcher"; import { parseSpecialTokens } from "../../utils/string"; import { getRawId, normalizeGroupId, normalizeUserId } from "../../utils/target_id"; -import { generateId } from "../../utils/utils"; +import { generateId, withTimeout } from "../../utils/utils"; import { isAllowedCore, splitEntry, whitelistEntries } from "../command_catalog"; import { resolveCommandTarget } from "../command_target"; @@ -24,6 +25,30 @@ export interface MCPAdapterContext { type MCPAdapter = (input: MCPAdapterContext) => Promise; +/** 获取核心假消息发送者的真实平台昵称/群名片,避免把当前 AI 会话用户昵称错传给 trigger。 */ +async function resolveCommandActorNickname(ctx: seal.MsgContext, userId: string): Promise { + const rawUserId = normalizeUserId(userId) || userId; + const rawGroupId = ctx.group ? normalizeGroupId(ctx.group.groupId) : ''; + if (!rawUserId) return ''; + try { + const action = rawGroupId ? 'get_group_member_info' : 'get_stranger_info'; + const params = rawGroupId + ? { group_id: rawGroupId.replace(/^.+:/, ''), user_id: rawUserId.replace(/^.+:/, ''), no_cache: true } + : { user_id: rawUserId.replace(/^.+:/, ''), no_cache: true }; + const info = await withTimeout( + () => callOb11ApiDirect(ctx.endPoint.userId, action, params), + 3000 + ); + const name = rawGroupId ? info && (info.card || info.nickname) : info && info.nickname; + if (typeof name === 'string' && name.trim()) return name.trim(); + } catch (e) { + Logger.debug(`[run_core_command] 获取 trigger=${rawUserId} 昵称失败,使用回退值: ${e instanceof Error ? e.message : String(e)}`); + } + return rawUserId === normalizeUserId(ctx.player && ctx.player.userId || '') + ? String(ctx.player && ctx.player.name || '') + : `用户${rawUserId}`; +} + /** 提取 MCP 结果中的文本内容(兼容 text 块、structuredContent) */ export function mcpText(result: MCPCallResult | null | undefined): string { if (!result) return ''; @@ -294,7 +319,7 @@ async function coreBridgeAdapter(input: MCPAdapterContext): Promise { target, actor: { userId: commandTarget.effectiveTrigger || target.selfId, - nickname: String(ctx.player && ctx.player.name || 'AI'), + nickname: await resolveCommandActorNickname(ctx, commandTarget.effectiveTrigger || target.selfId), role: 'member' }, maxMessages: options.capture.maxMessages, diff --git a/src/update.ts b/src/update.ts index 69a29e4..b040b89 100644 --- a/src/update.ts +++ b/src/update.ts @@ -1,260 +1,292 @@ -// 版本更新日志(updateInfo),供启动时展示更新说明 -// 版本更新日志,格式为 "版本号": "更新内容",版本号格式为 "x.y.z",按照时间顺序从新到旧排列。 -export const updateInfo: { [version: string]: string } = { - "4.15.2": `## 修复 -- 修复提示词分层缓存:静态提示词、长期记忆、总结记忆和知识库按会话与版本缓存,记忆写入/删除/总结后主动失效,避免同一轮工具循环重复检索和嵌入 -- 修复上下文 token 预算只裁剪历史消息的问题:现在统一估算 system、示例对话、历史消息和工具定义,优先保留 system/示例并从最早历史开始裁剪 -- 修复上下文消息净化:清理空 tool_calls、孤立 tool 消息和无匹配工具结果,避免上游 API 因非法消息结构返回 400 -- 修复示例对话中的空行导致 user/assistant 角色错位;修复定时器、指令入库、批量改名等异步调用缺少 rejection 兜底 -- 修复图片消息中 Milky file 对象被误当成字符串的问题,并持久化通过 URL 创建的图片资源 - -## 消息接收 -- 核心消息优先:直接使用 SealDice 原生 msg.message/消息事件,保留文本、图片、表情、at、reply、poke 等渲染标签 -- 补充 OB11 依赖消息:仅处理核心未接收的文件、视频、语音、卡片、音乐和合并转发等消息,避免重复入库 -- 完善 Milky 消息段转换:按 msg.segments 类型展开文本、图片、at、reply、face、file、video、record、music、json、消息节点等内容,并将 reply 序号转换为可引用的消息 ID;异常段只占位,不中断整条消息 -- 修复负数消息 ID、未知表情、全体 at、私聊消息和嵌套消息节点进入上下文时的标签与发送者信息 - -## 工具与指令 -- run_ext_command 与 run_core_command 支持 trigger 指定触发对象、at 指定群聊 @ 对象;扩展本地临时构造消息上下文,核心桥注入 OB11 的 user_id/sender.user_id 与 at 段。 -- 移除已由 run_ext_command 覆盖的 draw_deck 独立工具入口,牌堆、模组、今日人品等扩展能力统一通过扩展指令调用。 -## 工具与图片 -- 增加 MCP 总开关与图生图能力:text_to_image 支持图片 ID、头像、URL、data URL 和 base64,适配 aiplugin4-dependencies 新接口 -- 按需工具摘要限制数量并提供参数概览,减少 system prompt 中函数列表占用的 token -- 更新 SealDice 核心与内置扩展命令白名单:默认包含全部命令及别名,并补充对应的 Skills 调用帮助 - -## 配置项更改 -- 移除「提供给AI的牌堆名称」配置项;牌堆权限统一由「可调用指令白名单」控制。升级后无需恢复默认配置。`, - "4.15.1": `## 修复 -- 修复 MCP 服务器配置默认值 mcp-files-exec 的 url 缺少 /mcp 路径的问题:默认值修正为 http://127.0.0.1:3910/mcp,与后端 Streamable HTTP 实际路径一致(旧默认值初始化会 404,面板实测 list_dir / read_file 调用正常) -- 自动改名(昵称/群名片)增加 5 秒超时保护:ob11 网络连接依赖请求异常或未响应时不再让整条消息处理链路挂起,超时记 warning 后继续处理 -## 配置项更改 -- MCP 服务器配置默认值更新:mcp-files-exec 的 url 由 http://127.0.0.1:3910 改为 http://127.0.0.1:3910/mcp -## 重要 -- 若面板上已保存过旧的 mcp-files-exec 地址(http://127.0.0.1:3910),请在「工具 → MCP服务器配置」中改为 http://127.0.0.1:3910/mcp,或直接恢复默认配置`, - "4.15.0": `## 新功能 -- 配置热加载:删除所有「修改后保存并重载 js」提示,配置全部走缓存机制自动生效(缓存最多 1 分钟);MCP 服务器增删、技能配置、音乐服务等修改后无需重载 -- .ai help <一级子指令>:查看对应子命令的帮助(如 .ai help tool) -- 图片转文字结果超长时自动调用压缩智能体压缩后入库,不再截断 -- 允许连续调用函数次数默认调为 10,设为 0 时不限制连续调用次数 -- 移除单次对话 10 轮工具调用的硬编码上限:连续调用次数完全由「允许连续调用函数次数」配置控制(0 为不限制) -- 知识库重构:配置从 TOML 改为 Markdown 模板(每条配置一份完整文档,可直接粘贴 .md 文件,按 # / ## / ### 标题与段落自动分块);新增 kb_search / kb_read / kb_list 工具与 .ai kb 指令,知识库只读,内容由管理员配置维护,AI 按需检索 -- 摘要智能体与 add_memory 工具支持私有记忆:记忆条目可标记为 private(仅生成该记忆的会话可读,默认 public),仅当用户要求记忆只在本会话生效时才传入 visibility 参数 -- 文生图 / 自定义音色语音依赖插件切换到 aiplugin4-dependencies 的新 API(生成图片 tti / 生成音频 tts),替代旧的 AIDrawing / AITTS 插件 -- 用户消息上下文逐条补全发送者与时间:连续多条 user 消息合并后,每条消息独立标注 [from:名字(QQ号)](同一发送者连续发言仅首条带)与 [time:发送时间],不再只有第一条能区分发言者;助手消息同步带 [time] -## 配置项更改 -- prompt 模板全部转内置:删除「prompt模板」页签下的全部配置项,不再可配置 -- 删除「知识库记忆展示数量」;「知识库记忆」更名为「知识库」并改为 Markdown 模板(原 TOML 格式不再支持),新增「知识库注入阈值(字符)」(默认 5000,总内容超过阈值时只注入条目索引,模型用 kb_read 按 ID 读取详情) -- 「是否开启图片模型」「是否开启嵌入模型」开关移入「模型」页签,位于对应模型配置的前面 -- 删除「向量维度」配置:改为从「嵌入模型」配置的 [body] dimensions 读取(默认 1024),未配置时自动降级为关键词/分数检索 -- 「角色设定名称」合入「角色扮演设定」:每条设定第一行为角色设定名称(超过 20 字符自动截断),其余为设定内容 -- 删除「图片转文字最大字符数」配置,改为超长自动压缩 -- 删除「发送图片的概率/%」「偷取图片存储上限」配置及全部偷图功能(.ai img list stl / .ai img steal 相关指令同步移除) -- 删除「默认向量相似度」配置,改为内置硬编码 0.8 -- 「工具响应压缩触发字数」默认值 10000 → 5000 -- 恢复「短期记忆上限」配置(默认 10,超出后从最旧的开始淘汰) -- MCP 服务器配置仅支持 JSON 格式(默认值已改为标准 mcpServers 格式) -- 技能配置仅支持标准 SKILL.md 格式(旧 名称|描述|内容、JSON 格式不再支持) -- 音乐服务配置仅支持 JSON 格式(旧 平台|域名|Cookie 不再支持) -- 「回复最大字数」设为 0 时不限制 -- 修正「接收图片」「接收骰子发送的消息」「禁止回复复读」等配置描述 -- 图片模型默认 max_tokens 由 4096 调整为 2048(已保存过旧默认值的请恢复默认) -- 删除 get_time 工具:当前时间改为直接注入系统提示词(「当前时间:xxxx-xx-xx xx:xx:xx」),无需模型调用工具即可感知时间 -## 重要 -- 本次需要恢复默认的配置项:「角色扮演设定」「MCP服务器配置」「技能配置」「音乐服务配置」「知识库」(原「知识库记忆」为 TOML 格式,需改为 Markdown 模板);自定义过 prompt 模板的用户无需操作(模板已内置) -- 旧版「角色扮演设定」迁移到新格式时,每条设定的第一行会被当作角色名称剥离;若旧设定是多行文本且第一行不是名称,恢复默认前请先手动调整,避免内容丢失 -- 使用了偷图、随机发图功能的需要同步移除对应配置值 -- 完整豹包(AI骰娘4-扩展)新增三个依赖插件:ob11 网络连接依赖、生成音频(tts)、生成图片(tti);使用自定义音色或文生图功能需安装新依赖,旧版 aitts / aidrawing 插件不再使用 -## 文档 -- 新增模型配置参考文档 docs/MODELS-chat.md(对话模型)/ MODELS-image.md(图片模型)/ MODELS-embedding.md(嵌入模型):各平台完整 TOML 示例与兼容性说明,复制粘贴即可接入新服务商 -## 修复 -- 修复知识库 kb_read 首次调用未初始化导致的空结果;资源/语音/牌堆工具描述改为动态读取配置,修改配置后无需重载即可生效 -- 修复搜索 URL 未编码导致中文/空格查询失败;修复 search_chat 好友备注缺失时崩溃;修复流式模式忽略工具 callBack=false 回调标记;全员禁言补充机器人自身管理员权限校验;AI 声聊参数改为对象传参,避免特殊字符截断 -- 修复「允许连续调用函数次数」在重构后未真正累计生效的问题:同一触发流程内的多轮工具调用现在按配置累计(设为 0 时不再限制连续调用次数),每次触发开始时自动清零 -- 修复 kb_search 检索条数无上限的问题:单次返回条数限制在 1~50 之间,防止模型请求超大 topK 造成上下文与 API 浪费 -- 修复私有记忆权限遗漏:del_memory / clear_memory 现在与 search_memory 一致,其他会话创建的私有记忆不可删除或清空(跨会话删除时自动保留并提示) -- 修复摘要智能体总结出的记忆归属:按模型返回的 memory_type / target_id 定位目标用户或群聊会话后写入(与 add_memory 工具一致),不再全部落进当前会话;找不到目标时跳过该条并记录日志 -- 修复 search_memory 私有记忆展示标头:个人记忆不再误标为群聊(显示私聊会话与用户名) -- 修复摘要记忆对模型输出的容错:memories 非数组/条目缺 text 时跳过落库,缺失或未知 memory_type 兜底归属当前会话并计数,不再抛错导致「摘要已存、记忆未落库」的半成功状态 -- 修复知识库分块 ID 跨条目碰撞:相同内容的不同条目通过条目序号区分,ID 保持稳定且全局唯一 -- 修复 MCP 热加载残留:服务器内被删除的工具即时清理;服务器从配置移除后旧工具立即失效并清理,不再短暂可调用 -- 修复指令层记忆删除权限:.ai memo p/g del/clr 与工具层一致,其他会话创建的私有记忆不可删除或清空(跨会话操作时自动保留并提示);del_memory 工具的 id_list 参数类型更正为 string(记忆 ID 为字符串),避免误导模型 -- 修复连续用户消息合并压缩的分隔符表示:合并判断与上下文渲染统一使用真实 \f,避免字面 \\f 导致压缩前后比较误判`, - "4.14.4": `## 修复 -- 防标签注入:用户输入、AI 回复、工具回调、记忆/设定/总结内容统一剥离内部上下文标签([from]/[msg_id]/[system]/[time] 及其旧版 <|...|> 变体),内部标签只允许由代码注入上下文,避免提示词注入与渲染标签泄露 -- 「system prompt模板」默认内容新增「禁止模仿或生成内部标签」说明 -- 修复 milky 消息段 type() 抛异常(消息段结构损坏)会中断整条消息处理的问题:改为捕获异常并按 [未知消息段] 兜底,其余段正常展开 -- 修复不闭合/非法的 [CQ:... 字样被静默吞掉导致用户内容丢失的问题:无法解析的 CQ 段保留为纯文本 -- 修复消息 ID 为负数(NapCat 等实现产生负 int64 ID)时 base36 转换后引用/撤回/精华等操作生成 id=NaN 的问题:仅在可解析为有限数字时才生成引用或调用接口 -## 配置项更改 -- 删除「回复消息过滤正则表达式」「正则处理上下文消息模板」「正则处理回复消息模板」三项配置:回复过滤规则改为内置硬编码(同步支持新版 [xxx] 标签过滤,补全旧版可发送标签白名单),旧的自定义值将被忽略 -## 重要 -- 本次需要恢复默认的配置项:「system prompt模板」`, - "4.14.3": `## 修复 -- 修复 milky 协议纯文本消息被误判为「未知消息段」的问题:goja 反射会把 Go 命名 int 类型(ElementType)包装成 Number 对象而非原始值,导致类型分派时严格相等失败、纯文本被当成 [未知消息段] 占位,上下文看不到真实内容;现在先归一化为数字再做类型分派,文本/at/图片/回复等段按原类型正常展开 -## 说明 -- 无需恢复默认配置`, - "4.14.2": `## 修复 -- 修复上下文存在空 tool_calls 数组时请求被后端拒绝的问题(Invalid 'messages[N].tool_calls': empty array):构建请求时只在非空时附带 tool_calls,会话加载时自动清理历史遗留的空数组,并在流式/非流式请求入口统一兜底净化 -- 修复工具调用过滤时未剔除空 id 的 tool_call、tool 消息缺失 tool_call_id 时仍发送的问题 -## 说明 -- 无需恢复默认配置;长期使用过旧版本的会话会在下次加载时自动清理残留的空 tool_calls 数据`, - "4.14.1": `## 新功能 -- 用户消息上下文补全发送者与消息 ID:每条用户消息带 [from:名字(QQ号)] 前缀与 [msg_id:xxx] 标记,模型可直接用 [quote:xxx] 引用历史消息 -## 修复 -- 修复回复段缺失消息 ID 时生成空 [quote:] 标签、发送引用时出现 id=NaN 的问题(无 ID 时跳过引用) -## 配置项更改 -- 删除「展示号码」配置项:消息来源、发送者、群聊等一律固定展示数字号码,无需再配置 -- 「system prompt模板」默认内容更新:补充 [from]/[msg_id] 标签说明,自定义过该模板的用户请恢复默认 -## 重要 -- 本次需要恢复默认的配置项:「system prompt模板」`, - "4.14.0": `## 新功能 -- 提示词工程模式的函数调用改为代码块格式:模型把调用数组放入以 \`\`\`function 开头、\`\`\` 结尾的代码块中返回,与普通代码块区分;流式/非流式均支持 -- 更新后首次对话时,自动把上下文与记忆中的历史 <|...|> 渲染标签迁移为新格式 [xxx](如 <|img:123|> → [img:123]、<|at:xxx|> → [at:xxx]);迁移完成后解析层不再兼容旧标签,未迁移的旧标签将按普通文本处理 -## 重要 -- 本次需要恢复默认的配置项:「工具函数prompt模板」(函数调用格式由 改为代码块);其他自定义 prompt 模板若仍含旧版 <|...|> 标签,也请恢复默认`, - "4.13.5": `## 新功能 -- 接收卡片/视频/音乐/文件/语音/合并转发消息:订阅 ob11 网络连接依赖事件分发,把核心 milky 原生路径过滤掉的段接入非指令管线(黑名单/忽略/触发/待机全部生效),合并转发自动展开为可读文本,过长走既有压缩 -- 多模态模型支持:图片模型配置 use=chat 后可直接当对话模型用(.ai model 选择),上下文中的图片以 image_url 内容直接传给模型,不再转成文本标签;URL 图片自动转 base64(失败保留原 URL),兼容 anthropic 视觉模型 -- 技能配置兼容标准 SKILL.md:可直接粘贴 Claude/Codex/Cursor 的技能文件,frontmatter 的 name/description 自动解析为技能名与摘要 -- MCP 服务器配置兼容 mcpServers JSON:可直接粘贴 Claude Desktop/Cursor/.mcp.json 的服务器配置,支持自定义 headers 与 token 简写;stdio/SSE 服务器明确提示后跳过 -- web_read / render_markdown / render_html 后端改为 MCP 调用(后端 /mcp 端点) -## 修复 -- 修复 ob11 事件消息数组被 seal.Message.message 字符串绑定强转导致无法展开的问题(改为显式传段数组) -- 修复 get_forward_msg 响应字段读取错误(data.messages → data.message),兼容平铺与 node 两种返回形态,file 段字段兼容 milky/NapCat 差异 -## 文档 -- 新增 docs/10-TODO.md:MCP Streamable HTTP 环境适配后的改动清单 -## 优化 -- 音乐服务配置化:网易云/qq 的域名与 Cookie 移入「工具-音乐服务配置」(平台|域名|Cookie),不再硬编码 -- 新增 OB11 核心指令中转:run_ext_command/run_core_command 通过 MCP Streamable HTTP 调用中间件,由中间件向 SealDice 注入假消息;旧版控制 WS 兼容保留,按群/私聊 lane 串行化并收集多条响应,支持捕获后拦截或继续转发 -- 指令监听升级为多消息收集器:使用空闲窗口、最大消息数与超时结束,降低并发调用时的消息混淆 -- 内置扩展名单改为代码常量;核心扩展名统一为 core,core|ext 无需白名单即可查看所有扩展 -- 模型选取确定性:对话/图片/嵌入模型匹配不再随机抽取,固定取第一个符合用途的模型 -- ob11 网络连接依赖订阅等待窗口延长(10 次×3s → 20 次×5s),兼容慢加载环境 -- 用量统计过期归并修正:先收集过期 key 统一归并后再替换,避免遍历同一对象时增删丢数据;0-0-0 长期桶不再参与归并 -- 消息渲染标签改为方括号格式(如 [at:名称]、[img:图片ID]、[avatar:用户]、[group_avatar:群聊]、[quote:消息ID]、[face:表情]、[audio:音频ID]),提示词模板同步更新;解析层兼容历史 <|...|> 标签(自动归一化),旧模板未更新也能正常收发 -## 修复 -- 修复提示词工程模式下非流式回复未剥离 调用块:发送前先截断标签块,标签原文不再进入发送内容与上下文,调用内容仍按原样记录 -- 修复 web_search 分页计算错误:page 为 0/负数/非法值时按第 1 页处理,前后半页映射修正 -- 清理死代码:删除未使用的 callChat、parseBody、buildRequestMessages、Session.getMessages 等旧实现`, - "4.13.4": `## 新功能 -- 对外 API(globalThis.aiplugin4)新增三种模型直接调用:chatMessages(传入 OpenAI 风格 messages 数组调用对话模型)、imageToText(识图)、embed(文本向量),其他插件可构建消息直接拿回复 -- web_read 工具新增网页截图能力(screenshot=true,支持宽高/整页/延时),截图返回图片可直接发送 -- web_read / render_markdown / render_html 后端改为 MCP(Streamable HTTP)调用,配置默认值指向 /mcp 端点;对应后端 REST 接口已移除 -## 优化 -- seal.d.ts 类型补齐(配置注册 group 参数、async solve),启用 strict 类型检查 -- 移除 globalThis.http 旧依赖兜底,统一使用 ob11 网络连接依赖 -- 新增 AGENT.md 代理协作速查文档 -## 修复 -- 修复工具调用后孤立 tool 消息导致的 API 报错(tool 消息缺少对应 tool_calls)`, - "4.13.3": `## 新功能 -- 新增 run_ext_command/run_core_command 工具:扩展指令区分 builtin/non_builtin,核心白名单使用 core|指令名;新增「指令前缀」配置,默认为 . -- 指令技能(今日人品/COC 模组/属性展示/检定/san检定)统一改为通过 run_ext_command 调用 -- 工具按需加载:search_tools/call_tool/use_skill/run_ext_command/run_core_command/get_time 常驻,其余工具按需搜索并统一执行,system prompt 从约 2 万字符降到约 2800 字符 -## 优化 -- 更新链接改为官方 GitHub Release;商店 README 移除开发章节,只保留用户文档 -## 修复 -- tsconfig 改用 bundler 解析模式,兼容 TypeScript 6+ -- 工具超长返回仅记录日志不发送,并分段打印`, - "4.13.2": `## 新功能 -- 新增「是否开启图片模型」「是否开启嵌入模型」总开关(默认关闭,配置好对应模型后开启;关闭后图片识别、向量记忆的嵌入生成与检索不生效) -## 优化 -- 配置项描述全面增强引导:填写格式、必填/可选参数、取值说明与示例;三种模型(对话/图片/嵌入)的完整 TOML 示例放在默认值编辑框中,带注释可直接修改 -- 配置页签按重要性重排:基础/模型/对话/消息接收/消息触发/回复/工具/记忆/图片/后端/prompt模板/资源 -## 修复 -- 修正待机相关描述:待机为记录消息、不主动发言(会话的计数器/计时器/概率仍可触发) -- 嵌入模型输出维度自动处理,示例不再要求手动填写`, - "4.13.1": `## 新功能 -- 连续多条 user 消息合并后超阈值自动调用压缩智能体压缩,与单条超长消息统一处理 -- 消息压缩阈值默认改为 2000 -## 优化 -- 对话请求默认 max_tokens 4096 -> 8192,避免思考链过长导致输出截断 -## 修复 -- 修复压缩/总结等用途的模型匹配失败时静默失效:getChatModel 会回退到 chat 用途模型`, - "4.13.0": `## 新功能 -- 重构智能体编排:Agent.run/runStream 统一消息构建、工具调用与回复流程 -- 新增子智能体(压缩/摘要)与示例智能体 -- 新增对外 API(globalThis.aiplugin4):其他海豹插件可直接调用智能体(chat/run/getAgent)或注册工具(registerTool) -- 新增 MCP 工具注册与技能(Skills)系统 -- 新增黑名单功能:AI 建议拉黑(suggest_block 带冷却,骰主用 .ai block 确认)、unblock_user/get_block_list 工具;拉黑后不触发对话 -- 黑名单支持「拉黑前需要骰主确认」开关,关闭后 AI 可直接拉黑 -- 分段发送延时(回复配置):流式/非流式输出共用,多段回复从第二条起按配置延时发送,含图消息额外延时,防止乱序 -- 新增非指令正则触发开关(.ai on --r / .ai off --r,排在计数器/计时器之前,默认开启):关闭 AI 后不可用正则触发,.ai on 或 .ai on --r 重新开启 -- 新增模型请求并发限制(基础配置:请求并发上限/请求队列上限):超出并发上限进队列等待,队列满直接丢弃 -- draw_deck 工具新增 send 参数:AI 可选择直接发送抽牌结果,或返回结果由 AI 转述(默认转述,便于结合剧情再加工) -- 适配 anthropic(Claude)提供商:请求/响应格式自动转换(system 拆出、tool_result 合并、响应归一化);流式暂不支持,配置 stream=true 时自动回退非流式 -- 新增论坛工具:帖子列表/详情/搜索、发帖(支持图片自动上传)、评论管理、动态通知、帖子管理;后端配置新增论坛地址/API Token/签名密钥 -- 配套后端服务迁移到独立仓库 aiplugin4-backends:自带 launcher 一键管理(默认不启动任何后端,首次启动自动创建独立 venv 并安装依赖、异常退出自动拉起、可改端口、WebUI 管理界面) - -## 优化 -- 工具回调压缩与上下文 token 窗口 -- 工具回调压缩增强:独立触发阈值「工具响应压缩触发字数」,web_search 压缩附带搜索目标 -- 模型请求统一走 Provider(超时/重试/用量上报) -- 日志优化:级别语义化、密钥脱敏、超长截断、异常堆栈 -- 模型配置示例精简为单条并保留 body 示例 -- .ai tool call 返回内容过长(超过 500 字符)时不再直接发送,仅记录日志并提示 -- 后端默认配置改为本地回环地址+端口(图片转base64/网页读取/用量图表/md和html图片渲染),便于本机自建 - -## 修复 -- 修复模型请求体构造:缺失 model 字段、参数优先级与默认值覆盖问题 -- 修复空上下文消息入库崩溃与异步异常静默 -- 修复记忆检索(向量相似度过滤、知识库渲染) -- 修复工具 callBack 配置不生效:callBack=false 时工具静默执行,结果不回调对话 -- 修复开启「回复引用」时回复含戳戳(poke)导致消息无法显示:含戳戳时不再添加引用 -- 类型检查全面修复(strict 模式全绿)`, - "4.12.1": `- 新增按时间搜索记忆 -- 新增图片头像ID发送 -- 将img命令改为ai子命令 -- 新增render嵌入图片 -- 新增tti保存 -- 修复tool关闭状态发生错误反转的问题 -- 新增合并转发tool`, - "4.12.0": `- 新增通过名称选择角色设定功能 -- 修复获取好友、群聊等列表时的bug -- 修复了调用函数时,无需cmdArgs的函数也会报错的问题 -- 新增了修改上下文里的名字相关功能 -- 活跃时间添加上一条消息时间提示 -- 新增向量记忆 -- 新增知识库 -- 抛弃保存图片,功能合并到记忆 -- 新增渲染md和html功能`, - "4.11.2": `- 增加修复json解析错误的功能`, - "4.11.1": `- 修复了戳戳、权限检查、权限设置、帮助文本等相关问题`, - "4.11.0": `- 新增请求超时相关 -- 修复addMemory时,keywords可以为null的问题 -- 新增表情包制作工具 -- 新增活跃时间 -- 新增展示时间 -- 新增清除上下文标志位$gCLRMSGS,1:清除所有上下文,2:清除assistant和tool上下文,3:清除user上下文 -- 适配了戳一戳事件 -- 重构权限检查 -- 重构timer,增加间隔定时器 -- 为记忆新增images字段,用于存储图片id -- 新增查看精华消息,删除精华消息工具函数`, - "4.10.1": `- 可能修复了非指令无法响应的问题 -- 修复了构建ctx时,isPrivate始终为0的问题 -- 新增保存图片功能 -- 重构了定时任务的执行 -- 新增短期记忆单独控制开启 -- 新增memo status命令 -- 将ai pr命令改为ai status`, - "4.10.0": `- 新增了全局待机模式配置项 -- 修改了部分正则和部分默认配置项 -- 修复了无法调用内置指令 -- 重构过滤正则和消息分割逻辑 -- 提升用户ID和群组ID的兼容性 -- 新增了各种提示词构建模板 -- 重构长期记忆 -- 修改豹语变量$g人工智能插件专用角色设定序号为$gSYSPROMPT,请注意自定义回复的适配 -- 新增短期记忆`, - "4.9.2": `- 新增了各种错误捕获 -- 修复流式输出调用函数出错时无法正常工作 -- 增加去除首尾空白字符配置项 -- 修复提示词调用函数解析出错时无法禁止连续调用 -- 修复findId系列函数对空字符串不返回null -- 重构正则匹配相关代码,新增忽略正则 -- 新增get_msg工具函数`, - "4.9.1": `- 新增了版本校验功能和版本更新日志 -- 调整了默认角色设定 -- 去除冗余的trim函数,改为正则过滤`, - "0.0.0": `test第一!这是一个彩蛋!` -} +// 版本更新日志(updateInfo),供启动时展示更新说明 +// 版本更新日志,格式为 "版本号": "更新内容",版本号格式为 "x.y.z",按照时间顺序从新到旧排列。 +export const updateInfo: { [version: string]: string } = { + "4.16.0": `## 新功能 +- MCP 工具改为标准 Streamable HTTP 即插即用模式:按 mcpServers 配置连接服务器并通过 tools/list 自动发现工具,支持自定义请求头/token;常用文本、图片、网页读取、Markdown/HTML 渲染和核心指令桥接由适配器统一处理。 +- 新增 run_core_command 核心指令桥:通过 OB11 核心桥中间件注入假消息,支持核心命令多消息收集、空闲窗口结束、超时和并发 lane 串行化;MCP 会话失效或未初始化时自动重连。 +- 重构 run_ext_command:扩展指令改为本地直接调用扩展 solve,不再依赖旧的中间件指令转发;支持 trigger 指定模拟发送者、at 指定群聊 @ 用户,并默认使用当前会话用户作为触发者。 +- 指令目标支持真实身份注入:核心桥写入 user_id、sender.user_id、昵称/群名片和 CQ at;触发用户昵称通过 OB11 查询,避免 QQ 号与昵称错配。 +- 新增统一 call_ob11_api 工具和 OB11 能力目录/结果模型;支持网络后端与 SealDice 原生后端按能力自动选择,统一处理文本、图片、语音、视频、文件、卡片、音乐、合并转发、reply、at 等消息段。 +- 新增默认「录卡」技能:读取角色卡表格,优先从简化卡/骰娘导入内容提取可直接执行的 st 命令,使用核心 st 或 pc new 录入并查询确认。 +- SealDice 核心命令、内置扩展命令及别名自动生成默认白名单和 Skills 帮助;核心扩展统一使用 core 标识。 +- 本地资源路径支持文件、语音、视频,省略资源名时自动使用文件名;路径解析同时支持 Windows 与 Unix 分隔符。 + +## 功能调整与删除 +- MCP 工具名称按远端原名注册;同名工具自动跳过,避免服务器名前缀污染 AI 可见工具名和调用参数。 +- web_read、render_markdown、render_html 等旧的内置包装工具移除,统一通过标准 MCP 工具发现/适配;mcp-files-exec 的文件能力统一按后端实际工具暴露。 +- 删除独立 draw_deck 工具入口,牌堆、模组、今日人品等能力统一通过扩展指令调用。 +- 删除旧的内置消息/OB11 操作工具包装,消息发送、群管理、用户查询等协议动作统一收敛到 call_ob11_api。 +- 工具监听改为多消息收集器,按空闲窗口、最大消息数和超时结束;工具摘要按需加载并提供参数概览,减少系统提示词占用。 +- 上下文、记忆、会话和消息流水线统一使用新的目标 ID、OB11 消息段与监听模型,修正多会话隔离、消息发送捕获和上下文发送者信息。 + +## 修复 +- 修复核心/扩展假消息群身份丢失、裸用户 ID 无平台前缀、显式 trigger 仍沿用当前 AI 用户昵称等问题。 +- 修复核心 rav 等指令的 trigger/at 实际注入和多条返回消息收集;扩展指令发送内容不再只记录日志而不返回给 AI。 +- 修复 MCP 工具解析中的尾逗号、标准 MCP 响应块、工具重载清理和会话重连问题;远端工具冲突时不再注册错误工具。 +- 修复 OB11/Milky 文件、视频、语音、卡片、音乐、合并转发、reply、负数消息 ID、未知表情、全体 at、嵌套节点等消息进入上下文时的解析和发送者信息。 +- 修复资源路径、工具返回、记忆/会话隔离及部分核心命令类型定义问题;AI 回复中的字面量 \n / \r\n 发送前还原为真实换行,\f 继续作为多消息分隔符。 + +## 配置变更 +- 新增「是否启用MCP」总开关,默认关闭;MCP 服务器配置统一使用标准 mcpServers JSON,默认示例包含文件执行、网页读取、Markdown/HTML 渲染和 OB11 核心桥服务器。 +- 新增「禁止调用的 OB11 action」「默认关闭的 OB11 action」;普通函数禁用配置与协议 action 权限分开控制。 +- 「可调用指令白名单」改为代码生成的核心/内置扩展命令及别名清单,核心命令使用 core|命令/别名;新增/统一「指令前缀」配置。 +- 删除「内置扩展列表」和「提供给AI的牌堆名称」配置,扩展指令权限统一由可调用指令白名单控制;技能配置默认同步核心命令与 OB11 API 帮助。 +- 网页读取、Markdown/HTML 渲染后端配置迁移到 MCP 服务器配置;本地文件/语音/视频资源配置支持直接填写沙箱外路径并自动生效。 +- 工具转发参数 forward 默认值改为 true;需要静默执行时显式传 false。 + +## 文档与验证 +- 同步 README、工具系统、命令配置、后端项目、OB11 核心桥使用指南和常见问题文档,补充标准 MCP/OB11 配置与迁移说明。 +- 增加 OB11 运行时测试、工具监听测试、核心/扩展指令目标用户端到端测试和发版前构建/冒烟校验。`, + "4.15.2": `## 修复 +- 修复提示词分层缓存:静态提示词、长期记忆、总结记忆和知识库按会话与版本缓存,记忆写入/删除/总结后主动失效,避免同一轮工具循环重复检索和嵌入 +- 修复上下文 token 预算只裁剪历史消息的问题:现在统一估算 system、示例对话、历史消息和工具定义,优先保留 system/示例并从最早历史开始裁剪 +- 修复上下文消息净化:清理空 tool_calls、孤立 tool 消息和无匹配工具结果,避免上游 API 因非法消息结构返回 400 +- 修复示例对话中的空行导致 user/assistant 角色错位;修复定时器、指令入库、批量改名等异步调用缺少 rejection 兜底 +- 修复图片消息中 Milky file 对象被误当成字符串的问题,并持久化通过 URL 创建的图片资源 + +## 消息接收 +- 核心消息优先:直接使用 SealDice 原生 msg.message/消息事件,保留文本、图片、表情、at、reply、poke 等渲染标签 +- 补充 OB11 依赖消息:仅处理核心未接收的文件、视频、语音、卡片、音乐和合并转发等消息,避免重复入库 +- 完善 Milky 消息段转换:按 msg.segments 类型展开文本、图片、at、reply、face、file、video、record、music、json、消息节点等内容,并将 reply 序号转换为可引用的消息 ID;异常段只占位,不中断整条消息 +- 修复负数消息 ID、未知表情、全体 at、私聊消息和嵌套消息节点进入上下文时的标签与发送者信息 + +## 工具与图片 +- 增加 MCP 总开关与图生图能力:text_to_image 支持图片 ID、头像、URL、data URL 和 base64,适配 aiplugin4-dependencies 新接口 +- 按需工具摘要限制数量并提供参数概览,减少 system prompt 中函数列表占用的 token + +## 配置项更改 +- 无新增或删除配置项;本版本主要为消息接收、提示词缓存、工具调用和图片处理的实现修复。升级后无需恢复默认配置。`, + "4.15.1": `## 修复 +- 修复 MCP 服务器配置默认值 mcp-files-exec 的 url 缺少 /mcp 路径的问题:默认值修正为 http://127.0.0.1:3910/mcp,与后端 Streamable HTTP 实际路径一致(旧默认值初始化会 404,面板实测 list_dir / read_file 调用正常) +- 自动改名(昵称/群名片)增加 5 秒超时保护:ob11 网络连接依赖请求异常或未响应时不再让整条消息处理链路挂起,超时记 warning 后继续处理 +## 配置项更改 +- MCP 服务器配置默认值更新:mcp-files-exec 的 url 由 http://127.0.0.1:3910 改为 http://127.0.0.1:3910/mcp +## 重要 +- 若面板上已保存过旧的 mcp-files-exec 地址(http://127.0.0.1:3910),请在「工具 → MCP服务器配置」中改为 http://127.0.0.1:3910/mcp,或直接恢复默认配置`, + "4.15.0": `## 新功能 +- 配置热加载:删除所有「修改后保存并重载 js」提示,配置全部走缓存机制自动生效(缓存最多 1 分钟);MCP 服务器增删、技能配置、音乐服务等修改后无需重载 +- .ai help <一级子指令>:查看对应子命令的帮助(如 .ai help tool) +- 图片转文字结果超长时自动调用压缩智能体压缩后入库,不再截断 +- 允许连续调用函数次数默认调为 10,设为 0 时不限制连续调用次数 +- 移除单次对话 10 轮工具调用的硬编码上限:连续调用次数完全由「允许连续调用函数次数」配置控制(0 为不限制) +- 知识库重构:配置从 TOML 改为 Markdown 模板(每条配置一份完整文档,可直接粘贴 .md 文件,按 # / ## / ### 标题与段落自动分块);新增 kb_search / kb_read / kb_list 工具与 .ai kb 指令,知识库只读,内容由管理员配置维护,AI 按需检索 +- 摘要智能体与 add_memory 工具支持私有记忆:记忆条目可标记为 private(仅生成该记忆的会话可读,默认 public),仅当用户要求记忆只在本会话生效时才传入 visibility 参数 +- 文生图 / 自定义音色语音依赖插件切换到 aiplugin4-dependencies 的新 API(生成图片 tti / 生成音频 tts),替代旧的 AIDrawing / AITTS 插件 +- 用户消息上下文逐条补全发送者与时间:连续多条 user 消息合并后,每条消息独立标注 [from:名字(QQ号)](同一发送者连续发言仅首条带)与 [time:发送时间],不再只有第一条能区分发言者;助手消息同步带 [time] +## 配置项更改 +- prompt 模板全部转内置:删除「prompt模板」页签下的全部配置项,不再可配置 +- 删除「知识库记忆展示数量」;「知识库记忆」更名为「知识库」并改为 Markdown 模板(原 TOML 格式不再支持),新增「知识库注入阈值(字符)」(默认 5000,总内容超过阈值时只注入条目索引,模型用 kb_read 按 ID 读取详情) +- 「是否开启图片模型」「是否开启嵌入模型」开关移入「模型」页签,位于对应模型配置的前面 +- 删除「向量维度」配置:改为从「嵌入模型」配置的 [body] dimensions 读取(默认 1024),未配置时自动降级为关键词/分数检索 +- 「角色设定名称」合入「角色扮演设定」:每条设定第一行为角色设定名称(超过 20 字符自动截断),其余为设定内容 +- 删除「图片转文字最大字符数」配置,改为超长自动压缩 +- 删除「发送图片的概率/%」「偷取图片存储上限」配置及全部偷图功能(.ai img list stl / .ai img steal 相关指令同步移除) +- 删除「默认向量相似度」配置,改为内置硬编码 0.8 +- 「工具响应压缩触发字数」默认值 10000 → 5000 +- 恢复「短期记忆上限」配置(默认 10,超出后从最旧的开始淘汰) +- MCP 服务器配置仅支持 JSON 格式(默认值已改为标准 mcpServers 格式) +- 技能配置仅支持标准 SKILL.md 格式(旧 名称|描述|内容、JSON 格式不再支持) +- 音乐服务配置仅支持 JSON 格式(旧 平台|域名|Cookie 不再支持) +- 「回复最大字数」设为 0 时不限制 +- 修正「接收图片」「接收骰子发送的消息」「禁止回复复读」等配置描述 +- 图片模型默认 max_tokens 由 4096 调整为 2048(已保存过旧默认值的请恢复默认) +- 删除 get_time 工具:当前时间改为直接注入系统提示词(「当前时间:xxxx-xx-xx xx:xx:xx」),无需模型调用工具即可感知时间 +## 重要 +- 本次需要恢复默认的配置项:「角色扮演设定」「MCP服务器配置」「技能配置」「音乐服务配置」「知识库」(原「知识库记忆」为 TOML 格式,需改为 Markdown 模板);自定义过 prompt 模板的用户无需操作(模板已内置) +- 旧版「角色扮演设定」迁移到新格式时,每条设定的第一行会被当作角色名称剥离;若旧设定是多行文本且第一行不是名称,恢复默认前请先手动调整,避免内容丢失 +- 使用了偷图、随机发图功能的需要同步移除对应配置值 +- 完整豹包(AI骰娘4-扩展)新增三个依赖插件:ob11 网络连接依赖、生成音频(tts)、生成图片(tti);使用自定义音色或文生图功能需安装新依赖,旧版 aitts / aidrawing 插件不再使用 +## 文档 +- 新增模型配置参考文档 docs/MODELS-chat.md(对话模型)/ MODELS-image.md(图片模型)/ MODELS-embedding.md(嵌入模型):各平台完整 TOML 示例与兼容性说明,复制粘贴即可接入新服务商 +## 修复 +- 修复知识库 kb_read 首次调用未初始化导致的空结果;资源/语音/牌堆工具描述改为动态读取配置,修改配置后无需重载即可生效 +- 修复搜索 URL 未编码导致中文/空格查询失败;修复 search_chat 好友备注缺失时崩溃;修复流式模式忽略工具 callBack=false 回调标记;全员禁言补充机器人自身管理员权限校验;AI 声聊参数改为对象传参,避免特殊字符截断 +- 修复「允许连续调用函数次数」在重构后未真正累计生效的问题:同一触发流程内的多轮工具调用现在按配置累计(设为 0 时不再限制连续调用次数),每次触发开始时自动清零 +- 修复 kb_search 检索条数无上限的问题:单次返回条数限制在 1~50 之间,防止模型请求超大 topK 造成上下文与 API 浪费 +- 修复私有记忆权限遗漏:del_memory / clear_memory 现在与 search_memory 一致,其他会话创建的私有记忆不可删除或清空(跨会话删除时自动保留并提示) +- 修复摘要智能体总结出的记忆归属:按模型返回的 memory_type / target_id 定位目标用户或群聊会话后写入(与 add_memory 工具一致),不再全部落进当前会话;找不到目标时跳过该条并记录日志 +- 修复 search_memory 私有记忆展示标头:个人记忆不再误标为群聊(显示私聊会话与用户名) +- 修复摘要记忆对模型输出的容错:memories 非数组/条目缺 text 时跳过落库,缺失或未知 memory_type 兜底归属当前会话并计数,不再抛错导致「摘要已存、记忆未落库」的半成功状态 +- 修复知识库分块 ID 跨条目碰撞:相同内容的不同条目通过条目序号区分,ID 保持稳定且全局唯一 +- 修复 MCP 热加载残留:服务器内被删除的工具即时清理;服务器从配置移除后旧工具立即失效并清理,不再短暂可调用 +- 修复指令层记忆删除权限:.ai memo p/g del/clr 与工具层一致,其他会话创建的私有记忆不可删除或清空(跨会话操作时自动保留并提示);del_memory 工具的 id_list 参数类型更正为 string(记忆 ID 为字符串),避免误导模型 +- 修复连续用户消息合并压缩的分隔符表示:合并判断与上下文渲染统一使用真实 \f,避免字面 \\f 导致压缩前后比较误判`, + "4.14.4": `## 修复 +- 防标签注入:用户输入、AI 回复、工具回调、记忆/设定/总结内容统一剥离内部上下文标签([from]/[msg_id]/[system]/[time] 及其旧版 <|...|> 变体),内部标签只允许由代码注入上下文,避免提示词注入与渲染标签泄露 +- 「system prompt模板」默认内容新增「禁止模仿或生成内部标签」说明 +- 修复 milky 消息段 type() 抛异常(消息段结构损坏)会中断整条消息处理的问题:改为捕获异常并按 [未知消息段] 兜底,其余段正常展开 +- 修复不闭合/非法的 [CQ:... 字样被静默吞掉导致用户内容丢失的问题:无法解析的 CQ 段保留为纯文本 +- 修复消息 ID 为负数(NapCat 等实现产生负 int64 ID)时 base36 转换后引用/撤回/精华等操作生成 id=NaN 的问题:仅在可解析为有限数字时才生成引用或调用接口 +## 配置项更改 +- 删除「回复消息过滤正则表达式」「正则处理上下文消息模板」「正则处理回复消息模板」三项配置:回复过滤规则改为内置硬编码(同步支持新版 [xxx] 标签过滤,补全旧版可发送标签白名单),旧的自定义值将被忽略 +## 重要 +- 本次需要恢复默认的配置项:「system prompt模板」`, + "4.14.3": `## 修复 +- 修复 milky 协议纯文本消息被误判为「未知消息段」的问题:goja 反射会把 Go 命名 int 类型(ElementType)包装成 Number 对象而非原始值,导致类型分派时严格相等失败、纯文本被当成 [未知消息段] 占位,上下文看不到真实内容;现在先归一化为数字再做类型分派,文本/at/图片/回复等段按原类型正常展开 +## 说明 +- 无需恢复默认配置`, + "4.14.2": `## 修复 +- 修复上下文存在空 tool_calls 数组时请求被后端拒绝的问题(Invalid 'messages[N].tool_calls': empty array):构建请求时只在非空时附带 tool_calls,会话加载时自动清理历史遗留的空数组,并在流式/非流式请求入口统一兜底净化 +- 修复工具调用过滤时未剔除空 id 的 tool_call、tool 消息缺失 tool_call_id 时仍发送的问题 +## 说明 +- 无需恢复默认配置;长期使用过旧版本的会话会在下次加载时自动清理残留的空 tool_calls 数据`, + "4.14.1": `## 新功能 +- 用户消息上下文补全发送者与消息 ID:每条用户消息带 [from:名字(QQ号)] 前缀与 [msg_id:xxx] 标记,模型可直接用 [quote:xxx] 引用历史消息 +## 修复 +- 修复回复段缺失消息 ID 时生成空 [quote:] 标签、发送引用时出现 id=NaN 的问题(无 ID 时跳过引用) +## 配置项更改 +- 删除「展示号码」配置项:消息来源、发送者、群聊等一律固定展示数字号码,无需再配置 +- 「system prompt模板」默认内容更新:补充 [from]/[msg_id] 标签说明,自定义过该模板的用户请恢复默认 +## 重要 +- 本次需要恢复默认的配置项:「system prompt模板」`, + "4.14.0": `## 新功能 +- 提示词工程模式的函数调用改为代码块格式:模型把调用数组放入以 \`\`\`function 开头、\`\`\` 结尾的代码块中返回,与普通代码块区分;流式/非流式均支持 +- 更新后首次对话时,自动把上下文与记忆中的历史 <|...|> 渲染标签迁移为新格式 [xxx](如 <|img:123|> → [img:123]、<|at:xxx|> → [at:xxx]);迁移完成后解析层不再兼容旧标签,未迁移的旧标签将按普通文本处理 +## 重要 +- 本次需要恢复默认的配置项:「工具函数prompt模板」(函数调用格式由 改为代码块);其他自定义 prompt 模板若仍含旧版 <|...|> 标签,也请恢复默认`, + "4.13.5": `## 新功能 +- 接收卡片/视频/音乐/文件/语音/合并转发消息:订阅 ob11 网络连接依赖事件分发,把核心 milky 原生路径过滤掉的段接入非指令管线(黑名单/忽略/触发/待机全部生效),合并转发自动展开为可读文本,过长走既有压缩 +- 多模态模型支持:图片模型配置 use=chat 后可直接当对话模型用(.ai model 选择),上下文中的图片以 image_url 内容直接传给模型,不再转成文本标签;URL 图片自动转 base64(失败保留原 URL),兼容 anthropic 视觉模型 +- 技能配置兼容标准 SKILL.md:可直接粘贴 Claude/Codex/Cursor 的技能文件,frontmatter 的 name/description 自动解析为技能名与摘要 +- MCP 服务器配置兼容 mcpServers JSON:可直接粘贴 Claude Desktop/Cursor/.mcp.json 的服务器配置,支持自定义 headers 与 token 简写;stdio/SSE 服务器明确提示后跳过 +- web_read / render_markdown / render_html 后端改为 MCP 调用(后端 /mcp 端点) +## 修复 +- 修复 ob11 事件消息数组被 seal.Message.message 字符串绑定强转导致无法展开的问题(改为显式传段数组) +- 修复 get_forward_msg 响应字段读取错误(data.messages → data.message),兼容平铺与 node 两种返回形态,file 段字段兼容 milky/NapCat 差异 +## 文档 +- 新增 docs/10-TODO.md:MCP Streamable HTTP 环境适配后的改动清单 +## 优化 +- 音乐服务配置化:网易云/qq 的域名与 Cookie 移入「工具-音乐服务配置」(平台|域名|Cookie),不再硬编码 +- 新增 OB11 核心指令中转:run_ext_command/run_core_command 通过 MCP Streamable HTTP 调用中间件,由中间件向 SealDice 注入假消息;旧版控制 WS 兼容保留,按群/私聊 lane 串行化并收集多条响应,支持捕获后拦截或继续转发 +- 指令监听升级为多消息收集器:使用空闲窗口、最大消息数与超时结束,降低并发调用时的消息混淆 +- 内置扩展名单改为代码常量;核心扩展名统一为 core,core|ext 无需白名单即可查看所有扩展 +- 模型选取确定性:对话/图片/嵌入模型匹配不再随机抽取,固定取第一个符合用途的模型 +- ob11 网络连接依赖订阅等待窗口延长(10 次×3s → 20 次×5s),兼容慢加载环境 +- 用量统计过期归并修正:先收集过期 key 统一归并后再替换,避免遍历同一对象时增删丢数据;0-0-0 长期桶不再参与归并 +- 消息渲染标签改为方括号格式(如 [at:名称]、[img:图片ID]、[avatar:用户]、[group_avatar:群聊]、[quote:消息ID]、[face:表情]、[audio:音频ID]),提示词模板同步更新;解析层兼容历史 <|...|> 标签(自动归一化),旧模板未更新也能正常收发 +## 修复 +- 修复提示词工程模式下非流式回复未剥离 调用块:发送前先截断标签块,标签原文不再进入发送内容与上下文,调用内容仍按原样记录 +- 修复 web_search 分页计算错误:page 为 0/负数/非法值时按第 1 页处理,前后半页映射修正 +- 清理死代码:删除未使用的 callChat、parseBody、buildRequestMessages、Session.getMessages 等旧实现`, + "4.13.4": `## 新功能 +- 对外 API(globalThis.aiplugin4)新增三种模型直接调用:chatMessages(传入 OpenAI 风格 messages 数组调用对话模型)、imageToText(识图)、embed(文本向量),其他插件可构建消息直接拿回复 +- web_read 工具新增网页截图能力(screenshot=true,支持宽高/整页/延时),截图返回图片可直接发送 +- web_read / render_markdown / render_html 后端改为 MCP(Streamable HTTP)调用,配置默认值指向 /mcp 端点;对应后端 REST 接口已移除 +## 优化 +- seal.d.ts 类型补齐(配置注册 group 参数、async solve),启用 strict 类型检查 +- 移除 globalThis.http 旧依赖兜底,统一使用 ob11 网络连接依赖 +- 新增 AGENT.md 代理协作速查文档 +## 修复 +- 修复工具调用后孤立 tool 消息导致的 API 报错(tool 消息缺少对应 tool_calls)`, + "4.13.3": `## 新功能 +- 新增 run_ext_command/run_core_command 工具:扩展指令区分 builtin/non_builtin,核心白名单使用 core|指令名;新增「指令前缀」配置,默认为 . +- 指令技能(今日人品/COC 模组/属性展示/检定/san检定)统一改为通过 run_ext_command 调用 +- 工具按需加载:search_tools/call_tool/use_skill/run_ext_command/run_core_command/get_time 常驻,其余工具按需搜索并统一执行,system prompt 从约 2 万字符降到约 2800 字符 +## 优化 +- 更新链接改为官方 GitHub Release;商店 README 移除开发章节,只保留用户文档 +## 修复 +- tsconfig 改用 bundler 解析模式,兼容 TypeScript 6+ +- 工具超长返回仅记录日志不发送,并分段打印`, + "4.13.2": `## 新功能 +- 新增「是否开启图片模型」「是否开启嵌入模型」总开关(默认关闭,配置好对应模型后开启;关闭后图片识别、向量记忆的嵌入生成与检索不生效) +## 优化 +- 配置项描述全面增强引导:填写格式、必填/可选参数、取值说明与示例;三种模型(对话/图片/嵌入)的完整 TOML 示例放在默认值编辑框中,带注释可直接修改 +- 配置页签按重要性重排:基础/模型/对话/消息接收/消息触发/回复/工具/记忆/图片/后端/prompt模板/资源 +## 修复 +- 修正待机相关描述:待机为记录消息、不主动发言(会话的计数器/计时器/概率仍可触发) +- 嵌入模型输出维度自动处理,示例不再要求手动填写`, + "4.13.1": `## 新功能 +- 连续多条 user 消息合并后超阈值自动调用压缩智能体压缩,与单条超长消息统一处理 +- 消息压缩阈值默认改为 2000 +## 优化 +- 对话请求默认 max_tokens 4096 -> 8192,避免思考链过长导致输出截断 +## 修复 +- 修复压缩/总结等用途的模型匹配失败时静默失效:getChatModel 会回退到 chat 用途模型`, + "4.13.0": `## 新功能 +- 重构智能体编排:Agent.run/runStream 统一消息构建、工具调用与回复流程 +- 新增子智能体(压缩/摘要)与示例智能体 +- 新增对外 API(globalThis.aiplugin4):其他海豹插件可直接调用智能体(chat/run/getAgent)或注册工具(registerTool) +- 新增 MCP 工具注册与技能(Skills)系统 +- 新增黑名单功能:AI 建议拉黑(suggest_block 带冷却,骰主用 .ai block 确认)、unblock_user/get_block_list 工具;拉黑后不触发对话 +- 黑名单支持「拉黑前需要骰主确认」开关,关闭后 AI 可直接拉黑 +- 分段发送延时(回复配置):流式/非流式输出共用,多段回复从第二条起按配置延时发送,含图消息额外延时,防止乱序 +- 新增非指令正则触发开关(.ai on --r / .ai off --r,排在计数器/计时器之前,默认开启):关闭 AI 后不可用正则触发,.ai on 或 .ai on --r 重新开启 +- 新增模型请求并发限制(基础配置:请求并发上限/请求队列上限):超出并发上限进队列等待,队列满直接丢弃 +- draw_deck 工具新增 send 参数:AI 可选择直接发送抽牌结果,或返回结果由 AI 转述(默认转述,便于结合剧情再加工) +- 适配 anthropic(Claude)提供商:请求/响应格式自动转换(system 拆出、tool_result 合并、响应归一化);流式暂不支持,配置 stream=true 时自动回退非流式 +- 新增论坛工具:帖子列表/详情/搜索、发帖(支持图片自动上传)、评论管理、动态通知、帖子管理;后端配置新增论坛地址/API Token/签名密钥 +- 配套后端服务迁移到独立仓库 aiplugin4-backends:自带 launcher 一键管理(默认不启动任何后端,首次启动自动创建独立 venv 并安装依赖、异常退出自动拉起、可改端口、WebUI 管理界面) + +## 优化 +- 工具回调压缩与上下文 token 窗口 +- 工具回调压缩增强:独立触发阈值「工具响应压缩触发字数」,web_search 压缩附带搜索目标 +- 模型请求统一走 Provider(超时/重试/用量上报) +- 日志优化:级别语义化、密钥脱敏、超长截断、异常堆栈 +- 模型配置示例精简为单条并保留 body 示例 +- .ai tool call 返回内容过长(超过 500 字符)时不再直接发送,仅记录日志并提示 +- 后端默认配置改为本地回环地址+端口(图片转base64/网页读取/用量图表/md和html图片渲染),便于本机自建 + +## 修复 +- 修复模型请求体构造:缺失 model 字段、参数优先级与默认值覆盖问题 +- 修复空上下文消息入库崩溃与异步异常静默 +- 修复记忆检索(向量相似度过滤、知识库渲染) +- 修复工具 callBack 配置不生效:callBack=false 时工具静默执行,结果不回调对话 +- 修复开启「回复引用」时回复含戳戳(poke)导致消息无法显示:含戳戳时不再添加引用 +- 类型检查全面修复(strict 模式全绿)`, + "4.12.1": `- 新增按时间搜索记忆 +- 新增图片头像ID发送 +- 将img命令改为ai子命令 +- 新增render嵌入图片 +- 新增tti保存 +- 修复tool关闭状态发生错误反转的问题 +- 新增合并转发tool`, + "4.12.0": `- 新增通过名称选择角色设定功能 +- 修复获取好友、群聊等列表时的bug +- 修复了调用函数时,无需cmdArgs的函数也会报错的问题 +- 新增了修改上下文里的名字相关功能 +- 活跃时间添加上一条消息时间提示 +- 新增向量记忆 +- 新增知识库 +- 抛弃保存图片,功能合并到记忆 +- 新增渲染md和html功能`, + "4.11.2": `- 增加修复json解析错误的功能`, + "4.11.1": `- 修复了戳戳、权限检查、权限设置、帮助文本等相关问题`, + "4.11.0": `- 新增请求超时相关 +- 修复addMemory时,keywords可以为null的问题 +- 新增表情包制作工具 +- 新增活跃时间 +- 新增展示时间 +- 新增清除上下文标志位$gCLRMSGS,1:清除所有上下文,2:清除assistant和tool上下文,3:清除user上下文 +- 适配了戳一戳事件 +- 重构权限检查 +- 重构timer,增加间隔定时器 +- 为记忆新增images字段,用于存储图片id +- 新增查看精华消息,删除精华消息工具函数`, + "4.10.1": `- 可能修复了非指令无法响应的问题 +- 修复了构建ctx时,isPrivate始终为0的问题 +- 新增保存图片功能 +- 重构了定时任务的执行 +- 新增短期记忆单独控制开启 +- 新增memo status命令 +- 将ai pr命令改为ai status`, + "4.10.0": `- 新增了全局待机模式配置项 +- 修改了部分正则和部分默认配置项 +- 修复了无法调用内置指令 +- 重构过滤正则和消息分割逻辑 +- 提升用户ID和群组ID的兼容性 +- 新增了各种提示词构建模板 +- 重构长期记忆 +- 修改豹语变量$g人工智能插件专用角色设定序号为$gSYSPROMPT,请注意自定义回复的适配 +- 新增短期记忆`, + "4.9.2": `- 新增了各种错误捕获 +- 修复流式输出调用函数出错时无法正常工作 +- 增加去除首尾空白字符配置项 +- 修复提示词调用函数解析出错时无法禁止连续调用 +- 修复findId系列函数对空字符串不返回null +- 重构正则匹配相关代码,新增忽略正则 +- 新增get_msg工具函数`, + "4.9.1": `- 新增了版本校验功能和版本更新日志 +- 调整了默认角色设定 +- 去除冗余的trim函数,改为正则过滤`, + "0.0.0": `test第一!这是一个彩蛋!` +} diff --git a/src/utils/string.ts b/src/utils/string.ts index d934fbb..86fc899 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -1,924 +1,937 @@ -// 字符串工具:消息段转换/回复拆分过滤/复读检测等 -import Handlebars from "handlebars"; - -import Config from "../config/config"; -import { FACE_MAP } from "../config/static_config"; -import { Context } from "../context/context"; -import { logger } from "../logger"; -import Image from "../resource/image"; - -import { getRawId, normalizeGroupId, normalizeUserId } from "./target_id"; -import { getMilkyReplyQuoteId, resolveLocalPath, transformMsgId, transformMsgIdBack } from "./utils"; - -export function truncateText(text: string, maxLength: number): string { - if (!text || maxLength <= 0 || text.length <= maxLength) return text || ''; - return text.slice(0, maxLength) + '...'; -} - - -/* 先丢这一坨东西在这。之所以不用是因为被类型检查整烦了 - -export interface MessageSegmentText { - type: 'text'; - data: { - text: string; - }; -} - -export interface MessageSegmentAt { - type: 'at'; - data: { - qq: string; - }; -} - -export interface MessageSegmentImage { - type: 'image'; - data: { - file: string; - url?: string; - }; -} - -export interface MessageSegmentFace { - type: 'face'; - data: { - id: string; - }; -} - -export interface MessageSegmentJson { - type: 'json'; - data: { - data: string; - }; -} - -export interface MessageSegmentRecord { - type: 'record'; - data: { - file: string; - }; -} - -export interface MessageSegmentVideo { - type: 'video'; - data: { - file: string; - }; -} - -export interface MessageSegmentReply { - type: 'reply'; - data: { - id: string; - }; -} - -export interface MessageSegmentMusic { - type: 'music'; - data: { - type: 'qq' | '163'; - id: string; - } | { - type: 'custom'; - url: string; - audio: string; - title: string; - image: string; - }; -} - -export interface MessageSegmentDice { - type: 'dice'; -} - -export interface MessageSegmentRps { - type: 'rps'; -} - -export interface MessageSegmentFile { - type: 'file'; - data: { - file: string; - }; -} - -export interface MessageSegmentNode { // 这是干嘛的?是合并转发吗? - type: 'node'; - data: { - user_id: string; - nickname: string; - content: (MessageSegmentText | MessageSegmentAt | MessageSegmentImage | MessageSegmentFace | MessageSegmentJson | MessageSegmentRecord | MessageSegmentVideo | MessageSegmentReply | MessageSegmentMusic | MessageSegmentDice | MessageSegmentRps | MessageSegmentFile)[]; - }; -} - -export type MessageSegment = MessageSegmentText | MessageSegmentAt | MessageSegmentImage | MessageSegmentFace | MessageSegmentJson | MessageSegmentRecord | MessageSegmentVideo | MessageSegmentReply | MessageSegmentMusic | MessageSegmentDice | MessageSegmentRps | MessageSegmentFile | MessageSegmentNode; -*/ - -export interface MessageSegment { - type: string; - data: { - [key: string]: string - }; -} - -/** - * 文件消息的完整可读表示。不要只显示 name:OB11/不同适配器可能把真实路径、URL、 - * file_id、file_unique 等字段放在同一段里,AI 需要这些字段才能继续调用文件工具。 - */ -export function formatFileSegmentText(rawData: any): string { - const data = rawData && typeof rawData === 'object' ? rawData : {}; - const nestedFile = data.file && typeof data.file === 'object' ? data.file : {}; - const pick = (...values: any[]): string => { - for (const value of values) { - if (value === undefined || value === null) continue; - const text = String(value).replace(/[\r\n]+/g, ' ').trim(); - if (text) return text; - } - return ''; - }; - const name = pick(data.name, data.file_name, data.filename, nestedFile.name, nestedFile.file_name); - const path = pick(data.path, data.local_path, nestedFile.path, nestedFile.local_path); - const file = pick(typeof data.file === 'string' ? data.file : '', nestedFile.file); - const url = pick(data.url, data.file_url, data.download_url, nestedFile.url, nestedFile.file_url); - const fileId = pick(data.file_id, data.fileId, nestedFile.file_id, nestedFile.fileId); - const fileUnique = pick(data.file_unique, data.fileUnique, nestedFile.file_unique, nestedFile.fileUnique); - const size = pick(data.size, data.file_size, nestedFile.size, nestedFile.file_size); - const mime = pick(data.content_type, data.contentType, data.mime, nestedFile.content_type, nestedFile.contentType); - const label = name || path || file || url || fileId || '未知文件'; - const fields = [ - ['name', name], ['path', path], ['file', file], ['url', url], - ['file_id', fileId], ['file_unique', fileUnique], ['size', size], ['mime', mime] - ].filter(([, value]) => value).map(([key, value]) => `${key}=${value}`); - return `【文件】${label}${fields.length ? `\n${fields.join('\n')}` : ''}`; -} - -/** 为忽略/触发正则生成兼容 CQ 码的匹配文本,不改变消息段本身。 */ -export function formatMessageSegmentsForMatching(messageArray: MessageSegment[], fallback: string = ''): string { - const text = messageArray.map(item => { - if (item.type === 'text') return (item.data && item.data.text) || ''; - if (item.type === 'at') { - const qq = item.data && (item.data.qq || item.data.user_id); - return qq ? `[CQ:at,qq=${qq}]` : '[at]'; - } - return `[${item.type}]`; - }).join(''); - return text || fallback; -} - -/** - * 把海豹 milky 消息段(seal.MessageSegment,独立 Go 结构体 + type() 编号)直接映射为 - * 项目内部统一段格式。不经过 CQ 码:milky 下 msg.message 只有纯文本拼接, - * at/图片/回复等富文本信息必须从 segment 取。 - */ -export function expandMilkySegments(ctx: seal.MsgContext, segments: seal.MessageSegment[]): MessageSegment[] { - const result: MessageSegment[] = []; - for (const seg of segments) { - if (!seg || typeof seg !== 'object' || typeof seg.type !== 'function') continue; - // goja 反射会把 Go 命名 int(ElementType)包装成 Number 对象而非原始值, - // 导致严格相等 0 === seg.type() 不成立(字符串化却显示 "0"),必须先归一化为数字; - // type() 异常(如消息段结构损坏)时按未知段兜底,不中断整条消息处理 - let typeValue: number; - try { - typeValue = Number(seg.type()); - } catch (_e) { - logger.warning('milky 消息段 type() 解析异常,按未知段处理'); - result.push({ type: 'text', data: { text: '[未知消息段]' } }); - continue; - } - switch (typeValue) { - case 0: { // 文本 Text - const content = 'content' in seg ? seg.content : ''; - result.push({ type: 'text', data: { text: content === undefined || content === null ? '' : String(content) } }); - break; - } - case 1: { // 艾特 At - const target = 'target' in seg ? seg.target : ''; - result.push({ type: 'at', data: { qq: target === undefined || target === null ? '' : String(target) } }); - break; - } - case 2: { // 文件 File - result.push({ type: 'text', data: { text: formatFileSegmentText(seg) } }); - break; - } - case 3: { // 图片 Image - const url = 'url' in seg ? seg.url : ''; - const file = 'file' in seg - ? (typeof seg.file === 'string' - ? seg.file - : seg.file && typeof seg.file === 'object' - ? ((seg.file as any).url || (seg.file as any).file || '') - : '') - : ''; - result.push({ type: 'image', data: { url: url || file || '' } }); - break; - } - case 4: { // 文字转语音 TTS - const content = 'content' in seg ? seg.content : ''; - result.push({ type: 'text', data: { text: content === undefined || content === null ? '' : String(content) } }); - break; - } - case 5: { // 回复 Reply - const replySeq = 'replySeq' in seg ? seg.replySeq : ''; - result.push({ type: 'reply', data: { id: getMilkyReplyQuoteId(ctx, replySeq) } }); - break; - } - case 6: { // 语音 Record - result.push({ type: 'text', data: { text: '【语音】' } }); - break; - } - case 7: { // 表情 Face - const faceID = 'faceID' in seg ? seg.faceID : ''; - result.push({ type: 'face', data: { id: faceID === undefined || faceID === null ? '' : String(faceID) } }); - break; - } - case 8: { // 戳一戳 Poke - const target = 'target' in seg ? seg.target : ''; - result.push({ type: 'poke', data: { qq: target === undefined || target === null ? '' : String(target) } }); - break; - } - default: { - logger.debug(`milky 未知消息段类型: ${typeValue},按文本占位处理`); - result.push({ type: 'text', data: { text: '[未知消息段]' } }); - } - } - } - return result; -} - -/** 解析 QQ 卡片消息(CQ:json / OB11 json 段的 data 字段),提取标题/描述/链接等可读文本 */ -export function parseCardToText(raw: any): string { - if (!raw) return '[卡片消息]'; - - let obj: any = null; - try { - obj = typeof raw === 'string' ? JSON.parse(raw) : raw; - } catch (_e) { - return `[卡片消息] ${String(raw).slice(0, 200)}`; - } - - const str = (v: any): string => (typeof v === 'string' && v.trim()) ? v.trim() : ''; - - // 常见卡片结构:view / meta.news / meta.detail 等 - const view = (obj && (obj.view || (obj.meta && (obj.meta.news || obj.meta.detail || obj.meta.article)))) || obj || {}; - const title = str(view.title) || str(obj.desc) || str(view.desc) || ''; - const desc = str(view.desc) || str(view.summary) || (view.news && str(view.news.desc)) || ''; - const url = str(view.url) || str(view.jumpUrl) || (view.news && str(view.news.jumpUrl)) || (view.detail && str(view.detail.jumpUrl)) || ''; - - const parts = [title, desc].filter(Boolean); - if (parts.length === 0) return '[卡片消息]'; - return `【卡片】${parts.join('\n')}${url ? `\n${url}` : ''}`; -} - -/** 音乐段转可读文本(data 为 qq/163 id 或 custom 对象) */ -export function parseMusicToText(data: any): string { - if (data && data.title) return `【音乐】${data.title}`; - const type = data && data.type ? String(data.type) : ''; - const id = data && data.id ? String(data.id) : ''; - return `【音乐】${type}${id ? ` ${id}` : ''}`; -} - -export function transformTextToArray(text: string): MessageSegment[] { - const segments = text.split(/(\[CQ:.*?\])/).filter(segment => segment); - const messageArray: MessageSegment[] = []; - for (const segment of segments) { - if (segment.startsWith('[CQ:')) { - const match = segment.match(/^\[CQ:([^,]+),?([^\]]*)\]$/); - if (match) { - const type = match[1].trim(); - const params: { [key: string]: string } = {}; - if (match[2]) { - match[2].trim().split(',').forEach(param => { - const eqIndex = param.indexOf('='); - if (eqIndex === -1) return; - - const key = param.slice(0, eqIndex).trim(); - const value = param.slice(eqIndex + 1).trim(); - - if (type === 'image' && key === 'file') params['url'] = value; // 这对吗?nc是这样的吗? - if (key) params[key] = value; - }); - } - - messageArray.push({ type, data: params }); - } else { - // 无法解析的 [CQ:... 字样保留为纯文本,避免用户内容被静默吞掉 - logger.error(`无法解析CQ码,保留为文本:${segment}`); - messageArray.push({ type: 'text', data: { text: segment } }); - } - } else { - messageArray.push({ type: 'text', data: { text: segment } }); - } - } - return messageArray; -} - -export function transformArrayToText(messageArray: { type: string, data: { [key: string]: string } }[]): string { - let text = ''; - for (const message of messageArray) { - if (message.type === 'text') { - text += message.data['text']; - } else { - if (message.type === 'image') { - if (message.data['url']) { - text += `[CQ:image,file=${message.data['url']}]`; - } else if (message.data['file']) { - text += `[CQ:image,file=${message.data['file']}]`; - } - } else { - text += `[CQ:${message.type}`; - for (const key in message.data) { - if (typeof message.data[key] === 'string') { - text += `,${key}=${message.data[key]}`; - } - } - text += ']'; - } - } - } - return text; -} - -export async function transformArrayToContent(ctx: seal.MsgContext, messageArray: MessageSegment[]): Promise<{ content: string, images: Image[] }> { - let content = ''; - const images: Image[] = []; - for (const seg of messageArray) { - switch (seg.type) { - case 'text': { - // 防注入:用户输入中的内部上下文标签(from/msg_id/system/time)直接剥离,不进入上下文 - content += stripInternalTags(seg.data.text); - break; - } - case 'at': { - if (seg.data.qq === 'all') { - content += '[at:all]'; - break; - } - const userId = normalizeUserId(seg.data.qq || ''); - content += `[at:${userId ? getRawId(userId) : String(seg.data.qq || '')}]`; - break; - } - case 'poke': { - const userId = normalizeUserId(seg.data.qq || ''); - content += `[poke:${userId ? getRawId(userId) : String(seg.data.qq || '')}]`; - break; - } - case 'reply': { - const quoteId = transformMsgId(seg.data.id || ''); - if (quoteId) content += `[quote:${quoteId}]`; - break; - } - case 'image': { - const result = await Image.handleImageMessageSegment(ctx, seg); - content += result.content; - images.push(...result.images); - break; - } - case 'face': { - const faceName = FACE_MAP[String(seg.data.id)] || ''; - // 未知 face id 也保留占位,避免用户表情静默丢失;名称作为可见文案的一部分 - content += `[face:${faceName || `未知表情${String(seg.data.id)}`}]`; - break; - } - } - } - return { content, images }; -} - -/** - * 转换文本内容中的特殊标签为CQ码 - * @param ctx 消息上下文 - * @param ai AI实例 - * @param content 文本内容 - * @returns 包含处理后的结果和图片列表的对象 - */ -async function transformContentToText(ctx: seal.MsgContext, session: { context: Context }, content: string): Promise<{ text: string, images: Image[] }> { - const segs = parseSpecialTokens(content); - let text = ''; - const images: Image[] = []; - for (const seg of segs) { - switch (seg.type) { - case 'text': { - text += seg.content; - break; - } - case 'at': { - if (seg.content.trim().toLowerCase() === 'all') { - text += '[CQ:at,qq=all]'; - break; - } - const userId = normalizeUserId(seg.content); - if (userId) text += `[CQ:at,qq=${getRawId(userId)}]`; - else logger.warning(`用户ID格式无效:${seg.content}`); - break; - } - case 'poke': { - const userId = normalizeUserId(seg.content); - if (userId) text += `[CQ:poke,qq=${getRawId(userId)}]`; - else logger.warning(`用户ID格式无效:${seg.content}`); - break; - } - case 'quote': { - const msgId = seg.content; - if (msgId) { - const backId = transformMsgIdBack(msgId); - // msgid 可能为负数(base36 保留符号),仅当可解析时才生成引用,避免 id=NaN - if (Number.isFinite(backId)) text += `[CQ:reply,id=${backId}]`; - } - break; - } - case 'img': { - const id = seg.content; - // 兼容 [img:imageId:描述]:描述部分可能带冒号,整体找不到时取首个冒号前作为图片 id - const image = await session.context.findImage(ctx, id) || (id.includes(':') ? await session.context.findImage(ctx, id.split(':')[0]) : null); - - if (image) { - images.push(image); - text += image.CQCode; - } else { - logger.warning(`无法找到图片:${id}`); - } - break; - } - case 'avatar': { - const userId = normalizeUserId(seg.content); - if (userId) { - const image = Image.getUserAvatar(userId); - images.push(image); - text += image.CQCode; - } else logger.warning(`用户ID格式无效:${seg.content}`); - break; - } - case 'group_avatar': { - const groupId = normalizeGroupId(seg.content); - if (groupId) { - const image = Image.getGroupAvatar(groupId); - images.push(image); - text += image.CQCode; - } else logger.warning(`群ID格式无效:${seg.content}`); - break; - } - case 'audio': { - const id = seg.content; - const audios: { audioId: string, path: string }[] = Config.resource.LOCAL_AUDIOS || []; - const audio = audios.find(a => a.audioId === id); - - if (audio) { - text += `[语音:${resolveLocalPath(audio.path)}]`; - } else { - logger.warning(`无法找到本地语音:${id}`); - } - break; - } - case 'face': { - const faceId = Object.keys(FACE_MAP).find(key => FACE_MAP[key] === seg.content) || ''; - text += faceId ? `[CQ:face,id=${faceId}]` : ''; - break; - } - } - } - return { text, images }; -} - -export async function handleReply(ctx: seal.MsgContext, msg: seal.Message, session: { context: Context }, s: string): Promise<{ contextArray: string[], replyArray: string[], images: Image[] }> { - const { QUOTE_REPLY: replymsg, TRIM: isTrim } = Config.reply; - - // 兼容历史 <|...|> 标签:归一化后才能识别旧格式的 [from] 多轮分段 - s = normalizeRenderTags(s); - - // 分离AI臆想出来的多轮对话 - const segments = s - .split(/([[[]from.+?[\]]])/) - .filter(item => item.trim()); - if (segments.length === 0) { - return { contextArray: [], replyArray: [], images: [] }; - } - - s = ''; - for (let i = 0; i < segments.length; i++) { - const segment = segments[i]; - const match = segment.match(/^[[[]from[::]?\s?(.+?)[\]]]$/); - if (match) { - // 如果臆想对象是自己,那么将下一条消息添加到s中;只读取 [from] 中的显式 QQ 号。 - const numberMatch = match[1].match(/(?:^|\()([0-9]+)(?:\))?$/); - const fromUserId = numberMatch ? normalizeUserId(numberMatch[1]) : null; - const currentUserId = normalizeUserId(ctx.endPoint.userId); - if (fromUserId && currentUserId && fromUserId === currentUserId && i < segments.length - 1) s += segments[i + 1]; - } else if (i === 0) { - s = segment; - } - } - - // 如果臆想对象不包含自己,那么就随便把第一条消息添加到s中吧,毁灭吧世界 - if (!s.trim()) { - s = segments.find((segment: string) => !/[[[]from.+?[\]]]/.test(segment)) || ''; - if (!s || !s.trim()) return { contextArray: [], replyArray: [], images: [] }; - } - - // 剥离残留的内部上下文标签(msg_id/system/time 及未参与多轮分段的 from),不进入发送内容与上下文 - s = stripInternalTags(s); - - // 分离回复消息和戳一戳消息 - s = s.replace(/[[[]quote[::]?\s?(.+?)[\]]]/g, (match) => `\\f${match}`) - .replace(/[[[]poke[::]?\s?(.+?)[\]]]/g, (match) => `\\f${match}\\f`); - - const { contextArray, replyArray } = filterString(s); - const images: Image[] = []; - - // 处理回复消息 - for (let i = 0; i < replyArray.length; i++) { - const result = await transformContentToText(ctx, session, replyArray[i]); - const reply = isTrim ? result.text.trim() : result.text; - - // 含戳戳时引用前缀会导致消息无法显示,故跳过引用 - const prefix = (replymsg && msg.rawId && !/\[CQ:poke,/i.test(reply) && !/^\[CQ:reply,id=-?\d+\]/.test(reply)) ? `[CQ:reply,id=${msg.rawId}]` : ``; - replyArray[i] = prefix + reply; - images.push(...result.images); - } - - return { contextArray, replyArray, images }; -} - -export function checkRepeat(context: Context, s: string) { - const { STOP_REPEAT: stopRepeat, REPEAT_SIMILARITY: similarityLimit } = Config.reply; - - if (!stopRepeat) { - return false; - } - - const messages = context.messages; - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; - // 寻找最后一条文本消息 - if (message.role === 'assistant' && !(((message as any).toolCalls && (message as any).toolCalls.length > 0) || ((message as any).tool_calls && (message as any).tool_calls.length > 0))) { - const items = ((message as any).contentItems || (message as any).msgArray) || []; - const last = items[items.length - 1]; - const content = last ? (last.text || '') : ''; - const similarity = calculateSimilarity(content.trim(), s.trim()); - logger.info(`复读相似度:${similarity}`); - - if (similarity > similarityLimit) { - // 找到最近的一块assistant消息全部删除,防止触发tool相关的bug - let start = i; - let count = 1; - for (let j = i - 1; j >= 0; j--) { - const message = messages[j]; - if (message.role === 'tool' || (message.role === 'assistant' && (((message as any).toolCalls && (message as any).toolCalls.length > 0) || ((message as any).tool_calls && (message as any).tool_calls.length > 0)))) { - start = j; - count++; - } else { - break; - } - } - - messages.splice(start, count); - - return true; - } - - break; - } - } - return false; -} - -/** 内置回复过滤规则(原「回复消息过滤正则表达式」配置,改为硬编码): - * 第 0 条整条匹配并剔除(think 块/函数调用残留/旧版 <|...|> 未知标签/新版 [from]/[msg_id]/[system]/[time] 内部上下文标签), - * 其余按捕获组替换(代码块/加粗/删除线/列表/标题);旧格式标签名单补全 audio/avatar/group_avatar/user_avatar,避免误删可发送标签 */ -const REPLY_FILTER_PATTERNS = [ - "[\\s\\S]*<\\/think>|<[\\|│|]?func[^>]{0,9}$|[<<][\\|│|](?!at|poke|quote|img|face|audio|avatar|group_avatar|user_avatar).*?(?:[\\|│|][>>]|[\\|│|>>])|^[^\\|│|>>]{0,10}[\\|│|][>>]|[<<][\\|│|][^\\|│|>>]{0,20}$|[[[](?:from|msg_id|system|time)[::][^\\]]]*[\\]]]", - "<[\\|│|]?function(?:_call)?>[\\s\\S]*<\\/function(?:_call)?>", - "```.*\\n([\\s\\S]*?)\\n```", - "\\*\\*(.*?)\\*\\*", - "~~(.*?)~~", - "(?:^|\\n)\\s{0,12}[-*]\\s+(.*)", - "(?:^|\\n)#{1,6}\\s+(.*)" -]; -const REPLY_FILTER_CONTEXT_TEMPLATES = ["", "{{{match.[0]}}}", "{{{match.[0]}}}", "{{{match.[0]}}}", "{{{match.[0]}}}", "{{{match.[0]}}}", "{{{match.[0]}}}"]; -const REPLY_FILTER_REPLY_TEMPLATES = ["", "", "\n{{{match.[1]}}}\n", "{{{match.[1]}}}", "{{{match.[1]}}}", "\n{{{match.[1]}}}", "\n{{{match.[1]}}}"]; -const REPLY_FILTER_REGEX = new RegExp(REPLY_FILTER_PATTERNS.join('|')); - -let replyFilters: { regex: RegExp, contextTemplate: HandlebarsTemplateDelegate, replyTemplate: HandlebarsTemplateDelegate }[] | null = null; -function getReplyFilters() { - if (replyFilters) return replyFilters; - replyFilters = REPLY_FILTER_PATTERNS.map((pattern, index) => ({ - regex: new RegExp(pattern), - contextTemplate: Handlebars.compile(REPLY_FILTER_CONTEXT_TEMPLATES[index] || ''), - replyTemplate: Handlebars.compile(REPLY_FILTER_REPLY_TEMPLATES[index] || '') - })); - return replyFilters; -} - -function filterString(s: string): { contextArray: string[], replyArray: string[] } { - const { MAX_CHARS: maxChar } = Config.reply; - - const contextArray: string[] = []; - const replyArray: string[] = []; - let replyLength = 0; //只计算未被匹配的部分 - - const filters = getReplyFilters(); - - // 应用过滤正则表达式,并按照\f分割消息 - const segments = advancedSplit(s, REPLY_FILTER_REGEX).filter(Boolean); - for (let i = 0; i < segments.length; i++) { - const segment = segments[i]; - let isMatched = false; - for (let j = 0; j < filters.length; j++) { - const filter = filters[j]; - const match = segment.match(filter.regex); - if (match) { - isMatched = true; - const data = { - "match": match - } - - const contextString = filter.contextTemplate(data); - const replyString = filter.replyTemplate(data); - - if (contextArray.length === 0) { - contextArray.push(contextString); - replyArray.push(replyString); - } else { - contextArray[contextArray.length - 1] += contextString; - replyArray[replyArray.length - 1] += replyString; - } - - break; - } - } - - if (!isMatched) { - const segs = segment.split(/\\f|\f/g).filter(item => item); - - if (segment.startsWith('\\f') || segment.startsWith('\f')) { - contextArray.push(''); - replyArray.push(''); - } - - for (let j = 0; j < segs.length; j++) { - let seg = segs[j]; - - // 长度超过最大限制,直接截断(maxChar 为 0 时不限制) - if (maxChar > 0 && replyLength + seg.length > maxChar) { - seg = seg.slice(0, maxChar - replyLength); - } - - if (contextArray.length === 0 || j !== 0) { - contextArray.push(seg); - replyArray.push(seg); - } else { - contextArray[contextArray.length - 1] += seg; - replyArray[replyArray.length - 1] += seg; - } - - // 长度超过最大限制,直接退出 - replyLength += seg.length; - if (maxChar > 0 && replyLength > maxChar) { - break; - } - } - - if (segment.endsWith('\\f') || segment.endsWith('\f')) { - contextArray.push(''); - replyArray.push(''); - } - } - - // 长度超过最大限制,直接退出(maxChar 为 0 时不限制) - if (maxChar > 0 && replyLength > maxChar) { - break; - } - } - - return { contextArray, replyArray }; -} - -interface TokenSegment { - type: 'text' | 'at' | 'poke' | 'quote' | 'img' | 'avatar' | 'group_avatar' | 'face' | 'audio'; - content: string; -} - -/** 旧版 <|xxx|> 渲染标签归一化为新版 [xxx](含全角/缺竖杠变体);仅用于 4.14.0 首次对话的历史数据迁移,解析层不再兼容旧标签 */ -const RENDER_TAG_CONTENT = '[^||>>]+?'; -const RENDER_TAG_CLOSE = '(?:[\\|│|][>>]|[\\|│|>>])'; - -export function normalizeRenderTags(s: string): string { - if (!s.includes('<')) return s; - // 图片标签特殊处理:user_avatar/group_avatar 前缀拆成独立标签 [avatar]/[group_avatar] - s = s.replace( - new RegExp(`[<<][\\|│|]img[::]?\\s?(?:user_avatar[::]?(${RENDER_TAG_CONTENT})|group_avatar[::]?(${RENDER_TAG_CONTENT})|(${RENDER_TAG_CONTENT}))${RENDER_TAG_CLOSE}`, 'gi'), - (_all: string, user: string | undefined, group: string | undefined, img: string | undefined) => - user ? `[avatar:${user}]` : group ? `[group_avatar:${group}]` : `[img:${img}]` - ); - return s.replace( - new RegExp(`[<<][\\|│|](at|poke|quote|face|img|audio|from|msg_id|system|time|user_avatar|group_avatar)[::]?\\s?(${RENDER_TAG_CONTENT})${RENDER_TAG_CLOSE}`, 'gi'), - (_all: string, type: string, content: string) => - type === 'user_avatar' ? `[avatar:${content}]` : `[${type}:${content}]` - ); -} - -/** 内部上下文标签名:由代码注入上下文,其余任何来源(用户输入/AI 回复/工具回调/记忆等)出现时一律剥离 */ -const INTERNAL_TAG_NAMES = ['from', 'msg_id', 'system', 'time']; -const INTERNAL_TAG_TYPES = new Set(INTERNAL_TAG_NAMES); - -/** 剥离内部上下文标签(from/msg_id/system/time):先归一化旧版 <|...|> 变体,再移除新/旧方括号格式(含全角); - * 保留 at/poke/quote/img/avatar/group_avatar/face/audio 等可发送标签 */ -export function stripInternalTags(s: string): string { - if (!s) return s; - s = normalizeRenderTags(s); - return s.replace(new RegExp(`[[[](?:${INTERNAL_TAG_NAMES.join('|')})[::]?\\s?[^\\]]]*[\\]]]`, 'gi'), ''); -} - -export function parseSpecialTokens(s: string): TokenSegment[] { - const result: TokenSegment[] = []; - const segs = s.split(/([[[](?:at|poke|quote|face|img|avatar|group_avatar|audio|from|msg_id|system|time|user_avatar)[::]?[^\]]]*[\]]])/); - segs.forEach(seg => { - if (!seg) return; - const match = seg.match(/^[[[]([a-z_]+)[::]?\s?([^\]]]*)[\]]]$/i); - if (!match) { - result.push({ - type: 'text', - content: seg - }) - } else { - const [_, rawType = 'text', content = ''] = match; - const type = rawType.toLowerCase(); - // 内部上下文标签(来源/消息ID/系统/时间)不用于发送,直接丢弃避免泄露 - if (INTERNAL_TAG_TYPES.has(type)) return; - // 兼容旧格式 [user_avatar:xxx] 别名 - const mapped = type === 'user_avatar' ? 'avatar' : type; - if (!['at', 'poke', 'quote', 'img', 'avatar', 'group_avatar', 'face', 'audio'].includes(mapped)) { - result.push({ - type: 'text', - content: seg - }) - } else { - result.push({ - type: mapped as TokenSegment['type'], - content: content - }) - } - } - }) - return result; -} - -export function levenshteinDistance(s1: string, s2: string): number { - const len1 = s1.length; - const len2 = s2.length; - const dp = Array.from({ length: len1 + 1 }, () => Array(len2 + 1).fill(0)); - for (let i = 0; i <= len1; i++) { - dp[i][0] = i; - } - for (let j = 0; j <= len2; j++) { - dp[0][j] = j; - } - for (let i = 1; i <= len1; i++) { - for (let j = 1; j <= len2; j++) { - if (s1[i - 1] === s2[j - 1]) { - dp[i][j] = dp[i - 1][j - 1]; - } else { - dp[i][j] = Math.min( - dp[i - 1][j] + 1, // 删除 - dp[i][j - 1] + 1, // 插入 - dp[i - 1][j - 1] + 1 // 替换 - ); - } - } - } - return dp[len1][len2]; -} - -export function calculateSimilarity(s1: string, s2: string): number { - if (!s1 || !s2 || s1 === s2) { - return 0; - } - - const distance = levenshteinDistance(s1, s2); - const maxLength = Math.max(s1.length, s2.length); - return 1 - distance / maxLength || 0; -} - -/** - * 高级字符串分割函数,支持正则表达式匹配分割,保留匹配部分 - * @param s 待分割的字符串 - * @param r 正则表达式 - * @returns 分割后的字符串数组 - */ -function advancedSplit(s: string, r: RegExp) { - const parts = []; - let lastIndex = 0; - let match: RegExpExecArray | null; - - // 确保是全局正则 - if (!r.global) { - r = new RegExp(r.source, r.flags + "g"); - } - - while ((match = r.exec(s)) !== null) { - // 添加匹配前的部分 - if (match.index > lastIndex) { - parts.push(s.slice(lastIndex, match.index)); - } - - // 添加匹配部分 - parts.push(match[0]); - lastIndex = match.index + match[0].length; - - // 处理零长度匹配(避免死循环) - if (match[0].length === 0) { - if (r.lastIndex < s.length) { - r.lastIndex++; - } else { - break; - } - } - } - - // 添加剩余部分 - if (lastIndex < s.length) { - parts.push(s.slice(lastIndex)); - } - - return parts; -} - -export function fmtDate(timestamp: number) { - const date = new Date(timestamp * 1000); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const hour = String(date.getHours()).padStart(2, '0'); - const minute = String(date.getMinutes()).padStart(2, '0'); - const second = String(date.getSeconds()).padStart(2, '0'); - return `${year}-${month}-${day} ${hour}:${minute}:${second}`; -} - -/** - * 修复json字符串,将其中缺少前半双引号的字符串添加前半双引号,修复失败返回空字符串 - * @param s - * @returns - */ -export function fixJsonString(s: string): string { - try { - JSON.parse(s); - return s; - } catch (_err) { - const patterns = [ - // 匹配键缺少前半引号: {key": 或 ,key": - /([{,][\s\n]*)([a-zA-Z_$][a-zA-Z0-9_$]*)("[\s\n]*:)/g, - // 匹配值缺少前半引号: :value", 或 :value"} 或 - /(:[\s\n]*)([^"]+)("[\s\n]*[,}])/g, - // 匹配数组中的字符串缺少前半引号: [value", 或 [value"] 或 ,value", 或 ,value"] - /([\[,][\s\n]*)([^"]+)("[\s\n]*[,\]])/g - ]; - - let fixed = s; - let matched = false; - - for (const pattern of patterns) { - fixed = fixed.replace(pattern, (fullMatch, prefix, content, suffix) => { - matched = true; - const fixedContent = `${prefix}"${content}${suffix}`; - logger.info(`修复json字符串: ${fullMatch} -> ${fixedContent}`); - return fixedContent; - }); - - if (matched) { - try { - JSON.parse(fixed); - return fixed; - } catch (_err) { - matched = false; - continue; - } - } - } - - if (!matched) { - return ""; - } - - return fixed; - } -} - -export function parseActivityTime(s: string): [number, number, number] { - const arr = s.split('-').map((item, index) => { - const parts = item.split(/[::,,]+/).map(Number).map(i => isNaN(i) ? 0 : i); - if (index < 2) return Math.ceil((parts[0] * 60 + (parts[1] || 0)) % (24 * 60)); - return parts[0]; - }) - - const [start = 0, end = 0, segs = 1] = arr; - - if (start === end) throw new Error('活跃时间段开始时间和结束时间不能相同'); - - if (!Number.isInteger(segs)) throw new Error('活跃次数必须为整数'); - - const endReal = end >= start ? end : end + 24 * 60; - if (segs > endReal - start) throw new Error('活跃次数不能大于活跃时间段分钟数'); - - return [start, end, segs]; -} +// 字符串工具:消息段转换/回复拆分过滤/复读检测等 +import Handlebars from "handlebars"; + +import Config from "../config/config"; +import { FACE_MAP } from "../config/static_config"; +import { Context } from "../context/context"; +import { logger } from "../logger"; +import Image from "../resource/image"; + +import { getRawId, normalizeGroupId, normalizeUserId } from "./target_id"; +import { getMilkyReplyQuoteId, resolveLocalPath, transformMsgId, transformMsgIdBack } from "./utils"; + +export function truncateText(text: string, maxLength: number): string { + if (!text || maxLength <= 0 || text.length <= maxLength) return text || ''; + return text.slice(0, maxLength) + '...'; +} + + +/* 先丢这一坨东西在这。之所以不用是因为被类型检查整烦了 + +export interface MessageSegmentText { + type: 'text'; + data: { + text: string; + }; +} + +export interface MessageSegmentAt { + type: 'at'; + data: { + qq: string; + }; +} + +export interface MessageSegmentImage { + type: 'image'; + data: { + file: string; + url?: string; + }; +} + +export interface MessageSegmentFace { + type: 'face'; + data: { + id: string; + }; +} + +export interface MessageSegmentJson { + type: 'json'; + data: { + data: string; + }; +} + +export interface MessageSegmentRecord { + type: 'record'; + data: { + file: string; + }; +} + +export interface MessageSegmentVideo { + type: 'video'; + data: { + file: string; + }; +} + +export interface MessageSegmentReply { + type: 'reply'; + data: { + id: string; + }; +} + +export interface MessageSegmentMusic { + type: 'music'; + data: { + type: 'qq' | '163'; + id: string; + } | { + type: 'custom'; + url: string; + audio: string; + title: string; + image: string; + }; +} + +export interface MessageSegmentDice { + type: 'dice'; +} + +export interface MessageSegmentRps { + type: 'rps'; +} + +export interface MessageSegmentFile { + type: 'file'; + data: { + file: string; + }; +} + +export interface MessageSegmentNode { // 这是干嘛的?是合并转发吗? + type: 'node'; + data: { + user_id: string; + nickname: string; + content: (MessageSegmentText | MessageSegmentAt | MessageSegmentImage | MessageSegmentFace | MessageSegmentJson | MessageSegmentRecord | MessageSegmentVideo | MessageSegmentReply | MessageSegmentMusic | MessageSegmentDice | MessageSegmentRps | MessageSegmentFile)[]; + }; +} + +export type MessageSegment = MessageSegmentText | MessageSegmentAt | MessageSegmentImage | MessageSegmentFace | MessageSegmentJson | MessageSegmentRecord | MessageSegmentVideo | MessageSegmentReply | MessageSegmentMusic | MessageSegmentDice | MessageSegmentRps | MessageSegmentFile | MessageSegmentNode; +*/ + +export interface MessageSegment { + type: string; + data: { + [key: string]: string + }; +} + +/** + * 文件消息的完整可读表示。不要只显示 name:OB11/不同适配器可能把真实路径、URL、 + * file_id、file_unique 等字段放在同一段里,AI 需要这些字段才能继续调用文件工具。 + */ +export function formatFileSegmentText(rawData: any): string { + const data = rawData && typeof rawData === 'object' ? rawData : {}; + const nestedFile = data.file && typeof data.file === 'object' ? data.file : {}; + const pick = (...values: any[]): string => { + for (const value of values) { + if (value === undefined || value === null) continue; + const text = String(value).replace(/[\r\n]+/g, ' ').trim(); + if (text) return text; + } + return ''; + }; + const name = pick(data.name, data.file_name, data.filename, nestedFile.name, nestedFile.file_name); + const path = pick(data.path, data.local_path, nestedFile.path, nestedFile.local_path); + const file = pick(typeof data.file === 'string' ? data.file : '', nestedFile.file); + const url = pick(data.url, data.file_url, data.download_url, nestedFile.url, nestedFile.file_url); + const fileId = pick(data.file_id, data.fileId, nestedFile.file_id, nestedFile.fileId); + const fileUnique = pick(data.file_unique, data.fileUnique, nestedFile.file_unique, nestedFile.fileUnique); + const size = pick(data.size, data.file_size, nestedFile.size, nestedFile.file_size); + const mime = pick(data.content_type, data.contentType, data.mime, nestedFile.content_type, nestedFile.contentType); + const label = name || path || file || url || fileId || '未知文件'; + const fields = [ + ['name', name], ['path', path], ['file', file], ['url', url], + ['file_id', fileId], ['file_unique', fileUnique], ['size', size], ['mime', mime] + ].filter(([, value]) => value).map(([key, value]) => `${key}=${value}`); + return `【文件】${label}${fields.length ? `\n${fields.join('\n')}` : ''}`; +} + +/** 为忽略/触发正则生成兼容 CQ 码的匹配文本,不改变消息段本身。 */ +export function formatMessageSegmentsForMatching(messageArray: MessageSegment[], fallback: string = ''): string { + const text = messageArray.map(item => { + if (item.type === 'text') return (item.data && item.data.text) || ''; + if (item.type === 'at') { + const qq = item.data && (item.data.qq || item.data.user_id); + return qq ? `[CQ:at,qq=${qq}]` : '[at]'; + } + return `[${item.type}]`; + }).join(''); + return text || fallback; +} + +/** + * 把海豹 milky 消息段(seal.MessageSegment,独立 Go 结构体 + type() 编号)直接映射为 + * 项目内部统一段格式。不经过 CQ 码:milky 下 msg.message 只有纯文本拼接, + * at/图片/回复等富文本信息必须从 segment 取。 + */ +export function expandMilkySegments(ctx: seal.MsgContext, segments: seal.MessageSegment[]): MessageSegment[] { + const result: MessageSegment[] = []; + for (const seg of segments) { + if (!seg || typeof seg !== 'object' || typeof seg.type !== 'function') continue; + // goja 反射会把 Go 命名 int(ElementType)包装成 Number 对象而非原始值, + // 导致严格相等 0 === seg.type() 不成立(字符串化却显示 "0"),必须先归一化为数字; + // type() 异常(如消息段结构损坏)时按未知段兜底,不中断整条消息处理 + let typeValue: number; + try { + typeValue = Number(seg.type()); + } catch (_e) { + logger.warning('milky 消息段 type() 解析异常,按未知段处理'); + result.push({ type: 'text', data: { text: '[未知消息段]' } }); + continue; + } + switch (typeValue) { + case 0: { // 文本 Text + const content = 'content' in seg ? seg.content : ''; + result.push({ type: 'text', data: { text: content === undefined || content === null ? '' : String(content) } }); + break; + } + case 1: { // 艾特 At + const target = 'target' in seg ? seg.target : ''; + result.push({ type: 'at', data: { qq: target === undefined || target === null ? '' : String(target) } }); + break; + } + case 2: { // 文件 File + result.push({ type: 'text', data: { text: formatFileSegmentText(seg) } }); + break; + } + case 3: { // 图片 Image + const url = 'url' in seg ? seg.url : ''; + const file = 'file' in seg + ? (typeof seg.file === 'string' + ? seg.file + : seg.file && typeof seg.file === 'object' + ? ((seg.file as any).url || (seg.file as any).file || '') + : '') + : ''; + result.push({ type: 'image', data: { url: url || file || '' } }); + break; + } + case 4: { // 文字转语音 TTS + const content = 'content' in seg ? seg.content : ''; + result.push({ type: 'text', data: { text: content === undefined || content === null ? '' : String(content) } }); + break; + } + case 5: { // 回复 Reply + const replySeq = 'replySeq' in seg ? seg.replySeq : ''; + result.push({ type: 'reply', data: { id: getMilkyReplyQuoteId(ctx, replySeq) } }); + break; + } + case 6: { // 语音 Record + result.push({ type: 'text', data: { text: '【语音】' } }); + break; + } + case 7: { // 表情 Face + const faceID = 'faceID' in seg ? seg.faceID : ''; + result.push({ type: 'face', data: { id: faceID === undefined || faceID === null ? '' : String(faceID) } }); + break; + } + case 8: { // 戳一戳 Poke + const target = 'target' in seg ? seg.target : ''; + result.push({ type: 'poke', data: { qq: target === undefined || target === null ? '' : String(target) } }); + break; + } + default: { + logger.debug(`milky 未知消息段类型: ${typeValue},按文本占位处理`); + result.push({ type: 'text', data: { text: '[未知消息段]' } }); + } + } + } + return result; +} + +/** 解析 QQ 卡片消息(CQ:json / OB11 json 段的 data 字段),提取标题/描述/链接等可读文本 */ +export function parseCardToText(raw: any): string { + if (!raw) return '[卡片消息]'; + + let obj: any = null; + try { + obj = typeof raw === 'string' ? JSON.parse(raw) : raw; + } catch (_e) { + return `[卡片消息] ${String(raw).slice(0, 200)}`; + } + + const str = (v: any): string => (typeof v === 'string' && v.trim()) ? v.trim() : ''; + + // 常见卡片结构:view / meta.news / meta.detail 等 + const view = (obj && (obj.view || (obj.meta && (obj.meta.news || obj.meta.detail || obj.meta.article)))) || obj || {}; + const title = str(view.title) || str(obj.desc) || str(view.desc) || ''; + const desc = str(view.desc) || str(view.summary) || (view.news && str(view.news.desc)) || ''; + const url = str(view.url) || str(view.jumpUrl) || (view.news && str(view.news.jumpUrl)) || (view.detail && str(view.detail.jumpUrl)) || ''; + + const parts = [title, desc].filter(Boolean); + if (parts.length === 0) return '[卡片消息]'; + return `【卡片】${parts.join('\n')}${url ? `\n${url}` : ''}`; +} + +/** 音乐段转可读文本(data 为 qq/163 id 或 custom 对象) */ +export function parseMusicToText(data: any): string { + if (data && data.title) return `【音乐】${data.title}`; + const type = data && data.type ? String(data.type) : ''; + const id = data && data.id ? String(data.id) : ''; + return `【音乐】${type}${id ? ` ${id}` : ''}`; +} + +export function transformTextToArray(text: string): MessageSegment[] { + const segments = text.split(/(\[CQ:.*?\])/).filter(segment => segment); + const messageArray: MessageSegment[] = []; + for (const segment of segments) { + if (segment.startsWith('[CQ:')) { + const match = segment.match(/^\[CQ:([^,]+),?([^\]]*)\]$/); + if (match) { + const type = match[1].trim(); + const params: { [key: string]: string } = {}; + if (match[2]) { + match[2].trim().split(',').forEach(param => { + const eqIndex = param.indexOf('='); + if (eqIndex === -1) return; + + const key = param.slice(0, eqIndex).trim(); + const value = param.slice(eqIndex + 1).trim(); + + if (type === 'image' && key === 'file') params['url'] = value; // 这对吗?nc是这样的吗? + if (key) params[key] = value; + }); + } + + messageArray.push({ type, data: params }); + } else { + // 无法解析的 [CQ:... 字样保留为纯文本,避免用户内容被静默吞掉 + logger.error(`无法解析CQ码,保留为文本:${segment}`); + messageArray.push({ type: 'text', data: { text: segment } }); + } + } else { + messageArray.push({ type: 'text', data: { text: segment } }); + } + } + return messageArray; +} + +export function transformArrayToText(messageArray: { type: string, data: { [key: string]: string } }[]): string { + let text = ''; + for (const message of messageArray) { + if (message.type === 'text') { + text += message.data['text']; + } else { + if (message.type === 'image') { + if (message.data['url']) { + text += `[CQ:image,file=${message.data['url']}]`; + } else if (message.data['file']) { + text += `[CQ:image,file=${message.data['file']}]`; + } + } else { + text += `[CQ:${message.type}`; + for (const key in message.data) { + if (typeof message.data[key] === 'string') { + text += `,${key}=${message.data[key]}`; + } + } + text += ']'; + } + } + } + return text; +} + +export async function transformArrayToContent(ctx: seal.MsgContext, messageArray: MessageSegment[]): Promise<{ content: string, images: Image[] }> { + let content = ''; + const images: Image[] = []; + for (const seg of messageArray) { + switch (seg.type) { + case 'text': { + // 防注入:用户输入中的内部上下文标签(from/msg_id/system/time)直接剥离,不进入上下文 + content += stripInternalTags(seg.data.text); + break; + } + case 'at': { + if (seg.data.qq === 'all') { + content += '[at:all]'; + break; + } + const userId = normalizeUserId(seg.data.qq || ''); + content += `[at:${userId ? getRawId(userId) : String(seg.data.qq || '')}]`; + break; + } + case 'poke': { + const userId = normalizeUserId(seg.data.qq || ''); + content += `[poke:${userId ? getRawId(userId) : String(seg.data.qq || '')}]`; + break; + } + case 'reply': { + const quoteId = transformMsgId(seg.data.id || ''); + if (quoteId) content += `[quote:${quoteId}]`; + break; + } + case 'image': { + const result = await Image.handleImageMessageSegment(ctx, seg); + content += result.content; + images.push(...result.images); + break; + } + case 'face': { + const faceName = FACE_MAP[String(seg.data.id)] || ''; + // 未知 face id 也保留占位,避免用户表情静默丢失;名称作为可见文案的一部分 + content += `[face:${faceName || `未知表情${String(seg.data.id)}`}]`; + break; + } + } + } + return { content, images }; +} + +/** + * 转换文本内容中的特殊标签为CQ码 + * @param ctx 消息上下文 + * @param ai AI实例 + * @param content 文本内容 + * @returns 包含处理后的结果和图片列表的对象 + */ +async function transformContentToText(ctx: seal.MsgContext, session: { context: Context }, content: string): Promise<{ text: string, images: Image[] }> { + const segs = parseSpecialTokens(content); + let text = ''; + const images: Image[] = []; + for (const seg of segs) { + switch (seg.type) { + case 'text': { + text += seg.content; + break; + } + case 'at': { + if (seg.content.trim().toLowerCase() === 'all') { + text += '[CQ:at,qq=all]'; + break; + } + const userId = normalizeUserId(seg.content); + if (userId) text += `[CQ:at,qq=${getRawId(userId)}]`; + else logger.warning(`用户ID格式无效:${seg.content}`); + break; + } + case 'poke': { + const userId = normalizeUserId(seg.content); + if (userId) text += `[CQ:poke,qq=${getRawId(userId)}]`; + else logger.warning(`用户ID格式无效:${seg.content}`); + break; + } + case 'quote': { + const msgId = seg.content; + if (msgId) { + const backId = transformMsgIdBack(msgId); + // msgid 可能为负数(base36 保留符号),仅当可解析时才生成引用,避免 id=NaN + if (Number.isFinite(backId)) text += `[CQ:reply,id=${backId}]`; + } + break; + } + case 'img': { + const id = seg.content; + // 兼容 [img:imageId:描述]:描述部分可能带冒号,整体找不到时取首个冒号前作为图片 id + const image = await session.context.findImage(ctx, id) || (id.includes(':') ? await session.context.findImage(ctx, id.split(':')[0]) : null); + + if (image) { + images.push(image); + text += image.CQCode; + } else { + logger.warning(`无法找到图片:${id}`); + } + break; + } + case 'avatar': { + const userId = normalizeUserId(seg.content); + if (userId) { + const image = Image.getUserAvatar(userId); + images.push(image); + text += image.CQCode; + } else logger.warning(`用户ID格式无效:${seg.content}`); + break; + } + case 'group_avatar': { + const groupId = normalizeGroupId(seg.content); + if (groupId) { + const image = Image.getGroupAvatar(groupId); + images.push(image); + text += image.CQCode; + } else logger.warning(`群ID格式无效:${seg.content}`); + break; + } + case 'audio': { + const id = seg.content; + const audios: { audioId: string, path: string }[] = Config.resource.LOCAL_AUDIOS || []; + const audio = audios.find(a => a.audioId === id); + + if (audio) { + text += `[语音:${resolveLocalPath(audio.path)}]`; + } else { + logger.warning(`无法找到本地语音:${id}`); + } + break; + } + case 'face': { + const faceId = Object.keys(FACE_MAP).find(key => FACE_MAP[key] === seg.content) || ''; + text += faceId ? `[CQ:face,id=${faceId}]` : ''; + break; + } + } + } + return { text, images }; +} + +export async function handleReply(ctx: seal.MsgContext, msg: seal.Message, session: { context: Context }, s: string): Promise<{ contextArray: string[], replyArray: string[], images: Image[] }> { + const { QUOTE_REPLY: replymsg, TRIM: isTrim } = Config.reply; + + // 兼容历史 <|...|> 标签:归一化后才能识别旧格式的 [from] 多轮分段 + s = normalizeRenderTags(s); + + // 分离AI臆想出来的多轮对话 + const segments = s + .split(/([[[]from.+?[\]]])/) + .filter(item => item.trim()); + if (segments.length === 0) { + return { contextArray: [], replyArray: [], images: [] }; + } + + s = ''; + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + const match = segment.match(/^[[[]from[::]?\s?(.+?)[\]]]$/); + if (match) { + // 如果臆想对象是自己,那么将下一条消息添加到s中;只读取 [from] 中的显式 QQ 号。 + const numberMatch = match[1].match(/(?:^|\()([0-9]+)(?:\))?$/); + const fromUserId = numberMatch ? normalizeUserId(numberMatch[1]) : null; + const currentUserId = normalizeUserId(ctx.endPoint.userId); + if (fromUserId && currentUserId && fromUserId === currentUserId && i < segments.length - 1) s += segments[i + 1]; + } else if (i === 0) { + s = segment; + } + } + + // 如果臆想对象不包含自己,那么就随便把第一条消息添加到s中吧,毁灭吧世界 + if (!s.trim()) { + s = segments.find((segment: string) => !/[[[]from.+?[\]]]/.test(segment)) || ''; + if (!s || !s.trim()) return { contextArray: [], replyArray: [], images: [] }; + } + + // 剥离残留的内部上下文标签(msg_id/system/time 及未参与多轮分段的 from),不进入发送内容与上下文 + s = stripInternalTags(s); + + // 模型有时会输出字面量 \n / \r\n;在分段前还原为真实换行。 + // \f 不在这里处理,仍作为多消息分隔符交给 filterString。 + s = decodeEscapedNewlines(s); + + // 分离回复消息和戳一戳消息 + s = s.replace(/[[[]quote[::]?\s?(.+?)[\]]]/g, (match) => `\\f${match}`) + .replace(/[[[]poke[::]?\s?(.+?)[\]]]/g, (match) => `\\f${match}\\f`); + + const { contextArray, replyArray } = filterString(s); + const images: Image[] = []; + + // 处理回复消息 + for (let i = 0; i < replyArray.length; i++) { + const result = await transformContentToText(ctx, session, replyArray[i]); + const reply = isTrim ? result.text.trim() : result.text; + + // 含戳戳时引用前缀会导致消息无法显示,故跳过引用 + const prefix = (replymsg && msg.rawId && !/\[CQ:poke,/i.test(reply) && !/^\[CQ:reply,id=-?\d+\]/.test(reply)) ? `[CQ:reply,id=${msg.rawId}]` : ``; + replyArray[i] = prefix + reply; + images.push(...result.images); + } + + return { contextArray, replyArray, images }; +} + +export function checkRepeat(context: Context, s: string) { + const { STOP_REPEAT: stopRepeat, REPEAT_SIMILARITY: similarityLimit } = Config.reply; + + if (!stopRepeat) { + return false; + } + + const messages = context.messages; + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + // 寻找最后一条文本消息 + if (message.role === 'assistant' && !(((message as any).toolCalls && (message as any).toolCalls.length > 0) || ((message as any).tool_calls && (message as any).tool_calls.length > 0))) { + const items = ((message as any).contentItems || (message as any).msgArray) || []; + const last = items[items.length - 1]; + const content = last ? (last.text || '') : ''; + const similarity = calculateSimilarity(content.trim(), s.trim()); + logger.info(`复读相似度:${similarity}`); + + if (similarity > similarityLimit) { + // 找到最近的一块assistant消息全部删除,防止触发tool相关的bug + let start = i; + let count = 1; + for (let j = i - 1; j >= 0; j--) { + const message = messages[j]; + if (message.role === 'tool' || (message.role === 'assistant' && (((message as any).toolCalls && (message as any).toolCalls.length > 0) || ((message as any).tool_calls && (message as any).tool_calls.length > 0)))) { + start = j; + count++; + } else { + break; + } + } + + messages.splice(start, count); + + return true; + } + + break; + } + } + return false; +} + +/** 内置回复过滤规则(原「回复消息过滤正则表达式」配置,改为硬编码): + * 第 0 条整条匹配并剔除(think 块/函数调用残留/旧版 <|...|> 未知标签/新版 [from]/[msg_id]/[system]/[time] 内部上下文标签), + * 其余按捕获组替换(代码块/加粗/删除线/列表/标题);旧格式标签名单补全 audio/avatar/group_avatar/user_avatar,避免误删可发送标签 */ +const REPLY_FILTER_PATTERNS = [ + "[\\s\\S]*<\\/think>|<[\\|│|]?func[^>]{0,9}$|[<<][\\|│|](?!at|poke|quote|img|face|audio|avatar|group_avatar|user_avatar).*?(?:[\\|│|][>>]|[\\|│|>>])|^[^\\|│|>>]{0,10}[\\|│|][>>]|[<<][\\|│|][^\\|│|>>]{0,20}$|[[[](?:from|msg_id|system|time)[::][^\\]]]*[\\]]]", + "<[\\|│|]?function(?:_call)?>[\\s\\S]*<\\/function(?:_call)?>", + "```.*\\n([\\s\\S]*?)\\n```", + "\\*\\*(.*?)\\*\\*", + "~~(.*?)~~", + "(?:^|\\n)\\s{0,12}[-*]\\s+(.*)", + "(?:^|\\n)#{1,6}\\s+(.*)" +]; +const REPLY_FILTER_CONTEXT_TEMPLATES = ["", "{{{match.[0]}}}", "{{{match.[0]}}}", "{{{match.[0]}}}", "{{{match.[0]}}}", "{{{match.[0]}}}", "{{{match.[0]}}}"]; +const REPLY_FILTER_REPLY_TEMPLATES = ["", "", "\n{{{match.[1]}}}\n", "{{{match.[1]}}}", "{{{match.[1]}}}", "\n{{{match.[1]}}}", "\n{{{match.[1]}}}"]; +const REPLY_FILTER_REGEX = new RegExp(REPLY_FILTER_PATTERNS.join('|')); + +let replyFilters: { regex: RegExp, contextTemplate: HandlebarsTemplateDelegate, replyTemplate: HandlebarsTemplateDelegate }[] | null = null; +function getReplyFilters() { + if (replyFilters) return replyFilters; + replyFilters = REPLY_FILTER_PATTERNS.map((pattern, index) => ({ + regex: new RegExp(pattern), + contextTemplate: Handlebars.compile(REPLY_FILTER_CONTEXT_TEMPLATES[index] || ''), + replyTemplate: Handlebars.compile(REPLY_FILTER_REPLY_TEMPLATES[index] || '') + })); + return replyFilters; +} + +function filterString(s: string): { contextArray: string[], replyArray: string[] } { + const { MAX_CHARS: maxChar } = Config.reply; + + const contextArray: string[] = []; + const replyArray: string[] = []; + let replyLength = 0; //只计算未被匹配的部分 + + const filters = getReplyFilters(); + + // 应用过滤正则表达式,并按照\f分割消息 + const segments = advancedSplit(s, REPLY_FILTER_REGEX).filter(Boolean); + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + let isMatched = false; + for (let j = 0; j < filters.length; j++) { + const filter = filters[j]; + const match = segment.match(filter.regex); + if (match) { + isMatched = true; + const data = { + "match": match + } + + const contextString = filter.contextTemplate(data); + const replyString = filter.replyTemplate(data); + + if (contextArray.length === 0) { + contextArray.push(contextString); + replyArray.push(replyString); + } else { + contextArray[contextArray.length - 1] += contextString; + replyArray[replyArray.length - 1] += replyString; + } + + break; + } + } + + if (!isMatched) { + const segs = segment.split(/\\f|\f/g).filter(item => item); + + if (segment.startsWith('\\f') || segment.startsWith('\f')) { + contextArray.push(''); + replyArray.push(''); + } + + for (let j = 0; j < segs.length; j++) { + let seg = segs[j]; + + // 长度超过最大限制,直接截断(maxChar 为 0 时不限制) + if (maxChar > 0 && replyLength + seg.length > maxChar) { + seg = seg.slice(0, maxChar - replyLength); + } + + if (contextArray.length === 0 || j !== 0) { + contextArray.push(seg); + replyArray.push(seg); + } else { + contextArray[contextArray.length - 1] += seg; + replyArray[replyArray.length - 1] += seg; + } + + // 长度超过最大限制,直接退出 + replyLength += seg.length; + if (maxChar > 0 && replyLength > maxChar) { + break; + } + } + + if (segment.endsWith('\\f') || segment.endsWith('\f')) { + contextArray.push(''); + replyArray.push(''); + } + } + + // 长度超过最大限制,直接退出(maxChar 为 0 时不限制) + if (maxChar > 0 && replyLength > maxChar) { + break; + } + } + + return { contextArray, replyArray }; +} + +interface TokenSegment { + type: 'text' | 'at' | 'poke' | 'quote' | 'img' | 'avatar' | 'group_avatar' | 'face' | 'audio'; + content: string; +} + +/** 旧版 <|xxx|> 渲染标签归一化为新版 [xxx](含全角/缺竖杠变体);仅用于 4.14.0 首次对话的历史数据迁移,解析层不再兼容旧标签 */ +const RENDER_TAG_CONTENT = '[^||>>]+?'; +const RENDER_TAG_CLOSE = '(?:[\\|│|][>>]|[\\|│|>>])'; + +/** 将模型常见的字面量换行转义(\n / \r\n)还原为真实换行。 + * 注意:不处理 \f;\f 仍由回复过滤器作为多消息分隔符处理。 */ +export function decodeEscapedNewlines(s: string): string { + return s + .replace(/\\r\\n/g, '\n') + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\n'); +} + +export function normalizeRenderTags(s: string): string { + if (!s.includes('<')) return s; + // 图片标签特殊处理:user_avatar/group_avatar 前缀拆成独立标签 [avatar]/[group_avatar] + s = s.replace( + new RegExp(`[<<][\\|│|]img[::]?\\s?(?:user_avatar[::]?(${RENDER_TAG_CONTENT})|group_avatar[::]?(${RENDER_TAG_CONTENT})|(${RENDER_TAG_CONTENT}))${RENDER_TAG_CLOSE}`, 'gi'), + (_all: string, user: string | undefined, group: string | undefined, img: string | undefined) => + user ? `[avatar:${user}]` : group ? `[group_avatar:${group}]` : `[img:${img}]` + ); + return s.replace( + new RegExp(`[<<][\\|│|](at|poke|quote|face|img|audio|from|msg_id|system|time|user_avatar|group_avatar)[::]?\\s?(${RENDER_TAG_CONTENT})${RENDER_TAG_CLOSE}`, 'gi'), + (_all: string, type: string, content: string) => + type === 'user_avatar' ? `[avatar:${content}]` : `[${type}:${content}]` + ); +} + +/** 内部上下文标签名:由代码注入上下文,其余任何来源(用户输入/AI 回复/工具回调/记忆等)出现时一律剥离 */ +const INTERNAL_TAG_NAMES = ['from', 'msg_id', 'system', 'time']; +const INTERNAL_TAG_TYPES = new Set(INTERNAL_TAG_NAMES); + +/** 剥离内部上下文标签(from/msg_id/system/time):先归一化旧版 <|...|> 变体,再移除新/旧方括号格式(含全角); + * 保留 at/poke/quote/img/avatar/group_avatar/face/audio 等可发送标签 */ +export function stripInternalTags(s: string): string { + if (!s) return s; + s = normalizeRenderTags(s); + return s.replace(new RegExp(`[[[](?:${INTERNAL_TAG_NAMES.join('|')})[::]?\\s?[^\\]]]*[\\]]]`, 'gi'), ''); +} + +export function parseSpecialTokens(s: string): TokenSegment[] { + const result: TokenSegment[] = []; + const segs = s.split(/([[[](?:at|poke|quote|face|img|avatar|group_avatar|audio|from|msg_id|system|time|user_avatar)[::]?[^\]]]*[\]]])/); + segs.forEach(seg => { + if (!seg) return; + const match = seg.match(/^[[[]([a-z_]+)[::]?\s?([^\]]]*)[\]]]$/i); + if (!match) { + result.push({ + type: 'text', + content: seg + }) + } else { + const [_, rawType = 'text', content = ''] = match; + const type = rawType.toLowerCase(); + // 内部上下文标签(来源/消息ID/系统/时间)不用于发送,直接丢弃避免泄露 + if (INTERNAL_TAG_TYPES.has(type)) return; + // 兼容旧格式 [user_avatar:xxx] 别名 + const mapped = type === 'user_avatar' ? 'avatar' : type; + if (!['at', 'poke', 'quote', 'img', 'avatar', 'group_avatar', 'face', 'audio'].includes(mapped)) { + result.push({ + type: 'text', + content: seg + }) + } else { + result.push({ + type: mapped as TokenSegment['type'], + content: content + }) + } + } + }) + return result; +} + +export function levenshteinDistance(s1: string, s2: string): number { + const len1 = s1.length; + const len2 = s2.length; + const dp = Array.from({ length: len1 + 1 }, () => Array(len2 + 1).fill(0)); + for (let i = 0; i <= len1; i++) { + dp[i][0] = i; + } + for (let j = 0; j <= len2; j++) { + dp[0][j] = j; + } + for (let i = 1; i <= len1; i++) { + for (let j = 1; j <= len2; j++) { + if (s1[i - 1] === s2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min( + dp[i - 1][j] + 1, // 删除 + dp[i][j - 1] + 1, // 插入 + dp[i - 1][j - 1] + 1 // 替换 + ); + } + } + } + return dp[len1][len2]; +} + +export function calculateSimilarity(s1: string, s2: string): number { + if (!s1 || !s2 || s1 === s2) { + return 0; + } + + const distance = levenshteinDistance(s1, s2); + const maxLength = Math.max(s1.length, s2.length); + return 1 - distance / maxLength || 0; +} + +/** + * 高级字符串分割函数,支持正则表达式匹配分割,保留匹配部分 + * @param s 待分割的字符串 + * @param r 正则表达式 + * @returns 分割后的字符串数组 + */ +function advancedSplit(s: string, r: RegExp) { + const parts = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + + // 确保是全局正则 + if (!r.global) { + r = new RegExp(r.source, r.flags + "g"); + } + + while ((match = r.exec(s)) !== null) { + // 添加匹配前的部分 + if (match.index > lastIndex) { + parts.push(s.slice(lastIndex, match.index)); + } + + // 添加匹配部分 + parts.push(match[0]); + lastIndex = match.index + match[0].length; + + // 处理零长度匹配(避免死循环) + if (match[0].length === 0) { + if (r.lastIndex < s.length) { + r.lastIndex++; + } else { + break; + } + } + } + + // 添加剩余部分 + if (lastIndex < s.length) { + parts.push(s.slice(lastIndex)); + } + + return parts; +} + +export function fmtDate(timestamp: number) { + const date = new Date(timestamp * 1000); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hour = String(date.getHours()).padStart(2, '0'); + const minute = String(date.getMinutes()).padStart(2, '0'); + const second = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; +} + +/** + * 修复json字符串,将其中缺少前半双引号的字符串添加前半双引号,修复失败返回空字符串 + * @param s + * @returns + */ +export function fixJsonString(s: string): string { + try { + JSON.parse(s); + return s; + } catch (_err) { + const patterns = [ + // 匹配键缺少前半引号: {key": 或 ,key": + /([{,][\s\n]*)([a-zA-Z_$][a-zA-Z0-9_$]*)("[\s\n]*:)/g, + // 匹配值缺少前半引号: :value", 或 :value"} 或 + /(:[\s\n]*)([^"]+)("[\s\n]*[,}])/g, + // 匹配数组中的字符串缺少前半引号: [value", 或 [value"] 或 ,value", 或 ,value"] + /([\[,][\s\n]*)([^"]+)("[\s\n]*[,\]])/g + ]; + + let fixed = s; + let matched = false; + + for (const pattern of patterns) { + fixed = fixed.replace(pattern, (fullMatch, prefix, content, suffix) => { + matched = true; + const fixedContent = `${prefix}"${content}${suffix}`; + logger.info(`修复json字符串: ${fullMatch} -> ${fixedContent}`); + return fixedContent; + }); + + if (matched) { + try { + JSON.parse(fixed); + return fixed; + } catch (_err) { + matched = false; + continue; + } + } + } + + if (!matched) { + return ""; + } + + return fixed; + } +} + +export function parseActivityTime(s: string): [number, number, number] { + const arr = s.split('-').map((item, index) => { + const parts = item.split(/[::,,]+/).map(Number).map(i => isNaN(i) ? 0 : i); + if (index < 2) return Math.ceil((parts[0] * 60 + (parts[1] || 0)) % (24 * 60)); + return parts[0]; + }) + + const [start = 0, end = 0, segs = 1] = arr; + + if (start === end) throw new Error('活跃时间段开始时间和结束时间不能相同'); + + if (!Number.isInteger(segs)) throw new Error('活跃次数必须为整数'); + + const endReal = end >= start ? end : end + 24 * 60; + if (segs > endReal - start) throw new Error('活跃次数不能大于活跃时间段分钟数'); + + return [start, end, segs]; +}