@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/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Satori 适配器入口:单一适配器,支持 WS / Webhook,协议文档 https://satori.chat/zh-CN/introduction.html
3
+ */
4
+ import { usePlugin, type Plugin, type Context } from 'zhin.js';
5
+ import type { Router } from '@zhin.js/http';
6
+ import { SatoriAdapter } from './adapter.js';
7
+
8
+ export * from './types.js';
9
+ export { callSatoriApi } from './api.js';
10
+ export * from './utils.js';
11
+ export { SatoriWsClient } from './bot-ws.js';
12
+ export { SatoriWebhookBot } from './bot-webhook.js';
13
+ export { SatoriAdapter, type SatoriBot } from './adapter.js';
14
+
15
+ declare module 'zhin.js' {
16
+ namespace Plugin {
17
+ interface Contexts {
18
+ router: import('@zhin.js/http').Router;
19
+ }
20
+ }
21
+ interface Adapters {
22
+ satori: SatoriAdapter;
23
+ }
24
+ }
25
+
26
+ const { provide } = usePlugin();
27
+ provide({
28
+ name: 'satori',
29
+ description: 'Satori 协议适配器(WS 正向 / Webhook)',
30
+ mounted: async (p: Plugin) => {
31
+ const adapter = new SatoriAdapter(p);
32
+ await adapter.start();
33
+ return adapter;
34
+ },
35
+ dispose: async (adapter: SatoriAdapter) => {
36
+ await adapter.stop();
37
+ },
38
+ } as unknown as Context<'satori'>);
package/src/types.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Satori 适配器类型(参考 https://satori.chat 协议)
3
+ */
4
+
5
+ /** 配置公共字段;单一适配器 context 均为 'satori',连接方式由 connection 区分 */
6
+ export interface SatoriConfigBase {
7
+ context: 'satori';
8
+ name: string;
9
+ baseUrl: string;
10
+ /** 认证 token,API 用 Authorization: Bearer,WebSocket IDENTIFY 用 token 字段 */
11
+ token?: string;
12
+ }
13
+
14
+ /** WebSocket 连接(应用连 SDK /v1/events) */
15
+ export interface SatoriWsConfig extends SatoriConfigBase {
16
+ connection: 'ws';
17
+ /** 心跳间隔(毫秒),默认 10000 */
18
+ heartbeat_interval?: number;
19
+ }
20
+
21
+ /** WebHook(SDK POST 到应用提供的 path) */
22
+ export interface SatoriWebhookConfig extends SatoriConfigBase {
23
+ connection: 'webhook';
24
+ path: string;
25
+ }
26
+
27
+ export type SatoriBotConfig = SatoriWsConfig | SatoriWebhookConfig;
28
+
29
+ /** WebSocket/API 信号:op + body */
30
+ export interface SatoriSignal {
31
+ op: number;
32
+ body?: Record<string, unknown>;
33
+ }
34
+
35
+ /** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
36
+ export const SatoriOpcode = {
37
+ EVENT: 0,
38
+ PING: 1,
39
+ PONG: 2,
40
+ IDENTIFY: 3,
41
+ READY: 4,
42
+ META: 5,
43
+ } as const;
44
+
45
+ /** 事件体(EVENT 的 body):type、sn、timestamp、login、message、channel、user 等 */
46
+ export interface SatoriEventBody {
47
+ type?: string;
48
+ sn?: number;
49
+ timestamp?: number;
50
+ login?: SatoriLogin;
51
+ message?: SatoriMessage;
52
+ channel?: SatoriChannel;
53
+ user?: SatoriUser;
54
+ guild?: { id: string; name?: string };
55
+ member?: { user?: SatoriUser; nick?: string; roles?: string[] };
56
+ [key: string]: unknown;
57
+ }
58
+
59
+ /** Login 资源(READY 与事件中) */
60
+ export interface SatoriLogin {
61
+ platform?: string;
62
+ user?: SatoriUser;
63
+ status?: number;
64
+ sn?: number;
65
+ }
66
+
67
+ /** Channel 资源:id, type (0=TEXT,1=DIRECT,2=CATEGORY,3=VOICE) */
68
+ export interface SatoriChannel {
69
+ id: string;
70
+ type?: number;
71
+ name?: string;
72
+ parent_id?: string;
73
+ }
74
+
75
+ /** User 资源 */
76
+ export interface SatoriUser {
77
+ id: string;
78
+ name?: string;
79
+ avatar?: string;
80
+ }
81
+
82
+ /** Message 资源:id, content(元素字符串), channel, user 等 */
83
+ export interface SatoriMessage {
84
+ id: string;
85
+ content?: string;
86
+ channel?: SatoriChannel;
87
+ user?: SatoriUser;
88
+ member?: { user?: SatoriUser; nick?: string };
89
+ created_at?: number;
90
+ updated_at?: number;
91
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Satori 事件与 zhin Message 的转换
3
+ */
4
+ import type { SendContent } from 'zhin.js';
5
+ import { Message } from 'zhin.js';
6
+ import type { SatoriEventBody, SatoriMessage, SatoriChannel } from './types.js';
7
+
8
+ /** Satori Channel.type: 0=TEXT, 1=DIRECT, 2=CATEGORY, 3=VOICE */
9
+ export function isPrivateChannel(channel?: SatoriChannel): boolean {
10
+ return channel?.type === 1;
11
+ }
12
+
13
+ /** 将 Satori 事件体转为 zhin Message 的 MessageBase(仅 message-created/updated,供 $formatMessage 使用) */
14
+ export function formatSatoriMessagePayload(
15
+ body: SatoriEventBody,
16
+ adapterName: 'satori',
17
+ botName: string,
18
+ recallFn: (msgId: string) => Promise<void>,
19
+ replyFn: (channel: { id: string; type: 'group' | 'private' }, content: (string | { type: string; data?: Record<string, unknown> })[], quote?: boolean | string) => Promise<string>,
20
+ ): {
21
+ $id: string;
22
+ $adapter: 'satori';
23
+ $bot: string;
24
+ $channel: { id: string; type: 'group' | 'private' };
25
+ $sender: { id: string; name: string };
26
+ $content: Array<{ type: string; data: Record<string, unknown> }>;
27
+ $raw: string;
28
+ $timestamp: number;
29
+ $recall: () => Promise<void>;
30
+ $reply: (content: SendContent, quote?: boolean | string) => Promise<string>;
31
+ } {
32
+ const msg = body.message!;
33
+ const channel = body.channel ?? msg.channel;
34
+ const user = body.user ?? msg.user ?? msg.member?.user;
35
+ const channelId = channel?.id ?? '';
36
+ const isPrivate = isPrivateChannel(channel);
37
+ const content = msg.content ?? '';
38
+ const raw = typeof content === 'string' ? content : String(content);
39
+ const senderId = user?.id ?? '';
40
+ const senderName = user?.name ?? msg.member?.nick ?? senderId;
41
+
42
+ return {
43
+ $id: `${channelId}:${msg.id}`,
44
+ $adapter: 'satori',
45
+ $bot: botName,
46
+ $channel: { id: channelId, type: isPrivate ? 'private' : 'group' },
47
+ $sender: { id: senderId, name: senderName },
48
+ $content: [{ type: 'text', data: { text: raw } }],
49
+ $raw: raw,
50
+ $timestamp: body.timestamp ?? msg.created_at ?? Math.floor(Date.now() / 1000),
51
+ $recall: () => recallFn(`${channelId}:${msg.id}`),
52
+ $reply: (cnt: SendContent, quote?: boolean | string) =>
53
+ replyFn(
54
+ { id: channelId, type: isPrivate ? 'private' : 'group' },
55
+ (Array.isArray(cnt) ? cnt : [cnt]) as (string | { type: string; data?: Record<string, unknown> })[],
56
+ quote,
57
+ ),
58
+ };
59
+ }
60
+
61
+ /** 判断是否为消息相关事件(message-created / message-updated) */
62
+ export function isMessageEvent(body: SatoriEventBody): body is SatoriEventBody & { message: SatoriMessage } {
63
+ return (body.type === 'message-created' || body.type === 'message-updated') && !!body.message?.id;
64
+ }