@zhin.js/adapter-onebot12 1.1.17 → 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.
Files changed (61) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +33 -4
  3. package/adapters/onebot12/index.js +3 -32
  4. package/adapters/onebot12/index.ts +2 -38
  5. package/commands/onebot12/endpoint/definition.js +2 -2
  6. package/commands/onebot12/endpoint/definition.ts +2 -2
  7. package/lib/adapter.d.ts +21 -0
  8. package/lib/adapter.js +34 -0
  9. package/lib/client.d.ts +9 -11
  10. package/lib/client.js +23 -13
  11. package/lib/console-spec.d.ts +12 -0
  12. package/lib/console-spec.js +15 -0
  13. package/lib/console.d.ts +8 -0
  14. package/lib/console.js +13 -0
  15. package/lib/content-port.d.ts +2 -2
  16. package/lib/content-port.js +1 -1
  17. package/lib/endpoint-management.d.ts +5 -5
  18. package/lib/endpoint-management.js +17 -17
  19. package/lib/identity.d.ts +4 -0
  20. package/lib/identity.js +53 -0
  21. package/lib/index.d.ts +3 -2
  22. package/lib/index.js +2 -1
  23. package/lib/protocol.d.ts +32 -4
  24. package/lib/protocol.js +54 -53
  25. package/lib/response.d.ts +5 -0
  26. package/lib/response.js +17 -0
  27. package/lib/side-event-dispatch.d.ts +8 -1
  28. package/lib/side-event-dispatch.js +2 -1
  29. package/lib/webhook.d.ts +5 -11
  30. package/lib/webhook.js +72 -37
  31. package/lib/ws-endpoint.d.ts +5 -11
  32. package/lib/ws-endpoint.js +79 -45
  33. package/lib/ws-transport.d.ts +18 -10
  34. package/lib/ws-transport.js +26 -45
  35. package/lib/ws-types.d.ts +0 -6
  36. package/lib/wss-endpoint.d.ts +5 -11
  37. package/lib/wss-endpoint.js +82 -48
  38. package/package.json +22 -14
  39. package/pages/index/index.js +15 -0
  40. package/pages/index/index.ts +16 -0
  41. package/plugin.js +3 -5
  42. package/schema.json +15 -3
  43. package/src/adapter.ts +36 -0
  44. package/src/client.ts +26 -29
  45. package/src/console-spec.ts +20 -0
  46. package/src/console.ts +18 -0
  47. package/src/content-port.ts +3 -3
  48. package/src/endpoint-management.ts +25 -24
  49. package/src/identity.ts +52 -0
  50. package/src/index.ts +4 -1
  51. package/src/protocol.ts +71 -62
  52. package/src/response.ts +19 -0
  53. package/src/side-event-dispatch.ts +9 -7
  54. package/src/webhook.ts +70 -55
  55. package/src/ws-endpoint.ts +75 -63
  56. package/src/ws-transport.ts +37 -73
  57. package/src/ws-types.ts +0 -7
  58. package/src/wss-endpoint.ts +80 -66
  59. package/lib/onebot12-runtime-state.d.ts +0 -1
  60. package/lib/onebot12-runtime-state.js +0 -6
  61. package/src/onebot12-runtime-state.ts +0 -7
@@ -0,0 +1,53 @@
1
+ import WebSocket from 'ws';
2
+ import { buildWsConnectOptions, callOneBot12Action } from './protocol.js';
3
+ import { createOneBot12Rpc } from './ws-transport.js';
4
+ /** Separate read-only connection: never changes credentials or state of the running Endpoint. */
5
+ export async function probeOnebot12Identity(config, signal) {
6
+ signal.throwIfAborted();
7
+ const { url, headers } = buildWsConnectOptions(config);
8
+ const socket = new WebSocket(url, { headers });
9
+ const rpc = createOneBot12Rpc({ endpointId: config.id, signal, socket: () => socket, self: { platform: config.platform, user_id: config.userId }, });
10
+ let rejectOpen = () => undefined;
11
+ const abort = () => { rejectOpen(signal.reason); rpc.disconnect(); socket.terminate(); };
12
+ signal.addEventListener('abort', abort, { once: true });
13
+ socket.on('message', data => rpc.receive(data, () => undefined));
14
+ socket.on('close', () => { rejectOpen(new Error('Identity connection closed')); rpc.disconnect(); });
15
+ try {
16
+ await new Promise((resolve, reject) => { rejectOpen = reject; socket.once('open', resolve); socket.on('error', reject); });
17
+ signal.throwIfAborted();
18
+ const self = { platform: config.platform, user_id: config.userId };
19
+ return await identifySelectedBot({ selected: self, query: action => rpc.call({ action }), signal });
20
+ }
21
+ finally {
22
+ signal.removeEventListener('abort', abort);
23
+ rpc.dispose();
24
+ socket.terminate();
25
+ }
26
+ }
27
+ export async function probeOnebot12HttpIdentity(config, signal) {
28
+ const self = { platform: config.platform, user_id: config.userId };
29
+ return identifySelectedBot({ selected: self, query: action => callOneBot12Action({ url: config.api_url, access_token: config.access_token, signal, self, action }), signal });
30
+ }
31
+ async function identifySelectedBot({ selected, query, signal }) {
32
+ const status = await query('get_status');
33
+ signal.throwIfAborted();
34
+ if (status.status !== 'ok' || status.retcode !== 0)
35
+ throw new Error('OneBot12 identity status lookup failed');
36
+ const data = status.data;
37
+ if (!data || typeof data !== 'object' || !('bots' in data) || !Array.isArray(data.bots))
38
+ return undefined;
39
+ const match = data.bots.some((bot) => {
40
+ if (!bot || typeof bot !== 'object' || !('self' in bot) || !bot.self || typeof bot.self !== 'object')
41
+ return false;
42
+ return 'platform' in bot.self && 'user_id' in bot.self && bot.self.platform === selected.platform && bot.self.user_id === selected.user_id;
43
+ });
44
+ if (!match)
45
+ return undefined;
46
+ const account = await query('get_self_info');
47
+ signal.throwIfAborted();
48
+ if (account.status !== 'ok' || account.retcode !== 0)
49
+ throw new Error('OneBot12 account identity lookup failed');
50
+ if (!account.data || typeof account.data !== 'object' || !('user_id' in account.data) || account.data.user_id !== selected.user_id)
51
+ return undefined;
52
+ return `onebot12:${JSON.stringify([selected.platform, selected.user_id])}`;
53
+ }
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, onebot12Client, type Onebot12ClientEventMap, } from './client.js';
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, onebot12Client, } from './client.js';
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(segmentType: string, data: Record<string, unknown>, media: MediaRef): Record<string, unknown> | undefined;
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 declare function uploadOneBot12MediaSegments(payload: unknown, callAction: OneBot12CallAction, onUploadFailed?: (error: unknown) => void): Promise<unknown>;
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 declare function callOneBot12Action(options: OneBot12HttpOptions, action: string, params?: Record<string, unknown>, echo?: string): Promise<OneBot12ActionResponse>;
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
- /** upload_file 文件名:优先段 data.name/filename,URL/路径取 basename,再按 mime 给默认名。 */
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 `${segmentType}.${ext || 'bin'}`;
298
+ return `${type}.${ext || 'bin'}`;
297
299
  }
