@dsh-overdrive/gateway 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +33 -0
- package/dist/adapter.js +2 -0
- package/dist/adapter.js.map +1 -0
- package/dist/adapters/cli.d.ts +16 -0
- package/dist/adapters/cli.js +34 -0
- package/dist/adapters/cli.js.map +1 -0
- package/dist/adapters/dingtalk.d.ts +40 -0
- package/dist/adapters/dingtalk.js +93 -0
- package/dist/adapters/dingtalk.js.map +1 -0
- package/dist/adapters/discord.d.ts +38 -0
- package/dist/adapters/discord.js +88 -0
- package/dist/adapters/discord.js.map +1 -0
- package/dist/adapters/feishu.d.ts +43 -0
- package/dist/adapters/feishu.js +107 -0
- package/dist/adapters/feishu.js.map +1 -0
- package/dist/adapters/slack.d.ts +35 -0
- package/dist/adapters/slack.js +83 -0
- package/dist/adapters/slack.js.map +1 -0
- package/dist/adapters/telegram.d.ts +57 -0
- package/dist/adapters/telegram.js +94 -0
- package/dist/adapters/telegram.js.map +1 -0
- package/dist/adapters/wecom.d.ts +42 -0
- package/dist/adapters/wecom.js +177 -0
- package/dist/adapters/wecom.js.map +1 -0
- package/dist/adapters/whatsapp.d.ts +81 -0
- package/dist/adapters/whatsapp.js +193 -0
- package/dist/adapters/whatsapp.js.map +1 -0
- package/dist/commands.d.ts +18 -0
- package/dist/commands.js +29 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +73 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +190 -0
- package/dist/index.js.map +1 -0
- package/dist/session.d.ts +10 -0
- package/dist/session.js +15 -0
- package/dist/session.js.map +1 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +265 -0
- package/dist/setup.js.map +1 -0
- package/dist/status.d.ts +22 -0
- package/dist/status.js +47 -0
- package/dist/status.js.map +1 -0
- package/dist/trajectory.d.ts +10 -0
- package/dist/trajectory.js +43 -0
- package/dist/trajectory.js.map +1 -0
- package/package.json +7 -3
- package/src/setup.ts +252 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface NormalizedMessage {
|
|
2
|
+
chatId: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
text: string;
|
|
5
|
+
media?: {
|
|
6
|
+
kind: 'voice' | 'image' | 'video' | 'file';
|
|
7
|
+
url?: string;
|
|
8
|
+
mime?: string;
|
|
9
|
+
caption?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface OutboundButton {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
export interface OutboundPayload {
|
|
17
|
+
text: string;
|
|
18
|
+
buttons?: OutboundButton[];
|
|
19
|
+
}
|
|
20
|
+
/** 平台适配器契约:M2/M3 的 WhatsApp/Telegram/… 都实现它。 */
|
|
21
|
+
export interface Adapter {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
connect(): Promise<void>;
|
|
24
|
+
send(chatId: string, payload: OutboundPayload): Promise<void>;
|
|
25
|
+
/** 可选:平台"正在输入"指示(Telegram/WhatsApp 实现,其余默认无操作)。 */
|
|
26
|
+
sendTyping?(chatId: string): Promise<void>;
|
|
27
|
+
/** 可选:连接状态(供控制台)。 */
|
|
28
|
+
status?(): {
|
|
29
|
+
connected: boolean;
|
|
30
|
+
};
|
|
31
|
+
onMessage(cb: (msg: NormalizedMessage) => void): void;
|
|
32
|
+
onReply(cb: (buttonId: string) => void): void;
|
|
33
|
+
}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Adapter, NormalizedMessage, OutboundPayload } from '../adapter.js';
|
|
2
|
+
/** 本地命令行适配器:M1 用于验证全链路,也是 M2+ 平台适配器的样板。 */
|
|
3
|
+
export declare class CliAdapter implements Adapter {
|
|
4
|
+
readonly id = "cli";
|
|
5
|
+
private messageCb?;
|
|
6
|
+
private replyCb?;
|
|
7
|
+
private rl?;
|
|
8
|
+
connect(): Promise<void>;
|
|
9
|
+
send(_chatId: string, payload: OutboundPayload): Promise<void>;
|
|
10
|
+
onMessage(cb: (msg: NormalizedMessage) => void): void;
|
|
11
|
+
onReply(cb: (buttonId: string) => void): void;
|
|
12
|
+
/** CLI 是本地进程内适配器:恒为已连接。 */
|
|
13
|
+
status(): {
|
|
14
|
+
connected: boolean;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
/** 本地命令行适配器:M1 用于验证全链路,也是 M2+ 平台适配器的样板。 */
|
|
3
|
+
export class CliAdapter {
|
|
4
|
+
id = 'cli';
|
|
5
|
+
messageCb;
|
|
6
|
+
replyCb;
|
|
7
|
+
rl;
|
|
8
|
+
async connect() {
|
|
9
|
+
this.rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
10
|
+
this.rl.on('line', (line) => {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
if (!trimmed)
|
|
13
|
+
return;
|
|
14
|
+
const btn = trimmed.match(/^\/btn\s+(\S+)$/i);
|
|
15
|
+
if (btn) {
|
|
16
|
+
this.replyCb?.(btn[1]);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.messageCb?.({ chatId: 'cli', userId: 'local', text: trimmed });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async send(_chatId, payload) {
|
|
23
|
+
const lines = [payload.text];
|
|
24
|
+
for (const b of payload.buttons ?? []) {
|
|
25
|
+
lines.push(` [按钮] ${b.label} → 输入 /btn ${b.id}`);
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
28
|
+
}
|
|
29
|
+
onMessage(cb) { this.messageCb = cb; }
|
|
30
|
+
onReply(cb) { this.replyCb = cb; }
|
|
31
|
+
/** CLI 是本地进程内适配器:恒为已连接。 */
|
|
32
|
+
status() { return { connected: true }; }
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/adapters/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,2CAA2C;AAC3C,MAAM,OAAO,UAAU;IACZ,EAAE,GAAG,KAAK,CAAC;IACZ,SAAS,CAAoC;IAC7C,OAAO,CAA8B;IACrC,EAAE,CAAsC;IAEhD,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,EAAE,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7F,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;YAC9C,IAAI,GAAG,EAAE,CAAC;gBACR,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACvB,OAAO;YACT,CAAC;YACD,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAe,EAAE,OAAwB;QAClD,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;YACtC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,KAAK,cAAc,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;IAChD,CAAC;IAED,SAAS,CAAC,EAAoC,IAAU,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC,CAAC,CAAC;IAC9E,OAAO,CAAC,EAA8B,IAAU,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC;IACpE,2BAA2B;IAC3B,MAAM,KAA6B,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;CACjE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Adapter, NormalizedMessage, OutboundButton, OutboundPayload } from '../adapter.js';
|
|
2
|
+
import { type RobotMessage } from 'dingtalk-stream-sdk-nodejs';
|
|
3
|
+
export interface ParsedRobotMessage {
|
|
4
|
+
chatId: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
text: string;
|
|
7
|
+
sessionWebhook: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseBotMessage(data: RobotMessage): ParsedRobotMessage | null;
|
|
10
|
+
export declare function buildReplyBody(text: string): {
|
|
11
|
+
msgtype: 'text';
|
|
12
|
+
text: {
|
|
13
|
+
content: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export declare function buildNumberedText(text: string, buttons: OutboundButton[]): string;
|
|
17
|
+
export declare function matchNumberedButton(text: string, buttons: OutboundButton[]): OutboundButton | undefined;
|
|
18
|
+
export interface DingTalkAdapterOptions {
|
|
19
|
+
clientId: string;
|
|
20
|
+
clientSecret: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class DingTalkAdapter implements Adapter {
|
|
23
|
+
private readonly opts;
|
|
24
|
+
readonly id = "dingtalk";
|
|
25
|
+
private client?;
|
|
26
|
+
private connected;
|
|
27
|
+
private messageCb?;
|
|
28
|
+
private replyCb?;
|
|
29
|
+
private readonly pendingButtons;
|
|
30
|
+
/** conversationId → 最近的 sessionWebhook(回复通道,过期由钉钉侧管理) */
|
|
31
|
+
private readonly webhooks;
|
|
32
|
+
constructor(opts: DingTalkAdapterOptions);
|
|
33
|
+
connect(): Promise<void>;
|
|
34
|
+
send(chatId: string, payload: OutboundPayload): Promise<void>;
|
|
35
|
+
onMessage(cb: (msg: NormalizedMessage) => void): void;
|
|
36
|
+
onReply(cb: (buttonId: string) => void): void;
|
|
37
|
+
status(): {
|
|
38
|
+
connected: boolean;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { DWClient, TOPIC_ROBOT } from 'dingtalk-stream-sdk-nodejs';
|
|
2
|
+
export function parseBotMessage(data) {
|
|
3
|
+
if (data.msgtype !== 'text' || !data.text?.content)
|
|
4
|
+
return null;
|
|
5
|
+
if (!data.conversationId || !data.senderStaffId || !data.sessionWebhook)
|
|
6
|
+
return null;
|
|
7
|
+
return {
|
|
8
|
+
chatId: data.conversationId,
|
|
9
|
+
userId: data.senderStaffId,
|
|
10
|
+
text: data.text.content,
|
|
11
|
+
sessionWebhook: data.sessionWebhook,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function buildReplyBody(text) {
|
|
15
|
+
return { msgtype: 'text', text: { content: text } };
|
|
16
|
+
}
|
|
17
|
+
export function buildNumberedText(text, buttons) {
|
|
18
|
+
if (buttons.length === 0)
|
|
19
|
+
return text;
|
|
20
|
+
const options = buttons.map((b, i) => `${i + 1}) ${b.label}`).join('\n');
|
|
21
|
+
return `${text}\n\n${options}\n\n回复数字选择。`;
|
|
22
|
+
}
|
|
23
|
+
export function matchNumberedButton(text, buttons) {
|
|
24
|
+
const n = Number(text.trim());
|
|
25
|
+
if (!Number.isInteger(n) || n < 1 || n > buttons.length)
|
|
26
|
+
return undefined;
|
|
27
|
+
return buttons[n - 1];
|
|
28
|
+
}
|
|
29
|
+
export class DingTalkAdapter {
|
|
30
|
+
opts;
|
|
31
|
+
id = 'dingtalk';
|
|
32
|
+
client;
|
|
33
|
+
connected = false;
|
|
34
|
+
messageCb;
|
|
35
|
+
replyCb;
|
|
36
|
+
pendingButtons = new Map();
|
|
37
|
+
/** conversationId → 最近的 sessionWebhook(回复通道,过期由钉钉侧管理) */
|
|
38
|
+
webhooks = new Map();
|
|
39
|
+
constructor(opts) {
|
|
40
|
+
this.opts = opts;
|
|
41
|
+
}
|
|
42
|
+
async connect() {
|
|
43
|
+
const client = new DWClient({ clientId: this.opts.clientId, clientSecret: this.opts.clientSecret });
|
|
44
|
+
this.client = client;
|
|
45
|
+
client.registerCallbackListener(TOPIC_ROBOT, (msg) => {
|
|
46
|
+
let data;
|
|
47
|
+
try {
|
|
48
|
+
data = JSON.parse(msg.data);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const parsed = parseBotMessage(data);
|
|
54
|
+
if (!parsed)
|
|
55
|
+
return;
|
|
56
|
+
this.webhooks.set(parsed.chatId, parsed.sessionWebhook);
|
|
57
|
+
const pending = this.pendingButtons.get(parsed.chatId);
|
|
58
|
+
if (pending) {
|
|
59
|
+
const button = matchNumberedButton(parsed.text, pending);
|
|
60
|
+
if (button) {
|
|
61
|
+
this.pendingButtons.delete(parsed.chatId);
|
|
62
|
+
this.replyCb?.(button.id);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.messageCb?.({ chatId: parsed.chatId, userId: parsed.userId, text: parsed.text });
|
|
67
|
+
});
|
|
68
|
+
await client.connect();
|
|
69
|
+
this.connected = true;
|
|
70
|
+
console.log('[dingtalk] 钉钉 Stream 已连接');
|
|
71
|
+
}
|
|
72
|
+
async send(chatId, payload) {
|
|
73
|
+
const webhook = this.webhooks.get(chatId);
|
|
74
|
+
if (!webhook)
|
|
75
|
+
throw new Error(`钉钉会话 ${chatId} 无可用 sessionWebhook(先让用户发一条消息)`);
|
|
76
|
+
if (payload.buttons?.length)
|
|
77
|
+
this.pendingButtons.set(chatId, payload.buttons);
|
|
78
|
+
const text = buildNumberedText(payload.text, payload.buttons ?? []);
|
|
79
|
+
const res = await fetch(webhook, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'content-type': 'application/json' },
|
|
82
|
+
body: JSON.stringify(buildReplyBody(text)),
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const body = await res.text();
|
|
86
|
+
throw new Error(`钉钉回发失败 ${res.status}: ${body.slice(0, 200)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
onMessage(cb) { this.messageCb = cb; }
|
|
90
|
+
onReply(cb) { this.replyCb = cb; }
|
|
91
|
+
status() { return { connected: this.connected }; }
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=dingtalk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dingtalk.js","sourceRoot":"","sources":["../../src/adapters/dingtalk.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Adapter, NormalizedMessage, OutboundButton, OutboundPayload } from '../adapter.js';
|
|
2
|
+
export interface RawDiscordMessage {
|
|
3
|
+
channelId?: string;
|
|
4
|
+
author?: {
|
|
5
|
+
id?: string;
|
|
6
|
+
bot?: boolean;
|
|
7
|
+
};
|
|
8
|
+
content?: string;
|
|
9
|
+
attachments?: Array<{
|
|
10
|
+
url?: string;
|
|
11
|
+
contentType?: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/** 纯函数:取第一条附件的下载 URL(attachments.first().url);无附件返回 undefined。 */
|
|
15
|
+
export declare function discordAttachmentUrl(raw: RawDiscordMessage): string | undefined;
|
|
16
|
+
export declare function normalizeDiscordMessage(raw: RawDiscordMessage): NormalizedMessage | null;
|
|
17
|
+
export declare function discordComponents(buttons: OutboundButton[]): Array<{
|
|
18
|
+
type: 1;
|
|
19
|
+
components: unknown[];
|
|
20
|
+
}>;
|
|
21
|
+
export interface DiscordAdapterOptions {
|
|
22
|
+
token: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class DiscordAdapter implements Adapter {
|
|
25
|
+
readonly id = "discord";
|
|
26
|
+
private readonly client;
|
|
27
|
+
private connected;
|
|
28
|
+
private messageCb?;
|
|
29
|
+
private replyCb?;
|
|
30
|
+
constructor(opts: DiscordAdapterOptions);
|
|
31
|
+
connect(): Promise<void>;
|
|
32
|
+
send(chatId: string, payload: OutboundPayload): Promise<void>;
|
|
33
|
+
onMessage(cb: (msg: NormalizedMessage) => void): void;
|
|
34
|
+
onReply(cb: (buttonId: string) => void): void;
|
|
35
|
+
status(): {
|
|
36
|
+
connected: boolean;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, Client, Events, GatewayIntentBits, } from 'discord.js';
|
|
2
|
+
/** 纯函数:取第一条附件的下载 URL(attachments.first().url);无附件返回 undefined。 */
|
|
3
|
+
export function discordAttachmentUrl(raw) {
|
|
4
|
+
return raw.attachments?.[0]?.url;
|
|
5
|
+
}
|
|
6
|
+
function mediaKindFromMime(mime) {
|
|
7
|
+
if (mime?.startsWith('image/'))
|
|
8
|
+
return 'image';
|
|
9
|
+
if (mime?.startsWith('audio/'))
|
|
10
|
+
return 'voice';
|
|
11
|
+
if (mime?.startsWith('video/'))
|
|
12
|
+
return 'video';
|
|
13
|
+
return 'file';
|
|
14
|
+
}
|
|
15
|
+
export function normalizeDiscordMessage(raw) {
|
|
16
|
+
if (!raw.channelId || !raw.author?.id || raw.author.bot)
|
|
17
|
+
return null;
|
|
18
|
+
const text = raw.content ?? '';
|
|
19
|
+
const url = discordAttachmentUrl(raw);
|
|
20
|
+
if (!text && !url)
|
|
21
|
+
return null;
|
|
22
|
+
const out = { chatId: raw.channelId, userId: raw.author.id, text };
|
|
23
|
+
if (url)
|
|
24
|
+
out.media = { kind: mediaKindFromMime(raw.attachments?.[0]?.contentType), url };
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export function discordComponents(buttons) {
|
|
28
|
+
return [{
|
|
29
|
+
type: 1,
|
|
30
|
+
components: buttons.map((b) => ({
|
|
31
|
+
type: 2,
|
|
32
|
+
custom_id: b.id,
|
|
33
|
+
label: b.label,
|
|
34
|
+
style: 1, // ButtonStyle.Primary
|
|
35
|
+
})),
|
|
36
|
+
}];
|
|
37
|
+
}
|
|
38
|
+
export class DiscordAdapter {
|
|
39
|
+
id = 'discord';
|
|
40
|
+
client;
|
|
41
|
+
connected = false;
|
|
42
|
+
messageCb;
|
|
43
|
+
replyCb;
|
|
44
|
+
constructor(opts) {
|
|
45
|
+
this.client = new Client({
|
|
46
|
+
intents: [
|
|
47
|
+
GatewayIntentBits.Guilds,
|
|
48
|
+
GatewayIntentBits.GuildMessages,
|
|
49
|
+
GatewayIntentBits.DirectMessages,
|
|
50
|
+
GatewayIntentBits.MessageContent,
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
void opts.token;
|
|
54
|
+
this.client.login(opts.token).catch((e) => console.error('[discord] 登录失败:', e));
|
|
55
|
+
}
|
|
56
|
+
async connect() {
|
|
57
|
+
this.client.once(Events.ClientReady, () => { this.connected = true; console.log('[discord] 已连接 Discord'); });
|
|
58
|
+
this.client.on(Events.MessageCreate, (m) => {
|
|
59
|
+
const msg = normalizeDiscordMessage(m);
|
|
60
|
+
if (msg)
|
|
61
|
+
this.messageCb?.(msg);
|
|
62
|
+
});
|
|
63
|
+
this.client.on(Events.InteractionCreate, async (interaction) => {
|
|
64
|
+
if (!interaction.isButton())
|
|
65
|
+
return;
|
|
66
|
+
const button = interaction;
|
|
67
|
+
await button.deferUpdate().catch(() => undefined);
|
|
68
|
+
this.replyCb?.(button.customId);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async send(chatId, payload) {
|
|
72
|
+
const channel = await this.client.channels.fetch(chatId);
|
|
73
|
+
if (!channel || !('send' in channel)) {
|
|
74
|
+
console.error(`[discord] 无法向 ${chatId} 发送(channel 不可用)`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (payload.buttons?.length) {
|
|
78
|
+
const row = new ActionRowBuilder().addComponents(payload.buttons.map((b) => new ButtonBuilder().setCustomId(b.id).setLabel(b.label).setStyle(ButtonStyle.Primary)));
|
|
79
|
+
await channel.send({ content: payload.text, components: [row] });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await channel.send(payload.text);
|
|
83
|
+
}
|
|
84
|
+
onMessage(cb) { this.messageCb = cb; }
|
|
85
|
+
onReply(cb) { this.replyCb = cb; }
|
|
86
|
+
status() { return { connected: this.connected }; }
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=discord.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discord.js","sourceRoot":"","sources":["../../src/adapters/discord.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Adapter, NormalizedMessage, OutboundButton, OutboundPayload } from '../adapter.js';
|
|
2
|
+
export interface FeishuReceivePayload {
|
|
3
|
+
event?: {
|
|
4
|
+
message?: {
|
|
5
|
+
message_id?: string;
|
|
6
|
+
chat_id?: string;
|
|
7
|
+
message_type?: string;
|
|
8
|
+
content?: string;
|
|
9
|
+
};
|
|
10
|
+
sender?: {
|
|
11
|
+
sender_id?: {
|
|
12
|
+
open_id?: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function parseFeishuTextMessage(payload: FeishuReceivePayload): NormalizedMessage | null;
|
|
18
|
+
export declare function buildNumberedText(text: string, buttons: OutboundButton[]): string;
|
|
19
|
+
export declare function matchNumberedButton(text: string, buttons: OutboundButton[]): OutboundButton | undefined;
|
|
20
|
+
export interface FeishuAdapterOptions {
|
|
21
|
+
appId: string;
|
|
22
|
+
appSecret: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class FeishuAdapter implements Adapter {
|
|
25
|
+
private readonly opts;
|
|
26
|
+
readonly id = "feishu";
|
|
27
|
+
private readonly client;
|
|
28
|
+
private ws?;
|
|
29
|
+
private connected;
|
|
30
|
+
private messageCb?;
|
|
31
|
+
private replyCb?;
|
|
32
|
+
private readonly pendingButtons;
|
|
33
|
+
/** chatId → 最近一条入站消息的 message_id(send 优先 reply,缺失则 create 兜底) */
|
|
34
|
+
private readonly lastMessageIds;
|
|
35
|
+
constructor(opts: FeishuAdapterOptions);
|
|
36
|
+
connect(): Promise<void>;
|
|
37
|
+
send(chatId: string, payload: OutboundPayload): Promise<void>;
|
|
38
|
+
onMessage(cb: (msg: NormalizedMessage) => void): void;
|
|
39
|
+
onReply(cb: (buttonId: string) => void): void;
|
|
40
|
+
status(): {
|
|
41
|
+
connected: boolean;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import lark from '@larksuiteoapi/node-sdk';
|
|
2
|
+
// @larksuiteoapi/node-sdk 是 CommonJS 包(main=lib/index.js,无 "type":"module"):
|
|
3
|
+
// Node 原生 ESM 下必须 default 导入后解构(同 M2b 的 @slack/bolt 处理)。
|
|
4
|
+
const { Client, WSClient, EventDispatcher } = lark;
|
|
5
|
+
export function parseFeishuTextMessage(payload) {
|
|
6
|
+
const message = payload.event?.message;
|
|
7
|
+
const sender = payload.event?.sender?.sender_id?.open_id;
|
|
8
|
+
if (!message?.chat_id || !sender)
|
|
9
|
+
return null;
|
|
10
|
+
if (message.message_type !== 'text')
|
|
11
|
+
return null;
|
|
12
|
+
let text = '';
|
|
13
|
+
try {
|
|
14
|
+
text = JSON.parse(message.content ?? '{}').text ?? '';
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (!text)
|
|
20
|
+
return null;
|
|
21
|
+
return { chatId: message.chat_id, userId: sender, text };
|
|
22
|
+
}
|
|
23
|
+
export function buildNumberedText(text, buttons) {
|
|
24
|
+
if (buttons.length === 0)
|
|
25
|
+
return text;
|
|
26
|
+
const options = buttons.map((b, i) => `${i + 1}) ${b.label}`).join('\n');
|
|
27
|
+
return `${text}\n\n${options}\n\n回复数字选择。`;
|
|
28
|
+
}
|
|
29
|
+
export function matchNumberedButton(text, buttons) {
|
|
30
|
+
const n = Number(text.trim());
|
|
31
|
+
if (!Number.isInteger(n) || n < 1 || n > buttons.length)
|
|
32
|
+
return undefined;
|
|
33
|
+
return buttons[n - 1];
|
|
34
|
+
}
|
|
35
|
+
export class FeishuAdapter {
|
|
36
|
+
opts;
|
|
37
|
+
id = 'feishu';
|
|
38
|
+
client;
|
|
39
|
+
ws;
|
|
40
|
+
connected = false;
|
|
41
|
+
messageCb;
|
|
42
|
+
replyCb;
|
|
43
|
+
pendingButtons = new Map();
|
|
44
|
+
/** chatId → 最近一条入站消息的 message_id(send 优先 reply,缺失则 create 兜底) */
|
|
45
|
+
lastMessageIds = new Map();
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
this.opts = opts;
|
|
48
|
+
this.client = new Client({ appId: opts.appId, appSecret: opts.appSecret });
|
|
49
|
+
}
|
|
50
|
+
async connect() {
|
|
51
|
+
const dispatcher = new EventDispatcher({}).register({
|
|
52
|
+
'im.message.receive_v1': async (data) => {
|
|
53
|
+
const message = data.event?.message;
|
|
54
|
+
if (message?.chat_id && message.message_id) {
|
|
55
|
+
this.lastMessageIds.set(message.chat_id, message.message_id);
|
|
56
|
+
}
|
|
57
|
+
const normalized = parseFeishuTextMessage(data);
|
|
58
|
+
if (!normalized)
|
|
59
|
+
return;
|
|
60
|
+
const chatId = normalized.chatId;
|
|
61
|
+
const pending = this.pendingButtons.get(chatId);
|
|
62
|
+
if (pending) {
|
|
63
|
+
const button = matchNumberedButton(normalized.text, pending);
|
|
64
|
+
if (button) {
|
|
65
|
+
this.pendingButtons.delete(chatId);
|
|
66
|
+
this.replyCb?.(button.id);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.messageCb?.(normalized);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
this.ws = new WSClient({
|
|
74
|
+
appId: this.opts.appId,
|
|
75
|
+
appSecret: this.opts.appSecret,
|
|
76
|
+
loggerLevel: lark.LoggerLevel.error,
|
|
77
|
+
});
|
|
78
|
+
await this.ws.start({ eventDispatcher: dispatcher });
|
|
79
|
+
this.connected = true;
|
|
80
|
+
console.log('[feishu] 飞书长连接已建立');
|
|
81
|
+
}
|
|
82
|
+
async send(chatId, payload) {
|
|
83
|
+
if (payload.buttons?.length)
|
|
84
|
+
this.pendingButtons.set(chatId, payload.buttons);
|
|
85
|
+
const text = buildNumberedText(payload.text, payload.buttons ?? []);
|
|
86
|
+
const content = JSON.stringify({ text });
|
|
87
|
+
const messageId = this.lastMessageIds.get(chatId);
|
|
88
|
+
if (messageId) {
|
|
89
|
+
// 有最近入站消息:im.message.reply(path=message_id)回复原消息
|
|
90
|
+
await this.client.im.message.reply({
|
|
91
|
+
path: { message_id: messageId },
|
|
92
|
+
data: { msg_type: 'text', content },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
// 无入站消息(主动下发):im.message.create 按 receive_id=chat_id 发送
|
|
97
|
+
await this.client.im.message.create({
|
|
98
|
+
params: { receive_id_type: 'chat_id' },
|
|
99
|
+
data: { receive_id: chatId, msg_type: 'text', content },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
onMessage(cb) { this.messageCb = cb; }
|
|
104
|
+
onReply(cb) { this.replyCb = cb; }
|
|
105
|
+
status() { return { connected: this.connected }; }
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=feishu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feishu.js","sourceRoot":"","sources":["../../src/adapters/feishu.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Adapter, NormalizedMessage, OutboundButton, OutboundPayload } from '../adapter.js';
|
|
2
|
+
export interface RawSlackMessage {
|
|
3
|
+
channel?: string;
|
|
4
|
+
user?: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
subtype?: string;
|
|
7
|
+
bot_id?: string;
|
|
8
|
+
files?: Array<{
|
|
9
|
+
url_private?: string;
|
|
10
|
+
mimetype?: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
/** 纯函数:取第一条文件的私有 URL(files[0].url_private);无文件返回 undefined。 */
|
|
14
|
+
export declare function slackFileUrl(raw: RawSlackMessage): string | undefined;
|
|
15
|
+
export declare function normalizeSlackMessage(raw: RawSlackMessage): NormalizedMessage | null;
|
|
16
|
+
export declare function slackBlocks(text: string, buttons: OutboundButton[]): unknown[];
|
|
17
|
+
export interface SlackAdapterOptions {
|
|
18
|
+
botToken: string;
|
|
19
|
+
appToken: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class SlackAdapter implements Adapter {
|
|
22
|
+
readonly id = "slack";
|
|
23
|
+
private readonly app;
|
|
24
|
+
private connected;
|
|
25
|
+
private messageCb?;
|
|
26
|
+
private replyCb?;
|
|
27
|
+
constructor(opts: SlackAdapterOptions);
|
|
28
|
+
connect(): Promise<void>;
|
|
29
|
+
send(chatId: string, payload: OutboundPayload): Promise<void>;
|
|
30
|
+
onMessage(cb: (msg: NormalizedMessage) => void): void;
|
|
31
|
+
onReply(cb: (buttonId: string) => void): void;
|
|
32
|
+
status(): {
|
|
33
|
+
connected: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import Bolt from '@slack/bolt';
|
|
2
|
+
// @slack/bolt 是 CommonJS 包:Node 原生 ESM 下 `import { App }`(命名导入)会因
|
|
3
|
+
// cjs-module-lexer 无法识别其导出而失败(App 为 undefined / 静态导入抛 SyntaxError),
|
|
4
|
+
// 必须 default 导入后解构。
|
|
5
|
+
const { App } = Bolt;
|
|
6
|
+
/** 纯函数:取第一条文件的私有 URL(files[0].url_private);无文件返回 undefined。 */
|
|
7
|
+
export function slackFileUrl(raw) {
|
|
8
|
+
return raw.files?.[0]?.url_private;
|
|
9
|
+
}
|
|
10
|
+
function mediaKindFromMime(mime) {
|
|
11
|
+
if (mime?.startsWith('image/'))
|
|
12
|
+
return 'image';
|
|
13
|
+
if (mime?.startsWith('audio/'))
|
|
14
|
+
return 'voice';
|
|
15
|
+
if (mime?.startsWith('video/'))
|
|
16
|
+
return 'video';
|
|
17
|
+
return 'file';
|
|
18
|
+
}
|
|
19
|
+
export function normalizeSlackMessage(raw) {
|
|
20
|
+
if (!raw.channel || !raw.user || raw.subtype === 'bot_message' || raw.bot_id)
|
|
21
|
+
return null;
|
|
22
|
+
const text = raw.text ?? '';
|
|
23
|
+
const url = slackFileUrl(raw);
|
|
24
|
+
if (!text && !url)
|
|
25
|
+
return null;
|
|
26
|
+
const out = { chatId: raw.channel, userId: raw.user, text };
|
|
27
|
+
if (url)
|
|
28
|
+
out.media = { kind: mediaKindFromMime(raw.files?.[0]?.mimetype), url };
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
export function slackBlocks(text, buttons) {
|
|
32
|
+
const blocks = [{ type: 'section', text: { type: 'mrkdwn', text } }];
|
|
33
|
+
if (buttons.length > 0) {
|
|
34
|
+
blocks.push({
|
|
35
|
+
type: 'actions',
|
|
36
|
+
elements: buttons.map((b) => ({
|
|
37
|
+
type: 'button',
|
|
38
|
+
value: b.id,
|
|
39
|
+
text: { type: 'plain_text', text: b.label },
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return blocks;
|
|
44
|
+
}
|
|
45
|
+
export class SlackAdapter {
|
|
46
|
+
id = 'slack';
|
|
47
|
+
// 解构出的 App 只有值绑定(无类型绑定),实例类型需用 InstanceType<typeof App>
|
|
48
|
+
app;
|
|
49
|
+
connected = false;
|
|
50
|
+
messageCb;
|
|
51
|
+
replyCb;
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
this.app = new App({ token: opts.botToken, appToken: opts.appToken, socketMode: true });
|
|
54
|
+
}
|
|
55
|
+
async connect() {
|
|
56
|
+
this.app.message(async ({ message }) => {
|
|
57
|
+
const msg = normalizeSlackMessage(message);
|
|
58
|
+
if (msg)
|
|
59
|
+
this.messageCb?.(msg);
|
|
60
|
+
});
|
|
61
|
+
this.app.action(/^approve:|^reject:/, async ({ ack, body, respond }) => {
|
|
62
|
+
await ack();
|
|
63
|
+
const action = body.actions?.[0];
|
|
64
|
+
if (action?.value)
|
|
65
|
+
this.replyCb?.(action.value);
|
|
66
|
+
await respond({ text: '处理中…', replace_original: false }).catch(() => undefined);
|
|
67
|
+
});
|
|
68
|
+
await this.app.start(0); // Socket Mode 不需要端口;start(0) 仅建立连接
|
|
69
|
+
this.connected = true;
|
|
70
|
+
console.log('[slack] 已连接 Slack(Socket Mode)');
|
|
71
|
+
}
|
|
72
|
+
async send(chatId, payload) {
|
|
73
|
+
await this.app.client.chat.postMessage({
|
|
74
|
+
channel: chatId,
|
|
75
|
+
text: payload.text,
|
|
76
|
+
blocks: slackBlocks(payload.text, payload.buttons ?? []),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
onMessage(cb) { this.messageCb = cb; }
|
|
80
|
+
onReply(cb) { this.replyCb = cb; }
|
|
81
|
+
status() { return { connected: this.connected }; }
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=slack.js.map
|