/** * agent-bridge.js — Agent 路由桥接 * * 把 OpenClaw 的 Agent 分发流程桥接到我们的 orchestrator。 * * OpenClaw 的消息处理链路: * handleFeishuMessage() → resolveAgentRoute() → dispatchReplyFromConfig() * → Agent Runtime 处理消息 → 通过 reply-dispatcher 回复 * * 我们的桥接方式: * 在 feishu 的 channel monitor 收到消息后,消息会被传给 dispatchReplyFromConfig(), * dispatchReplyFromConfig() 内部会调用 Agent 来处理。 * * 我们通过配置 Agent 路由,让消息路由到我们自定义的 "agent", * 这个 agent 内部调用 orchestrator 来处理消息。 * * 用法: * const { createAgentBridge } = require('./plugins/agent-bridge'); * const bridge = createAgentBridge(orchestrator); */ /** * 创建 Agent 桥接器。 * * 桥接器提供一个统一的消息处理接口, * 把来自任何 OpenClaw channel(feishu/discord/slack 等)的消息 * 转发给我们的 orchestrator 进行多 Agent 编排。 * * @param {Object} orchestrator - 我们的编排器实例 * @param {Function} orchestrator.runOrchestration - 编排执行函数 * @param {Object} [options] - 选项 * @param {Object} [options.logger] - 日志接口 * @returns {Object} 桥接器 */ function createAgentBridge(orchestrator, options = {}) { const logger = options.logger || { info: console.log, warn: console.warn, error: console.error, debug: () => { } }; // 消息处理记录 const history = []; const MAX_HISTORY = 100; /** * 处理来自 OpenClaw channel 的入站消息。 * * @param {Object} ctx - 消息上下文(OpenClaw InboundContext 格式) * @param {string} ctx.Body - 消息正文 * @param {string} [ctx.BodyForAgent] - 格式化后的消息正文 * @param {string} ctx.SessionKey - 会话标识 * @param {string} [ctx.SenderId] - 发送者 ID * @param {string} [ctx.SenderName] - 发送者名称 * @param {string} [ctx.Channel] - 通道名称 * @param {Object} [ctx.Media] - 媒体附件 * @returns {Promise} 处理结果 { text, media } */ async function handleInbound(ctx) { const userMessage = ctx.BodyForAgent || ctx.Body || ''; const channel = ctx.Channel || 'unknown'; const sessionKey = ctx.SessionKey || 'default'; const senderId = ctx.SenderId || 'unknown'; const senderName = ctx.SenderName || senderId; const startTime = Date.now(); logger.info(`[agent-bridge] 收到消息: channel=${channel}, sender=${senderName}, length=${userMessage.length}`); logger.debug(`[agent-bridge] 消息内容: ${userMessage.substring(0, 200)}...`); try { // 调用编排器 const result = await orchestrator.runOrchestration({ task: userMessage, sessionKey, context: { channel, sender: senderId, senderName, media: ctx.Media || null, }, }); const elapsed = Date.now() - startTime; // 提取回复文本 const replyText = result.summary || result.output || result.text || (typeof result === 'string' ? result : JSON.stringify(result)); // 记录历史 const record = { timestamp: new Date().toISOString(), channel, sessionKey, senderId, inputLength: userMessage.length, outputLength: replyText.length, elapsedMs: elapsed, status: 'success', }; history.push(record); if (history.length > MAX_HISTORY) history.shift(); logger.info(`[agent-bridge] 完成: ${elapsed}ms, output=${replyText.length}字符`); return { text: replyText, media: result.media || null, }; } catch (err) { const elapsed = Date.now() - startTime; logger.error(`[agent-bridge] 编排失败: ${err.message}`); // 记录错误 const record = { timestamp: new Date().toISOString(), channel, sessionKey, senderId, inputLength: userMessage.length, elapsedMs: elapsed, status: 'error', error: err.message, }; history.push(record); if (history.length > MAX_HISTORY) history.shift(); return { text: `处理失败: ${err.message}`, media: null, }; } } /** * 创建一个 OpenClaw 兼容的 "Agent Provider", * 用于替换 OpenClaw 内置的 Agent(如 Claude/GPT), * 把消息路由到我们的编排器。 * * 当 OpenClaw 的 dispatchReplyFromConfig 内部需要调用 Agent 时, * 它会通过 Agent Provider 来获取回复。 * 我们的 provider 拦截这个调用,桥接到 orchestrator。 */ function createProvider() { return { id: 'multi-agent-orchestrator', name: 'Multi-Agent Orchestrator', async chat(messages, options) { // 从 messages 中提取最后一条用户消息 const lastUserMsg = messages .filter(m => m.role === 'user') .pop(); if (!lastUserMsg) { return { content: '(无有效输入)' }; } const result = await handleInbound({ Body: typeof lastUserMsg.content === 'string' ? lastUserMsg.content : JSON.stringify(lastUserMsg.content), SessionKey: options?.sessionKey || 'default', Channel: options?.channel || 'api', }); return { content: result.text }; }, }; } return { handleInbound, createProvider, /** 获取处理历史 */ getHistory() { return [...history]; }, /** 获取统计信息 */ getStats() { const successCount = history.filter(h => h.status === 'success').length; const errorCount = history.filter(h => h.status === 'error').length; const avgMs = successCount > 0 ? Math.round(history.filter(h => h.status === 'success').reduce((s, h) => s + h.elapsedMs, 0) / successCount) : 0; return { total: history.length, success: successCount, error: errorCount, avgResponseMs: avgMs, }; }, }; } module.exports = { createAgentBridge };