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