@zhin.js/adapter-onebot12 1.0.1 → 1.1.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/CHANGELOG.md +855 -0
- package/README.md +59 -69
- package/adapters/onebot12.js +50 -0
- package/adapters/onebot12.ts +64 -0
- package/commands/endpoint/add/[id].js +3 -0
- package/commands/endpoint/add/[id].ts +3 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[id].js +3 -0
- package/commands/endpoint/remove/[id].ts +3 -0
- package/lib/client.d.ts +20 -0
- package/lib/client.js +33 -0
- package/lib/content-port.d.ts +4 -0
- package/lib/content-port.js +45 -0
- package/lib/endpoint-management.d.ts +16 -0
- package/lib/endpoint-management.js +47 -0
- package/lib/index.d.ts +10 -19
- package/lib/index.js +7 -27
- package/lib/onebot12-endpoint-commands.d.ts +1 -0
- package/lib/onebot12-endpoint-commands.js +25 -0
- package/lib/onebot12-runtime-state.d.ts +1 -0
- package/lib/onebot12-runtime-state.js +6 -0
- package/lib/protocol.d.ts +173 -0
- package/lib/protocol.js +415 -0
- package/lib/side-event-dispatch.d.ts +7 -0
- package/lib/side-event-dispatch.js +36 -0
- package/lib/webhook.d.ts +23 -0
- package/lib/webhook.js +161 -0
- package/lib/ws-endpoint.d.ts +22 -0
- package/lib/ws-endpoint.js +255 -0
- package/lib/ws-types.d.ts +16 -0
- package/lib/ws-types.js +1 -0
- package/lib/wss-auth.d.ts +2 -0
- package/lib/wss-auth.js +16 -0
- package/lib/wss-endpoint.d.ts +25 -0
- package/lib/wss-endpoint.js +222 -0
- package/package.json +63 -17
- package/plugin.js +14 -0
- package/schema.json +102 -0
- package/src/client.ts +81 -0
- package/src/content-port.ts +49 -0
- package/src/endpoint-management.ts +73 -0
- package/src/index.ts +64 -37
- package/src/onebot12-endpoint-commands.ts +24 -0
- package/src/onebot12-runtime-state.ts +7 -0
- package/src/protocol.ts +574 -0
- package/src/side-event-dispatch.ts +48 -0
- package/src/webhook.ts +223 -0
- package/src/ws-endpoint.ts +321 -0
- package/src/ws-types.ts +19 -0
- package/src/wss-auth.ts +17 -0
- package/src/wss-endpoint.ts +278 -0
- package/lib/adapter.d.ts +0 -18
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -41
- package/lib/adapter.js.map +0 -1
- package/lib/api.d.ts +0 -14
- package/lib/api.d.ts.map +0 -1
- package/lib/api.js +0 -34
- package/lib/api.js.map +0 -1
- package/lib/endpoint-webhook.d.ts +0 -25
- package/lib/endpoint-webhook.d.ts.map +0 -1
- package/lib/endpoint-webhook.js +0 -118
- package/lib/endpoint-webhook.js.map +0 -1
- package/lib/endpoint-ws.d.ts +0 -26
- package/lib/endpoint-ws.d.ts.map +0 -1
- package/lib/endpoint-ws.js +0 -210
- package/lib/endpoint-ws.js.map +0 -1
- package/lib/endpoint-wss.d.ts +0 -26
- package/lib/endpoint-wss.d.ts.map +0 -1
- package/lib/endpoint-wss.js +0 -187
- package/lib/endpoint-wss.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 -77
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -5
- package/lib/types.js.map +0 -1
- package/lib/utils.d.ts +0 -45
- package/lib/utils.d.ts.map +0 -1
- package/lib/utils.js +0 -60
- package/lib/utils.js.map +0 -1
- package/src/adapter.ts +0 -54
- package/src/api.ts +0 -48
- package/src/endpoint-webhook.ts +0 -130
- package/src/endpoint-ws.ts +0 -222
- package/src/endpoint-wss.ts +0 -202
- package/src/types.ts +0 -86
- package/src/utils.ts +0 -87
- /package/{skills/onebot12/SKILL.md → agent/skills/onebot12.md} +0 -0
package/src/webhook.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot12 HTTP webhook endpoint — POST inbound + api_url outbound.
|
|
3
|
+
*/
|
|
4
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
+
import {
|
|
6
|
+
ClientEndpoint,
|
|
7
|
+
createRecallEndpointControl,
|
|
8
|
+
type EndpointControl,
|
|
9
|
+
type EndpointManagement,
|
|
10
|
+
type EndpointSendRequest,
|
|
11
|
+
} from 'zhin.js/adapter';
|
|
12
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
13
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
14
|
+
import type { CapabilityId } from 'zhin.js';
|
|
15
|
+
import { createOneBot12EndpointManagement } from './endpoint-management.js';
|
|
16
|
+
import {
|
|
17
|
+
buildSendMessageParams,
|
|
18
|
+
callOneBot12Action,
|
|
19
|
+
formatInboundContent,
|
|
20
|
+
formatOutboundSegments,
|
|
21
|
+
isBotMentioned,
|
|
22
|
+
isMessageEvent,
|
|
23
|
+
onebot12InboundConversation,
|
|
24
|
+
senderNickname,
|
|
25
|
+
senderUserId,
|
|
26
|
+
uploadOneBot12MediaSegments,
|
|
27
|
+
type OneBot12Event,
|
|
28
|
+
type OneBot12WebhookConfig,
|
|
29
|
+
} from './protocol.js';
|
|
30
|
+
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
31
|
+
import { createOneBot12ContentPort } from './content-port.js';
|
|
32
|
+
import { callOnebot12Client, createOnebot12EndpointClient, forwardOnebot12ClientEvents, type Onebot12Client } from './client.js';
|
|
33
|
+
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
34
|
+
|
|
35
|
+
const logger = getLogger('onebot12');
|
|
36
|
+
|
|
37
|
+
export interface OneBot12WebhookEndpointOptions {
|
|
38
|
+
readonly id: CapabilityId;
|
|
39
|
+
readonly http: HttpHost;
|
|
40
|
+
readonly config: OneBot12WebhookConfig;
|
|
41
|
+
readonly callAction?: typeof callOneBot12Action;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
45
|
+
readonly client: Onebot12Client;
|
|
46
|
+
readonly #options: OneBot12WebhookEndpointOptions;
|
|
47
|
+
readonly management: EndpointManagement;
|
|
48
|
+
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
49
|
+
readonly content;
|
|
50
|
+
readonly #callAction: typeof callOneBot12Action;
|
|
51
|
+
#routeReleases: HttpRouteRegistration[] = [];
|
|
52
|
+
#started = false;
|
|
53
|
+
|
|
54
|
+
constructor(options: OneBot12WebhookEndpointOptions) {
|
|
55
|
+
super();
|
|
56
|
+
this.#options = options;
|
|
57
|
+
this.#callAction = options.callAction ?? callOneBot12Action;
|
|
58
|
+
this.client = createOnebot12EndpointClient(options.config, (action, params) => this.#callApi(action, params));
|
|
59
|
+
const callApi = (action: string, params?: Record<string, unknown>) => callOnebot12Client(this.client, action, params);
|
|
60
|
+
this.management = createOneBot12EndpointManagement({ callApi });
|
|
61
|
+
this.content = createOneBot12ContentPort(callApi);
|
|
62
|
+
this.bindClientEvents(
|
|
63
|
+
(receive) => forwardOnebot12ClientEvents(this.client, receive),
|
|
64
|
+
(name, payload) => {
|
|
65
|
+
if (name === 'event') this.#admitRaw(payload as OneBot12Event);
|
|
66
|
+
},
|
|
67
|
+
(_name, error) => this.#warnPlatformEvent(error),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async start(): Promise<void> {
|
|
72
|
+
if (this.#started) return;
|
|
73
|
+
this.#started = true;
|
|
74
|
+
if (!this.#options.config.access_token) {
|
|
75
|
+
// webhook 模式未配 access_token 时任何 POST 都会被放行(verifyOneBotAccessToken 直接 return true)
|
|
76
|
+
logger.warn(formatCompact({
|
|
77
|
+
endpoint: this.#options.config.id,
|
|
78
|
+
mode: 'webhook',
|
|
79
|
+
ok: false,
|
|
80
|
+
error: 'missing access_token',
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
this.#setupRoutes();
|
|
84
|
+
logger.info(formatCompact({
|
|
85
|
+
op: 'listen',
|
|
86
|
+
endpoint: this.#options.config.id,
|
|
87
|
+
mode: 'webhook',
|
|
88
|
+
path: this.#options.config.path,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async stop(): Promise<void> {
|
|
93
|
+
this.close();
|
|
94
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
95
|
+
this.#started = false;
|
|
96
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.id }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
100
|
+
const materialized = await uploadOneBot12MediaSegments(
|
|
101
|
+
payload,
|
|
102
|
+
(action, params) => callOnebot12Client(this.client, action, params),
|
|
103
|
+
(error) => {
|
|
104
|
+
logger.warn(formatCompact({
|
|
105
|
+
op: 'onebot12_upload_failed',
|
|
106
|
+
endpoint: this.#options.config.id,
|
|
107
|
+
error: error instanceof Error ? error.message : String(error),
|
|
108
|
+
}));
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
const message = formatOutboundSegments(materialized);
|
|
112
|
+
const params = buildSendMessageParams(conversation, message);
|
|
113
|
+
const data = await callOnebot12Client<{ message_id?: string }>(this.client, 'send_message', params);
|
|
114
|
+
const messageId = data?.message_id ?? '';
|
|
115
|
+
logger.debug(formatCompact({
|
|
116
|
+
op: 'onebot12_send',
|
|
117
|
+
endpoint: this.#options.config.id,
|
|
118
|
+
kind: conversation.kind,
|
|
119
|
+
conversationId: conversation.id,
|
|
120
|
+
messageId,
|
|
121
|
+
}));
|
|
122
|
+
return messageId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async recallMessage(messageId: string): Promise<void> {
|
|
126
|
+
if (!messageId) return;
|
|
127
|
+
await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async #callApi(
|
|
131
|
+
action: string,
|
|
132
|
+
params: Record<string, unknown> = {},
|
|
133
|
+
): Promise<import('@imhelper/onebot-v12').OneBotV12Response> {
|
|
134
|
+
const apiUrl = this.#options.config.api_url;
|
|
135
|
+
if (!apiUrl) {
|
|
136
|
+
throw new Error('OneBot12 connection:webhook requires api_url for outbound api');
|
|
137
|
+
}
|
|
138
|
+
return this.#callAction(
|
|
139
|
+
{ url: apiUrl, access_token: this.#options.config.access_token },
|
|
140
|
+
action,
|
|
141
|
+
params,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#admitRaw(ev: OneBot12Event): void {
|
|
146
|
+
if (!isMessageEvent(ev)) {
|
|
147
|
+
receiveOneBot12SideEvent(
|
|
148
|
+
(name, payload) => this.emit(name, payload),
|
|
149
|
+
this.#options.config.id,
|
|
150
|
+
{ callApi: (action, params) => callOnebot12Client(this.client, action, params) },
|
|
151
|
+
ev,
|
|
152
|
+
logger,
|
|
153
|
+
);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const conversation = onebot12InboundConversation(String(this.#options.id), ev);
|
|
157
|
+
const content = formatInboundContent(ev);
|
|
158
|
+
const nickname = senderNickname(ev);
|
|
159
|
+
const mentioned = isBotMentioned(ev);
|
|
160
|
+
void this.emit('message.receive', {
|
|
161
|
+
conversation,
|
|
162
|
+
message: { conversation, id: ev.message_id },
|
|
163
|
+
content,
|
|
164
|
+
sender: { id: senderUserId(ev), ...(nickname ? { name: nickname } : {}) },
|
|
165
|
+
endpointId: this.#options.config.id,
|
|
166
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
167
|
+
metadata: Object.freeze({
|
|
168
|
+
detail_type: ev.detail_type,
|
|
169
|
+
user_id: ev.user_id,
|
|
170
|
+
group_id: ev.group_id,
|
|
171
|
+
channel_id: ev.channel_id,
|
|
172
|
+
guild_id: ev.guild_id,
|
|
173
|
+
time: ev.time,
|
|
174
|
+
...(nickname ? { nickname } : {}),
|
|
175
|
+
}),
|
|
176
|
+
}).catch((err) => {
|
|
177
|
+
logger.warn(formatCompact({
|
|
178
|
+
op: 'onebot12_gateway_receive_failed',
|
|
179
|
+
kind: conversation.kind,
|
|
180
|
+
conversationId: conversation.id,
|
|
181
|
+
error: err instanceof Error ? err.message : String(err),
|
|
182
|
+
}));
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#warnPlatformEvent(error: unknown): void {
|
|
187
|
+
logger.warn(formatCompact({
|
|
188
|
+
op: 'onebot12_platform_event_failed',
|
|
189
|
+
error: error instanceof Error ? error.message : String(error),
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#setupRoutes(): void {
|
|
194
|
+
const path = this.#options.config.path;
|
|
195
|
+
this.#routeReleases.push(
|
|
196
|
+
this.#options.http.route('POST', path, async (request, response) => {
|
|
197
|
+
await this.#handleWebhook(request, response);
|
|
198
|
+
}, { summary: 'OneBot12 webhook callback', tags: ['onebot12'] }),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async #handleWebhook(request: IncomingMessage, response: ServerResponse): Promise<void> {
|
|
203
|
+
try {
|
|
204
|
+
if (!verifyOneBotAccessToken(this.#options.config.access_token, request)) {
|
|
205
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
206
|
+
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!this.clientEventsOpen) {
|
|
210
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
211
|
+
response.end(JSON.stringify({ status: 'ok' }));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
await this.client.acceptHttp(request, response);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger.error('OneBot12 webhook error:', error);
|
|
217
|
+
if (!response.headersSent) {
|
|
218
|
+
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
219
|
+
response.end(JSON.stringify({ message: 'Internal Server Error' }));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot12 WS client endpoint — outbound connect to OneBot implementation.
|
|
3
|
+
*/
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import { clearTimeout } from 'node:timers';
|
|
6
|
+
import {
|
|
7
|
+
ClientEndpoint,
|
|
8
|
+
createRecallEndpointControl,
|
|
9
|
+
createEndpointLifecycle,
|
|
10
|
+
type EndpointConnectHandle,
|
|
11
|
+
type EndpointControl,
|
|
12
|
+
type EndpointLifecycle,
|
|
13
|
+
type EndpointManagement,
|
|
14
|
+
type EndpointSendRequest,
|
|
15
|
+
} from 'zhin.js/adapter';
|
|
16
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
17
|
+
import type { CapabilityId } from 'zhin.js';
|
|
18
|
+
import { createOneBot12EndpointManagement } from './endpoint-management.js';
|
|
19
|
+
import {
|
|
20
|
+
buildSendMessageParams,
|
|
21
|
+
buildWsConnectOptions,
|
|
22
|
+
formatInboundContent,
|
|
23
|
+
formatOutboundSegments,
|
|
24
|
+
isBotMentioned,
|
|
25
|
+
isMessageEvent,
|
|
26
|
+
onebot12InboundConversation,
|
|
27
|
+
senderNickname,
|
|
28
|
+
senderUserId,
|
|
29
|
+
uploadOneBot12MediaSegments,
|
|
30
|
+
type OneBot12ActionRequest,
|
|
31
|
+
type OneBot12ActionResponse,
|
|
32
|
+
type OneBot12Event,
|
|
33
|
+
type OneBot12WsConfig,
|
|
34
|
+
} from './protocol.js';
|
|
35
|
+
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
36
|
+
import { createOneBot12ContentPort } from './content-port.js';
|
|
37
|
+
import { callOnebot12Client, createOnebot12EndpointClient, forwardOnebot12ClientEvents, type Onebot12Client } from './client.js';
|
|
38
|
+
import {
|
|
39
|
+
type OneBot12WsCreateOptions,
|
|
40
|
+
type OneBot12WsSocket,
|
|
41
|
+
WS_OPEN,
|
|
42
|
+
} from './ws-types.js';
|
|
43
|
+
|
|
44
|
+
export interface OneBot12WsEndpointOptions {
|
|
45
|
+
readonly id: CapabilityId;
|
|
46
|
+
readonly config: OneBot12WsConfig;
|
|
47
|
+
readonly createWebSocket?: (
|
|
48
|
+
url: string,
|
|
49
|
+
options: OneBot12WsCreateOptions,
|
|
50
|
+
) => OneBot12WsSocket;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class OneBot12WsEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
54
|
+
readonly client: Onebot12Client;
|
|
55
|
+
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
56
|
+
|
|
57
|
+
readonly #options: OneBot12WsEndpointOptions;
|
|
58
|
+
readonly management: EndpointManagement;
|
|
59
|
+
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
60
|
+
readonly content;
|
|
61
|
+
readonly #lifecycle: EndpointLifecycle;
|
|
62
|
+
#ws?: OneBot12WsSocket;
|
|
63
|
+
#requestId = 0;
|
|
64
|
+
#pending = new Map<string, {
|
|
65
|
+
resolve: (value: OneBot12ActionResponse) => void;
|
|
66
|
+
reject: (err: Error) => void;
|
|
67
|
+
timeout: NodeJS.Timeout;
|
|
68
|
+
}>();
|
|
69
|
+
|
|
70
|
+
constructor(options: OneBot12WsEndpointOptions) {
|
|
71
|
+
super();
|
|
72
|
+
this.#logger = getAdapterLogger('onebot12', options.config.id);
|
|
73
|
+
this.#options = options;
|
|
74
|
+
this.client = createOnebot12EndpointClient(options.config, (action, params) => this.#callAction(action, params ?? {}));
|
|
75
|
+
const callApi = (action: string, params?: Record<string, unknown>) => callOnebot12Client(this.client, action, params);
|
|
76
|
+
this.management = createOneBot12EndpointManagement({ callApi });
|
|
77
|
+
this.content = createOneBot12ContentPort(callApi);
|
|
78
|
+
this.bindClientEvents(
|
|
79
|
+
(receive) => forwardOnebot12ClientEvents(this.client, receive),
|
|
80
|
+
(name, payload) => {
|
|
81
|
+
if (name === 'event') this.#admitRaw(payload as OneBot12Event);
|
|
82
|
+
},
|
|
83
|
+
(_name, error) => this.#warnPlatformEvent(error),
|
|
84
|
+
);
|
|
85
|
+
const { config } = options;
|
|
86
|
+
this.#lifecycle = createEndpointLifecycle({
|
|
87
|
+
name: config.id,
|
|
88
|
+
reconnect: {
|
|
89
|
+
initialIntervalMs: config.reconnect_interval,
|
|
90
|
+
// 固定间隔(multiplier 1、无抖动),对齐旧 reconnect_interval 语义
|
|
91
|
+
multiplier: 1,
|
|
92
|
+
maxIntervalMs: config.reconnect_interval,
|
|
93
|
+
jitterMs: 0,
|
|
94
|
+
},
|
|
95
|
+
heartbeat: { intervalMs: config.heartbeat_interval },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async start(): Promise<void> {
|
|
100
|
+
if (this.#lifecycle.started) return;
|
|
101
|
+
try {
|
|
102
|
+
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// start 失败必须清理现场(状态复位由基座保证)
|
|
105
|
+
if (this.#ws) {
|
|
106
|
+
try {
|
|
107
|
+
this.#ws.close();
|
|
108
|
+
} catch {
|
|
109
|
+
/* ignore */
|
|
110
|
+
}
|
|
111
|
+
this.#ws = undefined;
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async stop(): Promise<void> {
|
|
118
|
+
this.close();
|
|
119
|
+
// 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
|
|
120
|
+
await this.#lifecycle.stop();
|
|
121
|
+
for (const [, pending] of this.#pending) {
|
|
122
|
+
clearTimeout(pending.timeout);
|
|
123
|
+
pending.reject(new Error('连接已关闭'));
|
|
124
|
+
}
|
|
125
|
+
this.#pending.clear();
|
|
126
|
+
this.#ws = undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
130
|
+
const materialized = await uploadOneBot12MediaSegments(
|
|
131
|
+
payload,
|
|
132
|
+
(action, params) => callOnebot12Client(this.client, action, params),
|
|
133
|
+
(error) => {
|
|
134
|
+
this.#logger.warn(formatCompact({
|
|
135
|
+
op: 'onebot12_upload_failed',
|
|
136
|
+
endpoint: this.#options.config.id,
|
|
137
|
+
error: error instanceof Error ? error.message : String(error),
|
|
138
|
+
}));
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
const message = formatOutboundSegments(materialized);
|
|
142
|
+
const params = buildSendMessageParams(conversation, message);
|
|
143
|
+
const data = await callOnebot12Client<{ message_id?: string }>(this.client, 'send_message', params);
|
|
144
|
+
const messageId = data?.message_id ?? '';
|
|
145
|
+
this.#logger.debug(formatCompact({
|
|
146
|
+
op: 'onebot12_send',
|
|
147
|
+
endpoint: this.#options.config.id,
|
|
148
|
+
kind: conversation.kind,
|
|
149
|
+
conversationId: conversation.id,
|
|
150
|
+
messageId,
|
|
151
|
+
}));
|
|
152
|
+
return messageId;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async recallMessage(messageId: string): Promise<void> {
|
|
156
|
+
if (!messageId) return;
|
|
157
|
+
await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#admitRaw(ev: OneBot12Event): void {
|
|
161
|
+
if (!isMessageEvent(ev)) {
|
|
162
|
+
receiveOneBot12SideEvent(
|
|
163
|
+
(name, payload) => this.emit(name, payload),
|
|
164
|
+
this.#options.config.id,
|
|
165
|
+
{ callApi: (action, params) => callOnebot12Client(this.client, action, params) },
|
|
166
|
+
ev,
|
|
167
|
+
this.#logger,
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const conversation = onebot12InboundConversation(String(this.#options.id), ev);
|
|
172
|
+
const content = formatInboundContent(ev);
|
|
173
|
+
const nickname = senderNickname(ev);
|
|
174
|
+
const mentioned = isBotMentioned(ev);
|
|
175
|
+
void this.emit('message.receive', {
|
|
176
|
+
conversation,
|
|
177
|
+
message: { conversation, id: ev.message_id },
|
|
178
|
+
content,
|
|
179
|
+
sender: { id: senderUserId(ev), name: senderNickname(ev) },
|
|
180
|
+
endpointId: this.#options.config.id,
|
|
181
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
182
|
+
metadata: Object.freeze({
|
|
183
|
+
detail_type: ev.detail_type,
|
|
184
|
+
user_id: ev.user_id,
|
|
185
|
+
group_id: ev.group_id,
|
|
186
|
+
channel_id: ev.channel_id,
|
|
187
|
+
guild_id: ev.guild_id,
|
|
188
|
+
time: ev.time,
|
|
189
|
+
...(nickname ? { nickname } : {}),
|
|
190
|
+
}),
|
|
191
|
+
}).catch((err) => {
|
|
192
|
+
this.#logger.warn(formatCompact({
|
|
193
|
+
op: 'onebot12_gateway_receive_failed',
|
|
194
|
+
kind: conversation.kind,
|
|
195
|
+
conversationId: conversation.id,
|
|
196
|
+
error: err instanceof Error ? err.message : String(err),
|
|
197
|
+
}));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#warnPlatformEvent(error: unknown): void {
|
|
202
|
+
this.#logger.warn(formatCompact({
|
|
203
|
+
op: 'onebot12_platform_event_failed',
|
|
204
|
+
error: error instanceof Error ? error.message : String(error),
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async #connect(handle: EndpointConnectHandle): Promise<void> {
|
|
209
|
+
const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
|
|
210
|
+
const create = this.#options.createWebSocket
|
|
211
|
+
?? ((connectUrl: string, options: OneBot12WsCreateOptions) =>
|
|
212
|
+
new WebSocket(connectUrl, { headers: options.headers }) as unknown as OneBot12WsSocket);
|
|
213
|
+
|
|
214
|
+
await new Promise<void>((resolve, reject) => {
|
|
215
|
+
let settled = false;
|
|
216
|
+
const ws = create(url, { headers });
|
|
217
|
+
this.#ws = ws;
|
|
218
|
+
handle.onForceClose(() => {
|
|
219
|
+
try {
|
|
220
|
+
ws.close();
|
|
221
|
+
} catch {
|
|
222
|
+
/* ignore */
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
ws.on('open', () => {
|
|
227
|
+
if (settled) return;
|
|
228
|
+
settled = true;
|
|
229
|
+
this.#logger.debug(formatCompact({
|
|
230
|
+
endpoint: this.#options.config.id,
|
|
231
|
+
mode: 'ws',
|
|
232
|
+
url: safeUrl,
|
|
233
|
+
}));
|
|
234
|
+
this.#lifecycle.startHeartbeat(() => {
|
|
235
|
+
this.#callAction('get_status', {}).catch(() => {});
|
|
236
|
+
});
|
|
237
|
+
resolve();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
ws.on('message', (data) => {
|
|
241
|
+
this.#onMessage(data);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
ws.on('close', (code, reason) => {
|
|
245
|
+
const reasonStr = typeof reason === 'string'
|
|
246
|
+
? reason
|
|
247
|
+
: Buffer.isBuffer(reason)
|
|
248
|
+
? reason.toString()
|
|
249
|
+
: String(reason ?? '');
|
|
250
|
+
const codeNum = typeof code === 'number' ? code : Number(code ?? 0);
|
|
251
|
+
if (!settled) {
|
|
252
|
+
settled = true;
|
|
253
|
+
reject(new Error(`OneBot12 WS 关闭: ${codeNum} ${reasonStr}`));
|
|
254
|
+
}
|
|
255
|
+
// 断开日志与重连武装均由基座负责;仅曾 open 的连接才会武装重连,
|
|
256
|
+
// 初始连接失败由 start() 的拒绝路径复位,不产生僵尸重连。
|
|
257
|
+
handle.notifyClosed(`OneBot12 WS 关闭: ${codeNum} ${reasonStr || 'closed'}`);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
ws.on('error', (err) => {
|
|
261
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
262
|
+
this.#logger.warn(formatCompact({
|
|
263
|
+
op: 'ws_error',
|
|
264
|
+
endpoint: this.#options.config.id,
|
|
265
|
+
ok: false,
|
|
266
|
+
error: error.message,
|
|
267
|
+
}));
|
|
268
|
+
if (!settled) {
|
|
269
|
+
settled = true;
|
|
270
|
+
reject(error);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#onMessage(data: unknown): void {
|
|
277
|
+
try {
|
|
278
|
+
const raw = typeof data === 'string'
|
|
279
|
+
? data
|
|
280
|
+
: Buffer.isBuffer(data)
|
|
281
|
+
? data.toString()
|
|
282
|
+
: data instanceof ArrayBuffer
|
|
283
|
+
? new TextDecoder().decode(data)
|
|
284
|
+
: String(data ?? '');
|
|
285
|
+
const msg = JSON.parse(raw) as OneBot12Event | OneBot12ActionResponse;
|
|
286
|
+
if ('echo' in msg && typeof (msg as OneBot12ActionResponse).echo === 'string') {
|
|
287
|
+
const resp = msg as OneBot12ActionResponse;
|
|
288
|
+
const pending = this.#pending.get(resp.echo!);
|
|
289
|
+
if (pending) {
|
|
290
|
+
this.#pending.delete(resp.echo!);
|
|
291
|
+
clearTimeout(pending.timeout);
|
|
292
|
+
pending.resolve(resp);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
this.client.ingest(msg as Parameters<Onebot12Client['ingest']>[0]);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
this.#logger.warn(formatCompact({
|
|
299
|
+
op: 'onebot12_parse_failed',
|
|
300
|
+
endpoint: this.#options.config.id,
|
|
301
|
+
error: error instanceof Error ? error.message : String(error),
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#callAction(action: string, params: Record<string, unknown>): Promise<OneBot12ActionResponse> {
|
|
307
|
+
if (!this.#ws || this.#ws.readyState !== WS_OPEN) {
|
|
308
|
+
return Promise.reject(new Error('WebSocket 未连接'));
|
|
309
|
+
}
|
|
310
|
+
const echo = `ob12_${++this.#requestId}`;
|
|
311
|
+
const req: OneBot12ActionRequest = { action, params, echo };
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const timeout = setTimeout(() => {
|
|
314
|
+
this.#pending.delete(echo);
|
|
315
|
+
reject(new Error(`OneBot12 动作超时: ${action}`));
|
|
316
|
+
}, 30_000);
|
|
317
|
+
this.#pending.set(echo, { resolve, reject, timeout });
|
|
318
|
+
this.#ws!.send(JSON.stringify(req));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
package/src/ws-types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Minimal WS surface used by the endpoint (real `ws` or test mock). */
|
|
2
|
+
export interface OneBot12WsSocket {
|
|
3
|
+
readonly readyState: number;
|
|
4
|
+
send(data: string): void;
|
|
5
|
+
close(code?: number, reason?: string): void;
|
|
6
|
+
on(event: 'open' | 'message' | 'close' | 'error', listener: (...args: unknown[]) => void): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface OneBot12WsCreateOptions {
|
|
10
|
+
readonly headers?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const WS_OPEN = 1;
|
|
14
|
+
|
|
15
|
+
export interface OneBot12PendingAction {
|
|
16
|
+
resolve: (value: unknown) => void;
|
|
17
|
+
reject: (err: Error) => void;
|
|
18
|
+
timeout: NodeJS.Timeout;
|
|
19
|
+
}
|
package/src/wss-auth.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
|
|
3
|
+
export function verifyOneBotAccessToken(
|
|
4
|
+
accessToken: string | undefined,
|
|
5
|
+
request: IncomingMessage,
|
|
6
|
+
): boolean {
|
|
7
|
+
if (!accessToken) return true;
|
|
8
|
+
const auth = request.headers.authorization ?? '';
|
|
9
|
+
if (auth === `Bearer ${accessToken}`) return true;
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
12
|
+
if (url.searchParams.get('access_token') === accessToken) return true;
|
|
13
|
+
} catch {
|
|
14
|
+
/* ignore */
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|