@zhin.js/adapter-satori 0.0.0
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/LICENSE +21 -0
- package/README.md +86 -0
- package/lib/adapter.d.ts +12 -0
- package/lib/adapter.d.ts.map +1 -0
- package/lib/adapter.js +30 -0
- package/lib/adapter.js.map +1 -0
- package/lib/api.d.ts +15 -0
- package/lib/api.d.ts.map +1 -0
- package/lib/api.js +37 -0
- package/lib/api.js.map +1 -0
- package/lib/bot-webhook.d.ts +27 -0
- package/lib/bot-webhook.d.ts.map +1 -0
- package/lib/bot-webhook.js +98 -0
- package/lib/bot-webhook.js.map +1 -0
- package/lib/bot-ws.d.ts +30 -0
- package/lib/bot-ws.d.ts.map +1 -0
- package/lib/bot-ws.js +167 -0
- package/lib/bot-ws.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +25 -0
- package/lib/index.js.map +1 -0
- package/lib/types.d.ts +91 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +13 -0
- package/lib/types.js.map +1 -0
- package/lib/utils.d.ts +40 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/utils.js +33 -0
- package/lib/utils.js.map +1 -0
- package/package.json +49 -0
- package/src/adapter.ts +46 -0
- package/src/api.ts +48 -0
- package/src/bot-webhook.ts +117 -0
- package/src/bot-ws.ts +187 -0
- package/src/index.ts +38 -0
- package/src/types.ts +91 -0
- package/src/utils.ts +64 -0
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satori 适配器类型(参考 https://satori.chat 协议)
|
|
3
|
+
*/
|
|
4
|
+
/** 配置公共字段;单一适配器 context 均为 'satori',连接方式由 connection 区分 */
|
|
5
|
+
export interface SatoriConfigBase {
|
|
6
|
+
context: 'satori';
|
|
7
|
+
name: string;
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
/** 认证 token,API 用 Authorization: Bearer,WebSocket IDENTIFY 用 token 字段 */
|
|
10
|
+
token?: string;
|
|
11
|
+
}
|
|
12
|
+
/** WebSocket 连接(应用连 SDK /v1/events) */
|
|
13
|
+
export interface SatoriWsConfig extends SatoriConfigBase {
|
|
14
|
+
connection: 'ws';
|
|
15
|
+
/** 心跳间隔(毫秒),默认 10000 */
|
|
16
|
+
heartbeat_interval?: number;
|
|
17
|
+
}
|
|
18
|
+
/** WebHook(SDK POST 到应用提供的 path) */
|
|
19
|
+
export interface SatoriWebhookConfig extends SatoriConfigBase {
|
|
20
|
+
connection: 'webhook';
|
|
21
|
+
path: string;
|
|
22
|
+
}
|
|
23
|
+
export type SatoriBotConfig = SatoriWsConfig | SatoriWebhookConfig;
|
|
24
|
+
/** WebSocket/API 信号:op + body */
|
|
25
|
+
export interface SatoriSignal {
|
|
26
|
+
op: number;
|
|
27
|
+
body?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
/** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
|
|
30
|
+
export declare const SatoriOpcode: {
|
|
31
|
+
readonly EVENT: 0;
|
|
32
|
+
readonly PING: 1;
|
|
33
|
+
readonly PONG: 2;
|
|
34
|
+
readonly IDENTIFY: 3;
|
|
35
|
+
readonly READY: 4;
|
|
36
|
+
readonly META: 5;
|
|
37
|
+
};
|
|
38
|
+
/** 事件体(EVENT 的 body):type、sn、timestamp、login、message、channel、user 等 */
|
|
39
|
+
export interface SatoriEventBody {
|
|
40
|
+
type?: string;
|
|
41
|
+
sn?: number;
|
|
42
|
+
timestamp?: number;
|
|
43
|
+
login?: SatoriLogin;
|
|
44
|
+
message?: SatoriMessage;
|
|
45
|
+
channel?: SatoriChannel;
|
|
46
|
+
user?: SatoriUser;
|
|
47
|
+
guild?: {
|
|
48
|
+
id: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
};
|
|
51
|
+
member?: {
|
|
52
|
+
user?: SatoriUser;
|
|
53
|
+
nick?: string;
|
|
54
|
+
roles?: string[];
|
|
55
|
+
};
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
/** Login 资源(READY 与事件中) */
|
|
59
|
+
export interface SatoriLogin {
|
|
60
|
+
platform?: string;
|
|
61
|
+
user?: SatoriUser;
|
|
62
|
+
status?: number;
|
|
63
|
+
sn?: number;
|
|
64
|
+
}
|
|
65
|
+
/** Channel 资源:id, type (0=TEXT,1=DIRECT,2=CATEGORY,3=VOICE) */
|
|
66
|
+
export interface SatoriChannel {
|
|
67
|
+
id: string;
|
|
68
|
+
type?: number;
|
|
69
|
+
name?: string;
|
|
70
|
+
parent_id?: string;
|
|
71
|
+
}
|
|
72
|
+
/** User 资源 */
|
|
73
|
+
export interface SatoriUser {
|
|
74
|
+
id: string;
|
|
75
|
+
name?: string;
|
|
76
|
+
avatar?: string;
|
|
77
|
+
}
|
|
78
|
+
/** Message 资源:id, content(元素字符串), channel, user 等 */
|
|
79
|
+
export interface SatoriMessage {
|
|
80
|
+
id: string;
|
|
81
|
+
content?: string;
|
|
82
|
+
channel?: SatoriChannel;
|
|
83
|
+
user?: SatoriUser;
|
|
84
|
+
member?: {
|
|
85
|
+
user?: SatoriUser;
|
|
86
|
+
nick?: string;
|
|
87
|
+
};
|
|
88
|
+
created_at?: number;
|
|
89
|
+
updated_at?: number;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,2DAA2D;AAC3D,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,QAAQ,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,uCAAuC;AACvC,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACtD,UAAU,EAAE,IAAI,CAAC;IACjB,wBAAwB;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,oCAAoC;AACpC,MAAM,WAAW,mBAAoB,SAAQ,gBAAgB;IAC3D,UAAU,EAAE,SAAS,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,mBAAmB,CAAC;AAEnE,iCAAiC;AACjC,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,kEAAkE;AAClE,eAAO,MAAM,YAAY;;;;;;;CAOf,CAAC;AAEX,uEAAuE;AACvE,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,UAAU,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,2BAA2B;AAC3B,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,+DAA+D;AAC/D,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,cAAc;AACd,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,UAAU,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satori 适配器类型(参考 https://satori.chat 协议)
|
|
3
|
+
*/
|
|
4
|
+
/** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
|
|
5
|
+
export const SatoriOpcode = {
|
|
6
|
+
EVENT: 0,
|
|
7
|
+
PING: 1,
|
|
8
|
+
PONG: 2,
|
|
9
|
+
IDENTIFY: 3,
|
|
10
|
+
READY: 4,
|
|
11
|
+
META: 5,
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=types.js.map
|
package/lib/types.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAgCH,kEAAkE;AAClE,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,QAAQ,EAAE,CAAC;IACX,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;CACC,CAAC"}
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satori 事件与 zhin Message 的转换
|
|
3
|
+
*/
|
|
4
|
+
import type { SendContent } from 'zhin.js';
|
|
5
|
+
import type { SatoriEventBody, SatoriMessage, SatoriChannel } from './types.js';
|
|
6
|
+
/** Satori Channel.type: 0=TEXT, 1=DIRECT, 2=CATEGORY, 3=VOICE */
|
|
7
|
+
export declare function isPrivateChannel(channel?: SatoriChannel): boolean;
|
|
8
|
+
/** 将 Satori 事件体转为 zhin Message 的 MessageBase(仅 message-created/updated,供 $formatMessage 使用) */
|
|
9
|
+
export declare function formatSatoriMessagePayload(body: SatoriEventBody, adapterName: 'satori', botName: string, recallFn: (msgId: string) => Promise<void>, replyFn: (channel: {
|
|
10
|
+
id: string;
|
|
11
|
+
type: 'group' | 'private';
|
|
12
|
+
}, content: (string | {
|
|
13
|
+
type: string;
|
|
14
|
+
data?: Record<string, unknown>;
|
|
15
|
+
})[], quote?: boolean | string) => Promise<string>): {
|
|
16
|
+
$id: string;
|
|
17
|
+
$adapter: 'satori';
|
|
18
|
+
$bot: string;
|
|
19
|
+
$channel: {
|
|
20
|
+
id: string;
|
|
21
|
+
type: 'group' | 'private';
|
|
22
|
+
};
|
|
23
|
+
$sender: {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
};
|
|
27
|
+
$content: Array<{
|
|
28
|
+
type: string;
|
|
29
|
+
data: Record<string, unknown>;
|
|
30
|
+
}>;
|
|
31
|
+
$raw: string;
|
|
32
|
+
$timestamp: number;
|
|
33
|
+
$recall: () => Promise<void>;
|
|
34
|
+
$reply: (content: SendContent, quote?: boolean | string) => Promise<string>;
|
|
35
|
+
};
|
|
36
|
+
/** 判断是否为消息相关事件(message-created / message-updated) */
|
|
37
|
+
export declare function isMessageEvent(body: SatoriEventBody): body is SatoriEventBody & {
|
|
38
|
+
message: SatoriMessage;
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhF,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAEjE;AAED,+FAA+F;AAC/F,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,eAAe,EACrB,WAAW,EAAE,QAAQ,EACrB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EAC1C,OAAO,EAAE,CAAC,OAAO,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;CAAE,EAAE,OAAO,EAAE,CAAC,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GACjL;IACD,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;KAAE,CAAC;IACpD,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7E,CA4BA;AAED,qDAAqD;AACrD,wBAAgB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI,IAAI,eAAe,GAAG;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,CAE1G"}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Satori Channel.type: 0=TEXT, 1=DIRECT, 2=CATEGORY, 3=VOICE */
|
|
2
|
+
export function isPrivateChannel(channel) {
|
|
3
|
+
return channel?.type === 1;
|
|
4
|
+
}
|
|
5
|
+
/** 将 Satori 事件体转为 zhin Message 的 MessageBase(仅 message-created/updated,供 $formatMessage 使用) */
|
|
6
|
+
export function formatSatoriMessagePayload(body, adapterName, botName, recallFn, replyFn) {
|
|
7
|
+
const msg = body.message;
|
|
8
|
+
const channel = body.channel ?? msg.channel;
|
|
9
|
+
const user = body.user ?? msg.user ?? msg.member?.user;
|
|
10
|
+
const channelId = channel?.id ?? '';
|
|
11
|
+
const isPrivate = isPrivateChannel(channel);
|
|
12
|
+
const content = msg.content ?? '';
|
|
13
|
+
const raw = typeof content === 'string' ? content : String(content);
|
|
14
|
+
const senderId = user?.id ?? '';
|
|
15
|
+
const senderName = user?.name ?? msg.member?.nick ?? senderId;
|
|
16
|
+
return {
|
|
17
|
+
$id: `${channelId}:${msg.id}`,
|
|
18
|
+
$adapter: 'satori',
|
|
19
|
+
$bot: botName,
|
|
20
|
+
$channel: { id: channelId, type: isPrivate ? 'private' : 'group' },
|
|
21
|
+
$sender: { id: senderId, name: senderName },
|
|
22
|
+
$content: [{ type: 'text', data: { text: raw } }],
|
|
23
|
+
$raw: raw,
|
|
24
|
+
$timestamp: body.timestamp ?? msg.created_at ?? Math.floor(Date.now() / 1000),
|
|
25
|
+
$recall: () => recallFn(`${channelId}:${msg.id}`),
|
|
26
|
+
$reply: (cnt, quote) => replyFn({ id: channelId, type: isPrivate ? 'private' : 'group' }, (Array.isArray(cnt) ? cnt : [cnt]), quote),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** 判断是否为消息相关事件(message-created / message-updated) */
|
|
30
|
+
export function isMessageEvent(body) {
|
|
31
|
+
return (body.type === 'message-created' || body.type === 'message-updated') && !!body.message?.id;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=utils.js.map
|
package/lib/utils.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAOA,iEAAiE;AACjE,MAAM,UAAU,gBAAgB,CAAC,OAAuB;IACtD,OAAO,OAAO,EAAE,IAAI,KAAK,CAAC,CAAC;AAC7B,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,0BAA0B,CACxC,IAAqB,EACrB,WAAqB,EACrB,OAAe,EACf,QAA0C,EAC1C,OAAkL;IAalL,MAAM,GAAG,GAAG,IAAI,CAAC,OAAQ,CAAC;IAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC;IACvD,MAAM,SAAS,GAAG,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC;IACpC,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;IAClC,MAAM,GAAG,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC;IAChC,MAAM,UAAU,GAAG,IAAI,EAAE,IAAI,IAAI,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,QAAQ,CAAC;IAE9D,OAAO;QACL,GAAG,EAAE,GAAG,SAAS,IAAI,GAAG,CAAC,EAAE,EAAE;QAC7B,QAAQ,EAAE,QAAQ;QAClB,IAAI,EAAE,OAAO;QACb,QAAQ,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE;QAClE,OAAO,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE;QAC3C,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;QACjD,IAAI,EAAE,GAAG;QACT,UAAU,EAAE,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QAC7E,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,SAAS,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;QACjD,MAAM,EAAE,CAAC,GAAgB,EAAE,KAAwB,EAAE,EAAE,CACrD,OAAO,CACL,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,EACxD,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAkE,EACnG,KAAK,CACN;KACJ,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,cAAc,CAAC,IAAqB;IAClD,OAAO,CAAC,IAAI,CAAC,IAAI,KAAK,iBAAiB,IAAI,IAAI,CAAC,IAAI,KAAK,iBAAiB,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;AACpG,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zhin.js/adapter-satori",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Zhin.js adapter for Satori protocol",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"import": "./lib/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"zhin",
|
|
21
|
+
"bot",
|
|
22
|
+
"adapter",
|
|
23
|
+
"satori",
|
|
24
|
+
"chatbot"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"ws": "^8.18.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^24.3.0",
|
|
32
|
+
"@types/ws": "^8.18.1",
|
|
33
|
+
"typescript": "^5.3.0",
|
|
34
|
+
"zhin.js": "1.0.50"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"zhin.js": "1.0.50",
|
|
38
|
+
"@zhin.js/http": "1.0.44"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"@zhin.js/http": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc && (test -f ../../services/console/lib/bin.js && node ../../services/console/lib/bin.js build || echo 'Skipping client build: console not built yet')",
|
|
47
|
+
"clean": "rimraf lib"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satori 适配器:单一适配器支持 WS 正向 / Webhook,由 config.connection 区分
|
|
3
|
+
* 协议文档:https://satori.chat/zh-CN/introduction.html
|
|
4
|
+
*/
|
|
5
|
+
import type { Router } from '@zhin.js/http';
|
|
6
|
+
import { Adapter, Plugin } from 'zhin.js';
|
|
7
|
+
import { SatoriWsClient } from './bot-ws.js';
|
|
8
|
+
import { SatoriWebhookBot } from './bot-webhook.js';
|
|
9
|
+
import type {
|
|
10
|
+
SatoriBotConfig,
|
|
11
|
+
SatoriWsConfig,
|
|
12
|
+
SatoriWebhookConfig,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
import type { SatoriEventBody } from './types.js';
|
|
15
|
+
|
|
16
|
+
export type SatoriBot = SatoriWsClient | SatoriWebhookBot;
|
|
17
|
+
|
|
18
|
+
export class SatoriAdapter extends Adapter<SatoriBot> {
|
|
19
|
+
#router?: Router;
|
|
20
|
+
|
|
21
|
+
constructor(plugin: Plugin) {
|
|
22
|
+
super(plugin, 'satori', []);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
createBot(config: SatoriBotConfig): SatoriBot {
|
|
26
|
+
switch (config.connection) {
|
|
27
|
+
case 'ws':
|
|
28
|
+
return new SatoriWsClient(this, config as SatoriWsConfig);
|
|
29
|
+
case 'webhook':
|
|
30
|
+
if (!this.#router) {
|
|
31
|
+
throw new Error('Satori connection: webhook 需要 router,请安装并在配置中启用 @zhin.js/http');
|
|
32
|
+
}
|
|
33
|
+
return new SatoriWebhookBot(this, this.#router, config as SatoriWebhookConfig);
|
|
34
|
+
default:
|
|
35
|
+
throw new Error(`Unknown Satori connection: ${(config as SatoriBotConfig).connection}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async start(): Promise<void> {
|
|
40
|
+
this.#router = (this.plugin.inject as (key: string) => Router | undefined)('router');
|
|
41
|
+
(this.plugin.useContext as (key: string, fn: (router: Router) => void) => void)('router', (router: Router) => {
|
|
42
|
+
this.#router = router;
|
|
43
|
+
});
|
|
44
|
+
await super.start();
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satori HTTP API 封装:POST /v1/{resource}.{method},头 Satori-Platform、Satori-User-ID、Authorization
|
|
3
|
+
* 参考 https://satori.chat/en-US/protocol/api.html
|
|
4
|
+
*/
|
|
5
|
+
export interface SatoriApiOptions {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
platform: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
token?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 调用 Satori API:POST {baseUrl}/v1/{resource}.{method},JSON body
|
|
14
|
+
*/
|
|
15
|
+
export async function callSatoriApi<T = unknown>(
|
|
16
|
+
options: SatoriApiOptions,
|
|
17
|
+
resource: string,
|
|
18
|
+
method: string,
|
|
19
|
+
params: Record<string, unknown> = {},
|
|
20
|
+
): Promise<T> {
|
|
21
|
+
const { baseUrl, platform, userId, token } = options;
|
|
22
|
+
const url = `${baseUrl.replace(/\/$/, '')}/v1/${resource}.${method}`;
|
|
23
|
+
const headers: Record<string, string> = {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'Satori-Platform': platform,
|
|
26
|
+
'Satori-User-ID': userId,
|
|
27
|
+
};
|
|
28
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
29
|
+
|
|
30
|
+
const res = await fetch(url, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers,
|
|
33
|
+
body: JSON.stringify(params),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
if (res.status === 401) throw new Error(`Satori API 认证失败: ${text}`);
|
|
38
|
+
if (res.status === 403) throw new Error(`Satori API 权限不足: ${text}`);
|
|
39
|
+
if (res.status === 404) throw new Error(`Satori API 不存在: ${resource}.${method}`);
|
|
40
|
+
if (res.status >= 400) throw new Error(`Satori API 错误 ${res.status}: ${text}`);
|
|
41
|
+
|
|
42
|
+
if (!text || text.trim() === '') return undefined as T;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(text) as T;
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error(`Satori API 无效 JSON: ${text.slice(0, 200)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satori WebHook Bot:应用提供 POST path,SDK 推送 EVENT(Satori-Opcode: 0)
|
|
3
|
+
*/
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
import { Bot, Message, SendOptions, segment } from 'zhin.js';
|
|
6
|
+
import type { Router, RouterContext } from '@zhin.js/http';
|
|
7
|
+
import { callSatoriApi } from './api.js';
|
|
8
|
+
import type { SatoriWebhookConfig, SatoriEventBody, SatoriLogin } from './types.js';
|
|
9
|
+
import { SatoriOpcode } from './types.js';
|
|
10
|
+
import type { SatoriAdapter } from './adapter.js';
|
|
11
|
+
import { formatSatoriMessagePayload, isMessageEvent } from './utils.js';
|
|
12
|
+
|
|
13
|
+
export class SatoriWebhookBot extends EventEmitter implements Bot<SatoriWebhookConfig, SatoriEventBody> {
|
|
14
|
+
$connected: boolean = true;
|
|
15
|
+
/** 从首个事件的 login 得到,用于 API 的 platform / userId */
|
|
16
|
+
private login?: SatoriLogin;
|
|
17
|
+
|
|
18
|
+
get logger() {
|
|
19
|
+
return this.adapter.plugin.logger;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
public adapter: SatoriAdapter,
|
|
24
|
+
public router: Router,
|
|
25
|
+
public $config: SatoriWebhookConfig,
|
|
26
|
+
) {
|
|
27
|
+
super();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get $id() {
|
|
31
|
+
return this.$config.name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private apiOptions(): { baseUrl: string; platform: string; userId: string; token?: string } {
|
|
35
|
+
const platform = this.login?.platform ?? '';
|
|
36
|
+
const userId = this.login?.user?.id ?? '';
|
|
37
|
+
return { baseUrl: this.$config.baseUrl, platform, userId, token: this.$config.token };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async $connect(): Promise<void> {
|
|
41
|
+
const path = this.$config.path.startsWith('/') ? this.$config.path : `/${this.$config.path}`;
|
|
42
|
+
this.router.post(path, async (ctx: RouterContext) => {
|
|
43
|
+
const opcode = parseInt(ctx.headers['satori-opcode'] as string ?? '', 10);
|
|
44
|
+
const body = ctx.request.body as SatoriEventBody | undefined;
|
|
45
|
+
if (opcode === SatoriOpcode.EVENT && body) {
|
|
46
|
+
if (body.login && !this.login) this.login = body.login;
|
|
47
|
+
this.handleEvent(body);
|
|
48
|
+
}
|
|
49
|
+
ctx.status = 200;
|
|
50
|
+
ctx.body = {};
|
|
51
|
+
});
|
|
52
|
+
this.logger.info(`Satori WebHook 注册路径: ${path}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async $disconnect(): Promise<void> {
|
|
56
|
+
this.$connected = false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private handleEvent(body: SatoriEventBody): void {
|
|
60
|
+
if (isMessageEvent(body)) {
|
|
61
|
+
const message = this.$formatMessage(body);
|
|
62
|
+
this.adapter.emit('message.receive', message);
|
|
63
|
+
this.logger.debug(
|
|
64
|
+
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$formatMessage(body: SatoriEventBody): Message<SatoriEventBody> {
|
|
70
|
+
if (!isMessageEvent(body)) {
|
|
71
|
+
return Message.from(body, {
|
|
72
|
+
$id: '',
|
|
73
|
+
$adapter: 'satori',
|
|
74
|
+
$bot: this.$config.name,
|
|
75
|
+
$channel: { id: '', type: 'private' },
|
|
76
|
+
$sender: { id: '', name: '' },
|
|
77
|
+
$content: [],
|
|
78
|
+
$raw: '',
|
|
79
|
+
$timestamp: body.timestamp ?? 0,
|
|
80
|
+
$recall: async () => {},
|
|
81
|
+
$reply: async () => '',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const payload = formatSatoriMessagePayload(
|
|
85
|
+
body,
|
|
86
|
+
'satori',
|
|
87
|
+
this.$config.name,
|
|
88
|
+
(id) => this.$recallMessage(id),
|
|
89
|
+
(channel, content, _quote) =>
|
|
90
|
+
this.adapter.sendMessage({
|
|
91
|
+
...channel,
|
|
92
|
+
context: 'satori',
|
|
93
|
+
bot: this.$config.name,
|
|
94
|
+
content: content as import('zhin.js').SendContent,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
return Message.from(body, payload);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async $sendMessage(options: SendOptions): Promise<string> {
|
|
101
|
+
const channelId = options.id;
|
|
102
|
+
const contentRaw = segment.raw(options.content);
|
|
103
|
+
const result = await callSatoriApi(this.apiOptions(), 'message', 'create', {
|
|
104
|
+
channel_id: channelId,
|
|
105
|
+
content: contentRaw,
|
|
106
|
+
});
|
|
107
|
+
const list = Array.isArray(result) ? result : (result as { data?: unknown[] })?.data;
|
|
108
|
+
const msg = list?.[0] as { id?: string } | undefined;
|
|
109
|
+
const msgId = msg?.id ?? '';
|
|
110
|
+
return msgId ? `${channelId}:${msgId}` : '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async $recallMessage(id: string): Promise<void> {
|
|
114
|
+
const [channelId, messageId] = id.includes(':') ? id.split(':') : ['', id];
|
|
115
|
+
await callSatoriApi(this.apiOptions(), 'message', 'delete', { channel_id: channelId, message_id: messageId });
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/bot-ws.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Satori WebSocket Bot:应用连 SDK /v1/events,IDENTIFY 认证,收 EVENT 转 Message
|
|
3
|
+
*/
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import { EventEmitter } from 'events';
|
|
6
|
+
import { clearInterval } from 'node:timers';
|
|
7
|
+
import { Bot, Message, SendOptions, segment } from 'zhin.js';
|
|
8
|
+
import { callSatoriApi } from './api.js';
|
|
9
|
+
import type { SatoriWsConfig, SatoriSignal, SatoriEventBody, SatoriLogin } from './types.js';
|
|
10
|
+
import { SatoriOpcode } from './types.js';
|
|
11
|
+
import type { SatoriAdapter } from './adapter.js';
|
|
12
|
+
import { formatSatoriMessagePayload, isMessageEvent } from './utils.js';
|
|
13
|
+
|
|
14
|
+
export class SatoriWsClient extends EventEmitter implements Bot<SatoriWsConfig, SatoriEventBody> {
|
|
15
|
+
$connected: boolean;
|
|
16
|
+
private ws?: WebSocket;
|
|
17
|
+
private reconnectTimer?: NodeJS.Timeout;
|
|
18
|
+
private heartbeatTimer?: NodeJS.Timeout;
|
|
19
|
+
/** READY 后得到的当前登录,用于 API 的 platform / userId */
|
|
20
|
+
private login?: SatoriLogin;
|
|
21
|
+
private lastSn?: number;
|
|
22
|
+
|
|
23
|
+
get logger() {
|
|
24
|
+
return this.adapter.plugin.logger;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
constructor(public adapter: SatoriAdapter, public $config: SatoriWsConfig) {
|
|
28
|
+
super();
|
|
29
|
+
this.$connected = false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get $id() {
|
|
33
|
+
return this.$config.name;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private get wsUrl(): string {
|
|
37
|
+
const base = this.$config.baseUrl.replace(/\/$/, '');
|
|
38
|
+
return base.replace(/^http/, 'ws') + '/v1/events';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private apiOptions(): { baseUrl: string; platform: string; userId: string; token?: string } {
|
|
42
|
+
const platform = this.login?.platform ?? '';
|
|
43
|
+
const userId = this.login?.user?.id ?? '';
|
|
44
|
+
return { baseUrl: this.$config.baseUrl, platform, userId, token: this.$config.token };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private sendSignal(op: number, body?: Record<string, unknown>): void {
|
|
48
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
49
|
+
this.ws.send(JSON.stringify({ op, body: body ?? {} }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private startHeartbeat(): void {
|
|
53
|
+
const interval = this.$config.heartbeat_interval ?? 10000;
|
|
54
|
+
this.heartbeatTimer = setInterval(() => {
|
|
55
|
+
this.sendSignal(SatoriOpcode.PING);
|
|
56
|
+
}, interval);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private scheduleReconnect(): void {
|
|
60
|
+
if (this.reconnectTimer) return;
|
|
61
|
+
const delay = 5000;
|
|
62
|
+
this.reconnectTimer = setTimeout(() => {
|
|
63
|
+
this.reconnectTimer = undefined;
|
|
64
|
+
this.$connect().catch((err) => this.logger.warn('Satori 重连失败', err));
|
|
65
|
+
}, delay);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async $connect(): Promise<void> {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
71
|
+
|
|
72
|
+
this.ws.on('open', () => {
|
|
73
|
+
this.$connected = true;
|
|
74
|
+
this.sendSignal(SatoriOpcode.IDENTIFY, {
|
|
75
|
+
token: this.$config.token,
|
|
76
|
+
sn: this.lastSn,
|
|
77
|
+
});
|
|
78
|
+
this.startHeartbeat();
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.ws.on('message', (data) => {
|
|
83
|
+
try {
|
|
84
|
+
const signal = JSON.parse(data.toString()) as SatoriSignal;
|
|
85
|
+
if (signal.op === SatoriOpcode.READY && signal.body?.logins) {
|
|
86
|
+
const logins = signal.body.logins as SatoriLogin[];
|
|
87
|
+
this.login = logins[0];
|
|
88
|
+
if (!this.login?.platform || !this.login?.user?.id) {
|
|
89
|
+
this.logger.warn('Satori READY 未包含 platform/user,API 调用可能失败');
|
|
90
|
+
}
|
|
91
|
+
} else if (signal.op === SatoriOpcode.EVENT && signal.body) {
|
|
92
|
+
if (signal.body.sn != null) this.lastSn = signal.body.sn as number;
|
|
93
|
+
this.handleEvent(signal.body as SatoriEventBody);
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.emit('error', error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.ws.on('close', (code, reason) => {
|
|
101
|
+
this.$connected = false;
|
|
102
|
+
reject(new Error(`Satori WS closed: ${code} ${reason.toString()}`));
|
|
103
|
+
this.scheduleReconnect();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
this.ws.on('error', (error) => {
|
|
107
|
+
reject(error);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async $disconnect(): Promise<void> {
|
|
113
|
+
if (this.reconnectTimer) {
|
|
114
|
+
clearTimeout(this.reconnectTimer);
|
|
115
|
+
this.reconnectTimer = undefined;
|
|
116
|
+
}
|
|
117
|
+
if (this.heartbeatTimer) {
|
|
118
|
+
clearInterval(this.heartbeatTimer);
|
|
119
|
+
this.heartbeatTimer = undefined;
|
|
120
|
+
}
|
|
121
|
+
if (this.ws) {
|
|
122
|
+
this.ws.close();
|
|
123
|
+
this.ws = undefined;
|
|
124
|
+
}
|
|
125
|
+
this.$connected = false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private handleEvent(body: SatoriEventBody): void {
|
|
129
|
+
if (isMessageEvent(body)) {
|
|
130
|
+
const message = this.$formatMessage(body);
|
|
131
|
+
this.adapter.emit('message.receive', message);
|
|
132
|
+
this.logger.debug(
|
|
133
|
+
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
$formatMessage(body: SatoriEventBody): Message<SatoriEventBody> {
|
|
139
|
+
if (!isMessageEvent(body)) {
|
|
140
|
+
return Message.from(body, {
|
|
141
|
+
$id: '',
|
|
142
|
+
$adapter: 'satori',
|
|
143
|
+
$bot: this.$config.name,
|
|
144
|
+
$channel: { id: '', type: 'private' },
|
|
145
|
+
$sender: { id: '', name: '' },
|
|
146
|
+
$content: [],
|
|
147
|
+
$raw: '',
|
|
148
|
+
$timestamp: body.timestamp ?? 0,
|
|
149
|
+
$recall: async () => {},
|
|
150
|
+
$reply: async () => '',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const payload = formatSatoriMessagePayload(
|
|
154
|
+
body,
|
|
155
|
+
'satori',
|
|
156
|
+
this.$config.name,
|
|
157
|
+
(id) => this.$recallMessage(id),
|
|
158
|
+
(channel, content, _quote) =>
|
|
159
|
+
this.adapter.sendMessage({
|
|
160
|
+
...channel,
|
|
161
|
+
context: 'satori',
|
|
162
|
+
bot: this.$config.name,
|
|
163
|
+
content: content as import('zhin.js').SendContent,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
return Message.from(body, payload);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async $sendMessage(options: SendOptions): Promise<string> {
|
|
170
|
+
const channelId = options.id;
|
|
171
|
+
const contentRaw = segment.raw(options.content);
|
|
172
|
+
const result = await callSatoriApi(this.apiOptions(), 'message', 'create', {
|
|
173
|
+
channel_id: channelId,
|
|
174
|
+
content: contentRaw,
|
|
175
|
+
});
|
|
176
|
+
const list = Array.isArray(result) ? result : (result as { data?: unknown[] })?.data;
|
|
177
|
+
const msg = list?.[0] as { id?: string } | undefined;
|
|
178
|
+
const msgId = msg?.id ?? '';
|
|
179
|
+
this.logger.debug(`${this.$config.name} send ${options.type}(${channelId}):${contentRaw}`);
|
|
180
|
+
return msgId ? `${channelId}:${msgId}` : '';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async $recallMessage(id: string): Promise<void> {
|
|
184
|
+
const [channelId, messageId] = id.includes(':') ? id.split(':') : ['', id];
|
|
185
|
+
await callSatoriApi(this.apiOptions(), 'message', 'delete', { channel_id: channelId, message_id: messageId });
|
|
186
|
+
}
|
|
187
|
+
}
|