@zhin.js/adapter-satori 1.0.1 → 1.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/CHANGELOG.md +912 -0
- package/README.md +60 -38
- package/adapters/satori/index.js +38 -0
- package/adapters/satori/index.ts +49 -0
- package/commands/satori/endpoint/add/[id]/index.js +3 -0
- package/commands/satori/endpoint/add/[id]/index.ts +3 -0
- package/commands/satori/endpoint/definition.js +19 -0
- package/commands/satori/endpoint/definition.ts +19 -0
- package/commands/satori/endpoint/list/index.js +3 -0
- package/commands/satori/endpoint/list/index.ts +3 -0
- package/commands/satori/endpoint/remove/[id]/index.js +3 -0
- package/commands/satori/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +18 -0
- package/lib/client.js +21 -0
- package/lib/endpoint.d.ts +56 -0
- package/lib/endpoint.js +472 -0
- package/lib/index.d.ts +8 -18
- package/lib/index.js +6 -25
- package/lib/protocol.d.ts +148 -0
- package/lib/protocol.js +230 -0
- package/lib/satori-runtime-state.d.ts +1 -0
- package/lib/satori-runtime-state.js +6 -0
- package/lib/webhook.d.ts +12 -0
- package/lib/webhook.js +60 -0
- package/lib/ws.d.ts +13 -0
- package/lib/ws.js +5 -0
- package/package.json +68 -17
- package/plugin.js +14 -0
- package/schema.json +109 -0
- package/src/client.ts +57 -0
- package/src/endpoint.ts +580 -0
- package/src/index.ts +61 -35
- package/src/protocol.ts +353 -0
- package/src/satori-runtime-state.ts +7 -0
- package/src/webhook.ts +79 -0
- package/src/ws.ts +22 -0
- package/lib/adapter.d.ts +0 -17
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -35
- package/lib/adapter.js.map +0 -1
- package/lib/api.d.ts +0 -15
- package/lib/api.d.ts.map +0 -1
- package/lib/api.js +0 -37
- package/lib/api.js.map +0 -1
- package/lib/endpoint-webhook.d.ts +0 -27
- package/lib/endpoint-webhook.d.ts.map +0 -1
- package/lib/endpoint-webhook.js +0 -99
- package/lib/endpoint-webhook.js.map +0 -1
- package/lib/endpoint-ws.d.ts +0 -30
- package/lib/endpoint-ws.d.ts.map +0 -1
- package/lib/endpoint-ws.js +0 -195
- package/lib/endpoint-ws.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/types.d.ts +0 -91
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -13
- package/lib/types.js.map +0 -1
- package/lib/utils.d.ts +0 -40
- package/lib/utils.d.ts.map +0 -1
- package/lib/utils.js +0 -33
- package/lib/utils.js.map +0 -1
- package/skills/satori/SKILL.md +0 -33
- package/src/adapter.ts +0 -48
- package/src/api.ts +0 -48
- package/src/endpoint-webhook.ts +0 -117
- package/src/endpoint-ws.ts +0 -214
- package/src/types.ts +0 -91
- package/src/utils.ts +0 -64
package/src/api.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
}
|
package/src/endpoint-webhook.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Satori WebHook Bot:应用提供 POST path,SDK 推送 EVENT(Satori-Opcode: 0)
|
|
3
|
-
*/
|
|
4
|
-
import { EventEmitter } from 'events';
|
|
5
|
-
import { formatCompact, Endpoint, Message, segment, SendOptions } from 'zhin.js';
|
|
6
|
-
import { registerFetchRoute, type Router, type RouterContext } from '@zhin.js/host-router/router';
|
|
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 SatoriWebhookEndpoint extends EventEmitter implements Endpoint<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
|
-
registerFetchRoute(this.router, 'POST', path, async (ctx: RouterContext) => {
|
|
43
|
-
const opcode = parseInt(ctx.get('satori-opcode') ?? '', 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(formatCompact( { op: '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
|
-
$endpoint: 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
|
-
endpoint: 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/endpoint-ws.ts
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
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 { formatCompact, Endpoint, Message, segment, SendOptions } 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 Endpoint<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
|
-
const url=new URL(base);
|
|
39
|
-
if(this.$config.token) {
|
|
40
|
-
url.searchParams.set('access_token', this.$config.token);
|
|
41
|
-
}
|
|
42
|
-
return url.toString();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private apiOptions(): { baseUrl: string; platform: string; userId: string; token?: string } {
|
|
46
|
-
const platform = this.login?.platform ?? '';
|
|
47
|
-
const userId = this.login?.user?.id ?? '';
|
|
48
|
-
return { baseUrl: this.$config.baseUrl, platform, userId, token: this.$config.token };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private sendSignal(op: number, body?: Record<string, unknown>): void {
|
|
52
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
53
|
-
this.ws.send(JSON.stringify({ op, body: body ?? {} }));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private startHeartbeat(): void {
|
|
57
|
-
const interval = this.$config.heartbeat_interval ?? 10000;
|
|
58
|
-
this.heartbeatTimer = setInterval(() => {
|
|
59
|
-
this.sendSignal(SatoriOpcode.PING);
|
|
60
|
-
}, interval);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private scheduleReconnect(): void {
|
|
64
|
-
if (this.reconnectTimer) return;
|
|
65
|
-
const delay = 5000;
|
|
66
|
-
this.reconnectTimer = setTimeout(() => {
|
|
67
|
-
this.reconnectTimer = undefined;
|
|
68
|
-
this.$connect().catch((err) => this.logger.warn(formatCompact( {
|
|
69
|
-
op: 'reconnect',
|
|
70
|
-
endpoint: this.$id,
|
|
71
|
-
ok: false,
|
|
72
|
-
error: err instanceof Error ? err.message : String(err),
|
|
73
|
-
})));
|
|
74
|
-
}, delay);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async $connect(): Promise<void> {
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
const headers: Record<string, string> = {};
|
|
80
|
-
if (this.$config.token) headers['Authorization'] = `Bearer ${this.$config.token}`
|
|
81
|
-
this.ws = new WebSocket(this.wsUrl, { headers });
|
|
82
|
-
|
|
83
|
-
this.ws.on('open', () => {
|
|
84
|
-
this.$connected = true;
|
|
85
|
-
this.logger.info(formatCompact({ endpoint: this.$id, mode: 'ws' }));
|
|
86
|
-
this.sendSignal(SatoriOpcode.IDENTIFY, {
|
|
87
|
-
token: this.$config.token,
|
|
88
|
-
sn: this.lastSn,
|
|
89
|
-
});
|
|
90
|
-
this.startHeartbeat();
|
|
91
|
-
resolve();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
this.ws.on('message', (data) => {
|
|
95
|
-
try {
|
|
96
|
-
const signal = JSON.parse(data.toString()) as SatoriSignal;
|
|
97
|
-
if (signal.op === SatoriOpcode.READY && signal.body?.logins) {
|
|
98
|
-
const logins = signal.body.logins as SatoriLogin[];
|
|
99
|
-
this.login = logins[0];
|
|
100
|
-
if (!this.login?.platform || !this.login?.user?.id) {
|
|
101
|
-
this.logger.warn(formatCompact( { op: 'ready', ok: false, error: 'missing platform/user' }));
|
|
102
|
-
}
|
|
103
|
-
} else if (signal.op === SatoriOpcode.EVENT && signal.body) {
|
|
104
|
-
if (signal.body.sn != null) this.lastSn = signal.body.sn as number;
|
|
105
|
-
this.handleEvent(signal.body as SatoriEventBody);
|
|
106
|
-
}
|
|
107
|
-
} catch (error) {
|
|
108
|
-
this.emit('error', error);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
this.ws.on('close', (code, reason) => {
|
|
113
|
-
this.$connected = false;
|
|
114
|
-
const reasonStr = reason?.toString?.() || String(reason);
|
|
115
|
-
const codeHint = code === 1005 ? ' [无状态,多为服务端/代理未发 close 帧即断开]' : code === 1006 ? ' [异常关闭]' : '';
|
|
116
|
-
this.logger.warn(formatCompact( {
|
|
117
|
-
op: 'disconnect',
|
|
118
|
-
endpoint: this.$config.name,
|
|
119
|
-
code,
|
|
120
|
-
error: `${reasonStr || 'closed'}${codeHint}`,
|
|
121
|
-
reconnect_ms: 5000,
|
|
122
|
-
}));
|
|
123
|
-
reject(new Error(`Satori WS closed: ${code} ${reasonStr}`));
|
|
124
|
-
this.scheduleReconnect();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
this.ws.on('error', (error) => {
|
|
128
|
-
this.logger.warn(formatCompact( {
|
|
129
|
-
op: 'ws_error',
|
|
130
|
-
endpoint: this.$config.name,
|
|
131
|
-
ok: false,
|
|
132
|
-
error: error instanceof Error ? error.message : String(error),
|
|
133
|
-
}));
|
|
134
|
-
reject(error);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async $disconnect(): Promise<void> {
|
|
140
|
-
if (this.reconnectTimer) {
|
|
141
|
-
clearTimeout(this.reconnectTimer);
|
|
142
|
-
this.reconnectTimer = undefined;
|
|
143
|
-
}
|
|
144
|
-
if (this.heartbeatTimer) {
|
|
145
|
-
clearInterval(this.heartbeatTimer);
|
|
146
|
-
this.heartbeatTimer = undefined;
|
|
147
|
-
}
|
|
148
|
-
if (this.ws) {
|
|
149
|
-
this.ws.close();
|
|
150
|
-
this.ws = undefined;
|
|
151
|
-
}
|
|
152
|
-
this.$connected = false;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private handleEvent(body: SatoriEventBody): void {
|
|
156
|
-
if (isMessageEvent(body)) {
|
|
157
|
-
const message = this.$formatMessage(body);
|
|
158
|
-
this.adapter.emit('message.receive', message);
|
|
159
|
-
this.logger.debug(
|
|
160
|
-
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
$formatMessage(body: SatoriEventBody): Message<SatoriEventBody> {
|
|
166
|
-
if (!isMessageEvent(body)) {
|
|
167
|
-
return Message.from(body, {
|
|
168
|
-
$id: '',
|
|
169
|
-
$adapter: 'satori',
|
|
170
|
-
$endpoint: this.$config.name,
|
|
171
|
-
$channel: { id: '', type: 'private' },
|
|
172
|
-
$sender: { id: '', name: '' },
|
|
173
|
-
$content: [],
|
|
174
|
-
$raw: '',
|
|
175
|
-
$timestamp: body.timestamp ?? 0,
|
|
176
|
-
$recall: async () => {},
|
|
177
|
-
$reply: async () => '',
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
const payload = formatSatoriMessagePayload(
|
|
181
|
-
body,
|
|
182
|
-
'satori',
|
|
183
|
-
this.$config.name,
|
|
184
|
-
(id) => this.$recallMessage(id),
|
|
185
|
-
(channel, content, _quote) =>
|
|
186
|
-
this.adapter.sendMessage({
|
|
187
|
-
...channel,
|
|
188
|
-
context: 'satori',
|
|
189
|
-
endpoint: this.$config.name,
|
|
190
|
-
content: content as import('zhin.js').SendContent,
|
|
191
|
-
}),
|
|
192
|
-
);
|
|
193
|
-
return Message.from(body, payload);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async $sendMessage(options: SendOptions): Promise<string> {
|
|
197
|
-
const channelId = options.id;
|
|
198
|
-
const contentRaw = segment.raw(options.content);
|
|
199
|
-
const result = await callSatoriApi(this.apiOptions(), 'message', 'create', {
|
|
200
|
-
channel_id: channelId,
|
|
201
|
-
content: contentRaw,
|
|
202
|
-
});
|
|
203
|
-
const list = Array.isArray(result) ? result : (result as { data?: unknown[] })?.data;
|
|
204
|
-
const msg = list?.[0] as { id?: string } | undefined;
|
|
205
|
-
const msgId = msg?.id ?? '';
|
|
206
|
-
this.logger.debug(`${this.$config.name} send ${options.type}(${channelId}):${contentRaw}`);
|
|
207
|
-
return msgId ? `${channelId}:${msgId}` : '';
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async $recallMessage(id: string): Promise<void> {
|
|
211
|
-
const [channelId, messageId] = id.includes(':') ? id.split(':') : ['', id];
|
|
212
|
-
await callSatoriApi(this.apiOptions(), 'message', 'delete', { channel_id: channelId, message_id: messageId });
|
|
213
|
-
}
|
|
214
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
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 SatoriEndpointConfig = 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
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
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
|
-
endpointName: 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
|
-
$endpoint: 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
|
-
$endpoint: endpointName,
|
|
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
|
-
}
|