Skip to content

Commit 4dfd709

Browse files
author
linyuan.yang
committed
channel 支持主动 send
1 parent d143930 commit 4dfd709

11 files changed

Lines changed: 128 additions & 3 deletions

File tree

packages/channel.base/src/ChannelPlugin.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { SessionService } from "./SessionService";
44

55
export interface IChannelService {
66
createSessionHandler(session: SessionService): ChannelSessionHandler;
7-
dispose?(): void;
7+
sendText(sessionId: string, text: string): Promise<void>;
8+
sendFile(sessionId: string, file: string | Buffer, fileName?: string): Promise<void>;
9+
sendNative(sessionId: string, payload: any): Promise<void>;
10+
dispose(): void;
811
}
912

1013
export interface InitSessionContext {

packages/channel.lark/src/LarkService.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,19 @@ export class LarkService implements IChannelService {
147147
return new LarkSessionHandler(session, this);
148148
}
149149

150+
async sendText(sessionId: string, text: string): Promise<void> {
151+
await this.sendMarkdownMessage(LarkReceiveIdType.ChatId, sessionId, text);
152+
}
153+
154+
async sendFile(sessionId: string, file: string | Buffer, fileName?: string): Promise<void> {
155+
await this.sendFileMessage(LarkReceiveIdType.ChatId, sessionId, file, fileName);
156+
}
157+
158+
async sendNative(sessionId: string, payload: any): Promise<void> {
159+
const content = typeof payload === 'string' ? payload : JSON.stringify(payload);
160+
await this.sendMessage(LarkReceiveIdType.ChatId, sessionId, 'interactive', content);
161+
}
162+
150163
dispose() {
151164
try { this.larkWsClient?.close(); } catch (_) {}
152165
this.tenantAccessToken = '';

packages/channel.onebot/src/OnebotService.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ export class OnebotService implements IChannelService {
5050
return new OnebotSessionHandler(session, this);
5151
}
5252

53+
private parseSessionTarget(sessionId: string): { userId?: number; groupId?: number } {
54+
const parts = sessionId.split(':');
55+
if (parts[1] === 'group') return { groupId: Number(parts[2]), userId: Number(parts[3]) };
56+
return { userId: Number(parts[2]) };
57+
}
58+
59+
async sendText(sessionId: string, text: string): Promise<void> {
60+
await this.sendTextMessage(this.parseSessionTarget(sessionId), text);
61+
}
62+
63+
async sendFile(_sessionId: string, _file: string | Buffer, _fileName?: string): Promise<void> {}
64+
65+
async sendNative(_sessionId: string, _payload: any): Promise<void> {}
66+
5367
dispose() {
5468
for (const ws of this.connections) {
5569
try { ws.close(); } catch (_) {}

packages/channel.slack/src/SlackService.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ export class SlackService implements IChannelService {
4040
return new SlackSessionHandler(session, this);
4141
}
4242

43+
async sendText(sessionId: string, text: string): Promise<void> {
44+
await this.sendMessage(sessionId, text);
45+
}
46+
47+
async sendFile(_sessionId: string, _file: string | Buffer, _fileName?: string): Promise<void> {}
48+
49+
async sendNative(sessionId: string, payload: any): Promise<void> {
50+
await this.app.client.chat.postMessage({
51+
channel: sessionId,
52+
text: payload.text ?? '',
53+
...(payload.blocks ? { blocks: payload.blocks } : {}),
54+
});
55+
}
56+
4357
dispose() {
4458
this.app.stop().catch(() => {});
4559
}

packages/channel.wechat/src/WechatService.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ export class WechatService implements IChannelService {
5353
return new WechatSessionHandler(session, this);
5454
}
5555

56+
async sendText(sessionId: string, text: string): Promise<void> {
57+
await this.sendTextMessage(sessionId, text);
58+
}
59+
60+
async sendFile(sessionId: string, file: string | Buffer, fileName?: string): Promise<void> {
61+
await this.sendFileMessage(sessionId, file, fileName);
62+
}
63+
64+
async sendNative(_sessionId: string, _payload: any): Promise<void> {}
65+
5666
dispose(): void {
5767
this._running = false;
5868
this._abortController?.abort();

packages/channel.wecom/src/WecomService.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ export class WecomService implements IChannelService {
5858
return new WecomSessionHandler(session, this);
5959
}
6060

61+
async sendText(sessionId: string, text: string): Promise<void> {
62+
await this.sendMessage(sessionId, { msgtype: 'markdown', markdown: { content: text } } as any);
63+
}
64+
65+
async sendFile(sessionId: string, file: string | Buffer, fileName?: string): Promise<void> {
66+
await this.sendFileMessage(sessionId, file, fileName);
67+
}
68+
69+
async sendNative(sessionId: string, payload: any): Promise<void> {
70+
await this.sendMessage(sessionId, payload);
71+
}
72+
6173
dispose() {
6274
try { this.wsClient.disconnect(); } catch (_) {}
6375
}

packages/channel.xiaoai/src/XiaoaiService.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
import { login } from './mi/account';
66
import { getDeviceList, findDeviceByName } from './mi/mina';
77
import { MessagePoller, type PollingMessage } from './polling';
8+
import { speak } from './speaker';
89
import type { AuthedAccount } from './mi/types';
910
import { XiaoaiSessionHandler } from './XiaoaiSessionHandler';
1011

@@ -29,6 +30,7 @@ export interface XiaoaiServiceOptions {
2930
export class XiaoaiService implements IChannelService {
3031
private authed: AuthedAccount | undefined;
3132
private poller: MessagePoller | undefined;
33+
private deviceId: string = '';
3234
private logger?: ILogger;
3335
private options: XiaoaiServiceOptions;
3436

@@ -41,6 +43,16 @@ export class XiaoaiService implements IChannelService {
4143
return new XiaoaiSessionHandler(session, this);
4244
}
4345

46+
async sendText(_sessionId: string, text: string): Promise<void> {
47+
if (!this.authed || !this.deviceId) return;
48+
await speak(this.authed, this.deviceId, text, {
49+
chunkLimit: this.options.textChunkLimit,
50+
volume: this.options.volume,
51+
});
52+
}
53+
async sendFile(_sessionId: string, _file: string | Buffer, _fileName?: string): Promise<void> {}
54+
async sendNative(_sessionId: string, _payload: any): Promise<void> {}
55+
4456
getAuthedAccount(): AuthedAccount | undefined {
4557
return this.authed;
4658
}
@@ -75,6 +87,7 @@ export class XiaoaiService implements IChannelService {
7587
(msg) => this.handleMessage(msg),
7688
this.logger,
7789
);
90+
this.deviceId = matched.deviceID;
7891
this.poller.startDevice(matched.deviceID, device);
7992
}
8093

packages/channel.xiaoai/tsconfig.tsbuildinfo

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/sbot/src/Channel/ChannelManager.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,27 @@ export class ChannelManager {
201201
getChannel(channelId: string) { return config.getChannel(channelId); }
202202
getService(channelId: string) { return this.services.get(channelId); }
203203

204+
async sendText(channelId: string, sessionId: string, text: string): Promise<boolean> {
205+
const service = this.services.get(channelId);
206+
if (!service) return false;
207+
await service.sendText(sessionId, text);
208+
return true;
209+
}
210+
211+
async sendFile(channelId: string, sessionId: string, file: string | Buffer, fileName?: string): Promise<boolean> {
212+
const service = this.services.get(channelId);
213+
if (!service) return false;
214+
await service.sendFile(sessionId, file, fileName);
215+
return true;
216+
}
217+
218+
async sendNative(channelId: string, sessionId: string, payload: any): Promise<boolean> {
219+
const service = this.services.get(channelId);
220+
if (!service) return false;
221+
await service.sendNative(sessionId, payload);
222+
return true;
223+
}
224+
204225
async loadPlugin(moduleOrPath: string): Promise<ChannelPlugin | undefined> {
205226
const plugin = this.pluginLoader.loadPlugin(moduleOrPath);
206227
if (plugin) this.plugins.set(plugin.type, plugin);

packages/sbot/src/Server/HttpServer.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,31 @@ class HttpServer {
13911391
await database.destroy(database.channelSession, { where: { id } });
13921392
}));
13931393

1394+
app.post('/api/channels/:channelId/send', api(async req => {
1395+
const channelId = req.params.channelId as string;
1396+
const { sessionId, type, content, payload } = req.body as Record<string, any>;
1397+
if (!sessionId) throwBad('sessionId is required');
1398+
if (!type) throwBad('type is required');
1399+
let ok: boolean;
1400+
switch (type) {
1401+
case 'text':
1402+
if (!content) throwBad('content is required for type "text"');
1403+
ok = await channelManager.sendText(channelId, sessionId, content);
1404+
break;
1405+
case 'file':
1406+
if (!content) throwBad('content (file path) is required for type "file"');
1407+
ok = await channelManager.sendFile(channelId, sessionId, content, req.body.fileName);
1408+
break;
1409+
case 'native':
1410+
if (!payload) throwBad('payload is required for type "native"');
1411+
ok = await channelManager.sendNative(channelId, sessionId, payload);
1412+
break;
1413+
default:
1414+
throwBad(`Unknown type "${type}", expected "text" | "file" | "native"`);
1415+
}
1416+
if (!ok) throwBad(`Channel "${channelId}" not found or not running`);
1417+
}));
1418+
13941419
// --- QR code login ---
13951420
// Supports both /api/channel-plugins/:type/qrcode/:key (add flow)
13961421
// and /api/channels/:id/qrcode/:key (edit flow, auto-persists)

0 commit comments

Comments
 (0)