509 lines
18 KiB
Markdown
509 lines
18 KiB
Markdown
---
|
||
version: 2.0
|
||
last-updated: 2026-03-09
|
||
status: design
|
||
---
|
||
|
||
# 05 — Channel 多渠道接入
|
||
|
||
## 一、现状分析
|
||
|
||
### V1 现有实现
|
||
|
||
`IMService` (`renderer/services/integrations/im.ts`):
|
||
|
||
- 纯前端实现,支持 Discord / Telegram / DingTalk / Feishu
|
||
- 配置存储在内存中(`loadConfigs` / `saveConfigs` → 未完整实现)
|
||
- 各平台实现均为框架代码(TODO 占位)
|
||
- 无框架化的消息路由和 Agent 调度
|
||
|
||
**问题**:
|
||
|
||
1. 在 Renderer 进程中处理 IM 连接,窗口关闭后不可用
|
||
2. 各平台实现高度重复,无统一抽象
|
||
3. 收到 IM 消息后无法自动调度 Agent 处理
|
||
4. 无消息队列和重试机制
|
||
|
||
---
|
||
|
||
## 二、ChannelGateway 设计
|
||
|
||
### 2.1 核心思路
|
||
|
||
> **关键特色**:QimingClaw 配置的域名(如 `https://testagent.xspaceagi.com`)本身已是一个完整的 Agent 服务。Channel 网关的核心职责是将各 IM 平台的消息**转发到该 Agent 服务**,并将 Agent 的响应**回推到 IM 平台**。
|
||
|
||
```
|
||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||
│ 飞书 │ │ 钉钉 │ │ Telegram │ │ Discord │
|
||
│ Bot │ │ Bot │ │ Bot │ │ Bot │
|
||
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
|
||
│ │ │ │
|
||
└───────────────┼───────────────┼───────────────┘
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ ChannelGateway (Main Process) │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────┐ │
|
||
│ │ ChannelRouter (消息路由) │ │
|
||
│ │ IM 消息 ──→ 匹配目标 Agent Service ──→ 转发 │ │
|
||
│ └──────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌────────────────┼─────────────────┐ │
|
||
│ ▼ ▼ ▼ │
|
||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||
│ │ Agent Service│ │ Local Chat │ │ Agent Service│ │
|
||
│ │ (Remote URL) │ │ Engine │ │ (另一个域名) │ │
|
||
│ │ testagent. │ │ (本地推理) │ │ prodagent. │ │
|
||
│ │ xspaceagi.com│ │ │ │ xspaceagi.com│ │
|
||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||
│ │ │
|
||
│ ┌──────────────────────────────────────────────────┐ │
|
||
│ │ ResponseRelay (响应回推) │ │
|
||
│ │ Agent 响应 ──→ 格式化 ──→ 推送到 IM 平台 │ │
|
||
│ └──────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────┐ │
|
||
│ │ MessageQueue (消息队列) │ │
|
||
│ │ 重试 · 去重 · 限流 · 持久化 │ │
|
||
│ └──────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.2 两种消息路由模式
|
||
|
||
| 模式 | 描述 | 使用场景 |
|
||
| ---------------- | ---------------------------------- | --------------------------------------------------- |
|
||
| **Remote Agent** | 消息转发到配置的 Agent Service URL | 已有完整 Agent 服务(如 `testagent.xspaceagi.com`) |
|
||
| **Local Agent** | 消息路由到本地 `ChatEngine` | 使用本地模型/Provider 直接处理 |
|
||
|
||
```typescript
|
||
type ChannelRouteTarget =
|
||
| {
|
||
type: "remote-agent";
|
||
/** 完整的 Agent 服务 URL,本身已是一个工作的 Agent 后端 */
|
||
serviceUrl: string; // e.g. https://testagent.xspaceagi.com
|
||
/** 鉴权方式 */
|
||
auth?: {
|
||
type: "bearer" | "api-key" | "custom-header";
|
||
token: string;
|
||
headerName?: string; // custom-header 时使用
|
||
};
|
||
/** Chat 端点路径(默认 /computer/chat) */
|
||
chatEndpoint?: string;
|
||
/** SSE 端点路径(默认 /computer/progress/{session_id}) */
|
||
progressEndpoint?: string;
|
||
}
|
||
| {
|
||
type: "local-agent";
|
||
/** 本地 Provider + Model */
|
||
providerId: string;
|
||
modelId: string;
|
||
/** 可选系统提示 */
|
||
systemPrompt?: string;
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 三、Channel 抽象
|
||
|
||
### 3.1 统一 Channel 接口
|
||
|
||
```typescript
|
||
/** Channel 适配器接口 */
|
||
interface ChannelAdapter {
|
||
/** 平台标识 */
|
||
readonly platform: ChannelPlatform;
|
||
|
||
/** 初始化连接 */
|
||
connect(config: ChannelConfig): Promise<void>;
|
||
|
||
/** 断开连接 */
|
||
disconnect(): Promise<void>;
|
||
|
||
/** 连接状态 */
|
||
isConnected(): boolean;
|
||
|
||
/** 发送消息到 IM 平台 */
|
||
sendMessage(
|
||
target: MessageTarget,
|
||
content: ChannelMessageContent,
|
||
): Promise<SendResult>;
|
||
|
||
/** 注册消息回调 */
|
||
onMessage(handler: (message: IncomingChannelMessage) => void): void;
|
||
|
||
/** 注册事件回调(连接/断开/错误) */
|
||
onEvent(handler: (event: ChannelEvent) => void): void;
|
||
}
|
||
|
||
type ChannelPlatform =
|
||
| "feishu"
|
||
| "dingtalk"
|
||
| "telegram"
|
||
| "discord"
|
||
| "wechat-work"
|
||
| "slack";
|
||
|
||
interface ChannelConfig {
|
||
id: string; // Channel 实例 ID
|
||
platform: ChannelPlatform;
|
||
enabled: boolean;
|
||
name: string; // 显示名称
|
||
|
||
/** 平台凭据 */
|
||
credentials: ChannelCredentials;
|
||
|
||
/** 消息路由目标 */
|
||
routeTarget: ChannelRouteTarget;
|
||
|
||
/** 行为配置 */
|
||
behavior: {
|
||
autoReply: boolean; // 是否自动回复
|
||
allowedUsers?: string[]; // 用户白名单(空=全部)
|
||
allowedGroups?: string[]; // 群组白名单
|
||
triggerKeyword?: string; // 触发关键词(如 @bot)
|
||
replyFormat: "text" | "markdown" | "card"; // 回复格式
|
||
maxConcurrent: number; // 最大并发会话
|
||
};
|
||
}
|
||
|
||
type ChannelCredentials =
|
||
| { platform: "feishu"; appId: string; appSecret: string }
|
||
| {
|
||
platform: "dingtalk";
|
||
appKey: string;
|
||
appSecret: string;
|
||
robotCode?: string;
|
||
}
|
||
| { platform: "telegram"; botToken: string }
|
||
| { platform: "discord"; botToken: string; applicationId: string }
|
||
| { platform: "wechat-work"; corpId: string; agentId: string; secret: string }
|
||
| { platform: "slack"; botToken: string; appToken: string };
|
||
```
|
||
|
||
### 3.2 消息模型
|
||
|
||
```typescript
|
||
/** 收到的 IM 消息 */
|
||
interface IncomingChannelMessage {
|
||
id: string; // 消息 ID
|
||
platform: ChannelPlatform;
|
||
channelConfigId: string; // Channel 配置 ID
|
||
|
||
sender: {
|
||
id: string;
|
||
name: string;
|
||
avatar?: string;
|
||
};
|
||
|
||
/** 消息来源 */
|
||
source: {
|
||
type: "direct" | "group";
|
||
groupId?: string;
|
||
groupName?: string;
|
||
channelId?: string; // Discord/Slack channel
|
||
};
|
||
|
||
/** 消息内容 */
|
||
content: {
|
||
text: string;
|
||
mentions?: string[];
|
||
attachments?: { type: string; url: string; name: string }[];
|
||
};
|
||
|
||
/** 是否 @了机器人 */
|
||
mentionedBot: boolean;
|
||
|
||
timestamp: number;
|
||
replyToMessageId?: string;
|
||
}
|
||
|
||
/** 回复内容(平台无关) */
|
||
interface ChannelMessageContent {
|
||
text: string;
|
||
markdown?: string; // 如果平台支持
|
||
card?: ChannelCardMessage; // 如果平台支持卡片
|
||
attachments?: { name: string; data: Buffer; mimeType: string }[];
|
||
}
|
||
|
||
/** 卡片消息(飞书/钉钉) */
|
||
interface ChannelCardMessage {
|
||
title: string;
|
||
sections: {
|
||
type: "text" | "code" | "divider" | "action";
|
||
content?: string;
|
||
language?: string;
|
||
buttons?: { text: string; url?: string; value?: string }[];
|
||
}[];
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、ChannelGateway 服务
|
||
|
||
```typescript
|
||
class ChannelGateway {
|
||
private adapters: Map<string, ChannelAdapter> = new Map();
|
||
private configs: Map<string, ChannelConfig> = new Map();
|
||
private sessionMap: Map<string, string> = new Map(); // IM会话 → Agent会话
|
||
|
||
// ==================== 配置管理 ====================
|
||
|
||
/** 添加/更新 Channel 配置 */
|
||
upsertChannel(config: ChannelConfig): Promise<void>;
|
||
|
||
/** 删除 Channel */
|
||
deleteChannel(id: string): Promise<void>;
|
||
|
||
/** 列出所有 Channel */
|
||
listChannels(): ChannelConfig[];
|
||
|
||
// ==================== 连接管理 ====================
|
||
|
||
/** 连接指定 Channel */
|
||
connect(channelId: string): Promise<{ success: boolean; error?: string }>;
|
||
|
||
/** 断开指定 Channel */
|
||
disconnect(channelId: string): Promise<void>;
|
||
|
||
/** 连接所有启用的 Channel */
|
||
connectAll(): Promise<void>;
|
||
|
||
/** 获取连接状态 */
|
||
getStatus(): ChannelStatus[];
|
||
|
||
// ==================== 消息处理 ====================
|
||
|
||
/** 处理收到的 IM 消息 */
|
||
private handleIncomingMessage(message: IncomingChannelMessage): Promise<void>;
|
||
|
||
/**
|
||
* 路由到 Remote Agent Service
|
||
* → POST {serviceUrl}/computer/chat
|
||
* → 监听 SSE {serviceUrl}/computer/progress/{session_id}
|
||
* → 将 Agent 响应格式化后回推到 IM
|
||
*/
|
||
private routeToRemoteAgent(
|
||
message: IncomingChannelMessage,
|
||
target: RemoteAgentTarget,
|
||
): Promise<void>;
|
||
|
||
/**
|
||
* 路由到 Local ChatEngine
|
||
* → chatEngine.chat(sessionId, input)
|
||
* → 将响应格式化后回推到 IM
|
||
*/
|
||
private routeToLocalAgent(
|
||
message: IncomingChannelMessage,
|
||
target: LocalAgentTarget,
|
||
): Promise<void>;
|
||
}
|
||
```
|
||
|
||
### 4.1 Remote Agent 调用流程
|
||
|
||
```
|
||
IM 消息到达
|
||
│
|
||
▼
|
||
┌─── ChannelGateway ─────────────────────────────┐
|
||
│ │
|
||
│ 1. 查找/创建 Agent 会话映射 │
|
||
│ IM(userId+groupId) → agentSessionId │
|
||
│ │
|
||
│ 2. POST {serviceUrl}/computer/chat │
|
||
│ { │
|
||
│ user_id: sender.id, │
|
||
│ project_id: channelConfigId, │
|
||
│ message: content.text, │
|
||
│ session_id: agentSessionId │
|
||
│ } │
|
||
│ │
|
||
│ 3. 建立 SSE 连接 │
|
||
│ GET {serviceUrl}/computer/progress/{sid} │
|
||
│ │
|
||
│ 4. 监听 SSE 事件,组装回复 │
|
||
│ - text_delta → 累积文本 │
|
||
│ - tool_call → 可选展示工具调用 │
|
||
│ - stream_end → 发送最终回复到 IM │
|
||
│ │
|
||
│ 5. 格式化回复 │
|
||
│ - 纯文本 / Markdown / 卡片消息 │
|
||
│ - 按平台能力适配 │
|
||
└──────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 五、各平台适配器
|
||
|
||
### 5.1 飞书 (Feishu / Lark)
|
||
|
||
```typescript
|
||
class FeishuAdapter implements ChannelAdapter {
|
||
readonly platform = "feishu";
|
||
private eventServer: http.Server; // 事件订阅 HTTP 回调
|
||
private accessToken: string;
|
||
|
||
async connect(config: ChannelConfig): Promise<void> {
|
||
const cred = config.credentials as FeishuCredentials;
|
||
|
||
// 1. 获取 tenant_access_token
|
||
this.accessToken = await this.getTenantAccessToken(
|
||
cred.appId,
|
||
cred.appSecret,
|
||
);
|
||
|
||
// 2. 启动事件订阅 HTTP Server(接收飞书推送)
|
||
this.eventServer = http.createServer(this.handleEvent.bind(this));
|
||
// 动态分配端口,避免与本地其他服务冲突
|
||
const port =
|
||
config.callbackPort || (await this.findAvailablePort(9990, 9999));
|
||
this.eventServer.listen(port);
|
||
|
||
// 或使用 WebSocket 长连接模式
|
||
}
|
||
|
||
async sendMessage(
|
||
target: MessageTarget,
|
||
content: ChannelMessageContent,
|
||
): Promise<SendResult> {
|
||
// POST https://open.feishu.cn/open-apis/im/v1/messages
|
||
// 支持 text / interactive card / markdown
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 钉钉 (DingTalk)
|
||
|
||
```typescript
|
||
class DingtalkAdapter implements ChannelAdapter {
|
||
readonly platform = "dingtalk";
|
||
|
||
async connect(config: ChannelConfig): Promise<void> {
|
||
const cred = config.credentials as DingtalkCredentials;
|
||
// Stream 模式(推荐):SDK 长连接
|
||
// 或 HTTP 回调模式
|
||
}
|
||
|
||
async sendMessage(
|
||
target: MessageTarget,
|
||
content: ChannelMessageContent,
|
||
): Promise<SendResult> {
|
||
// POST https://oapi.dingtalk.com/robot/send
|
||
// 支持 text / markdown / actionCard
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.3 Telegram
|
||
|
||
```typescript
|
||
class TelegramAdapter implements ChannelAdapter {
|
||
readonly platform = "telegram";
|
||
|
||
async connect(config: ChannelConfig): Promise<void> {
|
||
const cred = config.credentials as TelegramCredentials;
|
||
// Long Polling 模式(无需公网 IP)
|
||
// getUpdates loop
|
||
}
|
||
|
||
async sendMessage(
|
||
target: MessageTarget,
|
||
content: ChannelMessageContent,
|
||
): Promise<SendResult> {
|
||
// POST https://api.telegram.org/bot{token}/sendMessage
|
||
// 支持 text / markdown / HTML
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.4 工厂模式
|
||
|
||
```typescript
|
||
class ChannelAdapterFactory {
|
||
static create(platform: ChannelPlatform): ChannelAdapter {
|
||
switch (platform) {
|
||
case "feishu":
|
||
return new FeishuAdapter();
|
||
case "dingtalk":
|
||
return new DingtalkAdapter();
|
||
case "telegram":
|
||
return new TelegramAdapter();
|
||
case "discord":
|
||
return new DiscordAdapter();
|
||
case "slack":
|
||
return new SlackAdapter();
|
||
default:
|
||
throw new Error(`Unsupported platform: ${platform}`);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、IPC 接口设计
|
||
|
||
```typescript
|
||
// Channel CRUD
|
||
'channel:list' → ChannelConfig[]
|
||
'channel:get' → ChannelConfig
|
||
'channel:upsert' → { success: boolean }
|
||
'channel:delete' → { success: boolean }
|
||
|
||
// 连接控制
|
||
'channel:connect' → { success: boolean; error?: string }
|
||
'channel:disconnect' → { success: boolean }
|
||
'channel:connectAll' → { success: boolean }
|
||
'channel:status' → ChannelStatus[]
|
||
|
||
// 消息日志
|
||
'channel:messages' → IncomingChannelMessage[] // 最近的 IM 消息日志
|
||
|
||
// Events (Main → Renderer)
|
||
'channel:event' Event: { type: 'connected' | 'disconnected' | 'message' | 'error'; ... }
|
||
```
|
||
|
||
---
|
||
|
||
## 七、UI 设计要点
|
||
|
||
```
|
||
┌─ Channel 管理 ──────────────────────────────────────────┐
|
||
│ │
|
||
│ ● 飞书 Bot ✓ 在线 │ 收到 156 条 │ 回复 148 条 │
|
||
│ ● 钉钉 Bot ✓ 在线 │ 收到 89 条 │ 回复 89 条 │
|
||
│ ○ Telegram ✗ 未配置 │ │
|
||
│ │
|
||
│ [+ 新增 Channel] [全部连接] [全部断开] │
|
||
│ │
|
||
│ ── 飞书 Bot 配置 ────────────────────── │
|
||
│ App ID: [cli_xxxxx ] │
|
||
│ App Secret: [•••••••••••••••• ] │
|
||
│ │
|
||
│ 消息路由: ● 远程 Agent 服务 │
|
||
│ URL: [https://testagent.xspaceagi.com ] │
|
||
│ 认证: [Bearer Token ▾] [••••••••••] │
|
||
│ ○ 本地 Agent │
|
||
│ Provider: [OpenAI ▾] Model: [gpt-4o ▾] │
|
||
│ │
|
||
│ 行为: │
|
||
│ ☑ 自动回复 触发词: [@QimingClaw ] │
|
||
│ ☑ 群聊回复 回复格式: [卡片消息 ▾] │
|
||
│ 最大并发: [5] │
|
||
│ │
|
||
│ [测试连接] [保存] │
|
||
└──────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 相关文档
|
||
|
||
- [总体架构](./01-ARCHITECTURE.md)
|
||
- [会话管理](./04-SESSION-CHAT.md) — Channel 消息创建的会话
|
||
- [V1 IMService](../../src/renderer/services/integrations/im.ts)
|