@zhin.js/adapter-onebot12 1.1.18 → 1.1.19
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 +36 -0
- package/README.md +24 -5
- package/adapters/onebot12/index.js +3 -32
- package/adapters/onebot12/index.ts +2 -38
- package/commands/onebot12/endpoint/definition.js +2 -2
- package/commands/onebot12/endpoint/definition.ts +2 -2
- package/lib/adapter.d.ts +21 -0
- package/lib/adapter.js +34 -0
- package/lib/client.d.ts +9 -11
- package/lib/client.js +23 -13
- package/lib/console-spec.js +2 -0
- package/lib/console.d.ts +1 -1
- package/lib/console.js +2 -5
- package/lib/content-port.d.ts +2 -2
- package/lib/content-port.js +1 -1
- package/lib/endpoint-management.d.ts +5 -5
- package/lib/endpoint-management.js +17 -17
- package/lib/identity.d.ts +4 -0
- package/lib/identity.js +53 -0
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -1
- package/lib/protocol.d.ts +32 -4
- package/lib/protocol.js +54 -53
- package/lib/response.d.ts +5 -0
- package/lib/response.js +17 -0
- package/lib/side-event-dispatch.d.ts +8 -1
- package/lib/side-event-dispatch.js +2 -1
- package/lib/webhook.d.ts +5 -11
- package/lib/webhook.js +72 -37
- package/lib/ws-endpoint.d.ts +5 -11
- package/lib/ws-endpoint.js +79 -45
- package/lib/ws-transport.d.ts +18 -10
- package/lib/ws-transport.js +26 -45
- package/lib/ws-types.d.ts +0 -6
- package/lib/wss-endpoint.d.ts +5 -11
- package/lib/wss-endpoint.js +82 -48
- package/package.json +15 -15
- package/plugin.js +0 -4
- package/schema.json +15 -3
- package/src/adapter.ts +36 -0
- package/src/client.ts +26 -29
- package/src/console-spec.ts +2 -0
- package/src/console.ts +2 -5
- package/src/content-port.ts +3 -3
- package/src/endpoint-management.ts +25 -24
- package/src/identity.ts +52 -0
- package/src/index.ts +4 -1
- package/src/protocol.ts +71 -62
- package/src/response.ts +19 -0
- package/src/side-event-dispatch.ts +9 -7
- package/src/webhook.ts +70 -55
- package/src/ws-endpoint.ts +75 -63
- package/src/ws-transport.ts +37 -73
- package/src/ws-types.ts +0 -7
- package/src/wss-endpoint.ts +80 -66
- package/lib/onebot12-runtime-state.d.ts +0 -1
- package/lib/onebot12-runtime-state.js +0 -6
- package/src/onebot12-runtime-state.ts +0 -7
package/src/response.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
2
|
+
import type { OneBotV12Response } from '@imhelper/onebot-v12';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One classifier for native SDK, HTTP and WS results.
|
|
6
|
+
* @see https://12.onebot.dev/connect/data-protocol/action-response/
|
|
7
|
+
*/
|
|
8
|
+
export function readOnebot12Response<T = unknown>(response: unknown): T | undefined {
|
|
9
|
+
if (response && typeof response === 'object') {
|
|
10
|
+
const result = response as Partial<OneBotV12Response>;
|
|
11
|
+
if (result.status === 'ok' && result.retcode === 0) return result.data as T | undefined;
|
|
12
|
+
if (result.status === 'failed' && typeof result.retcode === 'number' && Number.isSafeInteger(result.retcode) && result.retcode >= 10000 && result.retcode <= 99999) {
|
|
13
|
+
throw new EndpointDeliveryError('platform_rejected',
|
|
14
|
+
`OneBot 12 API rejected: retcode=${result.retcode}${result.message ? ` ${result.message}` : ''}`, 'rejected');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// OB12 defines only ok/0 or failed with an error code; inconsistent envelopes prove neither result.
|
|
18
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', 'OneBot 12 response did not confirm the action result', 'unknown');
|
|
19
|
+
}
|
|
@@ -7,13 +7,15 @@ export interface OneBot12SideEventCaller {
|
|
|
7
7
|
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
logger: ReturnType<typeof getAdapterLogger
|
|
16
|
-
|
|
10
|
+
/** Exact instance-owned dependencies for one canonical side-event projection. */
|
|
11
|
+
export interface OneBot12SideEventContext {
|
|
12
|
+
readonly emit: EndpointEventEmitter;
|
|
13
|
+
readonly endpointKey: string;
|
|
14
|
+
readonly caller: OneBot12SideEventCaller;
|
|
15
|
+
readonly logger: ReturnType<typeof getAdapterLogger>;
|
|
16
|
+
}
|
|
17
|
+
export function receiveOneBot12SideEvent(raw: OneBot12Event, context: OneBot12SideEventContext): void {
|
|
18
|
+
const { emit, endpointKey, caller, logger } = context;
|
|
17
19
|
if (!emit) return;
|
|
18
20
|
const record = raw as Record<string, unknown>;
|
|
19
21
|
const eventType = String(record.type ?? record.post_type ?? '');
|
package/src/webhook.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { acceptHttpIngress } from 'imhelper';
|
|
1
2
|
/**
|
|
2
3
|
* OneBot12 HTTP webhook endpoint — POST inbound + api_url outbound.
|
|
3
4
|
*/
|
|
4
5
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
+
createEndpointSpecification, endpointSpec,
|
|
8
|
+
type Endpoint, type EndpointSpecification,
|
|
7
9
|
createRecallEndpointControl,
|
|
8
10
|
type EndpointControl,
|
|
9
11
|
type EndpointManagement,
|
|
10
12
|
type EndpointSendRequest,
|
|
13
|
+
type PendingRpcRequest,
|
|
11
14
|
} from 'zhin.js/adapter';
|
|
12
15
|
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
13
16
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
@@ -15,6 +18,7 @@ import type { CapabilityId } from 'zhin.js';
|
|
|
15
18
|
import { createOneBot12EndpointManagement } from './endpoint-management.js';
|
|
16
19
|
import {
|
|
17
20
|
buildSendMessageParams,
|
|
21
|
+
matchesOneBot12Self,
|
|
18
22
|
callOneBot12Action,
|
|
19
23
|
formatInboundContent,
|
|
20
24
|
formatOutboundSegments,
|
|
@@ -29,46 +33,55 @@ import {
|
|
|
29
33
|
} from './protocol.js';
|
|
30
34
|
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
31
35
|
import { createOneBot12ContentPort } from './content-port.js';
|
|
32
|
-
import { callOnebot12Client,
|
|
36
|
+
import { callOnebot12Client, readOnebot12Response, onebot12ClientOptions, forwardOnebot12ClientEvents, Onebot12Client } from './client.js';
|
|
33
37
|
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
34
38
|
|
|
35
39
|
const logger = getLogger('onebot12');
|
|
36
40
|
|
|
37
41
|
export interface OneBot12WebhookEndpointOptions {
|
|
42
|
+
readonly signal?: AbortSignal;
|
|
38
43
|
readonly id: CapabilityId;
|
|
39
44
|
readonly http: HttpHost;
|
|
40
45
|
readonly config: OneBot12WebhookConfig;
|
|
41
46
|
readonly callAction?: typeof callOneBot12Action;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
export class OneBot12WebhookEndpoint extends
|
|
45
|
-
readonly
|
|
49
|
+
export class OneBot12WebhookEndpoint extends Onebot12Client implements Endpoint {
|
|
50
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
51
|
+
#open = false;
|
|
46
52
|
readonly #options: OneBot12WebhookEndpointOptions;
|
|
47
|
-
readonly management: EndpointManagement;
|
|
48
|
-
readonly control: EndpointControl = createRecallEndpointControl((id) => this
|
|
49
|
-
readonly content;
|
|
53
|
+
readonly #management: EndpointManagement;
|
|
54
|
+
readonly #control: EndpointControl = createRecallEndpointControl((id, signal?: AbortSignal) => this.#recallMessage(id, signal));
|
|
55
|
+
readonly #content;
|
|
56
|
+
readonly #releaseClientEvents: () => void;
|
|
50
57
|
readonly #callAction: typeof callOneBot12Action;
|
|
51
58
|
#routeReleases: HttpRouteRegistration[] = [];
|
|
52
59
|
#started = false;
|
|
53
60
|
|
|
54
61
|
constructor(options: OneBot12WebhookEndpointOptions) {
|
|
55
|
-
super();
|
|
62
|
+
super(onebot12ClientOptions(options.config, (action, params) => this.#callApi({ action, params })));
|
|
56
63
|
this.#options = options;
|
|
57
64
|
this.#callAction = options.callAction ?? callOneBot12Action;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this
|
|
61
|
-
this
|
|
62
|
-
|
|
63
|
-
(
|
|
64
|
-
(
|
|
65
|
+
const callApi = (request: PendingRpcRequest) => this.#callApi(request).then(readOnebot12Response);
|
|
66
|
+
this.#management = createOneBot12EndpointManagement({ callApi });
|
|
67
|
+
this.#content = createOneBot12ContentPort(callApi);
|
|
68
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
69
|
+
name: options.config.id,
|
|
70
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
71
|
+
close: () => { this.#open = false; }, stop: () => this.#stop(),
|
|
72
|
+
send: request => this.#send(request),
|
|
73
|
+
management: this.#management, control: this.#control, content: this.#content,
|
|
74
|
+
});
|
|
75
|
+
this.#releaseClientEvents = forwardOnebot12ClientEvents(this, (name, payload) => {
|
|
76
|
+
void this[endpointSpec].events.run(() => {
|
|
77
|
+
if (!this.#started) return;
|
|
78
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
65
79
|
if (name === 'event') this.#admitRaw(payload as OneBot12Event);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
);
|
|
80
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
81
|
+
});
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
async start(): Promise<void> {
|
|
84
|
+
async #start(): Promise<void> {
|
|
72
85
|
if (this.#started) return;
|
|
73
86
|
this.#started = true;
|
|
74
87
|
if (!this.#options.config.access_token) {
|
|
@@ -89,28 +102,26 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
89
102
|
}));
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
async stop(): Promise<void> {
|
|
93
|
-
this
|
|
105
|
+
async #stop(): Promise<void> {
|
|
106
|
+
this.#open = false;
|
|
94
107
|
for (const release of this.#routeReleases.splice(0)) release();
|
|
95
108
|
this.#started = false;
|
|
109
|
+
this.#releaseClientEvents();
|
|
96
110
|
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.id }));
|
|
97
111
|
}
|
|
98
112
|
|
|
99
|
-
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
100
|
-
const materialized = await uploadOneBot12MediaSegments(
|
|
101
|
-
payload,
|
|
102
|
-
(action, params) =>
|
|
103
|
-
|
|
104
|
-
logger.warn(formatCompact({
|
|
105
|
-
|
|
106
|
-
endpoint: this.#options.config.id,
|
|
107
|
-
error: error instanceof Error ? error.message : String(error),
|
|
108
|
-
}));
|
|
113
|
+
async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
114
|
+
const materialized = await uploadOneBot12MediaSegments({
|
|
115
|
+
payload, signal,
|
|
116
|
+
callAction: (action, params) => this.#callApi({ action, params, signal }).then(readOnebot12Response),
|
|
117
|
+
onUploadFailed: error => {
|
|
118
|
+
logger.warn(formatCompact({ op: 'onebot12_upload_failed', endpoint: this.#options.config.id,
|
|
119
|
+
error: error instanceof Error ? error.message : String(error) }));
|
|
109
120
|
},
|
|
110
|
-
);
|
|
121
|
+
});
|
|
111
122
|
const message = formatOutboundSegments(materialized);
|
|
112
123
|
const params = buildSendMessageParams(conversation, message);
|
|
113
|
-
const data =
|
|
124
|
+
const data = readOnebot12Response<{ message_id?: string }>(await this.#callApi({ action: 'send_message', params, signal }));
|
|
114
125
|
const messageId = data?.message_id ?? '';
|
|
115
126
|
logger.debug(formatCompact({
|
|
116
127
|
op: 'onebot12_send',
|
|
@@ -122,38 +133,38 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
122
133
|
return messageId;
|
|
123
134
|
}
|
|
124
135
|
|
|
125
|
-
async recallMessage(messageId: string): Promise<void> {
|
|
136
|
+
async #recallMessage(messageId: string, signal?: AbortSignal): Promise<void> {
|
|
126
137
|
if (!messageId) return;
|
|
127
|
-
await
|
|
138
|
+
await this.#callApi({ action: 'delete_message', params: { message_id: messageId }, signal }).then(readOnebot12Response);
|
|
128
139
|
}
|
|
129
140
|
|
|
130
|
-
async #callApi(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
action,
|
|
137
|
-
params,
|
|
138
|
-
);
|
|
141
|
+
async #callApi(request: PendingRpcRequest): Promise<import('@imhelper/onebot-v12').OneBotV12Response> {
|
|
142
|
+
const epoch = this.#options.signal;
|
|
143
|
+
const signal = epoch && request.signal ? AbortSignal.any([epoch, request.signal]) : epoch ?? request.signal;
|
|
144
|
+
return this.#callAction({ url: this.#options.config.api_url, access_token: this.#options.config.access_token,
|
|
145
|
+
self: { platform: this.#options.config.platform, user_id: this.#options.config.userId },
|
|
146
|
+
action: request.action, params: request.params, signal });
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
#admitRaw(ev: OneBot12Event): void {
|
|
150
|
+
if (!matchesOneBot12Self(this.#options.config, ev)) return;
|
|
151
|
+
this[endpointSpec].reportSelfUserId(ev.self.user_id);
|
|
142
152
|
if (!isMessageEvent(ev)) {
|
|
143
|
-
receiveOneBot12SideEvent(
|
|
144
|
-
(name, payload) => this.emit(name, payload),
|
|
145
|
-
this.#options.config.id,
|
|
146
|
-
{ callApi: (action, params) => callOnebot12Client(this
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
);
|
|
153
|
+
receiveOneBot12SideEvent(ev, {
|
|
154
|
+
emit: (name, payload) => this[endpointSpec].events.emit(name, payload),
|
|
155
|
+
endpointKey: this.#options.config.id,
|
|
156
|
+
caller: { callApi: (action, params) => callOnebot12Client({ client: this, action, params }) },
|
|
157
|
+
logger: logger,
|
|
158
|
+
});
|
|
150
159
|
return;
|
|
151
160
|
}
|
|
152
|
-
const conversation = onebot12InboundConversation(String(this.#options.id), ev);
|
|
161
|
+
const conversation = { ...onebot12InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
153
162
|
const content = formatInboundContent(ev);
|
|
154
163
|
const nickname = senderNickname(ev);
|
|
155
164
|
const mentioned = isBotMentioned(ev);
|
|
156
|
-
void this.
|
|
165
|
+
void this[endpointSpec].events.message({
|
|
166
|
+
event: ev,
|
|
167
|
+
...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
|
|
157
168
|
conversation,
|
|
158
169
|
message: { conversation, id: ev.message_id },
|
|
159
170
|
content,
|
|
@@ -202,12 +213,16 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
202
213
|
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
203
214
|
return;
|
|
204
215
|
}
|
|
205
|
-
if (!this
|
|
216
|
+
if (!this.#open) {
|
|
206
217
|
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
207
218
|
response.end(JSON.stringify({ status: 'ok' }));
|
|
208
219
|
return;
|
|
209
220
|
}
|
|
210
|
-
await
|
|
221
|
+
await acceptHttpIngress<OneBot12Event>(request, response, event => {
|
|
222
|
+
if (!matchesOneBot12Self(this.#options.config, event)) return;
|
|
223
|
+
void this[endpointSpec].events.run(() => this.ingest(event as Parameters<Onebot12Client['ingest']>[0]))
|
|
224
|
+
.catch(error => this.#warnPlatformEvent(error));
|
|
225
|
+
});
|
|
211
226
|
} catch (error) {
|
|
212
227
|
logger.error('OneBot12 webhook error:', error);
|
|
213
228
|
if (!response.headersSent) {
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import WebSocket from 'ws';
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
createEndpointSpecification, endpointSpec,
|
|
7
|
+
type Endpoint, type EndpointSpecification,
|
|
7
8
|
createRecallEndpointControl,
|
|
8
9
|
createEndpointLifecycle,
|
|
9
10
|
type EndpointConnectHandle,
|
|
@@ -11,12 +12,14 @@ import {
|
|
|
11
12
|
type EndpointLifecycle,
|
|
12
13
|
type EndpointManagement,
|
|
13
14
|
type EndpointSendRequest,
|
|
15
|
+
type PendingRpcRequest,
|
|
14
16
|
} from 'zhin.js/adapter';
|
|
15
17
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
16
18
|
import type { CapabilityId } from 'zhin.js';
|
|
17
19
|
import { createOneBot12EndpointManagement } from './endpoint-management.js';
|
|
18
20
|
import {
|
|
19
21
|
buildSendMessageParams,
|
|
22
|
+
matchesOneBot12Self,
|
|
20
23
|
buildWsConnectOptions,
|
|
21
24
|
formatInboundContent,
|
|
22
25
|
formatOutboundSegments,
|
|
@@ -31,19 +34,15 @@ import {
|
|
|
31
34
|
} from './protocol.js';
|
|
32
35
|
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
33
36
|
import { createOneBot12ContentPort } from './content-port.js';
|
|
34
|
-
import { callOnebot12Client,
|
|
37
|
+
import { callOnebot12Client, readOnebot12Response, onebot12ClientOptions, forwardOnebot12ClientEvents, Onebot12Client } from './client.js';
|
|
35
38
|
import {
|
|
36
|
-
type OneBot12PendingAction,
|
|
37
39
|
type OneBot12WsCreateOptions,
|
|
38
40
|
type OneBot12WsSocket,
|
|
39
41
|
} from './ws-types.js';
|
|
40
|
-
import {
|
|
41
|
-
callOneBot12WsAction,
|
|
42
|
-
handleOneBot12WsMessage,
|
|
43
|
-
rejectAllPending,
|
|
44
|
-
} from './ws-transport.js';
|
|
42
|
+
import { createOneBot12Rpc, type OneBot12Rpc } from './ws-transport.js';
|
|
45
43
|
|
|
46
44
|
export interface OneBot12WsEndpointOptions {
|
|
45
|
+
readonly signal?: AbortSignal;
|
|
47
46
|
readonly id: CapabilityId;
|
|
48
47
|
readonly config: OneBot12WsConfig;
|
|
49
48
|
readonly createWebSocket?: (
|
|
@@ -52,34 +51,48 @@ export interface OneBot12WsEndpointOptions {
|
|
|
52
51
|
) => OneBot12WsSocket;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
export class OneBot12WsEndpoint extends
|
|
56
|
-
readonly
|
|
54
|
+
export class OneBot12WsEndpoint extends Onebot12Client implements Endpoint {
|
|
55
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
56
|
+
#open = false;
|
|
57
57
|
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
58
58
|
|
|
59
59
|
readonly #options: OneBot12WsEndpointOptions;
|
|
60
|
-
readonly management: EndpointManagement;
|
|
61
|
-
readonly control: EndpointControl = createRecallEndpointControl((id) => this
|
|
62
|
-
readonly content;
|
|
60
|
+
readonly #management: EndpointManagement;
|
|
61
|
+
readonly #control: EndpointControl = createRecallEndpointControl((id, signal?: AbortSignal) => this.#recallMessage(id, signal));
|
|
62
|
+
readonly #content;
|
|
63
|
+
readonly #releaseClientEvents: () => void;
|
|
63
64
|
readonly #lifecycle: EndpointLifecycle;
|
|
64
65
|
#ws?: OneBot12WsSocket;
|
|
65
|
-
#
|
|
66
|
-
#pending = new Map<string, OneBot12PendingAction>();
|
|
66
|
+
readonly #rpc: OneBot12Rpc;
|
|
67
67
|
|
|
68
68
|
constructor(options: OneBot12WsEndpointOptions) {
|
|
69
|
-
|
|
69
|
+
const rpc = createOneBot12Rpc({ endpointId: options.config.id, signal: options.signal,
|
|
70
|
+
self: { platform: options.config.platform, user_id: options.config.userId },
|
|
71
|
+
socket: () => this.#lifecycle.state === 'open' ? this.#ws : undefined,
|
|
72
|
+
});
|
|
73
|
+
super(onebot12ClientOptions(options.config, (action, params) => rpc.call({ action, params })));
|
|
74
|
+
this.#rpc = rpc;
|
|
70
75
|
this.#logger = getAdapterLogger('onebot12', options.config.id);
|
|
71
76
|
this.#options = options;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this
|
|
75
|
-
|
|
76
|
-
this
|
|
77
|
-
|
|
78
|
-
(
|
|
77
|
+
const callApi = (request: PendingRpcRequest) => this.#rpc.call(request).then(readOnebot12Response);
|
|
78
|
+
this.#management = createOneBot12EndpointManagement({ callApi });
|
|
79
|
+
this.#content = createOneBot12ContentPort(callApi);
|
|
80
|
+
const readTransportState = () => this.#lifecycle.state;
|
|
81
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
82
|
+
name: options.config.id,
|
|
83
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
84
|
+
close: () => { this.#open = false; }, stop: () => this.#stop(),
|
|
85
|
+
send: request => this.#send(request),
|
|
86
|
+
get transportState() { return readTransportState(); },
|
|
87
|
+
management: this.#management, control: this.#control, content: this.#content,
|
|
88
|
+
});
|
|
89
|
+
this.#releaseClientEvents = forwardOnebot12ClientEvents(this, (name, payload) => {
|
|
90
|
+
void this[endpointSpec].events.run(() => {
|
|
91
|
+
if (!this.#open && !this.#lifecycle.started) return;
|
|
92
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
79
93
|
if (name === 'event') this.#admitRaw(payload as OneBot12Event);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
);
|
|
94
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
95
|
+
});
|
|
83
96
|
const { config } = options;
|
|
84
97
|
this.#lifecycle = createEndpointLifecycle({
|
|
85
98
|
name: config.id,
|
|
@@ -94,7 +107,7 @@ export class OneBot12WsEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
94
107
|
});
|
|
95
108
|
}
|
|
96
109
|
|
|
97
|
-
async start(): Promise<void> {
|
|
110
|
+
async #start(): Promise<void> {
|
|
98
111
|
if (this.#lifecycle.started) return;
|
|
99
112
|
try {
|
|
100
113
|
await this.#lifecycle.start((handle) => this.#connect(handle));
|
|
@@ -112,29 +125,29 @@ export class OneBot12WsEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
112
125
|
}
|
|
113
126
|
}
|
|
114
127
|
|
|
115
|
-
async stop(): Promise<void> {
|
|
116
|
-
this
|
|
128
|
+
async #stop(): Promise<void> {
|
|
129
|
+
this.#open = false;
|
|
117
130
|
// 基座负责:清重连/心跳定时器、强关 ws、唤醒 stop-during-connect 竞态
|
|
118
|
-
await this.#lifecycle.stop();
|
|
119
|
-
|
|
120
|
-
|
|
131
|
+
try { await this.#lifecycle.stop(); }
|
|
132
|
+
finally {
|
|
133
|
+
this.#rpc.dispose();
|
|
134
|
+
this.#releaseClientEvents();
|
|
135
|
+
this.#ws = undefined;
|
|
136
|
+
}
|
|
121
137
|
}
|
|
122
138
|
|
|
123
|
-
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
124
|
-
const materialized = await uploadOneBot12MediaSegments(
|
|
125
|
-
payload,
|
|
126
|
-
(action, params) =>
|
|
127
|
-
|
|
128
|
-
this.#logger.warn(formatCompact({
|
|
129
|
-
|
|
130
|
-
endpoint: this.#options.config.id,
|
|
131
|
-
error: error instanceof Error ? error.message : String(error),
|
|
132
|
-
}));
|
|
139
|
+
async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
|
|
140
|
+
const materialized = await uploadOneBot12MediaSegments({
|
|
141
|
+
payload, signal,
|
|
142
|
+
callAction: (action, params) => this.#rpc.call({ action, params, signal }).then(readOnebot12Response),
|
|
143
|
+
onUploadFailed: error => {
|
|
144
|
+
this.#logger.warn(formatCompact({ op: 'onebot12_upload_failed', endpoint: this.#options.config.id,
|
|
145
|
+
error: error instanceof Error ? error.message : String(error) }));
|
|
133
146
|
},
|
|
134
|
-
);
|
|
147
|
+
});
|
|
135
148
|
const message = formatOutboundSegments(materialized);
|
|
136
149
|
const params = buildSendMessageParams(conversation, message);
|
|
137
|
-
const data =
|
|
150
|
+
const data = readOnebot12Response<{ message_id?: string }>(await this.#rpc.call({ action: 'send_message', params, signal }));
|
|
138
151
|
const messageId = data?.message_id ?? '';
|
|
139
152
|
this.#logger.debug(formatCompact({
|
|
140
153
|
op: 'onebot12_send',
|
|
@@ -146,27 +159,30 @@ export class OneBot12WsEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
146
159
|
return messageId;
|
|
147
160
|
}
|
|
148
161
|
|
|
149
|
-
async recallMessage(messageId: string): Promise<void> {
|
|
162
|
+
async #recallMessage(messageId: string, signal?: AbortSignal): Promise<void> {
|
|
150
163
|
if (!messageId) return;
|
|
151
|
-
await
|
|
164
|
+
await this.#rpc.call({ action: 'delete_message', params: { message_id: messageId }, signal }).then(readOnebot12Response);
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
#admitRaw(ev: OneBot12Event): void {
|
|
168
|
+
if (!matchesOneBot12Self(this.#options.config, ev)) return;
|
|
169
|
+
this[endpointSpec].reportSelfUserId(ev.self.user_id);
|
|
155
170
|
if (!isMessageEvent(ev)) {
|
|
156
|
-
receiveOneBot12SideEvent(
|
|
157
|
-
(name, payload) => this.emit(name, payload),
|
|
158
|
-
this.#options.config.id,
|
|
159
|
-
{ callApi: (action, params) => callOnebot12Client(this
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
);
|
|
171
|
+
receiveOneBot12SideEvent(ev, {
|
|
172
|
+
emit: (name, payload) => this[endpointSpec].events.emit(name, payload),
|
|
173
|
+
endpointKey: this.#options.config.id,
|
|
174
|
+
caller: { callApi: (action, params) => callOnebot12Client({ client: this, action, params }) },
|
|
175
|
+
logger: this.#logger,
|
|
176
|
+
});
|
|
163
177
|
return;
|
|
164
178
|
}
|
|
165
|
-
const conversation = onebot12InboundConversation(String(this.#options.id), ev);
|
|
179
|
+
const conversation = { ...onebot12InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
166
180
|
const content = formatInboundContent(ev);
|
|
167
181
|
const nickname = senderNickname(ev);
|
|
168
182
|
const mentioned = isBotMentioned(ev);
|
|
169
|
-
void this.
|
|
183
|
+
void this[endpointSpec].events.message({
|
|
184
|
+
event: ev,
|
|
185
|
+
...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
|
|
170
186
|
conversation,
|
|
171
187
|
message: { conversation, id: ev.message_id },
|
|
172
188
|
content,
|
|
@@ -234,12 +250,8 @@ export class OneBot12WsEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
234
250
|
ws.on('message', (data) => {
|
|
235
251
|
if (this.#ws !== ws) return;
|
|
236
252
|
this.#lifecycle.notifyHeartbeatAck();
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
pending: this.#pending,
|
|
240
|
-
ingest: (event) => this.client.ingest(
|
|
241
|
-
event as Parameters<Onebot12Client['ingest']>[0],
|
|
242
|
-
),
|
|
253
|
+
this.#rpc.receive(data, event => {
|
|
254
|
+
if (matchesOneBot12Self(this.#options.config, event)) this.ingest(event as Parameters<Onebot12Client['ingest']>[0]);
|
|
243
255
|
});
|
|
244
256
|
});
|
|
245
257
|
|
|
@@ -256,7 +268,7 @@ export class OneBot12WsEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
256
268
|
}
|
|
257
269
|
if (this.#ws === ws) {
|
|
258
270
|
this.#ws = undefined;
|
|
259
|
-
|
|
271
|
+
this.#rpc.disconnect();
|
|
260
272
|
}
|
|
261
273
|
// 断开日志与重连武装均由基座负责;仅曾 open 的连接才会武装重连,
|
|
262
274
|
// 初始连接失败由 start() 的拒绝路径复位,不产生僵尸重连。
|
|
@@ -280,6 +292,6 @@ export class OneBot12WsEndpoint extends ClientEndpoint<Onebot12Client> {
|
|
|
280
292
|
}
|
|
281
293
|
|
|
282
294
|
#callAction(action: string, params: Record<string, unknown>) {
|
|
283
|
-
return
|
|
295
|
+
return this.#rpc.call({ action, params });
|
|
284
296
|
}
|
|
285
297
|
}
|
package/src/ws-transport.ts
CHANGED
|
@@ -1,86 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createPendingRpc, type PendingRpc, type PendingRpcRequest } from 'zhin.js/adapter';
|
|
2
2
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
OneBot12ActionResponse,
|
|
6
|
-
OneBot12Event,
|
|
7
|
-
} from './protocol.js';
|
|
8
|
-
import {
|
|
9
|
-
type OneBot12PendingAction,
|
|
10
|
-
type OneBot12WsSocket,
|
|
11
|
-
WS_OPEN,
|
|
12
|
-
} from './ws-types.js';
|
|
3
|
+
import type { OneBot12ActionResponse, OneBot12Event, OneBot12Self } from './protocol.js';
|
|
4
|
+
import { WS_OPEN, type OneBot12WsSocket } from './ws-types.js';
|
|
13
5
|
|
|
14
6
|
const logger = getLogger('onebot12');
|
|
15
|
-
|
|
16
|
-
export interface OneBot12WsMessageOptions {
|
|
7
|
+
export interface OneBot12RpcOptions {
|
|
17
8
|
readonly endpointId: string;
|
|
18
|
-
readonly
|
|
19
|
-
|
|
9
|
+
readonly signal?: AbortSignal;
|
|
10
|
+
/** Reads the lifecycle's selected socket; RPC never opens or reconnects it. */
|
|
11
|
+
readonly socket: () => OneBot12WsSocket | undefined;
|
|
12
|
+
readonly self: OneBot12Self;
|
|
20
13
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
data: unknown,
|
|
24
|
-
|
|
25
|
-
): void
|
|
26
|
-
|
|
27
|
-
const message = JSON.parse(decodeOneBot12WsPayload(data)) as
|
|
28
|
-
| OneBot12Event
|
|
29
|
-
| OneBot12ActionResponse;
|
|
30
|
-
if ('echo' in message && typeof message.echo === 'string') {
|
|
31
|
-
const response = message as OneBot12ActionResponse;
|
|
32
|
-
const pending = options.pending.get(response.echo!);
|
|
33
|
-
if (pending) {
|
|
34
|
-
options.pending.delete(response.echo!);
|
|
35
|
-
clearTimeout(pending.timeout);
|
|
36
|
-
pending.resolve(response);
|
|
37
|
-
}
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
options.ingest(message as OneBot12Event);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
logger.warn(formatCompact({
|
|
43
|
-
op: 'onebot12_parse_failed',
|
|
44
|
-
endpoint: options.endpointId,
|
|
45
|
-
error: error instanceof Error ? error.message : String(error),
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
14
|
+
export interface OneBot12Rpc {
|
|
15
|
+
call(request: PendingRpcRequest): Promise<OneBot12ActionResponse>;
|
|
16
|
+
receive(data: unknown, ingest: (event: OneBot12Event) => void): void;
|
|
17
|
+
disconnect: PendingRpc<OneBot12ActionResponse>['disconnect'];
|
|
18
|
+
dispose(): void;
|
|
19
|
+
readonly pendingCount: number;
|
|
48
20
|
}
|
|
49
21
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
pending
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
22
|
+
/** A Client's one request owner serves native SDK actions and framework operations. */
|
|
23
|
+
export function createOneBot12Rpc(options: OneBot12RpcOptions): OneBot12Rpc {
|
|
24
|
+
const pending = createPendingRpc<OneBot12ActionResponse>({
|
|
25
|
+
signal: options.signal, requestPrefix: 'ob12_', timeoutMs: 30_000,
|
|
26
|
+
transport: () => { const socket = options.socket(); return socket?.readyState === WS_OPEN ? socket : undefined; },
|
|
27
|
+
serialize: request => JSON.stringify({ action: request.action, params: request.params ?? {}, echo: request.id, self: options.self }),
|
|
28
|
+
});
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
call: pending.call, disconnect: pending.disconnect, dispose: pending.dispose,
|
|
31
|
+
get pendingCount() { return pending.pendingCount; },
|
|
32
|
+
receive(data: unknown, ingest: (event: OneBot12Event) => void) {
|
|
33
|
+
try {
|
|
34
|
+
const message = JSON.parse(decodeWsPayload(data)) as OneBot12Event | OneBot12ActionResponse;
|
|
35
|
+
if ('echo' in message && typeof message.echo === 'string') {
|
|
36
|
+
pending.accept({ id: message.echo, value: message as OneBot12ActionResponse }); return;
|
|
37
|
+
}
|
|
38
|
+
ingest(message as OneBot12Event);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger.warn(formatCompact({ op: 'onebot12_parse_failed', endpoint: options.endpointId,
|
|
41
|
+
error: error instanceof Error ? error.message : String(error) }));
|
|
42
|
+
}
|
|
43
|
+
},
|
|
69
44
|
});
|
|
70
45
|
}
|
|
71
46
|
|
|
72
|
-
export function
|
|
73
|
-
pending: Map<string, OneBot12PendingAction>,
|
|
74
|
-
message = '连接已关闭',
|
|
75
|
-
): void {
|
|
76
|
-
for (const [, entry] of pending) {
|
|
77
|
-
clearTimeout(entry.timeout);
|
|
78
|
-
entry.reject(new Error(message));
|
|
79
|
-
}
|
|
80
|
-
pending.clear();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function decodeOneBot12WsPayload(data: unknown): string {
|
|
47
|
+
export function decodeWsPayload(data: unknown): string {
|
|
84
48
|
if (typeof data === 'string') return data;
|
|
85
49
|
if (Buffer.isBuffer(data)) return data.toString();
|
|
86
50
|
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
package/src/ws-types.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { OneBot12ActionResponse } from './protocol.js';
|
|
2
1
|
|
|
3
2
|
/** Minimal WS surface used by the endpoint (real `ws` or test mock). */
|
|
4
3
|
export interface OneBot12WsSocket {
|
|
@@ -13,9 +12,3 @@ export interface OneBot12WsCreateOptions {
|
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export const WS_OPEN = 1;
|
|
16
|
-
|
|
17
|
-
export interface OneBot12PendingAction {
|
|
18
|
-
resolve: (value: OneBot12ActionResponse) => void;
|
|
19
|
-
reject: (err: Error) => void;
|
|
20
|
-
timeout: NodeJS.Timeout;
|
|
21
|
-
}
|