@zhin.js/adapter-onebot12 1.1.18 → 1.1.20
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 +43 -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/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export { buildSendMessageParams, buildWsConnectOptions, callOneBot12Action, formatInboundContent, formatOutboundSegments, getChannelId, isBotMentioned, isMessageEvent, mediaRefToOneBot12Fields, mediaRefToOneBot12UploadParams, onebot12InboundConversation, resolveOneBot12Config, senderNickname, senderUserId, uploadOneBot12MediaSegments, type OneBot12ActionRequest, type OneBot12ActionResponse, type OneBot12CallAction, type OneBot12ConfigBase, type OneBot12EndpointConfig, type OneBot12Event, type OneBot12HttpOptions, type OneBot12Segment, type OneBot12Self, type OneBot12WebhookConfig, type OneBot12WireSegment, type OneBot12WsConfig, type OneBot12WssConfig, type ResolvedOneBot12Config, } from './protocol.js';
|
|
1
|
+
export { buildSendMessageParams, buildWsConnectOptions, callOneBot12Action, formatInboundContent, formatOutboundSegments, getChannelId, isBotMentioned, isMessageEvent, mediaRefToOneBot12Fields, mediaRefToOneBot12UploadParams, onebot12InboundConversation, resolveOneBot12Config, senderNickname, senderUserId, uploadOneBot12MediaSegments, type OneBot12ActionRequest, type OneBot12ActionResponse, type OneBot12CallAction, type OneBot12ConfigBase, type OneBot12EndpointConfig, type OneBot12Event, type OneBot12HttpOptions, type OneBot12HttpRequest, type OneBot12MediaUpload, type OneBot12MediaSource, type OneBot12Segment, type OneBot12Self, type OneBot12WebhookConfig, type OneBot12WireSegment, type OneBot12WsConfig, type OneBot12WssConfig, type ResolvedOneBot12Config, } from './protocol.js';
|
|
2
2
|
export { OneBot12WebhookEndpoint, type OneBot12WebhookEndpointOptions, } from './webhook.js';
|
|
3
3
|
export { OneBot12WsEndpoint, type OneBot12WsEndpointOptions, } from './ws-endpoint.js';
|
|
4
4
|
export { OneBot12WssEndpoint, type OneBot12WssEndpointOptions, } from './wss-endpoint.js';
|
|
5
5
|
export type { OneBot12WsSocket, OneBot12WsCreateOptions } from './ws-types.js';
|
|
6
6
|
export { verifyOneBotAccessToken } from './wss-auth.js';
|
|
7
|
-
export { Onebot12Client,
|
|
7
|
+
export { Onebot12Client, type Onebot12ClientEventMap, } from './client.js';
|
|
8
8
|
export type { OneBotV12ActionUrlResolver, OneBotV12AdapterConfig as ImHelperOneBotV12AdapterConfig, OneBotV12Call, OneBotV12ClientConfig, OneBotV12Event as ImHelperOneBotV12Event, OneBotV12Response, } from '@imhelper/onebot-v12';
|
|
9
9
|
export { ProtocolError } from '@imhelper/onebot-v12';
|
|
10
10
|
export type { ProtocolErrorKind, ProtocolErrorOptions } from 'imhelper';
|
|
11
|
+
export { Onebot12Adapter } from './adapter.js';
|
package/lib/index.js
CHANGED
|
@@ -3,5 +3,6 @@ export { OneBot12WebhookEndpoint, } from './webhook.js';
|
|
|
3
3
|
export { OneBot12WsEndpoint, } from './ws-endpoint.js';
|
|
4
4
|
export { OneBot12WssEndpoint, } from './wss-endpoint.js';
|
|
5
5
|
export { verifyOneBotAccessToken } from './wss-auth.js';
|
|
6
|
-
export { Onebot12Client,
|
|
6
|
+
export { Onebot12Client, } from './client.js';
|
|
7
7
|
export { ProtocolError } from '@imhelper/onebot-v12';
|
|
8
|
+
export { Onebot12Adapter } from './adapter.js';
|
package/lib/protocol.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
|
|
7
7
|
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
8
8
|
export interface OneBot12EndpointConfig {
|
|
9
|
+
readonly platform: string;
|
|
10
|
+
readonly userId: string;
|
|
9
11
|
readonly connection?: 'ws' | 'webhook' | 'wss';
|
|
10
12
|
readonly id: string;
|
|
11
13
|
readonly access_token?: string;
|
|
@@ -16,6 +18,8 @@ export interface OneBot12EndpointConfig {
|
|
|
16
18
|
readonly heartbeat_interval?: number;
|
|
17
19
|
}
|
|
18
20
|
export interface OneBot12ConfigBase {
|
|
21
|
+
readonly platform: string;
|
|
22
|
+
readonly userId: string;
|
|
19
23
|
readonly context: 'onebot12';
|
|
20
24
|
readonly id: string;
|
|
21
25
|
readonly access_token?: string;
|
|
@@ -65,10 +69,10 @@ export interface OneBot12Segment {
|
|
|
65
69
|
data?: Record<string, unknown>;
|
|
66
70
|
}
|
|
67
71
|
export interface OneBot12ActionRequest {
|
|
72
|
+
readonly self: OneBot12Self;
|
|
68
73
|
action: string;
|
|
69
74
|
params: Record<string, unknown>;
|
|
70
75
|
echo?: string;
|
|
71
|
-
self?: OneBot12Self;
|
|
72
76
|
}
|
|
73
77
|
export interface OneBot12ActionResponse {
|
|
74
78
|
status: 'ok' | 'failed';
|
|
@@ -78,6 +82,8 @@ export interface OneBot12ActionResponse {
|
|
|
78
82
|
echo?: string;
|
|
79
83
|
}
|
|
80
84
|
export interface OneBot12HttpOptions {
|
|
85
|
+
readonly self: OneBot12Self;
|
|
86
|
+
readonly signal?: AbortSignal;
|
|
81
87
|
readonly url: string;
|
|
82
88
|
readonly access_token?: string;
|
|
83
89
|
}
|
|
@@ -130,15 +136,27 @@ export declare function mediaRefToOneBot12Fields(media: MediaRef): Record<string
|
|
|
130
136
|
export declare function formatOutboundSegments(payload: unknown): OneBot12Segment[];
|
|
131
137
|
/** 端点动作调用签名(WS echo 请求 / webhook api_url HTTP)。 */
|
|
132
138
|
export type OneBot12CallAction = (action: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
139
|
+
/** upload_file 文件名:优先段 data.name/filename,URL/路径取 basename,再按 mime 给默认名。 */
|
|
140
|
+
export interface OneBot12MediaSource {
|
|
141
|
+
readonly type: string;
|
|
142
|
+
readonly data: Readonly<Record<string, unknown>>;
|
|
143
|
+
readonly media: MediaRef;
|
|
144
|
+
}
|
|
133
145
|
/** canonical MediaRef → OB12 `upload_file` 动作参数(spec: type url/path/data)。 */
|
|
134
|
-
export declare function mediaRefToOneBot12UploadParams(
|
|
146
|
+
export declare function mediaRefToOneBot12UploadParams(source: OneBot12MediaSource): Record<string, unknown> | undefined;
|
|
135
147
|
/**
|
|
136
148
|
* 出站媒体段物化:image/voice/audio/video/file 段的 MediaRef(url/base64/path)
|
|
137
149
|
* 先经 `upload_file` 换 file_id(spec 正式投递形状),再交给
|
|
138
150
|
* {@link formatOutboundSegments} 编码;kind=file 的 MediaRef 直接按 file_id 复投。
|
|
139
151
|
* 上传失败保留原段(降级扩展字段透传)并回调 onUploadFailed。
|
|
140
152
|
*/
|
|
141
|
-
export
|
|
153
|
+
export interface OneBot12MediaUpload {
|
|
154
|
+
readonly payload: unknown;
|
|
155
|
+
readonly callAction: OneBot12CallAction;
|
|
156
|
+
readonly signal?: AbortSignal;
|
|
157
|
+
readonly onUploadFailed?: (error: unknown) => void;
|
|
158
|
+
}
|
|
159
|
+
export declare function uploadOneBot12MediaSegments(request: OneBot12MediaUpload): Promise<unknown>;
|
|
142
160
|
/**
|
|
143
161
|
* 结构化会话 → OB12 `send_message` 动作参数:kind 直映射 detail_type;
|
|
144
162
|
* channel 的 guild 容器取自 `conversation.parent`(kind 'channel')。
|
|
@@ -148,10 +166,20 @@ export declare function buildSendMessageParams(conversation: ConversationRef, me
|
|
|
148
166
|
* 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应。
|
|
149
167
|
* 供 webhook 出站与纯协议测试使用;WS 路径走 WebSocket echo 请求。
|
|
150
168
|
*/
|
|
151
|
-
export
|
|
169
|
+
export interface OneBot12HttpRequest extends OneBot12HttpOptions {
|
|
170
|
+
readonly action: string;
|
|
171
|
+
readonly params?: Record<string, unknown>;
|
|
172
|
+
readonly echo?: string;
|
|
173
|
+
}
|
|
174
|
+
/** HTTP owns submission evidence and cancellation for both SDK and framework callers. */
|
|
175
|
+
export declare function callOneBot12Action(request: OneBot12HttpRequest): Promise<OneBot12ActionResponse>;
|
|
152
176
|
/** Build WS connect URL + headers (access_token via Bearer + query). */
|
|
153
177
|
export declare function buildWsConnectOptions(config: OneBot12WsConfig): {
|
|
154
178
|
readonly url: string;
|
|
155
179
|
readonly headers: Record<string, string>;
|
|
156
180
|
readonly safeUrl: string;
|
|
157
181
|
};
|
|
182
|
+
/** Route only the explicitly selected bot; service-wide frames carry no account authority. */
|
|
183
|
+
export declare function matchesOneBot12Self(config: Pick<OneBot12ConfigBase, 'platform' | 'userId'>, event: OneBot12Event): event is OneBot12Event & {
|
|
184
|
+
readonly self: OneBot12Self;
|
|
185
|
+
};
|
package/lib/protocol.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
import { readOnebot12Response } from './response.js';
|
|
1
2
|
/**
|
|
2
3
|
* OneBot 12 protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
|
|
3
4
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
5
|
* Spec: https://12.onebot.dev/
|
|
5
6
|
*/
|
|
6
|
-
import { isMediaRef } from '@zhin.js/im-contract';
|
|
7
|
+
import { EndpointDeliveryError, isMediaRef } from '@zhin.js/im-contract';
|
|
7
8
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
8
9
|
const logger = getLogger('onebot12');
|
|
9
10
|
export function resolveOneBot12Config(config) {
|
|
10
11
|
const connection = config.connection ?? 'ws';
|
|
11
12
|
const id = requiredEndpointField(config.id, 'id');
|
|
13
|
+
const platform = requiredEndpointField(config.platform, 'platform');
|
|
14
|
+
const userId = requiredEndpointField(config.userId, 'userId');
|
|
12
15
|
const access_token = optionalEndpointField(config.access_token);
|
|
13
16
|
if (connection === 'ws') {
|
|
14
17
|
const url = requiredEndpointField(config.url, 'url');
|
|
15
18
|
return {
|
|
16
|
-
context: 'onebot12',
|
|
19
|
+
context: 'onebot12', platform, userId,
|
|
17
20
|
connection: 'ws',
|
|
18
21
|
id,
|
|
19
22
|
access_token,
|
|
@@ -26,7 +29,7 @@ export function resolveOneBot12Config(config) {
|
|
|
26
29
|
const path = requiredEndpointField(config.path, 'path');
|
|
27
30
|
const api_url = requiredEndpointField(config.api_url, 'api_url');
|
|
28
31
|
return {
|
|
29
|
-
context: 'onebot12',
|
|
32
|
+
context: 'onebot12', platform, userId,
|
|
30
33
|
connection: 'webhook',
|
|
31
34
|
id,
|
|
32
35
|
access_token,
|
|
@@ -37,7 +40,7 @@ export function resolveOneBot12Config(config) {
|
|
|
37
40
|
if (connection === 'wss') {
|
|
38
41
|
const path = requiredEndpointField(config.path, 'path');
|
|
39
42
|
return {
|
|
40
|
-
context: 'onebot12',
|
|
43
|
+
context: 'onebot12', platform, userId,
|
|
41
44
|
connection: 'wss',
|
|
42
45
|
id,
|
|
43
46
|
access_token,
|
|
@@ -272,8 +275,7 @@ export function formatOutboundSegments(payload) {
|
|
|
272
275
|
}
|
|
273
276
|
return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
|
|
274
277
|
}
|
|
275
|
-
|
|
276
|
-
function oneBot12UploadName(segmentType, data, media) {
|
|
278
|
+
function oneBot12UploadName({ type, data, media }) {
|
|
277
279
|
const named = data.name ?? data.filename;
|
|
278
280
|
if (typeof named === 'string' && named)
|
|
279
281
|
return named;
|
|
@@ -293,11 +295,12 @@ function oneBot12UploadName(segmentType, data, media) {
|
|
|
293
295
|
return base;
|
|
294
296
|
}
|
|
295
297
|
const ext = media.mime_type?.split('/')[1]?.split(';')[0];
|
|
296
|
-
return `${
|
|
298
|
+
return `${type}.${ext || 'bin'}`;
|
|
297
299
|
}
|
|
298
300
|
/** canonical MediaRef → OB12 `upload_file` 动作参数(spec: type url/path/data)。 */
|
|
299
|
-
export function mediaRefToOneBot12UploadParams(
|
|
300
|
-
const
|
|
301
|
+
export function mediaRefToOneBot12UploadParams(source) {
|
|
302
|
+
const { media } = source;
|
|
303
|
+
const name = oneBot12UploadName(source);
|
|
301
304
|
if (media.kind === 'url')
|
|
302
305
|
return { type: 'url', name, url: media.value };
|
|
303
306
|
if (media.kind === 'path') {
|
|
@@ -312,7 +315,7 @@ export function mediaRefToOneBot12UploadParams(segmentType, data, media) {
|
|
|
312
315
|
}
|
|
313
316
|
return undefined;
|
|
314
317
|
}
|
|
315
|
-
async function uploadOneMediaSegment(item,
|
|
318
|
+
async function uploadOneMediaSegment(item, request) {
|
|
316
319
|
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
317
320
|
return item;
|
|
318
321
|
const segment = item;
|
|
@@ -330,33 +333,29 @@ async function uploadOneMediaSegment(item, callAction, onUploadFailed) {
|
|
|
330
333
|
if (media.kind === 'file') {
|
|
331
334
|
return { type: segment.type, data: { ...oneBot12MediaExtra(data), file_id: media.value } };
|
|
332
335
|
}
|
|
333
|
-
const params = mediaRefToOneBot12UploadParams(segment.type, data, media);
|
|
336
|
+
const params = mediaRefToOneBot12UploadParams({ type: segment.type, data, media });
|
|
334
337
|
if (!params)
|
|
335
338
|
return item;
|
|
336
339
|
try {
|
|
337
|
-
const result = await callAction('upload_file', params);
|
|
340
|
+
const result = await request.callAction('upload_file', params);
|
|
338
341
|
if (result && typeof result.file_id === 'string' && result.file_id) {
|
|
339
342
|
return { type: segment.type, data: { ...oneBot12MediaExtra(data), file_id: result.file_id } };
|
|
340
343
|
}
|
|
341
344
|
throw new Error('upload_file 响应缺少 file_id');
|
|
342
345
|
}
|
|
343
346
|
catch (error) {
|
|
347
|
+
if (request.signal?.aborted)
|
|
348
|
+
throw error;
|
|
344
349
|
// 上传失败降级:保留原段,由 formatOutboundSegments 走扩展字段(url/data/path)
|
|
345
|
-
onUploadFailed?.(error);
|
|
350
|
+
request.onUploadFailed?.(error);
|
|
346
351
|
return item;
|
|
347
352
|
}
|
|
348
353
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
* {@link formatOutboundSegments} 编码;kind=file 的 MediaRef 直接按 file_id 复投。
|
|
353
|
-
* 上传失败保留原段(降级扩展字段透传)并回调 onUploadFailed。
|
|
354
|
-
*/
|
|
355
|
-
export async function uploadOneBot12MediaSegments(payload, callAction, onUploadFailed) {
|
|
356
|
-
if (Array.isArray(payload)) {
|
|
357
|
-
return Promise.all(payload.map((item) => uploadOneMediaSegment(item, callAction, onUploadFailed)));
|
|
354
|
+
export async function uploadOneBot12MediaSegments(request) {
|
|
355
|
+
if (Array.isArray(request.payload)) {
|
|
356
|
+
return Promise.all(request.payload.map(item => uploadOneMediaSegment(item, request)));
|
|
358
357
|
}
|
|
359
|
-
return uploadOneMediaSegment(payload,
|
|
358
|
+
return uploadOneMediaSegment(request.payload, request);
|
|
360
359
|
}
|
|
361
360
|
/**
|
|
362
361
|
* 结构化会话 → OB12 `send_message` 动作参数:kind 直映射 detail_type;
|
|
@@ -380,40 +379,38 @@ export function buildSendMessageParams(conversation, message) {
|
|
|
380
379
|
}
|
|
381
380
|
return params;
|
|
382
381
|
}
|
|
383
|
-
/**
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
export async function callOneBot12Action(options, action, params = {}, echo) {
|
|
382
|
+
/** HTTP owns submission evidence and cancellation for both SDK and framework callers. */
|
|
383
|
+
export async function callOneBot12Action(request) {
|
|
384
|
+
if (request.signal?.aborted)
|
|
385
|
+
throw new EndpointDeliveryError('endpoint_inactive', 'Endpoint operation cancelled', 'not_sent');
|
|
388
386
|
const headers = { 'Content-Type': 'application/json' };
|
|
389
|
-
if (
|
|
390
|
-
headers.Authorization = `Bearer ${
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (echo)
|
|
394
|
-
body.echo = echo;
|
|
395
|
-
const res = await fetch(options.url, {
|
|
396
|
-
method: 'POST',
|
|
397
|
-
headers,
|
|
398
|
-
body: JSON.stringify(body),
|
|
399
|
-
signal: AbortSignal.timeout(30_000),
|
|
400
|
-
});
|
|
401
|
-
const text = await res.text();
|
|
402
|
-
if (res.status === 401)
|
|
403
|
-
throw new Error(`OneBot12 鉴权失败: ${text}`);
|
|
404
|
-
if (res.status !== 200)
|
|
405
|
-
throw new Error(`OneBot12 HTTP ${res.status}: ${text}`);
|
|
406
|
-
let data;
|
|
387
|
+
if (request.access_token)
|
|
388
|
+
headers.Authorization = `Bearer ${request.access_token}`;
|
|
389
|
+
const body = { action: request.action, params: request.params ?? {}, self: request.self,
|
|
390
|
+
...(request.echo !== undefined ? { echo: request.echo } : {}) };
|
|
407
391
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
392
|
+
const response = await fetch(request.url, { method: 'POST', headers, body: JSON.stringify(body),
|
|
393
|
+
signal: request.signal ? AbortSignal.any([request.signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000) });
|
|
394
|
+
const text = await response.text();
|
|
395
|
+
if (response.status === 401)
|
|
396
|
+
throw new EndpointDeliveryError('platform_rejected', `OneBot12 鉴权失败: ${text}`, 'rejected');
|
|
397
|
+
if (response.status !== 200)
|
|
398
|
+
throw new EndpointDeliveryError('endpoint_http_failed', `OneBot12 HTTP ${response.status}: ${text}`, 'unknown');
|
|
399
|
+
let data;
|
|
400
|
+
try {
|
|
401
|
+
data = JSON.parse(text);
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', `OneBot12 无效响应: ${text.slice(0, 200)}`, 'unknown');
|
|
405
|
+
}
|
|
406
|
+
readOnebot12Response(data);
|
|
407
|
+
return data;
|
|
412
408
|
}
|
|
413
|
-
|
|
414
|
-
|
|
409
|
+
catch (cause) {
|
|
410
|
+
if (cause instanceof EndpointDeliveryError)
|
|
411
|
+
throw cause;
|
|
412
|
+
throw new EndpointDeliveryError(request.signal?.aborted ? 'endpoint_inactive' : 'endpoint_transport_failed', cause instanceof Error ? cause.message : String(cause), 'unknown', { cause });
|
|
415
413
|
}
|
|
416
|
-
return data;
|
|
417
414
|
}
|
|
418
415
|
/** Build WS connect URL + headers (access_token via Bearer + query). */
|
|
419
416
|
export function buildWsConnectOptions(config) {
|
|
@@ -429,3 +426,7 @@ export function buildWsConnectOptions(config) {
|
|
|
429
426
|
safeUrl.searchParams.delete('access_token');
|
|
430
427
|
return { url: connectUrl, headers, safeUrl: safeUrl.toString() };
|
|
431
428
|
}
|
|
429
|
+
/** Route only the explicitly selected bot; service-wide frames carry no account authority. */
|
|
430
|
+
export function matchesOneBot12Self(config, event) {
|
|
431
|
+
return event.self?.platform === config.platform && event.self?.user_id === config.userId;
|
|
432
|
+
}
|
package/lib/response.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
2
|
+
/**
|
|
3
|
+
* One classifier for native SDK, HTTP and WS results.
|
|
4
|
+
* @see https://12.onebot.dev/connect/data-protocol/action-response/
|
|
5
|
+
*/
|
|
6
|
+
export function readOnebot12Response(response) {
|
|
7
|
+
if (response && typeof response === 'object') {
|
|
8
|
+
const result = response;
|
|
9
|
+
if (result.status === 'ok' && result.retcode === 0)
|
|
10
|
+
return result.data;
|
|
11
|
+
if (result.status === 'failed' && typeof result.retcode === 'number' && Number.isSafeInteger(result.retcode) && result.retcode >= 10000 && result.retcode <= 99999) {
|
|
12
|
+
throw new EndpointDeliveryError('platform_rejected', `OneBot 12 API rejected: retcode=${result.retcode}${result.message ? ` ${result.message}` : ''}`, 'rejected');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// OB12 defines only ok/0 or failed with an error code; inconsistent envelopes prove neither result.
|
|
16
|
+
throw new EndpointDeliveryError('delivery_unconfirmed', 'OneBot 12 response did not confirm the action result', 'unknown');
|
|
17
|
+
}
|
|
@@ -4,4 +4,11 @@ import type { OneBot12Event } from './protocol.js';
|
|
|
4
4
|
export interface OneBot12SideEventCaller {
|
|
5
5
|
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
/** Exact instance-owned dependencies for one canonical side-event projection. */
|
|
8
|
+
export interface OneBot12SideEventContext {
|
|
9
|
+
readonly emit: EndpointEventEmitter;
|
|
10
|
+
readonly endpointKey: string;
|
|
11
|
+
readonly caller: OneBot12SideEventCaller;
|
|
12
|
+
readonly logger: ReturnType<typeof getAdapterLogger>;
|
|
13
|
+
}
|
|
14
|
+
export declare function receiveOneBot12SideEvent(raw: OneBot12Event, context: OneBot12SideEventContext): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { receiveOneBotLikeSideEvent } from '@zhin.js/core';
|
|
2
2
|
import { formatCompact } from '@zhin.js/logger';
|
|
3
|
-
export function receiveOneBot12SideEvent(
|
|
3
|
+
export function receiveOneBot12SideEvent(raw, context) {
|
|
4
|
+
const { emit, endpointKey, caller, logger } = context;
|
|
4
5
|
if (!emit)
|
|
5
6
|
return;
|
|
6
7
|
const record = raw;
|
package/lib/webhook.d.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
|
|
2
2
|
import type { HttpHost } from '@zhin.js/host-http';
|
|
3
3
|
import type { CapabilityId } from 'zhin.js';
|
|
4
4
|
import { callOneBot12Action, type OneBot12WebhookConfig } from './protocol.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Onebot12Client } from './client.js';
|
|
6
6
|
export interface OneBot12WebhookEndpointOptions {
|
|
7
|
+
readonly signal?: AbortSignal;
|
|
7
8
|
readonly id: CapabilityId;
|
|
8
9
|
readonly http: HttpHost;
|
|
9
10
|
readonly config: OneBot12WebhookConfig;
|
|
10
11
|
readonly callAction?: typeof callOneBot12Action;
|
|
11
12
|
}
|
|
12
|
-
export declare class OneBot12WebhookEndpoint extends
|
|
13
|
+
export declare class OneBot12WebhookEndpoint extends Onebot12Client implements Endpoint {
|
|
13
14
|
#private;
|
|
14
|
-
readonly
|
|
15
|
-
readonly management: EndpointManagement;
|
|
16
|
-
readonly control: EndpointControl;
|
|
17
|
-
readonly content: import("@zhin.js/adapter").EndpointContentPort;
|
|
15
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
18
16
|
constructor(options: OneBot12WebhookEndpointOptions);
|
|
19
|
-
start(): Promise<void>;
|
|
20
|
-
stop(): Promise<void>;
|
|
21
|
-
send({ conversation, payload }: EndpointSendRequest): Promise<string>;
|
|
22
|
-
recallMessage(messageId: string): Promise<void>;
|
|
23
17
|
}
|
package/lib/webhook.js
CHANGED
|
@@ -1,35 +1,49 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { acceptHttpIngress } from 'imhelper';
|
|
2
|
+
import { createEndpointSpecification, endpointSpec, createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
2
3
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
3
4
|
import { createOneBot12EndpointManagement } from './endpoint-management.js';
|
|
4
|
-
import { buildSendMessageParams, callOneBot12Action, formatInboundContent, formatOutboundSegments, isBotMentioned, isMessageEvent, onebot12InboundConversation, senderNickname, senderUserId, uploadOneBot12MediaSegments, } from './protocol.js';
|
|
5
|
+
import { buildSendMessageParams, matchesOneBot12Self, callOneBot12Action, formatInboundContent, formatOutboundSegments, isBotMentioned, isMessageEvent, onebot12InboundConversation, senderNickname, senderUserId, uploadOneBot12MediaSegments, } from './protocol.js';
|
|
5
6
|
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
6
7
|
import { createOneBot12ContentPort } from './content-port.js';
|
|
7
|
-
import { callOnebot12Client,
|
|
8
|
+
import { callOnebot12Client, readOnebot12Response, onebot12ClientOptions, forwardOnebot12ClientEvents, Onebot12Client } from './client.js';
|
|
8
9
|
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
9
10
|
const logger = getLogger('onebot12');
|
|
10
|
-
export class OneBot12WebhookEndpoint extends
|
|
11
|
-
|
|
11
|
+
export class OneBot12WebhookEndpoint extends Onebot12Client {
|
|
12
|
+
[endpointSpec];
|
|
13
|
+
#open = false;
|
|
12
14
|
#options;
|
|
13
|
-
management;
|
|
14
|
-
control = createRecallEndpointControl((id) => this
|
|
15
|
-
content;
|
|
15
|
+
#management;
|
|
16
|
+
#control = createRecallEndpointControl((id, signal) => this.#recallMessage(id, signal));
|
|
17
|
+
#content;
|
|
18
|
+
#releaseClientEvents;
|
|
16
19
|
#callAction;
|
|
17
20
|
#routeReleases = [];
|
|
18
21
|
#started = false;
|
|
19
22
|
constructor(options) {
|
|
20
|
-
super();
|
|
23
|
+
super(onebot12ClientOptions(options.config, (action, params) => this.#callApi({ action, params })));
|
|
21
24
|
this.#options = options;
|
|
22
25
|
this.#callAction = options.callAction ?? callOneBot12Action;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
this
|
|
26
|
-
this
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const callApi = (request) => this.#callApi(request).then(readOnebot12Response);
|
|
27
|
+
this.#management = createOneBot12EndpointManagement({ callApi });
|
|
28
|
+
this.#content = createOneBot12ContentPort(callApi);
|
|
29
|
+
this[endpointSpec] = createEndpointSpecification(this, {
|
|
30
|
+
name: options.config.id,
|
|
31
|
+
start: () => this.#start(), open: () => { this.#open = true; },
|
|
32
|
+
close: () => { this.#open = false; }, stop: () => this.#stop(),
|
|
33
|
+
send: request => this.#send(request),
|
|
34
|
+
management: this.#management, control: this.#control, content: this.#content,
|
|
35
|
+
});
|
|
36
|
+
this.#releaseClientEvents = forwardOnebot12ClientEvents(this, (name, payload) => {
|
|
37
|
+
void this[endpointSpec].events.run(() => {
|
|
38
|
+
if (!this.#started)
|
|
39
|
+
return;
|
|
40
|
+
void this[endpointSpec].events.platform(name, payload).catch(error => this.#warnPlatformEvent(error));
|
|
41
|
+
if (name === 'event')
|
|
42
|
+
this.#admitRaw(payload);
|
|
43
|
+
}).catch(error => this.#warnPlatformEvent(error));
|
|
44
|
+
});
|
|
31
45
|
}
|
|
32
|
-
async start() {
|
|
46
|
+
async #start() {
|
|
33
47
|
if (this.#started)
|
|
34
48
|
return;
|
|
35
49
|
this.#started = true;
|
|
@@ -50,24 +64,26 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint {
|
|
|
50
64
|
path: this.#options.config.path,
|
|
51
65
|
}));
|
|
52
66
|
}
|
|
53
|
-
async stop() {
|
|
54
|
-
this
|
|
67
|
+
async #stop() {
|
|
68
|
+
this.#open = false;
|
|
55
69
|
for (const release of this.#routeReleases.splice(0))
|
|
56
70
|
release();
|
|
57
71
|
this.#started = false;
|
|
72
|
+
this.#releaseClientEvents();
|
|
58
73
|
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.id }));
|
|
59
74
|
}
|
|
60
|
-
async send({ conversation, payload }) {
|
|
61
|
-
const materialized = await uploadOneBot12MediaSegments(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
async #send({ conversation, payload, signal }) {
|
|
76
|
+
const materialized = await uploadOneBot12MediaSegments({
|
|
77
|
+
payload, signal,
|
|
78
|
+
callAction: (action, params) => this.#callApi({ action, params, signal }).then(readOnebot12Response),
|
|
79
|
+
onUploadFailed: error => {
|
|
80
|
+
logger.warn(formatCompact({ op: 'onebot12_upload_failed', endpoint: this.#options.config.id,
|
|
81
|
+
error: error instanceof Error ? error.message : String(error) }));
|
|
82
|
+
},
|
|
67
83
|
});
|
|
68
84
|
const message = formatOutboundSegments(materialized);
|
|
69
85
|
const params = buildSendMessageParams(conversation, message);
|
|
70
|
-
const data = await
|
|
86
|
+
const data = readOnebot12Response(await this.#callApi({ action: 'send_message', params, signal }));
|
|
71
87
|
const messageId = data?.message_id ?? '';
|
|
72
88
|
logger.debug(formatCompact({
|
|
73
89
|
op: 'onebot12_send',
|
|
@@ -78,24 +94,38 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint {
|
|
|
78
94
|
}));
|
|
79
95
|
return messageId;
|
|
80
96
|
}
|
|
81
|
-
async recallMessage(messageId) {
|
|
97
|
+
async #recallMessage(messageId, signal) {
|
|
82
98
|
if (!messageId)
|
|
83
99
|
return;
|
|
84
|
-
await
|
|
100
|
+
await this.#callApi({ action: 'delete_message', params: { message_id: messageId }, signal }).then(readOnebot12Response);
|
|
85
101
|
}
|
|
86
|
-
async #callApi(
|
|
87
|
-
|
|
102
|
+
async #callApi(request) {
|
|
103
|
+
const epoch = this.#options.signal;
|
|
104
|
+
const signal = epoch && request.signal ? AbortSignal.any([epoch, request.signal]) : epoch ?? request.signal;
|
|
105
|
+
return this.#callAction({ url: this.#options.config.api_url, access_token: this.#options.config.access_token,
|
|
106
|
+
self: { platform: this.#options.config.platform, user_id: this.#options.config.userId },
|
|
107
|
+
action: request.action, params: request.params, signal });
|
|
88
108
|
}
|
|
89
109
|
#admitRaw(ev) {
|
|
110
|
+
if (!matchesOneBot12Self(this.#options.config, ev))
|
|
111
|
+
return;
|
|
112
|
+
this[endpointSpec].reportSelfUserId(ev.self.user_id);
|
|
90
113
|
if (!isMessageEvent(ev)) {
|
|
91
|
-
receiveOneBot12SideEvent(
|
|
114
|
+
receiveOneBot12SideEvent(ev, {
|
|
115
|
+
emit: (name, payload) => this[endpointSpec].events.emit(name, payload),
|
|
116
|
+
endpointKey: this.#options.config.id,
|
|
117
|
+
caller: { callApi: (action, params) => callOnebot12Client({ client: this, action, params }) },
|
|
118
|
+
logger: logger,
|
|
119
|
+
});
|
|
92
120
|
return;
|
|
93
121
|
}
|
|
94
|
-
const conversation = onebot12InboundConversation(String(this.#options.id), ev);
|
|
122
|
+
const conversation = { ...onebot12InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
|
|
95
123
|
const content = formatInboundContent(ev);
|
|
96
124
|
const nickname = senderNickname(ev);
|
|
97
125
|
const mentioned = isBotMentioned(ev);
|
|
98
|
-
void this.
|
|
126
|
+
void this[endpointSpec].events.message({
|
|
127
|
+
event: ev,
|
|
128
|
+
...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
|
|
99
129
|
conversation,
|
|
100
130
|
message: { conversation, id: ev.message_id },
|
|
101
131
|
content,
|
|
@@ -139,12 +169,17 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint {
|
|
|
139
169
|
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
140
170
|
return;
|
|
141
171
|
}
|
|
142
|
-
if (!this
|
|
172
|
+
if (!this.#open) {
|
|
143
173
|
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
144
174
|
response.end(JSON.stringify({ status: 'ok' }));
|
|
145
175
|
return;
|
|
146
176
|
}
|
|
147
|
-
await
|
|
177
|
+
await acceptHttpIngress(request, response, event => {
|
|
178
|
+
if (!matchesOneBot12Self(this.#options.config, event))
|
|
179
|
+
return;
|
|
180
|
+
void this[endpointSpec].events.run(() => this.ingest(event))
|
|
181
|
+
.catch(error => this.#warnPlatformEvent(error));
|
|
182
|
+
});
|
|
148
183
|
}
|
|
149
184
|
catch (error) {
|
|
150
185
|
logger.error('OneBot12 webhook error:', error);
|
package/lib/ws-endpoint.d.ts
CHANGED
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
|
|
2
2
|
import type { CapabilityId } from 'zhin.js';
|
|
3
3
|
import { type OneBot12WsConfig } from './protocol.js';
|
|
4
|
-
import {
|
|
4
|
+
import { Onebot12Client } from './client.js';
|
|
5
5
|
import { type OneBot12WsCreateOptions, type OneBot12WsSocket } from './ws-types.js';
|
|
6
6
|
export interface OneBot12WsEndpointOptions {
|
|
7
|
+
readonly signal?: AbortSignal;
|
|
7
8
|
readonly id: CapabilityId;
|
|
8
9
|
readonly config: OneBot12WsConfig;
|
|
9
10
|
readonly createWebSocket?: (url: string, options: OneBot12WsCreateOptions) => OneBot12WsSocket;
|
|
10
11
|
}
|
|
11
|
-
export declare class OneBot12WsEndpoint extends
|
|
12
|
+
export declare class OneBot12WsEndpoint extends Onebot12Client implements Endpoint {
|
|
12
13
|
#private;
|
|
13
|
-
readonly
|
|
14
|
-
readonly management: EndpointManagement;
|
|
15
|
-
readonly control: EndpointControl;
|
|
16
|
-
readonly content: import("@zhin.js/adapter").EndpointContentPort;
|
|
14
|
+
readonly [endpointSpec]: EndpointSpecification;
|
|
17
15
|
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
16
|
}
|