From 3a025a8c2cabc6d35e6c69669167d7b78e0bc2c3 Mon Sep 17 00:00:00 2001 From: luy <12696648@qq.com> Date: Sat, 21 Jun 2025 20:08:32 +0800 Subject: [PATCH 1/5] feat(agent): implement mcp server of multi-tab participant meeting for topic-based discussions --- src/main/events.ts | 6 + .../conversationSearchServer.ts | 313 +++++++++++++++++- src/main/presenter/threadPresenter/index.ts | 46 ++- src/renderer/src/events.ts | 6 + src/renderer/src/stores/chat.ts | 33 +- src/shared/presenter.d.ts | 3 +- 6 files changed, 396 insertions(+), 11 deletions(-) diff --git a/src/main/events.ts b/src/main/events.ts index 9dafca082..87673a7bf 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -100,6 +100,12 @@ export const MCP_EVENTS = { INITIALIZED: 'mcp:initialized' // 新增:MCP初始化完成事件 } +// 新增会议相关事件 +export const MEETING_EVENTS = { + INSTRUCTION: 'mcp:meeting-instruction', // 主进程向渲染进程发送指令 + MESSAGE_GENERATED: 'thread:message-generated' // 主进程内部事件,通知消息已生成 +} + // 同步相关事件 export const SYNC_EVENTS = { BACKUP_STARTED: 'sync:backup-started', diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts index 2bd8aa1e3..94e6a2110 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts @@ -6,7 +6,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema' import { Transport } from '@modelcontextprotocol/sdk/shared/transport' import { presenter } from '@/presenter' // 导入全局的 presenter 对象 import { eventBus } from '@/eventbus' // 引入 eventBus -import { TAB_EVENTS } from '@/events' // 引入 TAB_EVENTS +import { TAB_EVENTS, MEETING_EVENTS } from '@/events' // 引入 TAB_EVENTS // Schema definitions const SearchConversationsArgsSchema = z.object({ @@ -58,6 +58,67 @@ const CreateNewTabArgsSchema = z.object({ userInput: z.string().optional().describe('Optional initial user input for the new chat tab.') }) +// +++ 新增会议功能部分 +++ +// 参会者姓名列表 +const PARTICIPANT_NAMES = [ + "Alice", "Brian", "Chris", "David", "Emma", "Frank", "Grace", "Henry", + "Ian", "Jack", "Kate", "Lily", "Mike", "Nick", "Oliver", "Peter", + "Quinn", "Ryan", "Sarah", "Tom", "Uriel", "Victor", "Wendy", "Xavier", + "Yolanda", "Zoe" +]; + +// 单个参会者的Schema定义 (精确引导) +const ParticipantSchema = z.object({ + tab_id: z.number().optional().describe( + '通过Tab的【唯一标识】来精确指定参会者。' + + '这是一个内部ID,通常通过create_new_tab等工具获得。' + + '仅当你可以明确获得参会者Tab的唯一标识时,才应使用此字段。' + + '这是最精确的定位方式。如果使用此字段,则不应填写 tab_title。' + ), + tab_title: z.string().optional().describe( + '通过Tab的【当前显示标题】来指定参会者。' + + '当用户的指令中明确提到了Tab的名称(例如 "让标题为\'AI讨论\'的Tab...")时,应优先使用此字段。' + + '请注意,标题可能不是唯一的,系统会选择第一个匹配的Tab。如果使用此字段,则不应填写 tab_id。' + ), + profile: z.string().optional().describe('用于定义该参会者的完整画像,可包括且不限于其角色身份、观察视角、立场观点、表达方式、行为模式、发言约束及其他提示词,用于驱动其在会议中的一致性行为和语言风格。') +}) +// 对整个对象进行描述,强调其核心作用和互斥规则 +.describe( + '定义一位会议的参会者。' + + '你必须通过且只能通过 "tab_id" 或 "tab_title" 字段中的一个来指定该参会者。' + + '决策依据:如果用户的指令明确提到了Tab的标题,请优先使用 tab_title。仅当你可以明确获得参会者tab唯一数字标识时,才使用 tab_id。' +) +// 保持refine作为最终的硬性约束 +.refine( + (data) => { + const hasId = data.tab_id !== undefined && data.tab_id !== -1; + const hasTitle = data.tab_title !== undefined && data.tab_title.trim() !== ''; + return (hasId && !hasTitle) || (!hasId && hasTitle); + }, + { + message: '错误:必须且只能通过 "tab_id" 或 "tab_title" 中的一个来指定参会者,两者不能同时提供,也不能都为空。' + } +); + +// 新的会议工具Schema +const StartMeetingArgsSchema = z.object({ + participants: z + .array(ParticipantSchema) + .min(2, { message: "会议至少需要两位参会者。" }) + .describe('参会者列表。'), + topic: z.string().describe('会议的核心讨论主题。'), + rounds: z.number().optional().default(3).describe('讨论的轮次数,默认为3轮。') +}); + +// 内部使用的会议参与者信息接口,增加了会议代号 +interface MeetingParticipant { + meetingName: string; // 新增:会议中的代号,如 "Alice" + tabId: number; + conversationId: string; + originalTitle: string; // 保留原始Tab标题 + profile: string; +} + interface SearchResult { conversations?: Array<{ id: string @@ -498,6 +559,12 @@ export class ConversationSearchServer { description: 'Creates a new tab. If userInput is provided, it also creates a new chat session and sends the input as the first message, then returns tabId and threadId.', inputSchema: zodToJsonSchema(CreateNewTabArgsSchema) + }, + // 新增会议工具 + { + name: 'start_meeting', + description: '启动并主持一个由多个Tab(参会者)参与的关于特定主题的讨论会议。如果你当前已经是某个会议的参与者,请勿调用!', + inputSchema: zodToJsonSchema(StartMeetingArgsSchema) // 更新Schema } ] } @@ -642,6 +709,40 @@ export class ConversationSearchServer { ] } } + + case 'start_meeting': { + try { + // 1. 解析和验证参数。如果失败,抛出ZodError,被外层catch捕获,并快速失败。 + const meetingArgs = StartMeetingArgsSchema.parse(args) + + // 2. 启动会议,但 **不使用 await**。 + // 我们使用一个自执行的异步函数来包裹 `organizeMeeting`, + // 这样可以捕获它内部的异步错误,而不会让主线程崩溃。 + ;(async () => { + try { + await this.organizeMeeting(meetingArgs) + console.log('会议流程已在后台成功完成。') + } catch (meetingError: any) { + // 在后台执行期间发生的错误 + console.error(`会议执行过程中发生错误: ${meetingError.message}`) + // 这里可以添加通知逻辑,比如通知所有参会者会议已中止 + // 由于已经脱离了原始请求,只能通过 eventBus 通知 + } + })() + + // 3. 立即返回成功启动的消息,避免超时。 + return { + content: [{ type: 'text', text: '会议已成功启动,正在后台进行中...' }] + } + } catch (error: any) { + // 捕获启动阶段的错误(如参数验证失败) + return { + content: [{ type: 'text', text: `会议启动失败: ${error.message}` }], + isError: true + } + } + } + default: throw new Error(`Unknown tool: ${name}`) } @@ -659,4 +760,214 @@ export class ConversationSearchServer { } }) } + + // 新增会议功能的完整实现 + + /** + * 等待指定会话生成一个新消息。 + * @param conversationId 要等待的会话ID + * @param timeout 超时时间(毫秒) + * @returns 返回生成的消息对象 + */ + private waitForResponse(conversationId: string, timeout = 180000): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + eventBus.removeListener(MEETING_EVENTS.MESSAGE_GENERATED, listener) + reject( + new Error( + `超时: 等待会话 ${conversationId} 的回复超过 ${timeout / 1000} 秒,未收到响应。` + ) + ) + }, timeout) + + const listener = (data: { conversationId: string; message: any }) => { + if (data.conversationId === conversationId) { + clearTimeout(timer) + eventBus.removeListener(MEETING_EVENTS.MESSAGE_GENERATED, listener) + resolve(data.message) + } + } + + eventBus.on(MEETING_EVENTS.MESSAGE_GENERATED, listener) + }) + } + + /** + * 组织和执行整个会议流程 + * @param args 会议参数 + */ + private async organizeMeeting(args: z.infer): Promise { + const { participants, topic, rounds } = args + + // 1. 定位并验证参会者 (最终修正逻辑) + const mainWindowId = presenter.windowPresenter.mainWindow?.id + if (!mainWindowId) throw new Error('主窗口未找到,无法开始会议。') + + const allChatTabs = (await presenter.tabPresenter.getWindowTabsData(mainWindowId)) + + const meetingParticipants: MeetingParticipant[] = [] + let nameIndex = 0 + + for (const p of participants) { + let tabData + + // 优先使用 tab_id 查找,这个基本上是通过create_new_tab获得的tab_id + if (p.tab_id !== undefined) { + tabData = allChatTabs.find((t) => t.id === p.tab_id) + } + + // 如果通过 tab_id 没找到,再尝试使用 tab_title,这个基本上是现成的已有tab,所以无需再次创建对话ID之类 + let foundByTabTitle = false + if (!tabData && p.tab_title) { + tabData = allChatTabs.find((t) => t.title === p.tab_title) + if (tabData) foundByTabTitle = true + } + + // 找不到 tabData,跳过这个无法准备的参会者 + if (!tabData) continue + + // 如果是新建的tab,则必须创建激活会话 + if (!foundByTabTitle) { + let conversationId = await presenter.threadPresenter.getActiveConversationId(tabData.id) + + // 确保每个有效的参会者都有一个可用的conversationId + const meetingName = + nameIndex < PARTICIPANT_NAMES.length + ? PARTICIPANT_NAMES[nameIndex] + : `参会者${nameIndex + 1}` + nameIndex++ + + // 如果Tab没有活动会话,则为其创建并等待UI同步 + if (!conversationId) { + console.log(`参会者 (ID: ${tabData.id}) 没有活动会话,正在为其创建...`) + + // 步骤 a: 创建新会话。这将自动激活并广播 'conversation:activated' 事件。 + conversationId = await presenter.threadPresenter.createConversation( + `${meetingName}`, // 为新会话设置一个描述性标题,即会议代号 + {}, + tabData.id, + { forceNewAndActivate: true } // 传入强制创建并激活的选项 + ) + + if (!conversationId) { + console.warn(`为Tab ${tabData.id} 创建会话失败,将跳过此参会者。`) + continue // 跳过这个无法准备的参会者 + } + + // 步骤 b: 关键的UI同步点!等待渲染进程处理完激活事件并回传确认信号。 + try { + await awaitTabActivated(conversationId) + console.log(`会话 ${conversationId} 在Tab ${tabData.id} 中已成功激活。`) + } catch (error) { + console.error(`等待Tab ${tabData.id} 激活失败:`, error) + continue // 如果UI同步失败,也跳过此参会者 + } + } + + meetingParticipants.push({ + meetingName, + tabId: tabData.id, + conversationId, + originalTitle: tabData.title, + profile: p.profile || `你可以就“${topic}”这个话题,自由发表你的看法和观点。` + }) + // 如果是非新建的tab,则无需创建激活会话 + } else { + let conversationId = await presenter.threadPresenter.getActiveConversationId(tabData.id) + + if (!conversationId) { + console.warn(`为Tab ${tabData.id} 创建会话失败,将跳过此参会者。`) + continue // 跳过这个无法准备的参会者 + } + + // 确保每个有效的参会者都有一个可用的conversationId + const meetingName = + nameIndex < PARTICIPANT_NAMES.length + ? PARTICIPANT_NAMES[nameIndex] + : `参会者${nameIndex + 1}` + nameIndex++ + + meetingParticipants.push({ + meetingName, + tabId: tabData.id, + conversationId, + originalTitle: tabData.title, + profile: p.profile || `你可以就“${topic}”这个话题,自由发表你的看法和观点。` + }) + } + } + + if (meetingParticipants.length < 2) { + throw new Error( + `会议无法开始。只找到了 ${meetingParticipants.length} 位有效的参会者。请确保指定的Tab ID或标题正确,并且它们正在进行对话。` + ) + } + + // 2. 初始化会议 (使用会议代号) + const participantNames = meetingParticipants.map((p) => p.meetingName).join('、') + + for (const p of meetingParticipants) { + const initPrompt = `您好,${p.meetingName}。 +我是Argus,是当前会议的组织者,很荣幸能邀请您参加会议: +--- +会议主题: ${topic} +所有参会者: ${participantNames} +你的会议名称: ${p.meetingName} +你的角色画像: ${p.profile} +--- +会议规则: +1. 请严格围绕你的角色和观点进行发言。 +2. 请等待主持人指示后方可发言。 +3. 发言时,请清晰地陈述你的论点。 +4. 你的发言将被转发给其他所有参会者。 +5. 在他人发言时,你会收到其发言内容,但请勿回复,轮到你再发言。 +6. 作为会议参与者,你不得调用与会议相关的工具函数。 +--- +会议现在开始。请等待你的发言回合。 +` + eventBus.sendToTab(p.tabId, MEETING_EVENTS.INSTRUCTION, { prompt: initPrompt }) + + // 等待AI模型的确认性回复,以同步流程,但我们忽略其具体内容。 + await this.waitForResponse(p.conversationId) + } + + // 3. 会议循环 (使用会议代号) + let history = `会议记录\n主题: ${topic}\n` + for (let round = 1; round <= rounds; round++) { + for (const speaker of meetingParticipants) { + const speakPrompt = `第 ${round}/${rounds} 轮。现在轮到您(${speaker.meetingName})发言。请陈述您的观点。` + eventBus.sendToTab(speaker.tabId, MEETING_EVENTS.INSTRUCTION, { prompt: speakPrompt }) + + // 等待并捕获真正的、需要被记录和转发的发言内容。 + const speechMessage = await this.waitForResponse(speaker.conversationId) + + const speechText = Array.isArray(speechMessage.content) + ? speechMessage.content.find((c: any) => c.type === 'content')?.content || '[无内容]' + : speechMessage.content || '[无内容]' + history += `\n[第${round}轮] ${speaker.meetingName}: ${speechText}` + + // 转发给其他参会者,并等待他们的确认性回复。 + for (const listener of meetingParticipants) { + if (listener.tabId !== speaker.tabId) { + const forwardPrompt = `来自 ${speaker.meetingName} 的发言如下:\n\n---\n${speechText}\n---\n\n**以上信息仅供参考,请不要回复!**\n作为参会者,请您(${listener.meetingName})等待我(Argus)的指示。` + eventBus.sendToTab(listener.tabId, MEETING_EVENTS.INSTRUCTION, { + prompt: forwardPrompt + }) + + // 等待确认性回复,并忽略内容。 + await this.waitForResponse(listener.conversationId) + } + } + } + } + + // 4. 结束会议 (使用会议代号) + for (const p of meetingParticipants) { + const personalizedFinalPrompt = `讨论已结束。请您(${p.meetingName})根据整个对话过程,对您的观点进行最终总结。` + eventBus.sendToTab(p.tabId, MEETING_EVENTS.INSTRUCTION, { prompt: personalizedFinalPrompt }) + } + + // 注意:这里不再有返回值,因为函数是在后台执行的。 + console.log(`关于“${topic}”的会议流程已在后台正常结束。`) + } } diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index 933ed61c8..f8992541f 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -34,7 +34,7 @@ import { approximateTokenSize } from 'tokenx' import { generateSearchPrompt, SearchManager } from './searchManager' import { getFileContext } from './fileContext' import { ContentEnricher } from './contentEnricher' -import { CONVERSATION_EVENTS, STREAM_EVENTS, TAB_EVENTS } from '@/events' +import { CONVERSATION_EVENTS, STREAM_EVENTS, TAB_EVENTS, MEETING_EVENTS } from '@/events' import { DEFAULT_SETTINGS } from './const' interface GeneratingMessageState { @@ -224,6 +224,17 @@ export class ThreadPresenter implements IThreadPresenter { // 手动触发一次广播,因为这次更新没有经过其他会触发广播的方法 await this.broadcastThreadListUpdate() } + + // --- 新增逻辑:广播消息生成完成事件 --- + // 在所有数据库和状态更新完成后,获取最终的消息对象 + const finalMessage = await this.messageManager.getMessage(eventId) + if (finalMessage) { + // 这个事件只在主进程内部流通,用于通知其他监听者(如会议主持人) + eventBus.sendToMain(MEETING_EVENTS.MESSAGE_GENERATED, { + conversationId: finalMessage.conversationId, + message: finalMessage + }) + } } eventBus.sendToRenderer(STREAM_EVENTS.END, SendTarget.ALL_WINDOWS, msg) @@ -611,18 +622,24 @@ export class ThreadPresenter implements IThreadPresenter { async createConversation( title: string, settings: Partial = {}, - tabId: number + tabId: number, + options: { forceNewAndActivate?: boolean } = {} // 新增参数,允许强制创建新会话 ): Promise { console.log('createConversation', title, settings) + const latestConversation = await this.getLatestConversation() - if (latestConversation) { - const { list: messages } = await this.getMessages(latestConversation.id, 1, 1) - if (messages.length === 0) { - await this.setActiveConversation(latestConversation.id, tabId) - return latestConversation.id + // 只有在非强制模式下,才执行空会话的单例检查 + if (!options.forceNewAndActivate) { + if (latestConversation) { + const { list: messages } = await this.getMessages(latestConversation.id, 1, 1) + if (messages.length === 0) { + await this.setActiveConversation(latestConversation.id, tabId) + return latestConversation.id + } } } + let defaultSettings = DEFAULT_SETTINGS if (latestConversation?.settings) { defaultSettings = { ...latestConversation.settings } @@ -656,7 +673,20 @@ export class ThreadPresenter implements IThreadPresenter { mergedSettings.systemPrompt = settings.systemPrompt } const conversationId = await this.sqlitePresenter.createConversation(title, mergedSettings) - await this.setActiveConversation(conversationId, tabId) + + // 根据 forceNewAndActivate 标志决定激活行为 + if (options.forceNewAndActivate) { + // 强制模式:直接为当前 tabId 激活新会话,不进行任何检查 + this.activeConversationIds.set(tabId, conversationId) + eventBus.sendToRenderer(CONVERSATION_EVENTS.ACTIVATED, SendTarget.ALL_WINDOWS, { + conversationId, + tabId + }) + } else { + // 默认模式:保持原有的、防止重复打开的激活逻辑 + await this.setActiveConversation(conversationId, tabId) + } + await this.broadcastThreadListUpdate() // 必须广播 return conversationId } diff --git a/src/renderer/src/events.ts b/src/renderer/src/events.ts index 65e7c6764..0153cc03d 100644 --- a/src/renderer/src/events.ts +++ b/src/renderer/src/events.ts @@ -67,6 +67,12 @@ export const MCP_EVENTS = { TOOL_CALL_RESULT: 'mcp:tool-call-result', SERVER_STATUS_CHANGED: 'mcp:server-status-changed' } + +// 新增会议相关事件 +export const MEETING_EVENTS = { + INSTRUCTION: 'mcp:meeting-instruction' // 监听来自主进程的指令 +} + // 同步相关事件 export const SYNC_EVENTS = { BACKUP_STARTED: 'sync:backup-started', diff --git a/src/renderer/src/stores/chat.ts b/src/renderer/src/stores/chat.ts index 745129ad0..25d434c53 100644 --- a/src/renderer/src/stores/chat.ts +++ b/src/renderer/src/stores/chat.ts @@ -9,7 +9,7 @@ import type { } from '@shared/chat' import type { CONVERSATION, CONVERSATION_SETTINGS } from '@shared/presenter' import { usePresenter } from '@/composables/usePresenter' -import { CONVERSATION_EVENTS, DEEPLINK_EVENTS } from '@/events' +import { CONVERSATION_EVENTS, DEEPLINK_EVENTS, MEETING_EVENTS } from '@/events' import router from '@/router' import { useI18n } from 'vue-i18n' import { useSoundStore } from './sound' @@ -1008,7 +1008,38 @@ export const useChatStore = defineStore('chat', () => { return getThreadsWorkingStatus().get(threadId) || null } + /** + * 新增: 处理来自主进程的会议指令 + * @param data 包含指令文本的对象 + */ + const handleMeetingInstruction = async (data: { prompt: string }) => { + // 确保当前有活动的会话,否则指令无法执行 + if (!getActiveThreadId()) { + console.warn('收到会议指令,但没有活动的会话。指令被忽略。') + return + } + try { + // 将收到的指令作为用户输入,调用已有的sendMessage方法 + // 这样可以完全复用UI的加载状态、消息显示等所有逻辑 + await sendMessage({ + text: data.prompt, + files: [], + links: [], + think: false, + search: false, + content: [{ type: 'text', content: data.prompt }] + }) + } catch (error) { + console.error('处理会议指令时发生错误:', error) + } + } + const setupEventListeners = () => { + // 新增:监听来自主进程的会议指令 + window.electron.ipcRenderer.on(MEETING_EVENTS.INSTRUCTION, (_, data) => { + handleMeetingInstruction(data) + }) + // 监听:主进程推送的完整会话列表 window.electron.ipcRenderer.on( CONVERSATION_EVENTS.LIST_UPDATED, diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index fbe933548..c4fa6a0c1 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -548,7 +548,8 @@ export interface IThreadPresenter { createConversation( title: string, settings?: Partial, - tabId: number + tabId: number, + options?: { forceNewAndActivate?: boolean } // 新增 options 参数, 支持强制新建会话,避免空会话的单例检测 ): Promise deleteConversation(conversationId: string): Promise getConversation(conversationId: string): Promise From c3e177703525b1bdceedfa65196efb4f5affad79 Mon Sep 17 00:00:00 2001 From: luy <12696648@qq.com> Date: Sat, 21 Jun 2025 20:57:34 +0800 Subject: [PATCH 2/5] fix(event): categorize message event types about mcp meeting --- src/main/events.ts | 15 ++++++++------- .../inMemoryServers/conversationSearchServer.ts | 8 ++++---- src/main/presenter/threadPresenter/index.ts | 6 +++--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/main/events.ts b/src/main/events.ts index 87673a7bf..67710a04f 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -40,7 +40,9 @@ export const CONVERSATION_EVENTS = { ACTIVATED: 'conversation:activated', // 替代 conversation-activated DEACTIVATED: 'conversation:deactivated', // 替代 active-conversation-cleared - MESSAGE_EDITED: 'conversation:message-edited' // 替代 message-edited + MESSAGE_EDITED: 'conversation:message-edited', // 替代 message-edited + + MESSAGE_GENERATED: 'conversation:message-generated' // 主进程内部事件,一条完整的消息已生成 } // 通信相关事件 @@ -100,12 +102,6 @@ export const MCP_EVENTS = { INITIALIZED: 'mcp:initialized' // 新增:MCP初始化完成事件 } -// 新增会议相关事件 -export const MEETING_EVENTS = { - INSTRUCTION: 'mcp:meeting-instruction', // 主进程向渲染进程发送指令 - MESSAGE_GENERATED: 'thread:message-generated' // 主进程内部事件,通知消息已生成 -} - // 同步相关事件 export const SYNC_EVENTS = { BACKUP_STARTED: 'sync:backup-started', @@ -163,3 +159,8 @@ export const TRAY_EVENTS = { SHOW_HIDDEN_WINDOW: 'tray:show-hidden-window', // 从托盘显示/隐藏窗口 CHECK_FOR_UPDATES: 'tray:check-for-updates' // 托盘检查更新 } + +// MCP会议专用事件 +export const MEETING_EVENTS = { + INSTRUCTION: 'mcp:meeting-instruction', // 主进程向渲染进程发送指令 +} diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts index 94e6a2110..36e11bd8a 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts @@ -6,7 +6,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema' import { Transport } from '@modelcontextprotocol/sdk/shared/transport' import { presenter } from '@/presenter' // 导入全局的 presenter 对象 import { eventBus } from '@/eventbus' // 引入 eventBus -import { TAB_EVENTS, MEETING_EVENTS } from '@/events' // 引入 TAB_EVENTS +import { TAB_EVENTS, MEETING_EVENTS, CONVERSATION_EVENTS } from '@/events' // Schema definitions const SearchConversationsArgsSchema = z.object({ @@ -772,7 +772,7 @@ export class ConversationSearchServer { private waitForResponse(conversationId: string, timeout = 180000): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => { - eventBus.removeListener(MEETING_EVENTS.MESSAGE_GENERATED, listener) + eventBus.removeListener(CONVERSATION_EVENTS.MESSAGE_GENERATED, listener) reject( new Error( `超时: 等待会话 ${conversationId} 的回复超过 ${timeout / 1000} 秒,未收到响应。` @@ -783,12 +783,12 @@ export class ConversationSearchServer { const listener = (data: { conversationId: string; message: any }) => { if (data.conversationId === conversationId) { clearTimeout(timer) - eventBus.removeListener(MEETING_EVENTS.MESSAGE_GENERATED, listener) + eventBus.removeListener(CONVERSATION_EVENTS.MESSAGE_GENERATED, listener) resolve(data.message) } } - eventBus.on(MEETING_EVENTS.MESSAGE_GENERATED, listener) + eventBus.on(CONVERSATION_EVENTS.MESSAGE_GENERATED, listener) }) } diff --git a/src/main/presenter/threadPresenter/index.ts b/src/main/presenter/threadPresenter/index.ts index f8992541f..9a9d17456 100644 --- a/src/main/presenter/threadPresenter/index.ts +++ b/src/main/presenter/threadPresenter/index.ts @@ -34,7 +34,7 @@ import { approximateTokenSize } from 'tokenx' import { generateSearchPrompt, SearchManager } from './searchManager' import { getFileContext } from './fileContext' import { ContentEnricher } from './contentEnricher' -import { CONVERSATION_EVENTS, STREAM_EVENTS, TAB_EVENTS, MEETING_EVENTS } from '@/events' +import { CONVERSATION_EVENTS, STREAM_EVENTS, TAB_EVENTS } from '@/events' import { DEFAULT_SETTINGS } from './const' interface GeneratingMessageState { @@ -229,8 +229,8 @@ export class ThreadPresenter implements IThreadPresenter { // 在所有数据库和状态更新完成后,获取最终的消息对象 const finalMessage = await this.messageManager.getMessage(eventId) if (finalMessage) { - // 这个事件只在主进程内部流通,用于通知其他监听者(如会议主持人) - eventBus.sendToMain(MEETING_EVENTS.MESSAGE_GENERATED, { + // 该事件仅在主进程内部流通,用于通知其他监听者(如MCP会议主持人) + eventBus.sendToMain(CONVERSATION_EVENTS.MESSAGE_GENERATED, { conversationId: finalMessage.conversationId, message: finalMessage }) From 82470683fd355d85ab911fc5e1af6c87f7958a60 Mon Sep 17 00:00:00 2001 From: luy <12696648@qq.com> Date: Sat, 21 Jun 2025 23:26:45 +0800 Subject: [PATCH 3/5] refactor(mcp): restructure MCP meeting as a separate mcp server module --- .../configPresenter/mcpConfHelper.ts | 10 + .../mcpPresenter/inMemoryServers/builder.ts | 3 + .../conversationSearchServer.ts | 315 +----------------- src/renderer/src/i18n/en-US/mcp.json | 4 + src/renderer/src/i18n/fa-IR/mcp.json | 4 + src/renderer/src/i18n/fr-FR/mcp.json | 4 + src/renderer/src/i18n/ja-JP/mcp.json | 4 + src/renderer/src/i18n/ko-KR/mcp.json | 4 + src/renderer/src/i18n/ru-RU/mcp.json | 4 + src/renderer/src/i18n/zh-CN/mcp.json | 4 + src/renderer/src/i18n/zh-HK/mcp.json | 4 + src/renderer/src/i18n/zh-TW/mcp.json | 4 + 12 files changed, 51 insertions(+), 313 deletions(-) diff --git a/src/main/presenter/configPresenter/mcpConfHelper.ts b/src/main/presenter/configPresenter/mcpConfHelper.ts index c484de641..6078ae285 100644 --- a/src/main/presenter/configPresenter/mcpConfHelper.ts +++ b/src/main/presenter/configPresenter/mcpConfHelper.ts @@ -171,6 +171,16 @@ const DEFAULT_INMEMORY_SERVERS: Record = { command: 'deepchat-inmemory/conversation-search-server', env: {}, disable: false + }, + 'deepchat-inmemory/meeting-server': { + args: [], + descriptions: 'DeepChat内置会议服务,用于组织多Agent讨论', + icons: '👥', + autoApprove: ['all'], + type: 'inmemory' as MCPServerType, + command: 'deepchat-inmemory/meeting-server', + env: {}, + disable: false } } diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/builder.ts b/src/main/presenter/mcpPresenter/inMemoryServers/builder.ts index 899090ea0..2f8117ca7 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/builder.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/builder.ts @@ -11,6 +11,7 @@ import { CustomPromptsServer } from './customPromptsServer' import { DeepResearchServer } from './deepResearchServer' import { AutoPromptingServer } from './autoPromptingServer' import { ConversationSearchServer } from './conversationSearchServer' +import { MeetingServer } from './meetingServer' export function getInMemoryServer( serverName: string, @@ -76,6 +77,8 @@ export function getInMemoryServer( return new AutoPromptingServer() case 'deepchat-inmemory/conversation-search-server': return new ConversationSearchServer() + case 'deepchat-inmemory/meeting-server': + return new MeetingServer() default: throw new Error(`Unknown in-memory server: ${serverName}`) } diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts index 36e11bd8a..59b780637 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts @@ -6,7 +6,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema' import { Transport } from '@modelcontextprotocol/sdk/shared/transport' import { presenter } from '@/presenter' // 导入全局的 presenter 对象 import { eventBus } from '@/eventbus' // 引入 eventBus -import { TAB_EVENTS, MEETING_EVENTS, CONVERSATION_EVENTS } from '@/events' +import { TAB_EVENTS } from '@/events' // Schema definitions const SearchConversationsArgsSchema = z.object({ @@ -58,67 +58,6 @@ const CreateNewTabArgsSchema = z.object({ userInput: z.string().optional().describe('Optional initial user input for the new chat tab.') }) -// +++ 新增会议功能部分 +++ -// 参会者姓名列表 -const PARTICIPANT_NAMES = [ - "Alice", "Brian", "Chris", "David", "Emma", "Frank", "Grace", "Henry", - "Ian", "Jack", "Kate", "Lily", "Mike", "Nick", "Oliver", "Peter", - "Quinn", "Ryan", "Sarah", "Tom", "Uriel", "Victor", "Wendy", "Xavier", - "Yolanda", "Zoe" -]; - -// 单个参会者的Schema定义 (精确引导) -const ParticipantSchema = z.object({ - tab_id: z.number().optional().describe( - '通过Tab的【唯一标识】来精确指定参会者。' + - '这是一个内部ID,通常通过create_new_tab等工具获得。' + - '仅当你可以明确获得参会者Tab的唯一标识时,才应使用此字段。' + - '这是最精确的定位方式。如果使用此字段,则不应填写 tab_title。' - ), - tab_title: z.string().optional().describe( - '通过Tab的【当前显示标题】来指定参会者。' + - '当用户的指令中明确提到了Tab的名称(例如 "让标题为\'AI讨论\'的Tab...")时,应优先使用此字段。' + - '请注意,标题可能不是唯一的,系统会选择第一个匹配的Tab。如果使用此字段,则不应填写 tab_id。' - ), - profile: z.string().optional().describe('用于定义该参会者的完整画像,可包括且不限于其角色身份、观察视角、立场观点、表达方式、行为模式、发言约束及其他提示词,用于驱动其在会议中的一致性行为和语言风格。') -}) -// 对整个对象进行描述,强调其核心作用和互斥规则 -.describe( - '定义一位会议的参会者。' + - '你必须通过且只能通过 "tab_id" 或 "tab_title" 字段中的一个来指定该参会者。' + - '决策依据:如果用户的指令明确提到了Tab的标题,请优先使用 tab_title。仅当你可以明确获得参会者tab唯一数字标识时,才使用 tab_id。' -) -// 保持refine作为最终的硬性约束 -.refine( - (data) => { - const hasId = data.tab_id !== undefined && data.tab_id !== -1; - const hasTitle = data.tab_title !== undefined && data.tab_title.trim() !== ''; - return (hasId && !hasTitle) || (!hasId && hasTitle); - }, - { - message: '错误:必须且只能通过 "tab_id" 或 "tab_title" 中的一个来指定参会者,两者不能同时提供,也不能都为空。' - } -); - -// 新的会议工具Schema -const StartMeetingArgsSchema = z.object({ - participants: z - .array(ParticipantSchema) - .min(2, { message: "会议至少需要两位参会者。" }) - .describe('参会者列表。'), - topic: z.string().describe('会议的核心讨论主题。'), - rounds: z.number().optional().default(3).describe('讨论的轮次数,默认为3轮。') -}); - -// 内部使用的会议参与者信息接口,增加了会议代号 -interface MeetingParticipant { - meetingName: string; // 新增:会议中的代号,如 "Alice" - tabId: number; - conversationId: string; - originalTitle: string; // 保留原始Tab标题 - profile: string; -} - interface SearchResult { conversations?: Array<{ id: string @@ -559,12 +498,6 @@ export class ConversationSearchServer { description: 'Creates a new tab. If userInput is provided, it also creates a new chat session and sends the input as the first message, then returns tabId and threadId.', inputSchema: zodToJsonSchema(CreateNewTabArgsSchema) - }, - // 新增会议工具 - { - name: 'start_meeting', - description: '启动并主持一个由多个Tab(参会者)参与的关于特定主题的讨论会议。如果你当前已经是某个会议的参与者,请勿调用!', - inputSchema: zodToJsonSchema(StartMeetingArgsSchema) // 更新Schema } ] } @@ -709,40 +642,6 @@ export class ConversationSearchServer { ] } } - - case 'start_meeting': { - try { - // 1. 解析和验证参数。如果失败,抛出ZodError,被外层catch捕获,并快速失败。 - const meetingArgs = StartMeetingArgsSchema.parse(args) - - // 2. 启动会议,但 **不使用 await**。 - // 我们使用一个自执行的异步函数来包裹 `organizeMeeting`, - // 这样可以捕获它内部的异步错误,而不会让主线程崩溃。 - ;(async () => { - try { - await this.organizeMeeting(meetingArgs) - console.log('会议流程已在后台成功完成。') - } catch (meetingError: any) { - // 在后台执行期间发生的错误 - console.error(`会议执行过程中发生错误: ${meetingError.message}`) - // 这里可以添加通知逻辑,比如通知所有参会者会议已中止 - // 由于已经脱离了原始请求,只能通过 eventBus 通知 - } - })() - - // 3. 立即返回成功启动的消息,避免超时。 - return { - content: [{ type: 'text', text: '会议已成功启动,正在后台进行中...' }] - } - } catch (error: any) { - // 捕获启动阶段的错误(如参数验证失败) - return { - content: [{ type: 'text', text: `会议启动失败: ${error.message}` }], - isError: true - } - } - } - default: throw new Error(`Unknown tool: ${name}`) } @@ -760,214 +659,4 @@ export class ConversationSearchServer { } }) } - - // 新增会议功能的完整实现 - - /** - * 等待指定会话生成一个新消息。 - * @param conversationId 要等待的会话ID - * @param timeout 超时时间(毫秒) - * @returns 返回生成的消息对象 - */ - private waitForResponse(conversationId: string, timeout = 180000): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - eventBus.removeListener(CONVERSATION_EVENTS.MESSAGE_GENERATED, listener) - reject( - new Error( - `超时: 等待会话 ${conversationId} 的回复超过 ${timeout / 1000} 秒,未收到响应。` - ) - ) - }, timeout) - - const listener = (data: { conversationId: string; message: any }) => { - if (data.conversationId === conversationId) { - clearTimeout(timer) - eventBus.removeListener(CONVERSATION_EVENTS.MESSAGE_GENERATED, listener) - resolve(data.message) - } - } - - eventBus.on(CONVERSATION_EVENTS.MESSAGE_GENERATED, listener) - }) - } - - /** - * 组织和执行整个会议流程 - * @param args 会议参数 - */ - private async organizeMeeting(args: z.infer): Promise { - const { participants, topic, rounds } = args - - // 1. 定位并验证参会者 (最终修正逻辑) - const mainWindowId = presenter.windowPresenter.mainWindow?.id - if (!mainWindowId) throw new Error('主窗口未找到,无法开始会议。') - - const allChatTabs = (await presenter.tabPresenter.getWindowTabsData(mainWindowId)) - - const meetingParticipants: MeetingParticipant[] = [] - let nameIndex = 0 - - for (const p of participants) { - let tabData - - // 优先使用 tab_id 查找,这个基本上是通过create_new_tab获得的tab_id - if (p.tab_id !== undefined) { - tabData = allChatTabs.find((t) => t.id === p.tab_id) - } - - // 如果通过 tab_id 没找到,再尝试使用 tab_title,这个基本上是现成的已有tab,所以无需再次创建对话ID之类 - let foundByTabTitle = false - if (!tabData && p.tab_title) { - tabData = allChatTabs.find((t) => t.title === p.tab_title) - if (tabData) foundByTabTitle = true - } - - // 找不到 tabData,跳过这个无法准备的参会者 - if (!tabData) continue - - // 如果是新建的tab,则必须创建激活会话 - if (!foundByTabTitle) { - let conversationId = await presenter.threadPresenter.getActiveConversationId(tabData.id) - - // 确保每个有效的参会者都有一个可用的conversationId - const meetingName = - nameIndex < PARTICIPANT_NAMES.length - ? PARTICIPANT_NAMES[nameIndex] - : `参会者${nameIndex + 1}` - nameIndex++ - - // 如果Tab没有活动会话,则为其创建并等待UI同步 - if (!conversationId) { - console.log(`参会者 (ID: ${tabData.id}) 没有活动会话,正在为其创建...`) - - // 步骤 a: 创建新会话。这将自动激活并广播 'conversation:activated' 事件。 - conversationId = await presenter.threadPresenter.createConversation( - `${meetingName}`, // 为新会话设置一个描述性标题,即会议代号 - {}, - tabData.id, - { forceNewAndActivate: true } // 传入强制创建并激活的选项 - ) - - if (!conversationId) { - console.warn(`为Tab ${tabData.id} 创建会话失败,将跳过此参会者。`) - continue // 跳过这个无法准备的参会者 - } - - // 步骤 b: 关键的UI同步点!等待渲染进程处理完激活事件并回传确认信号。 - try { - await awaitTabActivated(conversationId) - console.log(`会话 ${conversationId} 在Tab ${tabData.id} 中已成功激活。`) - } catch (error) { - console.error(`等待Tab ${tabData.id} 激活失败:`, error) - continue // 如果UI同步失败,也跳过此参会者 - } - } - - meetingParticipants.push({ - meetingName, - tabId: tabData.id, - conversationId, - originalTitle: tabData.title, - profile: p.profile || `你可以就“${topic}”这个话题,自由发表你的看法和观点。` - }) - // 如果是非新建的tab,则无需创建激活会话 - } else { - let conversationId = await presenter.threadPresenter.getActiveConversationId(tabData.id) - - if (!conversationId) { - console.warn(`为Tab ${tabData.id} 创建会话失败,将跳过此参会者。`) - continue // 跳过这个无法准备的参会者 - } - - // 确保每个有效的参会者都有一个可用的conversationId - const meetingName = - nameIndex < PARTICIPANT_NAMES.length - ? PARTICIPANT_NAMES[nameIndex] - : `参会者${nameIndex + 1}` - nameIndex++ - - meetingParticipants.push({ - meetingName, - tabId: tabData.id, - conversationId, - originalTitle: tabData.title, - profile: p.profile || `你可以就“${topic}”这个话题,自由发表你的看法和观点。` - }) - } - } - - if (meetingParticipants.length < 2) { - throw new Error( - `会议无法开始。只找到了 ${meetingParticipants.length} 位有效的参会者。请确保指定的Tab ID或标题正确,并且它们正在进行对话。` - ) - } - - // 2. 初始化会议 (使用会议代号) - const participantNames = meetingParticipants.map((p) => p.meetingName).join('、') - - for (const p of meetingParticipants) { - const initPrompt = `您好,${p.meetingName}。 -我是Argus,是当前会议的组织者,很荣幸能邀请您参加会议: ---- -会议主题: ${topic} -所有参会者: ${participantNames} -你的会议名称: ${p.meetingName} -你的角色画像: ${p.profile} ---- -会议规则: -1. 请严格围绕你的角色和观点进行发言。 -2. 请等待主持人指示后方可发言。 -3. 发言时,请清晰地陈述你的论点。 -4. 你的发言将被转发给其他所有参会者。 -5. 在他人发言时,你会收到其发言内容,但请勿回复,轮到你再发言。 -6. 作为会议参与者,你不得调用与会议相关的工具函数。 ---- -会议现在开始。请等待你的发言回合。 -` - eventBus.sendToTab(p.tabId, MEETING_EVENTS.INSTRUCTION, { prompt: initPrompt }) - - // 等待AI模型的确认性回复,以同步流程,但我们忽略其具体内容。 - await this.waitForResponse(p.conversationId) - } - - // 3. 会议循环 (使用会议代号) - let history = `会议记录\n主题: ${topic}\n` - for (let round = 1; round <= rounds; round++) { - for (const speaker of meetingParticipants) { - const speakPrompt = `第 ${round}/${rounds} 轮。现在轮到您(${speaker.meetingName})发言。请陈述您的观点。` - eventBus.sendToTab(speaker.tabId, MEETING_EVENTS.INSTRUCTION, { prompt: speakPrompt }) - - // 等待并捕获真正的、需要被记录和转发的发言内容。 - const speechMessage = await this.waitForResponse(speaker.conversationId) - - const speechText = Array.isArray(speechMessage.content) - ? speechMessage.content.find((c: any) => c.type === 'content')?.content || '[无内容]' - : speechMessage.content || '[无内容]' - history += `\n[第${round}轮] ${speaker.meetingName}: ${speechText}` - - // 转发给其他参会者,并等待他们的确认性回复。 - for (const listener of meetingParticipants) { - if (listener.tabId !== speaker.tabId) { - const forwardPrompt = `来自 ${speaker.meetingName} 的发言如下:\n\n---\n${speechText}\n---\n\n**以上信息仅供参考,请不要回复!**\n作为参会者,请您(${listener.meetingName})等待我(Argus)的指示。` - eventBus.sendToTab(listener.tabId, MEETING_EVENTS.INSTRUCTION, { - prompt: forwardPrompt - }) - - // 等待确认性回复,并忽略内容。 - await this.waitForResponse(listener.conversationId) - } - } - } - } - - // 4. 结束会议 (使用会议代号) - for (const p of meetingParticipants) { - const personalizedFinalPrompt = `讨论已结束。请您(${p.meetingName})根据整个对话过程,对您的观点进行最终总结。` - eventBus.sendToTab(p.tabId, MEETING_EVENTS.INSTRUCTION, { prompt: personalizedFinalPrompt }) - } - - // 注意:这里不再有返回值,因为函数是在后台执行的。 - console.log(`关于“${topic}”的会议流程已在后台正常结束。`) - } -} +} \ No newline at end of file diff --git a/src/renderer/src/i18n/en-US/mcp.json b/src/renderer/src/i18n/en-US/mcp.json index 4ff4a11e1..a937a0909 100644 --- a/src/renderer/src/i18n/en-US/mcp.json +++ b/src/renderer/src/i18n/en-US/mcp.json @@ -191,6 +191,10 @@ "deepchat-inmemory/conversation-search-server": { "name": "Conversation History Search", "desc": "DeepChat built-in conversation history search service, can search historical conversation records and message contents" + }, + "deepchat-inmemory/meeting-server": { + "name": "Multi-Agent Meeting", + "desc": "DeepChat's built-in meeting service enables hosting and facilitating multi-agent discussions." } }, "prompts": { diff --git a/src/renderer/src/i18n/fa-IR/mcp.json b/src/renderer/src/i18n/fa-IR/mcp.json index fe516bff4..6764cc1e6 100644 --- a/src/renderer/src/i18n/fa-IR/mcp.json +++ b/src/renderer/src/i18n/fa-IR/mcp.json @@ -191,6 +191,10 @@ "deepchat-inmemory/conversation-search-server": { "name": "جستجوی تاریخچه گفت‌وگو", "desc": "خدمات جستجوی تاریخچه گفت‌وگوی داخلی دیپ‌چت، میتواند در تاریخچه و محتوای پیام‌های گفت‌وگو جستجو کند" + }, + "deepchat-inmemory/meeting-server": { + "name": "جلسه چند-عامل", + "desc": "سرویس جلسه داخلی DeepChat امکان میزبانی و مدیریت بحث‌های چند-عامله را فراهم می‌کند." } }, "prompts": { diff --git a/src/renderer/src/i18n/fr-FR/mcp.json b/src/renderer/src/i18n/fr-FR/mcp.json index 0604079bd..a7d095f5d 100644 --- a/src/renderer/src/i18n/fr-FR/mcp.json +++ b/src/renderer/src/i18n/fr-FR/mcp.json @@ -191,6 +191,10 @@ "deepchat-inmemory/conversation-search-server": { "name": "Recherche d'Historique de Conversation", "desc": "Service de recherche d'historique de conversation intégré DeepChat, peut rechercher les enregistrements de conversation historiques et le contenu des messages" + }, + "deepchat-inmemory/meeting-server": { + "name": "Réunion Multi-Agent", + "desc": "Le service de réunion intégré de DeepChat permet d’organiser et d’animer des discussions multi-agents." } }, "prompts": { diff --git a/src/renderer/src/i18n/ja-JP/mcp.json b/src/renderer/src/i18n/ja-JP/mcp.json index 4f91d05af..450ac28f6 100644 --- a/src/renderer/src/i18n/ja-JP/mcp.json +++ b/src/renderer/src/i18n/ja-JP/mcp.json @@ -191,6 +191,10 @@ "deepchat-inmemory/conversation-search-server": { "name": "会話履歴検索", "desc": "DeepChat内蔵の会話履歴検索サービス、過去の会話記録とメッセージ内容を検索できます" + }, + "deepchat-inmemory/meeting-server": { + "name": "マルチエージェント会議", + "desc": "DeepChatの内蔵会議サービスは、マルチエージェントによる討論の開催と進行を可能にします。" } }, "prompts": { diff --git a/src/renderer/src/i18n/ko-KR/mcp.json b/src/renderer/src/i18n/ko-KR/mcp.json index 6e7467ffd..11f429a1b 100644 --- a/src/renderer/src/i18n/ko-KR/mcp.json +++ b/src/renderer/src/i18n/ko-KR/mcp.json @@ -191,6 +191,10 @@ "deepchat-inmemory/conversation-search-server": { "name": "대화 기록 검색", "desc": "DeepChat 내장 대화 기록 검색 서비스, 과거 대화 기록과 메시지 내용을 검색할 수 있습니다" + }, + "deepchat-inmemory/meeting-server": { + "name": "멀티 에이전트 회의", + "desc": "DeepChat의 내장 회의 서비스는 다중 에이전트 토론의 주최와 진행을 지원합니다." } }, "prompts": { diff --git a/src/renderer/src/i18n/ru-RU/mcp.json b/src/renderer/src/i18n/ru-RU/mcp.json index aba533ff7..b0b1eb3ef 100644 --- a/src/renderer/src/i18n/ru-RU/mcp.json +++ b/src/renderer/src/i18n/ru-RU/mcp.json @@ -191,6 +191,10 @@ "deepchat-inmemory/conversation-search-server": { "name": "Поиск истории разговоров", "desc": "Встроенная служба поиска истории разговоров DeepChat, может искать записи исторических разговоров и содержимое сообщений" + }, + "deepchat-inmemory/meeting-server": { + "name": "Мультиагентная встреча", + "desc": "Встроенный сервис встреч DeepChat позволяет организовывать и проводить обсуждения между несколькими агентами." } }, "prompts": { diff --git a/src/renderer/src/i18n/zh-CN/mcp.json b/src/renderer/src/i18n/zh-CN/mcp.json index 391c2d333..0dc0e40c7 100644 --- a/src/renderer/src/i18n/zh-CN/mcp.json +++ b/src/renderer/src/i18n/zh-CN/mcp.json @@ -211,6 +211,10 @@ "deepchat-inmemory/conversation-search-server": { "name": "对话历史搜索", "desc": "DeepChat内置对话历史搜索服务,可搜索历史对话记录和消息内容" + }, + "deepchat-inmemory/meeting-server": { + "name": "多智能体会议", + "desc": "DeepChat 内置会议服务,支持发起和主持多智能体讨论" } }, "prompts": { diff --git a/src/renderer/src/i18n/zh-HK/mcp.json b/src/renderer/src/i18n/zh-HK/mcp.json index e06953c8c..0cb5c0ae2 100644 --- a/src/renderer/src/i18n/zh-HK/mcp.json +++ b/src/renderer/src/i18n/zh-HK/mcp.json @@ -191,6 +191,10 @@ "deepchat-inmemory/conversation-search-server": { "name": "對話歷史搜尋", "desc": "DeepChat內建對話歷史搜尋服務,可搜尋歷史對話記錄和訊息內容" + }, + "deepchat-inmemory/meeting-server": { + "name": "多智能體會議", + "desc": "DeepChat 內置的會議功能支援舉辦和主持多智能體討論。" } }, "prompts": { diff --git a/src/renderer/src/i18n/zh-TW/mcp.json b/src/renderer/src/i18n/zh-TW/mcp.json index be96b4e3e..03be55a95 100644 --- a/src/renderer/src/i18n/zh-TW/mcp.json +++ b/src/renderer/src/i18n/zh-TW/mcp.json @@ -211,6 +211,10 @@ "deepchat-inmemory/conversation-search-server": { "name": "對話歷史搜索", "desc": "DeepChat內建對話歷史搜索服務,可搜索歷史對話記錄和訊息內容" + }, + "deepchat-inmemory/meeting-server": { + "name": "多智能體會議", + "desc": "DeepChat 內建的會議服務可用於發起與主持多智能體討論。" } }, "prompts": { From da59fcafc354fc99b6b1175ba529b16e28c0f771 Mon Sep 17 00:00:00 2001 From: luy <12696648@qq.com> Date: Sat, 21 Jun 2025 23:35:28 +0800 Subject: [PATCH 4/5] fix: missing server module for MCP meeting --- .../inMemoryServers/meetingServer.ts | 398 ++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts new file mode 100644 index 000000000..97f5e1fc6 --- /dev/null +++ b/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts @@ -0,0 +1,398 @@ +// src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' +import { zodToJsonSchema } from 'zod-to-json-schema' +import { Transport } from '@modelcontextprotocol/sdk/shared/transport' +import { presenter } from '@/presenter' +import { eventBus } from '@/eventbus' +import { TAB_EVENTS, MEETING_EVENTS, CONVERSATION_EVENTS } from '@/events' + +// --- 会议常量和Schema定义 --- + +// 预设的参会者代号,用于在会议中标识不同的Tab +const PARTICIPANT_NAMES = [ + 'Alice', + 'Brian', + 'Chris', + 'David', + 'Emma', + 'Frank', + 'Grace', + 'Henry', + 'Ian', + 'Jack', + 'Kate', + 'Lily', + 'Mike', + 'Nick', + 'Oliver', + 'Peter', + 'Quinn', + 'Ryan', + 'Sarah', + 'Tom', + 'Uriel', + 'Victor', + 'Wendy', + 'Xavier', + 'Yolanda', + 'Zoe' +] + +// 定义单个参会者的Zod Schema,用于验证和解析LLM的工具调用参数 +const ParticipantSchema = z + .object({ + tab_id: z + .number() + .optional() + .describe( + '通过Tab的【唯一标识】来精确指定参会者。' + + '这是一个内部ID,通常通过create_new_tab等工具获得。' + + '仅当你可以明确获得参会者Tab的唯一标识时,才应使用此字段。' + + '这是最精确的定位方式。如果使用此字段,则不应填写 tab_title。' + ), + tab_title: z + .string() + .optional() + .describe( + '通过Tab的【当前显示标题】来指定参会者。' + + '当用户的指令中明确提到了Tab的名称(例如 "让标题为\'AI讨论\'的Tab...")时,应优先使用此字段。' + + '请注意,标题可能不是唯一的,系统会选择第一个匹配的Tab。如果使用此字段,则不应填写 tab_id。' + ), + profile: z + .string() + .optional() + .describe( + '用于定义该参会者的完整画像,可包括且不限于其角色身份、观察视角、立场观点、表达方式、行为模式、发言约束及其他提示词,用于驱动其在会议中的一致性行为和语言风格。' + ) + }) + .describe( + '定义一位会议的参会者。' + + '你必须通过且只能通过 "tab_id" 或 "tab_title" 字段中的一个来指定该参会者。' + + '决策依据:如果用户的指令明确提到了Tab的标题,请优先使用 tab_title。仅当你可以明确获得参会者tab唯一数字标识时,才使用 tab_id。' + ) + .refine( + (data) => { + const hasId = data.tab_id !== undefined && data.tab_id !== -1 + const hasTitle = data.tab_title !== undefined && data.tab_title.trim() !== '' + return (hasId && !hasTitle) || (!hasId && hasTitle) + }, + { + message: + '错误:必须且只能通过 "tab_id" 或 "tab_title" 中的一个来指定参会者,两者不能同时提供,也不能都为空。' + } + ) + +// 定义开启会议工具的Zod Schema +const StartMeetingArgsSchema = z.object({ + participants: z + .array(ParticipantSchema) + .min(2, { message: '会议至少需要两位参会者。' }) + .describe('参会者列表。'), + topic: z.string().describe('会议的核心讨论主题。'), + rounds: z.number().optional().default(3).describe('讨论的轮次数,默认为3轮。') +}) + +// --- 内部数据结构 --- + +// 内部使用的会议参与者信息接口,增加了会议代号 +interface MeetingParticipant { + meetingName: string + tabId: number + conversationId: string + originalTitle: string + profile: string +} + +// --- 辅助函数 --- + +// 等待Tab会话被渲染进程激活的异步函数。这是保证主进程可以安全地向目标Tab发送消息的关键同步点。 +function awaitTabActivated(threadId: string, timeout = 5000): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + eventBus.removeListener(TAB_EVENTS.RENDERER_TAB_ACTIVATED, listener) + reject(new Error(`等待会话 ${threadId} 激活超时。`)) + }, timeout) + + const listener = (activatedThreadId: string) => { + if (activatedThreadId === threadId) { + clearTimeout(timer) + eventBus.removeListener(TAB_EVENTS.RENDERER_TAB_ACTIVATED, listener) + resolve() + } + } + eventBus.on(TAB_EVENTS.RENDERER_TAB_ACTIVATED, listener) + }) +} + +/** + * MeetingServer 类 + * 负责处理所有与MCP会议相关的逻辑,包括工具定义和会议流程的组织。 + * Notes by luy: + * Risk 1: create_new_tab triggered by MCP may overlap with start_meeting due to lack of a clear completion event, causing race conditions. + * Risk 2: organizeMeeting relies on tab_title to find participants, but the title may change by user or another process might change the tab title between tool call generation and execution, leading to tab-lookup failures. + * Risk 3: Using title to locate conversation ID assumes the conversation ID persists even when the conversation is cleared (i.e., its length is zero), which may be changed in the future. + * Risk 4: If any participant's session fails, the entire meeting in background is aborted peacefully. This is by design for now. LGTM. + */ +export class MeetingServer { + private server: Server + + constructor() { + this.server = new Server( + { name: 'meeting-server', version: '1.0.0' }, + { capabilities: { tools: {} } } + ) + this.setupRequestHandlers() + } + + public startServer(transport: Transport): void { + this.server.connect(transport) + } + + private setupRequestHandlers(): void { + // 注册 `start_meeting` 工具 + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: [ + { + name: 'start_meeting', + description: + '启动并主持一个由多个Tab(参会者)参与的关于特定主题的讨论会议。如果你当前已经是某个会议的参与者,请勿调用!', + inputSchema: zodToJsonSchema(StartMeetingArgsSchema) + } + ] + })) + + // 处理 `start_meeting` 工具的调用 + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + if (name !== 'start_meeting') throw new Error(`未知的工具: ${name}`) + + try { + const meetingArgs = StartMeetingArgsSchema.parse(args) + + // 关键点: 立即返回成功响应,将耗时的会议流程放到后台执行,避免LLM工具调用超时。 + ;(async () => { + try { + await this.organizeMeeting(meetingArgs) + console.log('会议流程已在后台成功完成。') + } catch (meetingError: any) { + console.error(`会议执行过程中发生错误: ${meetingError.message}`) + } + })() + + return { content: [{ type: 'text', text: '会议已成功启动,正在后台进行中...' }] } + } catch (error: any) { + return { + content: [{ type: 'text', text: `会议启动失败: ${error.message}` }], + isError: true + } + } + }) + } + + /** + * 等待指定会话生成一个新消息。 + * 这是会议流程同步的关键,通过监听 `MESSAGE_GENERATED` 事件来确保按序进行。 + * @param conversationId 要等待的会话ID + * @param timeout 超时时间(毫秒) + * @returns 返回生成的消息对象 + */ + private waitForResponse(conversationId: string, timeout = 180000): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + eventBus.removeListener(CONVERSATION_EVENTS.MESSAGE_GENERATED, listener) + reject( + new Error( + `超时: 等待会话 ${conversationId} 的回复超过 ${timeout / 1000} 秒,未收到响应。` + ) + ) + }, timeout) + + const listener = (data: { conversationId: string; message: any }) => { + if (data.conversationId === conversationId) { + clearTimeout(timer) + eventBus.removeListener(CONVERSATION_EVENTS.MESSAGE_GENERATED, listener) + resolve(data.message) + } + } + eventBus.on(CONVERSATION_EVENTS.MESSAGE_GENERATED, listener) + }) + } + + /** + * 组织和执行整个会议流程。 + * @param args 会议参数 + */ + private async organizeMeeting(args: z.infer): Promise { + const { participants, topic, rounds } = args + + // 1. 准备阶段: 定位、验证并准备所有参会者 + const mainWindowId = presenter.windowPresenter.mainWindow?.id + if (!mainWindowId) throw new Error('主窗口未找到,无法开始会议。') + + const allChatTabs = await presenter.tabPresenter.getWindowTabsData(mainWindowId) + const meetingParticipants: MeetingParticipant[] = [] + let nameIndex = 0 + + for (const p of participants) { + let tabData + + // 优先使用 tab_id 查找 + if (p.tab_id !== undefined) { + tabData = allChatTabs.find((t) => t.id === p.tab_id) + } + + // 如果通过 tab_id 没找到,再尝试使用 tab_title + let foundByTabTitle = false + if (!tabData && p.tab_title) { + tabData = allChatTabs.find((t) => t.title === p.tab_title) + if (tabData) foundByTabTitle = true + } + + // 找不到 tabData,跳过这个参会者 + if (!tabData) continue + + // 关键点: 根据tab定位方法的不同,采取不同处理方法 + if (!foundByTabTitle) { + // 关键点1: 通过tabId定位到的tab通常为create_new_tab的结果,需要创建会话并激活 + let conversationId = await presenter.threadPresenter.getActiveConversationId(tabData.id) + + // 确保每个参会者都有一个参会名称,超过26个用‘参会者27’之类命名 + const meetingName = + nameIndex < PARTICIPANT_NAMES.length + ? PARTICIPANT_NAMES[nameIndex] + : `参会者${nameIndex + 1}` + nameIndex++ + + // 确保每个参会者都要有活动会话,如没有,则需创建并等待UI同步 + if (!conversationId) { + console.log(`参会者 (ID: ${tabData.id}) 没有活动会话,正在为其创建...`) + + // 步骤 a: 创建新会话,将自动激活并广播 'conversation:activated' 事件 + conversationId = await presenter.threadPresenter.createConversation( + `${meetingName}`, + {}, + tabData.id, + { forceNewAndActivate: true } //强制创建并激活空会话,避免冗余的空会话单例检测 + ) + if (!conversationId) { + console.warn(`为Tab ${tabData.id} 创建会话失败,将跳过此参会者。`) + continue + } + + // 步骤 b: 关键的UI同步点,等待渲染进程处理完激活事件并回传确认信号 + try { + await awaitTabActivated(conversationId) + console.log(`会话 ${conversationId} 在Tab ${tabData.id} 中已成功激活。`) + } catch (error) { + console.error(`等待Tab ${tabData.id} 激活失败:`, error) + continue + } + } + + meetingParticipants.push({ + meetingName, + tabId: tabData.id, + conversationId, + originalTitle: tabData.title, + profile: p.profile || `你可以就“${topic}”这个话题,自由发表你的看法和观点。` + }) + } else { + // 关键点2: 通过title定位到的tab,通常为已有tab,无需重新创建会话并激活 + let conversationId = await presenter.threadPresenter.getActiveConversationId(tabData.id) + if (!conversationId) { + console.warn(`为Tab ${tabData.id} 创建会话失败,将跳过此参会者。`) + continue + } + + // 确保每个有效的参会者都有一个可用的conversationId + const meetingName = + nameIndex < PARTICIPANT_NAMES.length + ? PARTICIPANT_NAMES[nameIndex] + : `参会者${nameIndex + 1}` + nameIndex++ + + meetingParticipants.push({ + meetingName, + tabId: tabData.id, + conversationId, + originalTitle: tabData.title, + profile: p.profile || `你可以就“${topic}”这个话题,自由发表你的看法和观点。` + }) + } + } + + if (meetingParticipants.length < 2) { + throw new Error( + `会议无法开始。只找到了 ${meetingParticipants.length} 位有效的参会者。请确保指定的Tab ID或Tab标题正确,并且它们正在进行对话。` + ) + } + + // 2. 初始化会议(使用会议代号): 向所有参与者发送会议主题、规则和角色画像 + const participantNames = meetingParticipants.map((p) => p.meetingName).join('、') + for (const p of meetingParticipants) { + const initPrompt = `您好,${p.meetingName}。 +我是Argus,是当前会议的组织者,很荣幸能邀请您参加会议: +--- +会议主题: ${topic} +所有参会者: ${participantNames} +你的会议名称: ${p.meetingName} +你的角色画像: ${p.profile} +--- +会议规则: +1. 请严格围绕你的角色和观点进行发言。 +2. 请等待主持人指示后方可发言。 +3. 发言时,请清晰地陈述你的论点。 +4. 你的发言将被转发给其他所有参会者。 +5. 在他人发言时,你会收到其发言内容,但请勿回复,轮到你再发言。 +6. 作为会议参与者,你不得调用与会议相关的工具函数。 +--- +会议现在开始。请等待你的发言回合。 +` + eventBus.sendToTab(p.tabId, MEETING_EVENTS.INSTRUCTION, { prompt: initPrompt }) + + // 等待AI模型的确认性回复,以同步流程,忽略其具体内容 + await this.waitForResponse(p.conversationId) + } + + // 3. 会议循环: 按轮次进行发言和广播 + let history = `会议记录\n主题: ${topic}\n` + for (let round = 1; round <= rounds; round++) { + for (const speaker of meetingParticipants) { + const speakPrompt = `第 ${round}/${rounds} 轮。现在轮到您(${speaker.meetingName})发言。请陈述您的观点。` + eventBus.sendToTab(speaker.tabId, MEETING_EVENTS.INSTRUCTION, { prompt: speakPrompt }) + + // 等待并捕获真正的、需要被记录和转发的发言内容 + const speechMessage = await this.waitForResponse(speaker.conversationId) + const speechText = Array.isArray(speechMessage.content) + ? speechMessage.content.find((c: any) => c.type === 'content')?.content || '[无内容]' + : speechMessage.content || '[无内容]' + history += `\n[第${round}轮] ${speaker.meetingName}: ${speechText}` + + // 广播发言给其他参会者,并等待他们的确认性回复 + for (const listener of meetingParticipants) { + if (listener.tabId !== speaker.tabId) { + const forwardPrompt = `来自 ${speaker.meetingName} 的发言如下:\n\n---\n${speechText}\n---\n\n**以上信息仅供参考,请不要回复!**\n作为参会者,请您(${listener.meetingName})等待我(Argus)的指示。` + eventBus.sendToTab(listener.tabId, MEETING_EVENTS.INSTRUCTION, { + prompt: forwardPrompt + }) + + // 等待确认性回复,并忽略内容 + await this.waitForResponse(listener.conversationId) + } + } + } + } + + // 4. 结束会议: 要求所有参与者总结 + for (const p of meetingParticipants) { + const personalizedFinalPrompt = `讨论已结束。请您(${p.meetingName})根据整个对话过程,对您的观点进行最终总结。` + eventBus.sendToTab(p.tabId, MEETING_EVENTS.INSTRUCTION, { prompt: personalizedFinalPrompt }) + } + + // 注意:这里不再有返回值,因为函数在后台执行 + console.log(`关于“${topic}”的会议流程已在后台正常结束。`) + } +} From 6ff196cbee8dd50996bdd839cae66e393ebb9768 Mon Sep 17 00:00:00 2001 From: luy <12696648@qq.com> Date: Sun, 22 Jun 2025 02:53:08 +0800 Subject: [PATCH 5/5] fix: adjust deepseek-chat token limit, refine meeting server logic --- .../presenter/configPresenter/modelDefaultSettings.ts | 2 +- .../mcpPresenter/inMemoryServers/meetingServer.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/presenter/configPresenter/modelDefaultSettings.ts b/src/main/presenter/configPresenter/modelDefaultSettings.ts index adefc75c2..fe627aaac 100644 --- a/src/main/presenter/configPresenter/modelDefaultSettings.ts +++ b/src/main/presenter/configPresenter/modelDefaultSettings.ts @@ -554,7 +554,7 @@ export const defaultModelsSettings: DefaultModelSetting[] = [ id: 'deepseek-chat', name: 'DeepSeek chat', temperature: 1, - maxTokens: 16000, + maxTokens: 8192, contextLength: 65536, match: ['deepseek-chat'], vision: false, diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts index 97f5e1fc6..db49f3971 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/meetingServer.ts @@ -275,7 +275,7 @@ export class MeetingServer { `${meetingName}`, {}, tabData.id, - { forceNewAndActivate: true } //强制创建并激活空会话,避免冗余的空会话单例检测 + { forceNewAndActivate: true } //强制创建并激活空会话,避免冗余的空会话单例检测 ) if (!conversationId) { console.warn(`为Tab ${tabData.id} 创建会话失败,将跳过此参会者。`) @@ -330,6 +330,11 @@ export class MeetingServer { ) } + // 在循环开始前,切换到第一个参与者的Tab + if (meetingParticipants.length > 0) { + await presenter.tabPresenter.switchTab(meetingParticipants[0].tabId) + } + // 2. 初始化会议(使用会议代号): 向所有参与者发送会议主题、规则和角色画像 const participantNames = meetingParticipants.map((p) => p.meetingName).join('、') for (const p of meetingParticipants) { @@ -347,7 +352,7 @@ export class MeetingServer { 3. 发言时,请清晰地陈述你的论点。 4. 你的发言将被转发给其他所有参会者。 5. 在他人发言时,你会收到其发言内容,但请勿回复,轮到你再发言。 -6. 作为会议参与者,你不得调用与会议相关的工具函数。 +6. 参会期间禁止调用会议相关的工具函数,如start_meeting等。 --- 会议现在开始。请等待你的发言回合。 `