From 77c9c6bd76a5b811b82af001a001eff0a93a3eeb Mon Sep 17 00:00:00 2001 From: buyun14 <2135481470@qq.com> Date: Tue, 10 Mar 2026 16:18:37 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=99=AE?= =?UTF-8?q?=E9=80=9A=E5=AF=B9=E8=AF=9D=E6=80=9D=E8=80=83=E8=BF=87=E7=A8=8B?= =?UTF-8?q?=E8=BD=AC=E5=8F=91=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E5=91=BD=E4=BB=A4=E5=BC=80=E5=90=AF=E6=88=96?= =?UTF-8?q?=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++- apps/chat.js | 12 ++++++++---- apps/management.js | 12 ++++++++++++ config/config.js | 5 ++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5392b3ba3..37ea41445 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ ChatGPT-Plugin 以 Chaite 为内核,将多模型渠道、工具、处理器、 basic: toggleMode: at # at / prefix togglePrefix: "#chat" # prefix 模式下的触发词 + sendReasoning: true # 普通对话是否转发思考/工具过程 commandPrefix: "#chatgpt" # 管理指令前缀 llm: defaultModel: "gpt-4o-mini" @@ -88,7 +89,7 @@ memory: whitelist: ["123456789"] ``` -- **basic**:控制触发方式、调试与命令前缀。 +- **basic**:控制触发方式、普通对话思考过程转发、调试与命令前缀。 - **llm**:定义默认模型、嵌入模型、群上下文等。`defaultChatPresetId` 需在面板或命令中提前创建。 - **chaite**:`storage` 默认 SQLite,会在 `plugins/chatgpt-plugin/data/data.db` 生成数据文件;如接入 Chaite Cloud,请填入 `cloudApiKey` 并开放 `host/port`。 - **bym**:配置伪人触发概率、关键词映射、撤回与思考内容开关。 @@ -107,6 +108,7 @@ memory: ### 管理命令 & 面板 - `#chatgpt管理面板`:生成一次性 token,访问 `http://:` 即可使用 Chaite Web 面板。 +- `#chatgpt开启思考转发` / `#chatgpt关闭思考转发`:控制 `@Bot` 或前缀触发普通对话时是否发送思考过程转发消息。 - CRUD 命令示例(均支持 `列表 / 添加 / 查看 / 删除`): ``` #chatgpt渠道列表 diff --git a/apps/chat.js b/apps/chat.js index b6eca32a4..56e5570a6 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -53,8 +53,10 @@ export class Chat extends plugin { if (msgs.length > 0) { await e.reply(msgs) } - for (let forwardElement of forward) { - this.reply(forwardElement) + if (Config.basic.sendReasoning) { + for (let forwardElement of forward) { + this.reply(forwardElement) + } } } const userMessage = await intoUserMessage(e, { @@ -117,8 +119,10 @@ export class Chat extends plugin { if (msgs.length > 0) { await e.reply(msgs, true) } - for (let forwardElement of forward) { - this.reply(forwardElement) + if (Config.basic.sendReasoning) { + for (let forwardElement of forward) { + this.reply(forwardElement) + } } await processUserMemory({ event: e, diff --git a/apps/management.js b/apps/management.js index ce2fd18ae..33c5ee59c 100644 --- a/apps/management.js +++ b/apps/management.js @@ -27,6 +27,11 @@ export class ChatGPTManagement extends plugin { fnc: 'setDefaultBymPreset', permission: 'master' }, + { + reg: `^${cmdPrefix}(开启|关闭)(普通对话)?(思考|推理)(过程)?(转发|回显)$`, + fnc: 'toggleChatReasoningForward', + permission: 'master' + }, { reg: `^${cmdPrefix}(查看)?(当前)?(配置|信息|统计信息|状态)$`, fnc: 'currentStatus', @@ -87,6 +92,12 @@ export class ChatGPTManagement extends plugin { } } + toggleChatReasoningForward (e) { + const enable = e.msg.includes('开启') + ChatGPTConfig.basic.sendReasoning = enable + this.reply(`普通对话思考过程转发已${enable ? '开启' : '关闭'}`) + } + async destroyConversation (e) { if (e.msg.includes('全部')) { if (!e.isMaster) { @@ -149,6 +160,7 @@ export class ChatGPTManagement extends plugin { const defaultChatPresetId = ChatGPTConfig.llm.defaultChatPresetId const currentPreset = await Chaite.getInstance().getChatPresetManager().getInstance(defaultChatPresetId) msgs.push(`当前预设:${currentPreset?.name || '未设置'}${currentPreset ? ('\n\n' + currentPreset.toFormatedString(false)) : ''}`) + msgs.push(`普通对话思考过程转发:${ChatGPTConfig.basic.sendReasoning ? '开启' : '关闭'}\n伪人思考过程转发:${ChatGPTConfig.bym.sendReasoning ? '开启' : '关闭'}`) const allTools = await Chaite.getInstance().getToolsManager().listInstances() let toolsMsg = `工具总数:${allTools.length}\n` diff --git a/config/config.js b/config/config.js index e93bec101..b80e8b5ad 100644 --- a/config/config.js +++ b/config/config.js @@ -13,6 +13,7 @@ class ChatGPTConfig { * 基本配置 * @type {{ * toggleMode: 'at' | 'prefix', + * sendReasoning: boolean, * debug: boolean, * }} */ @@ -21,6 +22,8 @@ class ChatGPTConfig { toggleMode: 'at', // 触发前缀,仅在前缀触发时有效 togglePrefix: '#chat', + // @ 或前缀触发对话时,是否发送思考/工具过程转发消息 + sendReasoning: true, // 是否开启调试模式 debug: false, // 一般命令的开头 @@ -112,7 +115,7 @@ class ChatGPTConfig { // 如果blockStrategy为mask,屏蔽词的替换字符 blockWordMask: '***', // 是否开启群组上下文 - enableGroupContext: false, + enableGroupContext: true, // 群组上下文长度 groupContextLength: 20, // 用于组装群聊上下文提示词的模板前缀 From cd0c92b800426f232691e91e220e25eac0af39ea Mon Sep 17 00:00:00 2001 From: buyun14 <2135481470@qq.com> Date: Tue, 10 Mar 2026 17:50:49 +0800 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=B8=83=E5=B0=94=E6=A0=87=E5=BF=97=E7=9A=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=B9=B6=E5=9C=A8=E5=A4=9A=E4=B8=AA=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E4=B8=AD=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/bym.js | 10 ++++++---- apps/chat.js | 5 +++-- apps/management.js | 9 ++++++++- config/config.js | 2 +- index.js | 8 +++++++- models/memory/extractor.js | 8 ++++++++ utils/common.js | 26 ++++++++++++++++++++++++++ utils/message.js | 3 ++- 8 files changed, 61 insertions(+), 10 deletions(-) diff --git a/apps/bym.js b/apps/bym.js index b68854c5b..469716403 100644 --- a/apps/bym.js +++ b/apps/bym.js @@ -3,7 +3,7 @@ import { Chaite } from 'chaite' import { intoUserMessage, toYunzai } from '../utils/message.js' import common from '../../../lib/common/common.js' import { getGroupContextPrompt } from '../utils/group.js' -import { formatTimeToBeiJing } from '../utils/common.js' +import { formatTimeToBeiJing, parseBooleanFlag } from '../utils/common.js' import { extractTextFromUserMessage, processUserMemory } from '../models/memory/userMemoryManager.js' import { buildMemoryPrompt } from '../models/memory/prompt.js' @@ -97,8 +97,10 @@ export class bym extends plugin { if (msgs.length > 0) { await e.reply(msgs) } - for (let forwardElement of forward) { - this.reply(forwardElement) + if (parseBooleanFlag(ChatGPTConfig.bym.sendReasoning, false)) { + for (let forwardElement of forward) { + this.reply(forwardElement) + } } } const systemSegments = [] @@ -138,7 +140,7 @@ export class bym extends plugin { await common.sleep(Math.floor(Math.random() * 2000) + 1000) } } - if (ChatGPTConfig.bym.sendReasoning) { + if (parseBooleanFlag(ChatGPTConfig.bym.sendReasoning, false)) { for (let forwardElement of forward) { await e.reply(forwardElement, false, { recallMsg: recall ? 10 : 0 }) } diff --git a/apps/chat.js b/apps/chat.js index 56e5570a6..687ab15d6 100644 --- a/apps/chat.js +++ b/apps/chat.js @@ -5,6 +5,7 @@ import { YunzaiUserState } from '../models/chaite/storage/lowdb/user_state_stora import { getGroupContextPrompt } from '../utils/group.js' import { buildMemoryPrompt } from '../models/memory/prompt.js' import { extractTextFromUserMessage, processUserMemory } from '../models/memory/userMemoryManager.js' +import { parseBooleanFlag } from '../utils/common.js' import * as crypto from 'node:crypto' export class Chat extends plugin { @@ -53,7 +54,7 @@ export class Chat extends plugin { if (msgs.length > 0) { await e.reply(msgs) } - if (Config.basic.sendReasoning) { + if (parseBooleanFlag(Config.basic.sendReasoning, true)) { for (let forwardElement of forward) { this.reply(forwardElement) } @@ -119,7 +120,7 @@ export class Chat extends plugin { if (msgs.length > 0) { await e.reply(msgs, true) } - if (Config.basic.sendReasoning) { + if (parseBooleanFlag(Config.basic.sendReasoning, true)) { for (let forwardElement of forward) { this.reply(forwardElement) } diff --git a/apps/management.js b/apps/management.js index 33c5ee59c..764dd848a 100644 --- a/apps/management.js +++ b/apps/management.js @@ -3,6 +3,7 @@ import { createCRUDCommandRules, createSwitchCommandRules } from '../utils/comma import { Chaite, VERSION } from 'chaite' import * as crypto from 'node:crypto' import common from '../../../lib/common/common.js' +import { parseBooleanFlag } from '../utils/common.js' export class ChatGPTManagement extends plugin { constructor () { @@ -95,6 +96,10 @@ export class ChatGPTManagement extends plugin { toggleChatReasoningForward (e) { const enable = e.msg.includes('开启') ChatGPTConfig.basic.sendReasoning = enable + if (typeof ChatGPTConfig.saveToFile === 'function') { + ChatGPTConfig.saveToFile('code') + } + logger.info(`[ChatGPT-Plugin] set basic.sendReasoning=${ChatGPTConfig.basic.sendReasoning} (type=${typeof ChatGPTConfig.basic.sendReasoning})`) this.reply(`普通对话思考过程转发已${enable ? '开启' : '关闭'}`) } @@ -160,7 +165,9 @@ export class ChatGPTManagement extends plugin { const defaultChatPresetId = ChatGPTConfig.llm.defaultChatPresetId const currentPreset = await Chaite.getInstance().getChatPresetManager().getInstance(defaultChatPresetId) msgs.push(`当前预设:${currentPreset?.name || '未设置'}${currentPreset ? ('\n\n' + currentPreset.toFormatedString(false)) : ''}`) - msgs.push(`普通对话思考过程转发:${ChatGPTConfig.basic.sendReasoning ? '开启' : '关闭'}\n伪人思考过程转发:${ChatGPTConfig.bym.sendReasoning ? '开启' : '关闭'}`) + const chatReasoningEnabled = parseBooleanFlag(ChatGPTConfig.basic.sendReasoning, true) + const bymReasoningEnabled = parseBooleanFlag(ChatGPTConfig.bym.sendReasoning, false) + msgs.push(`普通对话思考过程转发:${chatReasoningEnabled ? '开启' : '关闭'}\n伪人思考过程转发:${bymReasoningEnabled ? '开启' : '关闭'}`) const allTools = await Chaite.getInstance().getToolsManager().listInstances() let toolsMsg = `工具总数:${allTools.length}\n` diff --git a/config/config.js b/config/config.js index b80e8b5ad..29c016d9b 100644 --- a/config/config.js +++ b/config/config.js @@ -23,7 +23,7 @@ class ChatGPTConfig { // 触发前缀,仅在前缀触发时有效 togglePrefix: '#chat', // @ 或前缀触发对话时,是否发送思考/工具过程转发消息 - sendReasoning: true, + sendReasoning: false, // 是否开启调试模式 debug: false, // 一般命令的开头 diff --git a/index.js b/index.js index 12672820e..44e86b577 100644 --- a/index.js +++ b/index.js @@ -36,7 +36,13 @@ global.chatgpt = { } -ChatGPTConfig.startSync('./plugins/chatgpt-plugin/data') +// startSync returns proxied objects; bind them back so nested writes auto-persist +const proxiedConfig = ChatGPTConfig.startSync('./plugins/chatgpt-plugin/data') +for (const key of ['basic', 'bym', 'llm', 'management', 'chaite', 'memory']) { + if (proxiedConfig?.[key]) { + ChatGPTConfig[key] = proxiedConfig[key] + } +} initChaite() logger.info('chatgpt-plugin加载成功') logger.info(`当前版本${ChatGPTConfig.version}`) diff --git a/models/memory/extractor.js b/models/memory/extractor.js index fb6b1bb7d..ae7c136bb 100644 --- a/models/memory/extractor.js +++ b/models/memory/extractor.js @@ -251,6 +251,10 @@ export async function extractGroupFacts (messages) { logger.debug('[Memory] group fact extraction returned non-array content') return [] } catch (err) { + const detail = err?.error ?? err?.body ?? err?.response ?? err?.cause + if (detail) { + logger.error('Failed to extract group facts (API detail):', typeof detail === 'object' ? JSON.stringify(detail) : detail) + } logger.error('Failed to extract group facts:', err) return [] } @@ -300,6 +304,10 @@ export async function extractUserMemories (messages, existingMemories = []) { logger.debug('[Memory] user memory extraction returned non-array content') return [] } catch (err) { + const detail = err?.error ?? err?.body ?? err?.response ?? err?.cause + if (detail) { + logger.error('Failed to extract user memories:', typeof detail === 'object' ? JSON.stringify(detail) : detail) + } logger.error('Failed to extract user memories:', err) return [] } diff --git a/utils/common.js b/utils/common.js index bb697f90a..409ff52c0 100644 --- a/utils/common.js +++ b/utils/common.js @@ -75,3 +75,29 @@ if (!fs.existsSync(dataDir)) { export function generateId () { return Date.now().toString(36) + Math.random().toString(36).substring(2, 15) } + +/** + * Parse config flag values from panel/file into boolean. + * Accepts: true/false, 1/0, on/off, yes/no, enable/disable. + * @param {any} value + * @param {boolean} defaultValue + * @returns {boolean} + */ +export function parseBooleanFlag (value, defaultValue = false) { + if (typeof value === 'boolean') { + return value + } + if (typeof value === 'number') { + return value !== 0 + } + if (typeof value === 'string') { + const normalized = value.trim().toLowerCase() + if (['true', '1', 'on', 'yes', 'y', 'enable', 'enabled', '开启'].includes(normalized)) { + return true + } + if (['false', '0', 'off', 'no', 'n', 'disable', 'disabled', '关闭'].includes(normalized)) { + return false + } + } + return defaultValue +} diff --git a/utils/message.js b/utils/message.js index 34ef140c2..40ebbb9ba 100644 --- a/utils/message.js +++ b/utils/message.js @@ -227,7 +227,8 @@ export async function toYunzai (e, contents) { if (forward.length > 1) { const newForward = [await common.makeForwardMsg(e, forward, '多次思考过程')] return { - msgs: msgs.filter(i => !!i), newForward + msgs: msgs.filter(i => !!i), + forward: newForward } } return { From 85620991c28b464b3c7c6419d7f62e7e0874c681 Mon Sep 17 00:00:00 2001 From: buyun14 <2135481470@qq.com> Date: Tue, 14 Apr 2026 12:03:17 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat(models):=20=E4=BC=98=E5=8C=96Chaite?= =?UTF-8?q?=E4=BA=91=E6=9C=8D=E5=8A=A1=E9=85=8D=E7=BD=AE=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加authKey变更时的空值检查和重复性验证 - 改进配置合并过程中对authKey的处理逻辑 - 增加当前认证密钥的备份和恢复机制 - 避免不必要的全局配置同步操作 refactor(package): 更新项目依赖包版本 - 升级better-sqlite3从9.4.3到12.8.0版本 - 新增puppeteer相关依赖用于浏览器自动化 - 添加turndown依赖用于HTML到Markdown转换功能 --- models/chaite/cloud.js | 22 +++++++++++++++++++--- package.json | 5 ++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/models/chaite/cloud.js b/models/chaite/cloud.js index dfa8fd4e2..073b6cf50 100644 --- a/models/chaite/cloud.js +++ b/models/chaite/cloud.js @@ -148,8 +148,8 @@ export async function initChaite () { // 监听Chaite配置变化,同步需要同步的配置 chaite.on('config-change', obj => { const { key, newVal, oldVal } = obj - if (key === 'authKey') { - ChatGPTConfig.serverAuthKey = newVal + if (key === 'authKey' && newVal && ChatGPTConfig.chaite?.authKey !== newVal) { + ChatGPTConfig.chaite.authKey = newVal } logger.debug(`Chaite config changed: ${key} from ${oldVal} to ${newVal}`) }) @@ -161,6 +161,16 @@ export async function initChaite () { ChatGPTConfig._saveOrigin = 'chaite' try { + const currentAuthKey = ChatGPTConfig.chaite?.authKey || chaite.getGlobalConfig().getAuthKey() + if (config?.chaite && typeof config.chaite === 'object') { + const incomingAuthKey = typeof config.chaite.authKey === 'string' + ? config.chaite.authKey.trim() + : config.chaite.authKey + if (!incomingAuthKey) { + delete config.chaite.authKey + } + } + Object.keys(config).forEach(key => { if (typeof config[key] === 'object' && config[key] !== null && ChatGPTConfig[key]) { deepMerge(ChatGPTConfig[key], config[key]) @@ -169,9 +179,15 @@ export async function initChaite () { } }) + if (!ChatGPTConfig.chaite.authKey && currentAuthKey) { + ChatGPTConfig.chaite.authKey = currentAuthKey + } + // 回传部分需要同步的配置 chaite.getGlobalConfig().setDebug(ChatGPTConfig.basic.debug) - chaite.getGlobalConfig().setAuthKey(ChatGPTConfig.chaite.authKey) + if (ChatGPTConfig.chaite.authKey && chaite.getGlobalConfig().getAuthKey() !== ChatGPTConfig.chaite.authKey) { + chaite.getGlobalConfig().setAuthKey(ChatGPTConfig.chaite.authKey) + } // 使用新的触发保存方法,而不是直接调用saveToFile ChatGPTConfig._triggerSave('chaite') diff --git a/package.json b/package.json index 7a4e110fa..b7fc6989f 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,17 @@ "type": "module", "author": "ikechan8370", "dependencies": { - "better-sqlite3": "^9.4.3", "adm-zip": "^0.5.10", + "better-sqlite3": "^12.8.0", "chaite": "^1.8.2", "js-yaml": "^4.1.0", "keyv": "^5.3.1", "keyv-file": "^5.1.2", "lowdb": "^7.0.1", + "puppeteer-extra": "^3.3.6", + "puppeteer-extra-plugin-stealth": "^2.11.2", "sqlite-vec": "^0.1.7-alpha.2", + "turndown": "^7.2.2", "vectra": "^0.9.0" }, "peerDependencies": { From e8a2616ed3f0afc559402fed58a3fac18db73df6 Mon Sep 17 00:00:00 2001 From: buyun14 <2135481470@qq.com> Date: Tue, 14 Apr 2026 12:17:02 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20MCP=20?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E9=85=8D=E7=BD=AE=E5=92=8C=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加了完整的 MCP (Model Context Protocol) 兼容层实现,支持将 MCP 工具动态桥接为 Chaite CustomTool。新增 mcp 配置项,包括启用状态、 工具名称前缀、服务器配置等。实现了 MCP 客户端连接管理、工具路由注 册、桥接工具创建等功能。同时更新了配置文件结构和初始化流程以集成新的 MCP 功能模块。 --- config/config.js | 45 ++++++++- index.js | 2 +- models/chaite/cloud.js | 6 ++ package.json | 1 + utils/mcp/manager.js | 208 +++++++++++++++++++++++++++++++++++++++++ utils/mcp/registry.js | 112 ++++++++++++++++++++++ 6 files changed, 372 insertions(+), 2 deletions(-) create mode 100644 utils/mcp/manager.js create mode 100644 utils/mcp/registry.js diff --git a/config/config.js b/config/config.js index 29c016d9b..5b56df62d 100644 --- a/config/config.js +++ b/config/config.js @@ -187,6 +187,47 @@ class ChatGPTConfig { storage: 'sqlite' } + /** + * MCP 兼容配置(标准 Model Context Protocol) + * @type {{ + * enable: boolean, + * toolNamePrefix: string, + * removeStaleBridgeToolsOnStart: boolean, + * servers: Array<{ + * id: string, + * enable: boolean, + * transport: 'stdio' | 'sse' | 'streamable-http', + * command?: string, + * args?: string[], + * env?: Record, + * cwd?: string, + * url?: string, + * includeTools?: string[], + * excludeTools?: string[] + * }> + * }} + */ + mcp = { + enable: false, + // 生成到 Chaite 工具池中的前缀,避免与本地工具重名 + toolNamePrefix: 'mcp', + // 启动时清理旧的桥接工具(id 前缀为 mcp_bridge_) + removeStaleBridgeToolsOnStart: true, + servers: [ + { + id: 'filesystem', + enable: false, + transport: 'stdio', + command: '', + args: [], + env: {}, + cwd: '', + includeTools: [], + excludeTools: [] + } + ] + } + /** * 记忆系统配置 * @type {{ @@ -511,7 +552,7 @@ Return a JSON array of **strings**, and nothing else, without any other characte return result } - const sections = ['version', 'basic', 'bym', 'llm', 'management', 'chaite', 'memory'] + const sections = ['version', 'basic', 'bym', 'llm', 'management', 'chaite', 'mcp', 'memory'] for (const key of sections) { const loadedValue = loadedConfig[key] if (loadedValue === undefined) { @@ -561,6 +602,7 @@ Return a JSON array of **strings**, and nothing else, without any other characte llm: this.llm, management: this.management, chaite: this.chaite, + mcp: this.mcp, memory: this.memory } @@ -582,6 +624,7 @@ Return a JSON array of **strings**, and nothing else, without any other characte llm: this.llm, management: this.management, chaite: this.chaite, + mcp: this.mcp, memory: this.memory } } diff --git a/index.js b/index.js index 44e86b577..d38ae11ca 100644 --- a/index.js +++ b/index.js @@ -38,7 +38,7 @@ global.chatgpt = { // startSync returns proxied objects; bind them back so nested writes auto-persist const proxiedConfig = ChatGPTConfig.startSync('./plugins/chatgpt-plugin/data') -for (const key of ['basic', 'bym', 'llm', 'management', 'chaite', 'memory']) { +for (const key of ['basic', 'bym', 'llm', 'management', 'chaite', 'mcp', 'memory']) { if (proxiedConfig?.[key]) { ChatGPTConfig[key] = proxiedConfig[key] } diff --git a/models/chaite/cloud.js b/models/chaite/cloud.js index 073b6cf50..edc59c772 100644 --- a/models/chaite/cloud.js +++ b/models/chaite/cloud.js @@ -34,6 +34,7 @@ import SQLiteTriggerStorage from './storage/sqlite/trigger_storage.js' import LowDBTriggerStorage from './storage/lowdb/trigger_storage,.js' import { createChaiteVectorizer } from './vectorizer.js' import { MemoryRouter, authenticateMemoryRequest } from '../memory/router.js' +import { disposeMcpCompatibility, initMcpCompatibility } from '../../utils/mcp/manager.js' /** * 认证,以便共享上传 @@ -141,6 +142,7 @@ export async function initChaite () { } } await initRagManager(ChatGPTConfig.llm.embeddingModel, ChatGPTConfig.llm.dimensions) + await initMcpCompatibility(toolsManager) if (!ChatGPTConfig.chaite.authKey) { ChatGPTConfig.chaite.authKey = Chaite.getInstance().getFrontendAuthHandler().generateToken(0, true) } @@ -206,6 +208,10 @@ export async function initChaite () { chaite.runApiServer(app => { app.use('/api/memory', authenticateMemoryRequest, MemoryRouter) }) + + process.once('beforeExit', async () => { + await disposeMcpCompatibility() + }) } function deepMerge (target, source) { diff --git a/package.json b/package.json index b7fc6989f..8bf1a404d 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "type": "module", "author": "ikechan8370", "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", "adm-zip": "^0.5.10", "better-sqlite3": "^12.8.0", "chaite": "^1.8.2", diff --git a/utils/mcp/manager.js b/utils/mcp/manager.js new file mode 100644 index 000000000..425a4f06e --- /dev/null +++ b/utils/mcp/manager.js @@ -0,0 +1,208 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import ChatGPTConfig from '../../config/config.js' +import { md5 } from '../common.js' +import { + clearMcpToolRoutes, + disposeMcpRegistry, + registerMcpServerClient, + registerMcpToolRoute +} from './registry.js' + +const BRIDGE_TOOL_ID_PREFIX = 'mcp_bridge_' + +function sanitizeToolName (name = '') { + return String(name).replace(/[^a-zA-Z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '') +} + +function buildBridgeToolName (serverId, mcpToolName) { + const prefix = sanitizeToolName(ChatGPTConfig.mcp?.toolNamePrefix || 'mcp') || 'mcp' + const s = sanitizeToolName(serverId) || 'server' + const t = sanitizeToolName(mcpToolName) || 'tool' + return `${prefix}_${s}_${t}` +} + +function buildBridgeToolCode (className, bridgeToolName, schema, description = '') { + const fnSchema = JSON.stringify({ + name: bridgeToolName, + description: description || `MCP bridge tool: ${bridgeToolName}`, + parameters: schema && typeof schema === 'object' + ? schema + : { + type: 'object', + properties: {}, + required: [] + } + }, null, 2) + + return `import { CustomTool } from 'chaite'\nimport { callMcpBridgeTool } from '../mcp/registry.js'\n\nclass ${className} extends CustomTool {\n name = '${bridgeToolName}'\n\n function = ${fnSchema}\n\n async run(args) {\n return callMcpBridgeTool('${bridgeToolName}', args || {})\n }\n}\n\nexport default new ${className}()\n` +} + +function shouldIncludeTool (server, mcpToolName) { + const includes = Array.isArray(server.includeTools) ? server.includeTools : [] + const excludes = Array.isArray(server.excludeTools) ? server.excludeTools : [] + + if (includes.length > 0 && !includes.includes(mcpToolName)) { + return false + } + if (excludes.includes(mcpToolName)) { + return false + } + return true +} + +async function clearOldBridgeTools (toolsManager) { + const all = await toolsManager.listInstances() + for (const t of all) { + if (t?.id?.startsWith(BRIDGE_TOOL_ID_PREFIX)) { + try { + await toolsManager.deleteInstance(t.id) + } catch (err) { + logger.warn(`[MCP] 删除旧桥接工具失败 ${t.id}: ${err?.message || err}`) + } + } + } +} + +async function createStdioTransport (server) { + if (!server.command || !String(server.command).trim()) { + throw new Error(`MCP server ${server.id} 缺少 command 配置`) + } + + const transport = new StdioClientTransport({ + command: String(server.command).trim(), + args: Array.isArray(server.args) ? server.args.map(a => String(a)) : [], + env: server.env && typeof server.env === 'object' + ? Object.fromEntries(Object.entries(server.env).map(([k, v]) => [String(k), String(v)])) + : undefined, + cwd: server.cwd && String(server.cwd).trim() + ? String(server.cwd).trim() + : undefined, + stderr: 'pipe' + }) + + if (transport.stderr) { + transport.stderr.on('data', (buf) => { + const line = String(buf || '').trim() + if (line) { + logger.debug(`[MCP:${server.id}:stderr] ${line}`) + } + }) + } + + return transport +} + +async function connectServer (server) { + if (server.transport !== 'stdio') { + logger.warn(`[MCP] 暂不支持 transport=${server.transport},当前仅实现 stdio`) + return null + } + + const client = new Client( + { + name: 'chatgpt-plugin-mcp-client', + version: ChatGPTConfig.version || '3.0.0' + }, + { + capabilities: {} + } + ) + + const transport = await createStdioTransport(server) + await client.connect(transport) + return { client, transport } +} + +/** + * 初始化 MCP 兼容层:把 MCP tools 动态桥接成 Chaite CustomTool + * @param {import('chaite').ToolManager} toolsManager + */ +export async function initMcpCompatibility (toolsManager) { + const conf = ChatGPTConfig.mcp + if (!conf?.enable) { + return + } + + const servers = Array.isArray(conf.servers) ? conf.servers : [] + if (servers.length === 0) { + logger.warn('[MCP] 已开启但未配置 servers') + return + } + + await disposeMcpRegistry() + clearMcpToolRoutes() + + if (conf.removeStaleBridgeToolsOnStart !== false) { + await clearOldBridgeTools(toolsManager) + } + + const nameSet = new Set() + + for (const server of servers) { + if (!server?.enable) { + continue + } + if (!server.id) { + logger.warn('[MCP] 忽略一个未设置 id 的 server 配置') + continue + } + + try { + const connected = await connectServer(server) + if (!connected) { + continue + } + + registerMcpServerClient(server.id, connected) + const toolList = await connected.client.listTools() + const tools = Array.isArray(toolList?.tools) ? toolList.tools : [] + + logger.info(`[MCP] 服务 ${server.id} 已连接,发现工具 ${tools.length} 个`) + + for (const tool of tools) { + const mcpToolName = tool?.name + if (!mcpToolName || !shouldIncludeTool(server, mcpToolName)) { + continue + } + + let bridgeToolName = buildBridgeToolName(server.id, mcpToolName) + if (nameSet.has(bridgeToolName)) { + bridgeToolName = `${bridgeToolName}_${md5(`${server.id}:${mcpToolName}`).slice(0, 6)}` + } + nameSet.add(bridgeToolName) + + registerMcpToolRoute(bridgeToolName, { + serverId: server.id, + mcpToolName + }) + + const className = `McpBridgeTool_${md5(`${server.id}:${mcpToolName}`).slice(0, 10)}` + const toolCode = buildBridgeToolCode( + className, + bridgeToolName, + tool.inputSchema, + `[MCP:${server.id}] ${tool.description || mcpToolName}` + ) + + await toolsManager.addInstance({ + id: `${BRIDGE_TOOL_ID_PREFIX}${md5(`${server.id}:${mcpToolName}`)}`, + name: bridgeToolName, + description: `[MCP:${server.id}] ${tool.description || mcpToolName}`, + code: toolCode, + permission: 'private', + status: 'enabled' + }) + } + } catch (err) { + logger.error(`[MCP] 初始化 server=${server.id} 失败:`, err) + } + } +} + +/** + * 关闭 MCP 连接 + */ +export async function disposeMcpCompatibility () { + await disposeMcpRegistry() +} diff --git a/utils/mcp/registry.js b/utils/mcp/registry.js new file mode 100644 index 000000000..01abc9ebc --- /dev/null +++ b/utils/mcp/registry.js @@ -0,0 +1,112 @@ +const mcpServerClients = new Map() +const mcpToolRoutes = new Map() + +/** + * 注册 MCP 服务器客户端 + * @param {string} serverId + * @param {{ client: any, transport: any }} payload + */ +export function registerMcpServerClient (serverId, payload) { + mcpServerClients.set(serverId, payload) +} + +/** + * 注册 Chaite 工具名到 MCP 工具路由 + * @param {string} chaiteToolName + * @param {{ serverId: string, mcpToolName: string }} route + */ +export function registerMcpToolRoute (chaiteToolName, route) { + mcpToolRoutes.set(chaiteToolName, route) +} + +/** + * 清空所有工具路由 + */ +export function clearMcpToolRoutes () { + mcpToolRoutes.clear() +} + +/** + * 关闭并清空所有 MCP 客户端 + */ +export async function disposeMcpRegistry () { + const tasks = [] + for (const [serverId, payload] of mcpServerClients.entries()) { + tasks.push((async () => { + try { + await payload?.transport?.close?.() + } catch (err) { + logger.warn(`[MCP] 关闭服务 ${serverId} 失败: ${err?.message || err}`) + } + })()) + } + await Promise.allSettled(tasks) + mcpServerClients.clear() + mcpToolRoutes.clear() +} + +function normalizeMcpCallResult (result) { + if (!result) { + return '' + } + + const chunks = [] + if (Array.isArray(result.content)) { + for (const item of result.content) { + if (!item) { + continue + } + if (item.type === 'text') { + chunks.push(item.text || '') + continue + } + if (item.type === 'image') { + chunks.push(`[image] ${item.mimeType || ''}`.trim()) + continue + } + chunks.push(JSON.stringify(item)) + } + } + + if (chunks.length === 0 && result.structuredContent !== undefined) { + chunks.push(JSON.stringify(result.structuredContent, null, 2)) + } + + if (chunks.length === 0) { + chunks.push(JSON.stringify(result, null, 2)) + } + + const text = chunks.join('\n').trim() + if (result.isError) { + return `MCP工具调用失败: ${text || 'unknown error'}` + } + return text +} + +/** + * 由桥接工具调用 MCP 服务 + * @param {string} chaiteToolName + * @param {Record} args + */ +export async function callMcpBridgeTool (chaiteToolName, args = {}) { + const route = mcpToolRoutes.get(chaiteToolName) + if (!route) { + return `MCP路由不存在: ${chaiteToolName}` + } + + const payload = mcpServerClients.get(route.serverId) + if (!payload?.client) { + return `MCP服务未连接: ${route.serverId}` + } + + try { + const result = await payload.client.callTool({ + name: route.mcpToolName, + arguments: args || {} + }) + return normalizeMcpCallResult(result) + } catch (err) { + logger.error(`[MCP] 调用工具失败 ${route.serverId}/${route.mcpToolName}:`, err) + return `MCP工具调用异常: ${err?.message || String(err)}` + } +} From 49b64168a134ca1e54e5ac4bbd9e9e3b0543c4a4 Mon Sep 17 00:00:00 2001 From: buyun14 <2135481470@qq.com> Date: Tue, 14 Apr 2026 16:14:10 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat(mcp):=20=E6=B7=BB=E5=8A=A0MCP=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7=E6=94=AF=E6=8C=81=E5=B9=B6=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入initMcpCompatibility函数用于初始化MCP兼容性 - 支持stdio和streamable-http两种传输类型 - 添加环境变量占位符解析功能 - 增加服务器ID和启用状态标准化处理 - 在管理插件中添加刷新MCP工具命令 - 实现MCP工具的动态加载和桥接功能 --- apps/management.js | 30 +++++++++++ utils/mcp/manager.js | 120 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 132 insertions(+), 18 deletions(-) diff --git a/apps/management.js b/apps/management.js index 764dd848a..9a81e47c4 100644 --- a/apps/management.js +++ b/apps/management.js @@ -4,6 +4,7 @@ import { Chaite, VERSION } from 'chaite' import * as crypto from 'node:crypto' import common from '../../../lib/common/common.js' import { parseBooleanFlag } from '../utils/common.js' +import { initMcpCompatibility } from '../utils/mcp/manager.js' export class ChatGPTManagement extends plugin { constructor () { @@ -37,6 +38,11 @@ export class ChatGPTManagement extends plugin { reg: `^${cmdPrefix}(查看)?(当前)?(配置|信息|统计信息|状态)$`, fnc: 'currentStatus', permission: 'master' + }, + { + reg: `^${cmdPrefix}(刷新|重载)(MCP|mcp)(工具)?$`, + fnc: 'refreshMcpTools', + permission: 'master' } ] }) @@ -188,4 +194,28 @@ export class ChatGPTManagement extends plugin { const m = await common.makeForwardMsg(e, msgs, e.msg) e.reply(m) } + + async refreshMcpTools (e) { + if (!ChatGPTConfig.mcp?.enable) { + await this.reply('MCP 未开启,请先在配置中启用 mcp.enable') + return false + } + + const toolsManager = Chaite.getInstance().getToolsManager() + const before = await toolsManager.listInstances() + + await this.reply('开始刷新 MCP 工具,请稍候...') + try { + await initMcpCompatibility(toolsManager) + const after = await toolsManager.listInstances() + const mcpPrefix = `${ChatGPTConfig.mcp?.toolNamePrefix || 'mcp'}_` + const mcpCount = after.filter(t => String(t?.name || '').startsWith(mcpPrefix)).length + await this.reply(`MCP 工具刷新完成\n工具总数:${before.length} -> ${after.length}\n桥接工具数:${mcpCount}`) + return true + } catch (err) { + logger.error('[MCP] 手动刷新工具失败:', err) + await this.reply(`MCP 工具刷新失败:${err?.message || err}`) + return false + } + } } diff --git a/utils/mcp/manager.js b/utils/mcp/manager.js index 425a4f06e..65071eeb2 100644 --- a/utils/mcp/manager.js +++ b/utils/mcp/manager.js @@ -1,5 +1,6 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import ChatGPTConfig from '../../config/config.js' import { md5 } from '../common.js' import { @@ -11,6 +12,41 @@ import { const BRIDGE_TOOL_ID_PREFIX = 'mcp_bridge_' +function resolveEnvPlaceholders (value) { + if (typeof value !== 'string') { + return value + } + return value.replace(/\$\{([A-Z0-9_]+)\}/gi, (_m, varName) => { + const envVal = process.env[varName] + return envVal == null ? '' : String(envVal) + }) +} + +function normalizeTransportType (server) { + const raw = String(server.transport || server.type || 'stdio').trim().toLowerCase() + if (raw === 'streamablehttp' || raw === 'streamable-http' || raw === 'streamable_http') { + return 'streamable-http' + } + if (raw === 'sse') { + return 'sse' + } + return 'stdio' +} + +function normalizeServerId (server) { + return String(server.id || server.name || '').trim() +} + +function normalizeServerEnabled (server) { + if (typeof server.enable === 'boolean') { + return server.enable + } + if (typeof server.isActive === 'boolean') { + return server.isActive + } + return false +} + function sanitizeToolName (name = '') { return String(name).replace(/[^a-zA-Z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '') } @@ -65,8 +101,9 @@ async function clearOldBridgeTools (toolsManager) { } async function createStdioTransport (server) { + const serverId = normalizeServerId(server) if (!server.command || !String(server.command).trim()) { - throw new Error(`MCP server ${server.id} 缺少 command 配置`) + throw new Error(`MCP server ${serverId} 缺少 command 配置`) } const transport = new StdioClientTransport({ @@ -93,12 +130,46 @@ async function createStdioTransport (server) { return transport } -async function connectServer (server) { - if (server.transport !== 'stdio') { - logger.warn(`[MCP] 暂不支持 transport=${server.transport},当前仅实现 stdio`) - return null +async function createStreamableHttpTransport (server) { + const serverId = normalizeServerId(server) + const rawUrl = String(server.baseUrl || server.url || '').trim() + if (!rawUrl) { + throw new Error(`MCP server ${serverId} 缺少 baseUrl/url 配置`) } + const rawHeaders = server.headers && typeof server.headers === 'object' + ? server.headers + : {} + + const headers = {} + for (const [k, v] of Object.entries(rawHeaders)) { + headers[String(k)] = resolveEnvPlaceholders(String(v || '')) + } + + const auth = headers.Authorization || headers.authorization + if (/\$\{[A-Z0-9_]+\}/i.test(String(rawHeaders.Authorization || rawHeaders.authorization || ''))) { + const authValue = String(auth || '').trim() + const hasToken = /^Bearer\s+\S+/i.test(authValue) + if (!hasToken) { + logger.warn(`[MCP] 服务 ${serverId} 的 Authorization 占位变量未设置,已跳过连接`) + return null + } + } + + const requestInit = {} + if (Object.keys(headers).length > 0) { + requestInit.headers = headers + } + + return new StreamableHTTPClientTransport(new URL(rawUrl), { + requestInit + }) +} + +async function connectServer (server) { + const transportType = normalizeTransportType(server) + const serverId = normalizeServerId(server) + const client = new Client( { name: 'chatgpt-plugin-mcp-client', @@ -109,7 +180,19 @@ async function connectServer (server) { } ) - const transport = await createStdioTransport(server) + let transport + if (transportType === 'stdio') { + transport = await createStdioTransport(server) + } else if (transportType === 'streamable-http') { + transport = await createStreamableHttpTransport(server) + if (!transport) { + return null + } + } else { + logger.warn(`[MCP] 暂不支持 transport=${transportType},server=${serverId}`) + return null + } + await client.connect(transport) return { client, transport } } @@ -140,10 +223,11 @@ export async function initMcpCompatibility (toolsManager) { const nameSet = new Set() for (const server of servers) { - if (!server?.enable) { + if (!normalizeServerEnabled(server)) { continue } - if (!server.id) { + const serverId = normalizeServerId(server) + if (!serverId) { logger.warn('[MCP] 忽略一个未设置 id 的 server 配置') continue } @@ -154,11 +238,11 @@ export async function initMcpCompatibility (toolsManager) { continue } - registerMcpServerClient(server.id, connected) + registerMcpServerClient(serverId, connected) const toolList = await connected.client.listTools() const tools = Array.isArray(toolList?.tools) ? toolList.tools : [] - logger.info(`[MCP] 服务 ${server.id} 已连接,发现工具 ${tools.length} 个`) + logger.info(`[MCP] 服务 ${serverId} 已连接,发现工具 ${tools.length} 个`) for (const tool of tools) { const mcpToolName = tool?.name @@ -166,36 +250,36 @@ export async function initMcpCompatibility (toolsManager) { continue } - let bridgeToolName = buildBridgeToolName(server.id, mcpToolName) + let bridgeToolName = buildBridgeToolName(serverId, mcpToolName) if (nameSet.has(bridgeToolName)) { - bridgeToolName = `${bridgeToolName}_${md5(`${server.id}:${mcpToolName}`).slice(0, 6)}` + bridgeToolName = `${bridgeToolName}_${md5(`${serverId}:${mcpToolName}`).slice(0, 6)}` } nameSet.add(bridgeToolName) registerMcpToolRoute(bridgeToolName, { - serverId: server.id, + serverId, mcpToolName }) - const className = `McpBridgeTool_${md5(`${server.id}:${mcpToolName}`).slice(0, 10)}` + const className = `McpBridgeTool_${md5(`${serverId}:${mcpToolName}`).slice(0, 10)}` const toolCode = buildBridgeToolCode( className, bridgeToolName, tool.inputSchema, - `[MCP:${server.id}] ${tool.description || mcpToolName}` + `[MCP:${serverId}] ${tool.description || mcpToolName}` ) await toolsManager.addInstance({ - id: `${BRIDGE_TOOL_ID_PREFIX}${md5(`${server.id}:${mcpToolName}`)}`, + id: `${BRIDGE_TOOL_ID_PREFIX}${md5(`${serverId}:${mcpToolName}`)}`, name: bridgeToolName, - description: `[MCP:${server.id}] ${tool.description || mcpToolName}`, + description: `[MCP:${serverId}] ${tool.description || mcpToolName}`, code: toolCode, permission: 'private', status: 'enabled' }) } } catch (err) { - logger.error(`[MCP] 初始化 server=${server.id} 失败:`, err) + logger.error(`[MCP] 初始化 server=${serverId} 失败:`, err) } } } From 9271e8ad8d14cbc646c5ee7ebe72f239c1e245d9 Mon Sep 17 00:00:00 2001 From: buyun14 <2135481470@qq.com> Date: Tue, 14 Apr 2026 16:28:54 +0800 Subject: [PATCH 6/9] =?UTF-8?q?docs(README):=20=E6=9B=B4=E6=96=B0readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 225 +++++++++++++++++++------------------------------- readme_old.md | 170 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+), 141 deletions(-) create mode 100644 readme_old.md diff --git a/README.md b/README.md index 37ea41445..153464f90 100644 --- a/README.md +++ b/README.md @@ -1,170 +1,113 @@ -![chatgpt-plugin](https://socialify.git.ci/ikechan8370/chatgpt-plugin/image?description=1&font=Jost&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light) -
+# chatgpt-plugin - - - - -
+基于 Chaite 内核的 Yunzai 对话插件,支持多模型渠道、工具调用、记忆系统、伪人模式,以及标准 MCP 工具桥接(stdio / streamable-http)。 +旧版说明文档已保留为 `readme_old.md`。 +## 主要能力 -![26224FE397F1E74104C1C007C1A32DDE](https://user-images.githubusercontent.com/21212372/227718994-4d33da74-6886-41d5-afd0-73986b086df0.gif) - +- 多渠道与预设管理(可在面板或命令行操作) +- 群上下文注入与多轮会话管理 +- 工具调用与推理过程回显 +- 群记忆 / 用户记忆(可独立开关) +- BYM 伪人模式 +- MCP 兼容桥接: + - 本地 `stdio` MCP 服务 + - 远程 `streamable-http` MCP 服务 + - 动态生成 Chaite 工具并加入工具池 +## 安装 +```bash +cd plugins +git clone https://github.com/ikechan8370/chatgpt-plugin.git +cd chatgpt-plugin +pnpm install +``` -> 插件v3大幅重构中,基本可用,持续完善中。遇到问题请提issue,欢迎PR。 -> todo列表: -> - [x] 插件v3重构完成,插件基本功能可用,持续完善中。 -> - [ ] RAG知识库 -> - [ ] 预设更详细的配置 -> - [x] 自定义触发器 -> - [ ] 自定义插件 -> - [ ] 兼容mcp +然后重启 Yunzai。 +## 关键配置 -## 插件简介 +运行时配置文件:`plugins/chatgpt-plugin/data/config.json` -ChatGPT-Plugin 以 Chaite 为内核,将多模型渠道、工具、处理器、触发器、RAG 和管控面板封装成一套适配 Miao-Yunzai / Yunzai-Bot 的插件方案。通过 Chaite 的 API 服务器与可插拔的存储层(默认 SQLite),插件可以在本地完成高并发对话、知识库检索、伪人陪聊以及记忆管理,亦可接入 Chaite Cloud 复用在线渠道与工具。 +基础配置示例: -## 核心特性 +```json +{ + "basic": { + "toggleMode": "at", + "togglePrefix": "#chat", + "commandPrefix": "#chatgpt" + } +} +``` -- **多渠道与预设体系**:依托 Chaite 的 ChannelsManager 与 ChatPresetManager,支持为不同模型配置流量、负载均衡与个性化 prompt,群友也可在授权后自助切换预设。 -- **高级消息适配**:前后文触发方式支持 `@Bot` 与前缀;自动处理引用、图片、语音等多模态输入,并在工具调用或推理阶段通过转发消息回显。 -- **群上下文与伪人模式**:可按配置注入指定条数的群聊记录;BYM 伪人模式支持概率触发、关键词命中、预设覆盖及限时撤回,营造更拟人的陪聊体验。 -- **记忆与 RAG**:内置 memoryService + vectra 向量索引,提供群记忆、私人记忆与外部知识库(RAGManager)注入能力,支持混合检索与手动管理。 -- **可视化与指令双管控**:`#chatgpt管理面板` 一键获取面板 token,Web 端即可操作渠道、工具、触发器;同时保留完整的命令行 CRUD 指令。 -- **自动更新与依赖管理**:`#chatgpt更新` / `#chatgpt强制更新` 调用 git 同步仓库并自动更新 chaite 依赖,减少手动维护成本。 +## MCP 配置示例 -## 快速安装 +### 1) 本地 stdio(示例:AntV 图表) -1. **克隆代码** - ```bash - cd plugins - git clone https://github.com/ikechan8370/chatgpt-plugin.git - ``` -2. **安装依赖**(推荐 Node.js ≥ 18 + pnpm ≥ 8) - ```bash - cd chatgpt-plugin - pnpm install - ``` - 若安装日志出现 `Ignored build scripts: better-sqlite3` 或运行时报错找不到 `better-sqlite3` bindings,可执行 `pnpm approve-builds`,在交互列表中勾选全部或仅 `better-sqlite3` 以允许编译。然后再次 `pnpm install` -3. **在 Yunzai 中启用插件** - - 重启机器人或运行 `node app` 让插件自动加载。 - - 首次启动会在 `plugins/chatgpt-plugin/config/` 下生成 `config.json / config.yaml`。 -4. **保持更新** - - 主人账号发送 `#chatgpt更新` 获取最新版本。 - - `#chatgpt强制更新` 会放弃本地修改后重新拉取,请谨慎使用。 +```json +{ + "id": "antv_chart", + "enable": true, + "transport": "stdio", + "command": "npx", + "args": ["-y", "@antv/mcp-server-chart"] +} +``` -## 配置指引 +### 2) 远程 streamable-http(示例:阿里云代码解释器) + +```json +{ + "id": "code_interpreter_mcp", + "enable": true, + "type": "streamableHttp", + "transport": "streamable-http", + "baseUrl": "https://dashscope.aliyuncs.com/api/v1/mcps/code_interpreter_mcp/mcp", + "headers": { + "Authorization": "Bearer ${DASHSCOPE_API_KEY}" + } +} +``` -配置文件默认位于 `plugins/chatgpt-plugin/config/config.json`,也可改写为 YAML。常用字段示例: +环境变量建议放在:`config/pm2.yaml` ```yaml -basic: - toggleMode: at # at / prefix - togglePrefix: "#chat" # prefix 模式下的触发词 - sendReasoning: true # 普通对话是否转发思考/工具过程 - commandPrefix: "#chatgpt" # 管理指令前缀 -llm: - defaultModel: "gpt-4o-mini" - defaultChatPresetId: "default" - enableGroupContext: true - groupContextLength: 20 -bym: - enable: false - probability: 0.02 - defaultPreset: "bym_default" -chaite: - cloudApiKey: "" # 可选,接入 Chaite Cloud - host: "0.0.0.0" - port: 48370 -memory: - group: - enable: false - enabledGroups: ["123456"] - user: - enable: false - whitelist: ["123456789"] +env: + app_type: pm2 + DASHSCOPE_API_KEY: "你的 DashScope Key" ``` -- **basic**:控制触发方式、普通对话思考过程转发、调试与命令前缀。 -- **llm**:定义默认模型、嵌入模型、群上下文等。`defaultChatPresetId` 需在面板或命令中提前创建。 -- **chaite**:`storage` 默认 SQLite,会在 `plugins/chatgpt-plugin/data/data.db` 生成数据文件;如接入 Chaite Cloud,请填入 `cloudApiKey` 并开放 `host/port`。 -- **bym**:配置伪人触发概率、关键词映射、撤回与思考内容开关。 -- **memory**:为群记忆或私人记忆开启检索、模型与提示词,可按需启用 `extensions.simple` 以加载自定义词典。 - -修改后保存文件,插件会自动热加载;在 Chaite 面板修改配置时也会反向写回本地文件。 - -## 使用方式 - -### 基础对话 - -- `@Bot 你好` 或 `#chat 今天天气如何` 触发默认预设,插件会保持用户 `conversationId` 与 `messageId`,自动续写多轮对话。 -- 回复图片/文本可作为上下文输入,模型返回的图片、语音与思考内容会自动转换为 QQ 消息或转发记录。 -- `#结束对话` 仅清空自己的会话;`#结束全部对话` 需主人权限。 - -### 管理命令 & 面板 - -- `#chatgpt管理面板`:生成一次性 token,访问 `http://:` 即可使用 Chaite Web 面板。 -- `#chatgpt开启思考转发` / `#chatgpt关闭思考转发`:控制 `@Bot` 或前缀触发普通对话时是否发送思考过程转发消息。 -- CRUD 命令示例(均支持 `列表 / 添加 / 查看 / 删除`): - ``` - #chatgpt渠道列表 - #chatgpt预设添加 角色扮演 {...} - #chatgpt工具删除 web-search - #chatgpt处理器查看 markdown - ``` -- `#chatgpt调试模式开关`、`#chatgpt伪人开关` 等指令可快速切换全局开关。 - -### 伪人(BYM)模式 - -1. 在配置中启用 `bym.enable` 并指定 `defaultPreset` 或 `presetMap`。 -2. 伪人会在命中关键词或达到概率阈值时主动发言,可通过 `presetPrefix` 调整统一人设,`temperature/maxTokens` 控制语气与长度。 -3. 支持为不同关键词配置 `recall` 秒数,实现“发完撤回”效果。 - -### 记忆系统 - -- **群记忆指令** - ``` - #群记忆 #仅群聊 - #删除群记忆 1 #主人或群管 - #记忆列表 #主人查看全局开关 - ``` -- **私人记忆指令** - ``` - #记忆 / 我的记忆 - #他的记忆 @xxx #群聊内查看他人(需其授权) - #删除记忆 1 - ``` -- 记忆抽取依赖配置中的 `memory.group` / `memory.user` 模型与预设,collector 会定期读取群聊历史,必要时可在 `enabledGroups` 中按群号白名单控制。 - -### 更新与维护 - -- `#chatgpt更新`:git pull 插件仓库并使用 pnpm/npm 更新 chaite 依赖。 -- `#chatgpt强制更新`:在更新前执行 `git checkout .`,用于舍弃本地改动。 -- 日志会通过转发消息发送最近 20 条 commit,方便追踪版本变化。 - - -## 赞助 - -如果觉得本项目好玩或者对你有帮助,愿意的话可以赞助我一口快乐水: +## 常用命令 -https://afdian.net/a/ikechan8370 +- `#chatgpt管理面板` +- `#chatgpt查看状态` +- `#结束对话` +- `#chatgpt开启思考转发` +- `#chatgpt关闭思考转发` -## 贡献者 +MCP 相关: -感谢以下贡献者 +- `#chatgpt刷新MCP` +- `#chatgpt重载MCP工具` - - - +这两个命令会重新连接已启用 MCP 服务并重建桥接工具,不必重启机器人。 +## 故障排查 -![Alt](https://repobeats.axiom.co/api/embed/076d597ede41432208435f233d18cb20052fb90a.svg "Repobeats analytics image") +- 提示 `No API-key provided`:检查 `DASHSCOPE_API_KEY` 是否注入到 PM2 环境。 +- 启动后无 MCP 工具: + - 确认 `mcp.enable=true` + - 确认服务项 `enable=true` + - 发送 `#chatgpt刷新MCP` 强制重载 +- `stdio` 服务连接失败:检查 `command/args` 是否可在当前机器执行。 -## Star History +## 目录说明 -[![Star History Chart](https://api.star-history.com/svg?repos=ikechan8370/chatgpt-plugin&type=Date)](https://star-history.com/#ikechan8370/chatgpt-plugin&Date) +- `apps/`:命令与消息入口 +- `models/`:Chaite 与存储实现 +- `utils/mcp/`:MCP 注册与桥接管理 +- `data/`:运行时配置与数据 diff --git a/readme_old.md b/readme_old.md new file mode 100644 index 000000000..37ea41445 --- /dev/null +++ b/readme_old.md @@ -0,0 +1,170 @@ +![chatgpt-plugin](https://socialify.git.ci/ikechan8370/chatgpt-plugin/image?description=1&font=Jost&forks=1&issues=1&language=1&name=1&owner=1&pulls=1&stargazers=1&theme=Light) +
+ + + + + +
+ + + +![26224FE397F1E74104C1C007C1A32DDE](https://user-images.githubusercontent.com/21212372/227718994-4d33da74-6886-41d5-afd0-73986b086df0.gif) + + + + +> 插件v3大幅重构中,基本可用,持续完善中。遇到问题请提issue,欢迎PR。 +> todo列表: +> - [x] 插件v3重构完成,插件基本功能可用,持续完善中。 +> - [ ] RAG知识库 +> - [ ] 预设更详细的配置 +> - [x] 自定义触发器 +> - [ ] 自定义插件 +> - [ ] 兼容mcp + + +## 插件简介 + +ChatGPT-Plugin 以 Chaite 为内核,将多模型渠道、工具、处理器、触发器、RAG 和管控面板封装成一套适配 Miao-Yunzai / Yunzai-Bot 的插件方案。通过 Chaite 的 API 服务器与可插拔的存储层(默认 SQLite),插件可以在本地完成高并发对话、知识库检索、伪人陪聊以及记忆管理,亦可接入 Chaite Cloud 复用在线渠道与工具。 + +## 核心特性 + +- **多渠道与预设体系**:依托 Chaite 的 ChannelsManager 与 ChatPresetManager,支持为不同模型配置流量、负载均衡与个性化 prompt,群友也可在授权后自助切换预设。 +- **高级消息适配**:前后文触发方式支持 `@Bot` 与前缀;自动处理引用、图片、语音等多模态输入,并在工具调用或推理阶段通过转发消息回显。 +- **群上下文与伪人模式**:可按配置注入指定条数的群聊记录;BYM 伪人模式支持概率触发、关键词命中、预设覆盖及限时撤回,营造更拟人的陪聊体验。 +- **记忆与 RAG**:内置 memoryService + vectra 向量索引,提供群记忆、私人记忆与外部知识库(RAGManager)注入能力,支持混合检索与手动管理。 +- **可视化与指令双管控**:`#chatgpt管理面板` 一键获取面板 token,Web 端即可操作渠道、工具、触发器;同时保留完整的命令行 CRUD 指令。 +- **自动更新与依赖管理**:`#chatgpt更新` / `#chatgpt强制更新` 调用 git 同步仓库并自动更新 chaite 依赖,减少手动维护成本。 + +## 快速安装 + +1. **克隆代码** + ```bash + cd plugins + git clone https://github.com/ikechan8370/chatgpt-plugin.git + ``` +2. **安装依赖**(推荐 Node.js ≥ 18 + pnpm ≥ 8) + ```bash + cd chatgpt-plugin + pnpm install + ``` + 若安装日志出现 `Ignored build scripts: better-sqlite3` 或运行时报错找不到 `better-sqlite3` bindings,可执行 `pnpm approve-builds`,在交互列表中勾选全部或仅 `better-sqlite3` 以允许编译。然后再次 `pnpm install` +3. **在 Yunzai 中启用插件** + - 重启机器人或运行 `node app` 让插件自动加载。 + - 首次启动会在 `plugins/chatgpt-plugin/config/` 下生成 `config.json / config.yaml`。 +4. **保持更新** + - 主人账号发送 `#chatgpt更新` 获取最新版本。 + - `#chatgpt强制更新` 会放弃本地修改后重新拉取,请谨慎使用。 + +## 配置指引 + +配置文件默认位于 `plugins/chatgpt-plugin/config/config.json`,也可改写为 YAML。常用字段示例: + +```yaml +basic: + toggleMode: at # at / prefix + togglePrefix: "#chat" # prefix 模式下的触发词 + sendReasoning: true # 普通对话是否转发思考/工具过程 + commandPrefix: "#chatgpt" # 管理指令前缀 +llm: + defaultModel: "gpt-4o-mini" + defaultChatPresetId: "default" + enableGroupContext: true + groupContextLength: 20 +bym: + enable: false + probability: 0.02 + defaultPreset: "bym_default" +chaite: + cloudApiKey: "" # 可选,接入 Chaite Cloud + host: "0.0.0.0" + port: 48370 +memory: + group: + enable: false + enabledGroups: ["123456"] + user: + enable: false + whitelist: ["123456789"] +``` + +- **basic**:控制触发方式、普通对话思考过程转发、调试与命令前缀。 +- **llm**:定义默认模型、嵌入模型、群上下文等。`defaultChatPresetId` 需在面板或命令中提前创建。 +- **chaite**:`storage` 默认 SQLite,会在 `plugins/chatgpt-plugin/data/data.db` 生成数据文件;如接入 Chaite Cloud,请填入 `cloudApiKey` 并开放 `host/port`。 +- **bym**:配置伪人触发概率、关键词映射、撤回与思考内容开关。 +- **memory**:为群记忆或私人记忆开启检索、模型与提示词,可按需启用 `extensions.simple` 以加载自定义词典。 + +修改后保存文件,插件会自动热加载;在 Chaite 面板修改配置时也会反向写回本地文件。 + +## 使用方式 + +### 基础对话 + +- `@Bot 你好` 或 `#chat 今天天气如何` 触发默认预设,插件会保持用户 `conversationId` 与 `messageId`,自动续写多轮对话。 +- 回复图片/文本可作为上下文输入,模型返回的图片、语音与思考内容会自动转换为 QQ 消息或转发记录。 +- `#结束对话` 仅清空自己的会话;`#结束全部对话` 需主人权限。 + +### 管理命令 & 面板 + +- `#chatgpt管理面板`:生成一次性 token,访问 `http://:` 即可使用 Chaite Web 面板。 +- `#chatgpt开启思考转发` / `#chatgpt关闭思考转发`:控制 `@Bot` 或前缀触发普通对话时是否发送思考过程转发消息。 +- CRUD 命令示例(均支持 `列表 / 添加 / 查看 / 删除`): + ``` + #chatgpt渠道列表 + #chatgpt预设添加 角色扮演 {...} + #chatgpt工具删除 web-search + #chatgpt处理器查看 markdown + ``` +- `#chatgpt调试模式开关`、`#chatgpt伪人开关` 等指令可快速切换全局开关。 + +### 伪人(BYM)模式 + +1. 在配置中启用 `bym.enable` 并指定 `defaultPreset` 或 `presetMap`。 +2. 伪人会在命中关键词或达到概率阈值时主动发言,可通过 `presetPrefix` 调整统一人设,`temperature/maxTokens` 控制语气与长度。 +3. 支持为不同关键词配置 `recall` 秒数,实现“发完撤回”效果。 + +### 记忆系统 + +- **群记忆指令** + ``` + #群记忆 #仅群聊 + #删除群记忆 1 #主人或群管 + #记忆列表 #主人查看全局开关 + ``` +- **私人记忆指令** + ``` + #记忆 / 我的记忆 + #他的记忆 @xxx #群聊内查看他人(需其授权) + #删除记忆 1 + ``` +- 记忆抽取依赖配置中的 `memory.group` / `memory.user` 模型与预设,collector 会定期读取群聊历史,必要时可在 `enabledGroups` 中按群号白名单控制。 + +### 更新与维护 + +- `#chatgpt更新`:git pull 插件仓库并使用 pnpm/npm 更新 chaite 依赖。 +- `#chatgpt强制更新`:在更新前执行 `git checkout .`,用于舍弃本地改动。 +- 日志会通过转发消息发送最近 20 条 commit,方便追踪版本变化。 + + +## 赞助 + +如果觉得本项目好玩或者对你有帮助,愿意的话可以赞助我一口快乐水: + +https://afdian.net/a/ikechan8370 + +## 贡献者 + +感谢以下贡献者 + + + + + + +![Alt](https://repobeats.axiom.co/api/embed/076d597ede41432208435f233d18cb20052fb90a.svg "Repobeats analytics image") + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=ikechan8370/chatgpt-plugin&type=Date)](https://star-history.com/#ikechan8370/chatgpt-plugin&Date) + From d8ec1b5c3fa259c36b0913555a0b81d47aae061e Mon Sep 17 00:00:00 2001 From: buyun14 <2135481470@qq.com> Date: Fri, 24 Apr 2026 13:04:51 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat(mcp):=20=E6=B7=BB=E5=8A=A0JSON=20Schem?= =?UTF-8?q?a=E6=A0=87=E5=87=86=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增normalizeSchemaTypeName函数用于将类型名称标准化为标准JSON Schema类型, 如bool转为boolean,int转为integer,float/double转为number。 - 新增normalizeJsonSchema函数递归处理整个JSON Schema对象, 确保所有类型定义都被正确标准化。构建桥接工具代码时使用标准化后的schema。 --- utils/mcp/manager.js | 47 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/utils/mcp/manager.js b/utils/mcp/manager.js index 65071eeb2..d5f4f361d 100644 --- a/utils/mcp/manager.js +++ b/utils/mcp/manager.js @@ -58,12 +58,55 @@ function buildBridgeToolName (serverId, mcpToolName) { return `${prefix}_${s}_${t}` } +function normalizeSchemaTypeName (typeName) { + if (typeof typeName !== 'string') { + return typeName + } + + const lowered = typeName.trim().toLowerCase() + switch (lowered) { + case 'bool': + return 'boolean' + case 'int': + return 'integer' + case 'float': + case 'double': + return 'number' + default: + return lowered + } +} + +function normalizeJsonSchema (schema) { + if (Array.isArray(schema)) { + return schema.map(normalizeJsonSchema) + } + + if (!schema || typeof schema !== 'object') { + return schema + } + + const out = {} + for (const [k, v] of Object.entries(schema)) { + if (k === 'type') { + out[k] = Array.isArray(v) + ? v.map(normalizeSchemaTypeName) + : normalizeSchemaTypeName(v) + continue + } + out[k] = normalizeJsonSchema(v) + } + + return out +} + function buildBridgeToolCode (className, bridgeToolName, schema, description = '') { + const normalizedSchema = normalizeJsonSchema(schema) const fnSchema = JSON.stringify({ name: bridgeToolName, description: description || `MCP bridge tool: ${bridgeToolName}`, - parameters: schema && typeof schema === 'object' - ? schema + parameters: normalizedSchema && typeof normalizedSchema === 'object' + ? normalizedSchema : { type: 'object', properties: {}, From 317ba6e0bb1a65721b6567879ced9a69fbc886e2 Mon Sep 17 00:00:00 2001 From: buyun14 <2135481470@qq.com> Date: Sun, 26 Apr 2026 01:33:25 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix(config):=20=E5=85=B3=E9=97=AD=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E6=97=B6=E6=B8=85=E7=90=86=E6=97=A7=E6=A1=A5=E6=8E=A5?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 启动时不清理旧桥接工具,避免触发工具文件频繁 unlink/rescan 日志刷屏问题。 --- config/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.js b/config/config.js index 5b56df62d..f3c6d7968 100644 --- a/config/config.js +++ b/config/config.js @@ -211,8 +211,8 @@ class ChatGPTConfig { enable: false, // 生成到 Chaite 工具池中的前缀,避免与本地工具重名 toolNamePrefix: 'mcp', - // 启动时清理旧的桥接工具(id 前缀为 mcp_bridge_) - removeStaleBridgeToolsOnStart: true, + // 启动时不清理旧桥接工具,避免触发工具文件频繁 unlink/rescan 日志刷屏 + removeStaleBridgeToolsOnStart: false, servers: [ { id: 'filesystem', From 05c1e997e2f61ea7a38fd38de2091cdd0f16917c Mon Sep 17 00:00:00 2001 From: buyun14 <2135481470@qq.com> Date: Tue, 14 Jul 2026 08:02:33 +0800 Subject: [PATCH 9/9] feat: add JSON comment stripping and enhance error handling in response parsing --- models/memory/extractor.js | 75 +++++++++++++++++++++++++++++++------- package.json | 2 +- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/models/memory/extractor.js b/models/memory/extractor.js index f5853fb5f..ed29da59e 100644 --- a/models/memory/extractor.js +++ b/models/memory/extractor.js @@ -13,6 +13,38 @@ function collectTextFromResponse (response) { .trim() } +function stripJsonComments (text) { + // Character-based JSON comment stripper. + // Removes // line comments outside of JSON string values. + let result = '' + let inString = false + let i = 0 + while (i < text.length) { + const ch = text[i] + if (inString) { + result += ch + if (ch === '\\') { + if (i + 1 < text.length) result += text[++i] + } else if (ch === '"') { + inString = false + } + } else { + if (ch === '\\') { + if (i + 1 < text.length) result += text[++i] + } else if (ch === '"') { + inString = true + result += ch + } else if (ch === '/' && text[i + 1] === '/') { + while (i < text.length && text[i] !== '\n') i++ + } else { + result += ch + } + } + i++ + } + return result +} + function parseJSON (text) { if (!text) { return null @@ -23,8 +55,13 @@ function parseJSON (text) { try { return JSON.parse(payload) } catch (err) { - logger.warn('Failed to parse JSON from memory extractor response:', text) - return null + // Retry after stripping JSON comments (some models like DeepSeek-R1 add // comments) + try { + return JSON.parse(stripJsonComments(payload)) + } catch (err2) { + logger.warn('Failed to parse JSON from memory extractor response:', text) + return null + } } } @@ -222,15 +259,11 @@ async function callModel ({ prompt, systemPrompt, model, maxToken = 4096, temper } const resolvedModel = options.model const { client } = await getClientForModel(resolvedModel) - const response = await client.sendMessage({ - role: 'user', - content: [ - { - type: 'text', - text: prompt - } - ] - }, SendMessageOption.create({ + // 记忆提取不需要工具调用,清空 client 上的工具列表,防止不支持 function calling 的模型报 400 + client.tools = [] + client.options.tools = [] + client.options.builtinToolCategories = [] + const smeOption = SendMessageOption.create({ ...options, model: options.model, temperature: options.temperature ?? temperature, @@ -238,8 +271,24 @@ async function callModel ({ prompt, systemPrompt, model, maxToken = 4096, temper systemOverride: systemPrompt ?? options.systemOverride, disableHistoryRead: true, disableHistorySave: true, - stream: false - })) + stream: false, + builtinToolCategories: [] + }) + const userMsg = { + role: 'user', + content: [ + { + type: 'text', + text: prompt + } + ] + } + let response + try { + response = await client.sendMessage(userMsg, smeOption) + } catch (err) { + throw err + } return collectTextFromResponse(response) } diff --git a/package.json b/package.json index 769fda824..e8f501fd8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@modelcontextprotocol/sdk": "^1.29.0", "adm-zip": "^0.5.10", "better-sqlite3": "^12.8.0", - "chaite": "^1.8.2", + "chaite": "^1.13.0", "js-yaml": "^4.1.0", "keyv": "^5.3.1", "keyv-file": "^5.1.2",