298
300
  /** canonical MediaRef → OB12 `upload_file` 动作参数(spec: type url/path/data)。 */
299
- export function mediaRefToOneBot12UploadParams(segmentType, data, media) {
300
- const name = oneBot12UploadName(segmentType, data, media);
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, callAction, onUploadFailed) {
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
- * 出站媒体段物化:image/voice/audio/video/file 段的 MediaRef(url/base64/path)
351
- * 先经 `upload_file` 换 file_id(spec 正式投递形状),再交给
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, callAction, onUploadFailed);
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
- * 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应。
385
- * 供 webhook 出站与纯协议测试使用;WS 路径走 WebSocket echo 请求。
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 (options.access_token) {
390
- headers.Authorization = `Bearer ${options.access_token}`;
391
- }
392
- const body = { action, params };
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
- data = JSON.parse(text);
409
- }
410
- catch {
411
- throw new Error(`OneBot12 无效响应: ${text.slice(0, 200)}`);
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
- if (data.status === 'failed' && data.retcode !== 0) {
414
- throw new Error(`OneBot12 动作失败 retcode=${data.retcode}: ${data.message}`);
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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * One classifier for native SDK, HTTP and WS results.
3
+ * @see https://12.onebot.dev/connect/data-protocol/action-response/
4
+ */
5
+ export declare function readOnebot12Response<T = unknown>(response: unknown): T | undefined;
@@ -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
- export declare function receiveOneBot12SideEvent(emit: EndpointEventEmitter, endpointKey: string, caller: OneBot12SideEventCaller, raw: OneBot12Event, logger: ReturnType<typeof getAdapterLogger>): void;
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(emit, endpointKey, caller, raw, logger) {
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 { ClientEndpoint, type EndpointControl, type EndpointManagement, type EndpointSendRequest } from 'zhin.js/adapter';
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 { type Onebot12Client } from './client.js';
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 ClientEndpoint<Onebot12Client> {
13
+ export declare class OneBot12WebhookEndpoint extends Onebot12Client implements Endpoint {
13
14
  #private;
14
- readonly client: Onebot12Client;
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 { ClientEndpoint, createRecallEndpointControl, } from 'zhin.js/adapter';
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, createOnebot12EndpointClient, forwardOnebot12ClientEvents } from './client.js';
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 ClientEndpoint {
11
- client;
11
+ export class OneBot12WebhookEndpoint extends Onebot12Client {
12
+ [endpointSpec];
13
+ #open = false;
12
14
  #options;
13
- management;
14
- control = createRecallEndpointControl((id) => this.recallMessage(id));
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
- 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));
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.close();
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(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
- }));
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 callOnebot12Client(this.client, 'send_message', params);
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 callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
100
+ await this.#callApi({ action: 'delete_message', params: { message_id: messageId }, signal }).then(readOnebot12Response);
85
101
  }
86
- async #callApi(action, params = {}) {
87
- return this.#callAction({ url: this.#options.config.api_url, access_token: this.#options.config.access_token }, action, params);
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((name, payload) => this.emit(name, payload), this.#options.config.id, { callApi: (action, params) => callOnebot12Client(this.client, action, params) }, ev, logger);
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.emit('message.receive', {
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.clientEventsOpen) {
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 this.client.acceptHttp(request, response);
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);