@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.
Files changed (92) hide show
  1. package/CHANGELOG.md +940 -0
  2. package/README.md +61 -69
  3. package/adapters/onebot12/index.js +50 -0
  4. package/adapters/onebot12/index.ts +64 -0
  5. package/commands/onebot12/endpoint/add/[id]/index.js +3 -0
  6. package/commands/onebot12/endpoint/add/[id]/index.ts +3 -0
  7. package/commands/onebot12/endpoint/definition.js +26 -0
  8. package/commands/onebot12/endpoint/definition.ts +24 -0
  9. package/commands/onebot12/endpoint/list/index.js +3 -0
  10. package/commands/onebot12/endpoint/list/index.ts +3 -0
  11. package/commands/onebot12/endpoint/remove/[id]/index.js +3 -0
  12. package/commands/onebot12/endpoint/remove/[id]/index.ts +3 -0
  13. package/lib/client.d.ts +20 -0
  14. package/lib/client.js +33 -0
  15. package/lib/content-port.d.ts +4 -0
  16. package/lib/content-port.js +45 -0
  17. package/lib/endpoint-management.d.ts +16 -0
  18. package/lib/endpoint-management.js +47 -0
  19. package/lib/index.d.ts +10 -19
  20. package/lib/index.js +7 -27
  21. package/lib/onebot12-runtime-state.d.ts +1 -0
  22. package/lib/onebot12-runtime-state.js +6 -0
  23. package/lib/protocol.d.ts +157 -0
  24. package/lib/protocol.js +431 -0
  25. package/lib/side-event-dispatch.d.ts +7 -0
  26. package/lib/side-event-dispatch.js +36 -0
  27. package/lib/webhook.d.ts +23 -0
  28. package/lib/webhook.js +157 -0
  29. package/lib/ws-endpoint.d.ts +22 -0
  30. package/lib/ws-endpoint.js +219 -0
  31. package/lib/ws-transport.d.ts +12 -0
  32. package/lib/ws-transport.js +58 -0
  33. package/lib/ws-types.d.ts +17 -0
  34. package/lib/ws-types.js +1 -0
  35. package/lib/wss-auth.d.ts +2 -0
  36. package/lib/wss-auth.js +16 -0
  37. package/lib/wss-endpoint.d.ts +25 -0
  38. package/lib/wss-endpoint.js +193 -0
  39. package/package.json +67 -16
  40. package/plugin.js +14 -0
  41. package/schema.json +120 -0
  42. package/src/client.ts +81 -0
  43. package/src/content-port.ts +49 -0
  44. package/src/endpoint-management.ts +73 -0
  45. package/src/index.ts +63 -37
  46. package/src/onebot12-runtime-state.ts +7 -0
  47. package/src/protocol.ts +575 -0
  48. package/src/side-event-dispatch.ts +48 -0
  49. package/src/webhook.ts +219 -0
  50. package/src/ws-endpoint.ts +285 -0
  51. package/src/ws-transport.ts +88 -0
  52. package/src/ws-types.ts +21 -0
  53. package/src/wss-auth.ts +17 -0
  54. package/src/wss-endpoint.ts +248 -0
  55. package/lib/adapter.d.ts +0 -18
  56. package/lib/adapter.d.ts.map +0 -1
  57. package/lib/adapter.js +0 -41
  58. package/lib/adapter.js.map +0 -1
  59. package/lib/api.d.ts +0 -14
  60. package/lib/api.d.ts.map +0 -1
  61. package/lib/api.js +0 -34
  62. package/lib/api.js.map +0 -1
  63. package/lib/endpoint-webhook.d.ts +0 -25
  64. package/lib/endpoint-webhook.d.ts.map +0 -1
  65. package/lib/endpoint-webhook.js +0 -118
  66. package/lib/endpoint-webhook.js.map +0 -1
  67. package/lib/endpoint-ws.d.ts +0 -26
  68. package/lib/endpoint-ws.d.ts.map +0 -1
  69. package/lib/endpoint-ws.js +0 -210
  70. package/lib/endpoint-ws.js.map +0 -1
  71. package/lib/endpoint-wss.d.ts +0 -26
  72. package/lib/endpoint-wss.d.ts.map +0 -1
  73. package/lib/endpoint-wss.js +0 -187
  74. package/lib/endpoint-wss.js.map +0 -1
  75. package/lib/index.d.ts.map +0 -1
  76. package/lib/index.js.map +0 -1
  77. package/lib/types.d.ts +0 -77
  78. package/lib/types.d.ts.map +0 -1
  79. package/lib/types.js +0 -5
  80. package/lib/types.js.map +0 -1
  81. package/lib/utils.d.ts +0 -45
  82. package/lib/utils.d.ts.map +0 -1
  83. package/lib/utils.js +0 -60
  84. package/lib/utils.js.map +0 -1
  85. package/skills/onebot12/SKILL.md +0 -36
  86. package/src/adapter.ts +0 -54
  87. package/src/api.ts +0 -48
  88. package/src/endpoint-webhook.ts +0 -130
  89. package/src/endpoint-ws.ts +0 -222
  90. package/src/endpoint-wss.ts +0 -202
  91. package/src/types.ts +0 -86
  92. package/src/utils.ts +0 -87
package/lib/utils.js DELETED
@@ -1,60 +0,0 @@
1
- /** 判断是否为消息事件(type=message) */
2
- export function isMessageEvent(ev) {
3
- return ev.type === 'message' && !!ev.message_id;
4
- }
5
- /** 从事件得到 zhin 场景 id:私聊 user_id,群 group_id,频道 channel_id 或 guild_id:channel_id */
6
- export function getChannelId(ev) {
7
- if (ev.detail_type === 'private' && ev.user_id)
8
- return ev.user_id;
9
- if (ev.detail_type === 'group' && ev.group_id)
10
- return ev.group_id;
11
- if (ev.detail_type === 'channel' && ev.channel_id) {
12
- return ev.guild_id ? `${ev.guild_id}:${ev.channel_id}` : ev.channel_id;
13
- }
14
- return '';
15
- }
16
- /** 从事件得到 channel 类型 */
17
- export function getChannelType(ev) {
18
- if (ev.detail_type === 'private')
19
- return 'private';
20
- return 'group';
21
- }
22
- /** 将 OneBot 12 消息事件转为 zhin Message 的 MessageBase 所需字段 */
23
- export function formatOneBot12MessagePayload(ev, endpointName, recallFn, replyFn) {
24
- const channelId = getChannelId(ev);
25
- const channelType = getChannelType(ev);
26
- const raw = ev.alt_message ?? (Array.isArray(ev.message) ? ev.message.map((s) => (s.type === 'text' ? s.data?.text ?? '' : '')).join('') : '');
27
- const content = Array.isArray(ev.message)
28
- ? ev.message.map((s) => ({ type: s.type, data: (s.data ?? {}) }))
29
- : [{ type: 'text', data: { text: raw } }];
30
- const senderId = ev.user_id ?? '';
31
- const senderName = ev['user.name'] ?? ev['qq.nickname'] ?? senderId;
32
- return {
33
- $id: ev.message_id,
34
- $adapter: 'onebot12',
35
- $endpoint: endpointName,
36
- $channel: { id: channelId, type: channelType },
37
- $sender: { id: senderId, name: senderName },
38
- $content: content,
39
- $raw: raw,
40
- $timestamp: ev.time ?? 0,
41
- $recall: () => recallFn(ev.message_id),
42
- $reply: (cnt, quote) => replyFn({ id: channelId, type: channelType }, (Array.isArray(cnt) ? cnt : [cnt]), quote),
43
- };
44
- }
45
- /** 将 zhin 的 content(segment 数组或字符串)转为 OneBot 12 message 段数组;简单实现仅 text */
46
- export function contentToOb12Segments(content) {
47
- const arr = Array.isArray(content) ? content : [content];
48
- const segs = [];
49
- for (const c of arr) {
50
- if (typeof c === 'string') {
51
- segs.push({ type: 'text', data: { text: c } });
52
- }
53
- else if (c && typeof c === 'object' && 'type' in c) {
54
- const el = c;
55
- segs.push({ type: el.type, data: el.data ?? {} });
56
- }
57
- }
58
- return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
59
- }
60
- //# sourceMappingURL=utils.js.map
package/lib/utils.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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"}
@@ -1,36 +0,0 @@
1
- ---
2
- name: onebot12
3
- platforms:
4
- - onebot12
5
- description: >-
6
- OneBot 12 协议适配器:支持 WebSocket 正向连接、Webhook 回调和反向 WebSocket
7
- 三种连接模式,兼容 OneBot 12 标准协议。纯消息通道,无额外 AI 工具。
8
- 支持私聊、群聊、频道消息。
9
- keywords:
10
- - onebot12
11
- - onebot
12
- - adapter:onebot12
13
- - protocol
14
- - websocket
15
- - webhook
16
- - 协议
17
- tags:
18
- - onebot12
19
- - protocol
20
- - adapter
21
- tools: []
22
- ---
23
-
24
- # OneBot 12 协议适配器
25
-
26
- 纯协议适配器,收发消息。无 AI 工具可调用。
27
-
28
- ## 连接模式
29
-
30
- - **WS 正向**:Bot 主动连接 OneBot 12 服务端
31
- - **Webhook**:OneBot 12 向配置的 URL 推送事件
32
- - **WS 反向**:OneBot 12 主动连接 Endpoint 的 WS 服务端
33
-
34
- ## 与 OneBot11 的区别
35
-
36
- OneBot 12 标准化了事件格式、消息段类型和 API 响应结构。如果需要群管理等 AI 工具,使用 OneBot11 适配器。
package/src/adapter.ts DELETED
@@ -1,54 +0,0 @@
1
- /**
2
- * OneBot 12 适配器:单一适配器支持正向 WS / Webhook / 反向 WS,由 config.connection 区分
3
- * 协议文档:https://12.onebot.dev/
4
- */
5
- import { formatCompact, Adapter, Plugin } from 'zhin.js';
6
- import type { Router } from '@zhin.js/host-router';
7
- import { OneBot12WsClient } from './endpoint-ws.js';
8
- import { OneBot12WebhookEndpoint } from './endpoint-webhook.js';
9
- import { OneBot12WssServer } from './endpoint-wss.js';
10
- import type {
11
- OneBot12EndpointConfig,
12
- OneBot12WsConfig,
13
- OneBot12WebhookConfig,
14
- OneBot12WssConfig,
15
- } from './types.js';
16
-
17
- export type OneBot12Bot = OneBot12WsClient | OneBot12WebhookEndpoint | OneBot12WssServer;
18
-
19
- export class OneBot12Adapter extends Adapter<OneBot12Bot> {
20
- static override readonly capabilities = ['inbound', 'outbound'] as const;
21
-
22
- #router?: Router;
23
-
24
- constructor(plugin: Plugin) {
25
- super(plugin, 'onebot12', []);
26
- }
27
-
28
- createEndpoint(config: OneBot12EndpointConfig): OneBot12Bot {
29
- switch (config.connection) {
30
- case 'ws':
31
- return new OneBot12WsClient(this, config as OneBot12WsConfig);
32
- case 'webhook':
33
- if (!this.#router) {
34
- throw new Error('OneBot12 connection: webhook 需要 router,请安装并在配置中启用 @zhin.js/host-router');
35
- }
36
- return new OneBot12WebhookEndpoint(this, this.#router, config as OneBot12WebhookConfig);
37
- case 'wss':
38
- if (!this.#router) {
39
- throw new Error('OneBot12 connection: wss 需要 router,请安装并在配置中启用 @zhin.js/host-router');
40
- }
41
- return new OneBot12WssServer(this, this.#router, config as OneBot12WssConfig);
42
- default:
43
- throw new Error(`Unknown OneBot12 connection: ${(config as OneBot12EndpointConfig).connection}`);
44
- }
45
- }
46
-
47
- async start(): Promise<void> {
48
- this.#router = (this.plugin.inject as (key: string) => Router | undefined)('router');
49
- (this.plugin.useContext as (key: string, fn: (router: Router) => void) => void)('router', (router: Router) => {
50
- this.#router = router;
51
- });
52
- await super.start();
53
- }
54
- }
package/src/api.ts DELETED
@@ -1,48 +0,0 @@
1
- /**
2
- * OneBot 12 HTTP 动作调用:POST 动作请求到实现的 HTTP 端点,返回动作响应
3
- * 参考 https://12.onebot.dev/connect/communication/http/
4
- */
5
- import type { OneBot12ActionRequest, OneBot12ActionResponse } from './types.js';
6
-
7
- export interface OneBot12HttpOptions {
8
- url: string;
9
- access_token?: string;
10
- }
11
-
12
- /**
13
- * 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应
14
- */
15
- export async function callOneBot12Action(
16
- options: OneBot12HttpOptions,
17
- action: string,
18
- params: Record<string, unknown> = {},
19
- echo?: string,
20
- ): Promise<OneBot12ActionResponse> {
21
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
22
- if (options.access_token) {
23
- headers['Authorization'] = `Bearer ${options.access_token}`;
24
- }
25
- const body: OneBot12ActionRequest = { action, params };
26
- if (echo) body.echo = echo;
27
-
28
- const res = await fetch(options.url, {
29
- method: 'POST',
30
- headers,
31
- body: JSON.stringify(body),
32
- });
33
-
34
- const text = await res.text();
35
- if (res.status === 401) throw new Error(`OneBot12 鉴权失败: ${text}`);
36
- if (res.status !== 200) throw new Error(`OneBot12 HTTP ${res.status}: ${text}`);
37
-
38
- let data: OneBot12ActionResponse;
39
- try {
40
- data = JSON.parse(text) as OneBot12ActionResponse;
41
- } catch {
42
- throw new Error(`OneBot12 无效响应: ${text.slice(0, 200)}`);
43
- }
44
- if (data.status === 'failed' && data.retcode !== 0) {
45
- throw new Error(`OneBot12 动作失败 retcode=${data.retcode}: ${data.message}`);
46
- }
47
- return data;
48
- }
@@ -1,130 +0,0 @@
1
- /**
2
- * OneBot 12 HTTP Webhook Bot:OneBot 实现 POST 事件到应用 path,可选 api_url 用于发消息
3
- */
4
- import { EventEmitter } from 'events';
5
- import { formatCompact, Endpoint, Message, segment, SendOptions } from 'zhin.js';
6
- import { registerFetchRoute, type Router, type RouterContext } from '@zhin.js/host-router/router';
7
- import { callOneBot12Action } from './api.js';
8
- import type { OneBot12WebhookConfig, OneBot12Event } from './types.js';
9
- import type { OneBot12Adapter } from './adapter.js';
10
- import { formatOneBot12MessagePayload, isMessageEvent, contentToOb12Segments } from './utils.js';
11
-
12
- export class OneBot12WebhookEndpoint extends EventEmitter implements Endpoint<OneBot12WebhookConfig, OneBot12Event> {
13
- $connected: boolean = true;
14
-
15
- get logger() {
16
- return this.adapter.plugin.logger;
17
- }
18
-
19
- constructor(
20
- public adapter: OneBot12Adapter,
21
- public router: Router,
22
- public $config: OneBot12WebhookConfig,
23
- ) {
24
- super();
25
- }
26
-
27
- get $id() {
28
- return this.$config.name;
29
- }
30
-
31
- private httpOptions() {
32
- const url = this.$config.api_url?.replace(/\/$/, '') ?? '';
33
- return { url: url || '', access_token: this.$config.access_token };
34
- }
35
-
36
- async $connect(): Promise<void> {
37
- const path = this.$config.path.startsWith('/') ? this.$config.path : `/${this.$config.path}`;
38
- registerFetchRoute(this.router, 'POST', path, async (ctx: RouterContext) => {
39
- const body = ctx.request.body as OneBot12Event | undefined;
40
- if (!body || typeof body !== 'object' || !body.id || body.time == null || !body.type) {
41
- ctx.status = 400;
42
- ctx.body = { message: 'Invalid OneBot12 event' };
43
- return;
44
- }
45
- const token = this.$config.access_token;
46
- const auth = ctx.get('authorization');
47
- if (token && auth !== `Bearer ${token}`) {
48
- ctx.status = 401;
49
- ctx.body = { message: 'Unauthorized' };
50
- return;
51
- }
52
- this.handleEvent(body);
53
- ctx.status = 204;
54
- ctx.body = undefined;
55
- });
56
- this.logger.info(formatCompact( { op: 'webhook', path }));
57
- }
58
-
59
- async $disconnect(): Promise<void> {
60
- this.$connected = false;
61
- }
62
-
63
- private handleEvent(ev: OneBot12Event): void {
64
- if (ev.type === 'message' && isMessageEvent(ev)) {
65
- const message = this.$formatMessage(ev);
66
- this.adapter.emit('message.receive', message);
67
- this.logger.debug(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`);
68
- }
69
- }
70
-
71
- $formatMessage(ev: OneBot12Event): Message<OneBot12Event> {
72
- if (!isMessageEvent(ev)) {
73
- return Message.from(ev, {
74
- $id: '',
75
- $adapter: 'onebot12',
76
- $endpoint: this.$config.name,
77
- $channel: { id: '', type: 'private' },
78
- $sender: { id: '', name: '' },
79
- $content: [],
80
- $raw: '',
81
- $timestamp: ev.time ?? 0,
82
- $recall: async () => {},
83
- $reply: async () => '',
84
- });
85
- }
86
- const payload = formatOneBot12MessagePayload(
87
- ev,
88
- this.$config.name,
89
- (id) => this.$recallMessage(id),
90
- (channel, content, _quote) =>
91
- this.adapter.sendMessage({
92
- ...channel,
93
- context: 'onebot12',
94
- endpoint: this.$config.name,
95
- content: content as import('zhin.js').SendContent,
96
- }),
97
- );
98
- return Message.from(ev, payload);
99
- }
100
-
101
- async $sendMessage(options: SendOptions): Promise<string> {
102
- const apiUrl = this.$config.api_url;
103
- if (!apiUrl) {
104
- throw new Error('OneBot12 Webhook 发消息需要配置 api_url(OneBot 实现的 HTTP 端点)');
105
- }
106
- const message = contentToOb12Segments(options.content);
107
- const params: Record<string, unknown> = { message };
108
- if (options.type === 'private') {
109
- params.detail_type = 'private';
110
- params.user_id = options.id;
111
- } else if (options.type === 'group') {
112
- params.detail_type = 'group';
113
- params.group_id = options.id;
114
- } else {
115
- const [guildId, channelId] = options.id.includes(':') ? options.id.split(':') : [undefined, options.id];
116
- params.detail_type = 'channel';
117
- params.channel_id = channelId ?? options.id;
118
- if (guildId) params.guild_id = guildId;
119
- }
120
- const data = await callOneBot12Action(this.httpOptions(), 'send_message', params);
121
- const out = (data as { message_id?: string })?.message_id ?? '';
122
- return out;
123
- }
124
-
125
- async $recallMessage(id: string): Promise<void> {
126
- const apiUrl = this.$config.api_url;
127
- if (!apiUrl) throw new Error('OneBot12 Webhook 撤回需要配置 api_url');
128
- await callOneBot12Action(this.httpOptions(), 'delete_message', { message_id: id });
129
- }
130
- }
@@ -1,222 +0,0 @@
1
- /**
2
- * OneBot 12 正向 WebSocket Bot:应用连 OneBot 实现的 WS,收事件、发动作
3
- */
4
- import WebSocket from 'ws';
5
- import { EventEmitter } from 'events';
6
- import { clearInterval, clearTimeout } from 'node:timers';
7
- import { formatCompact, Endpoint, Message, segment, SendOptions } from 'zhin.js';
8
- import type { OneBot12WsConfig, OneBot12Event, OneBot12ActionRequest, OneBot12ActionResponse } from './types.js';
9
- import type { OneBot12Adapter } from './adapter.js';
10
- import { formatOneBot12MessagePayload, isMessageEvent, contentToOb12Segments } from './utils.js';
11
-
12
- export class OneBot12WsClient extends EventEmitter implements Endpoint<OneBot12WsConfig, OneBot12Event> {
13
- $connected: boolean;
14
- private ws?: WebSocket;
15
- private reconnectTimer?: NodeJS.Timeout;
16
- private heartbeatTimer?: NodeJS.Timeout;
17
- private requestId = 0;
18
- private pendingRequests = new Map<string, { resolve: (value: unknown) => void; reject: (err: Error) => void; timeout: NodeJS.Timeout }>();
19
-
20
- get logger() {
21
- return this.adapter.plugin.logger;
22
- }
23
-
24
- constructor(public adapter: OneBot12Adapter, public $config: OneBot12WsConfig) {
25
- super();
26
- this.$connected = false;
27
- }
28
-
29
- get $id() {
30
- return this.$config.name;
31
- }
32
-
33
- private startHeartbeat(): void {
34
- const interval = this.$config.heartbeat_interval ?? 30000;
35
- this.heartbeatTimer = setInterval(() => {
36
- this.callAction('get_status', {}).catch(() => {});
37
- }, interval);
38
- }
39
-
40
- private scheduleReconnect(): void {
41
- if (this.reconnectTimer) return;
42
- const delay = this.$config.reconnect_interval ?? 5000;
43
- this.reconnectTimer = setTimeout(() => {
44
- this.reconnectTimer = undefined;
45
- this.$connect().catch((err) => this.logger.warn(formatCompact( {
46
- op: 'reconnect',
47
- endpoint: this.$id,
48
- ok: false,
49
- error: err instanceof Error ? err.message : String(err),
50
- })));
51
- }, delay);
52
- }
53
-
54
- private callAction(action: string, params: Record<string, unknown>): Promise<OneBot12ActionResponse['data']> {
55
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
56
- return Promise.reject(new Error('WebSocket 未连接'));
57
- }
58
- const echo = `ob12_${++this.requestId}`;
59
- const req: OneBot12ActionRequest = { action, params, echo };
60
- return new Promise((resolve, reject) => {
61
- const timeout = setTimeout(() => {
62
- this.pendingRequests.delete(echo);
63
- reject(new Error(`OneBot12 动作超时: ${action}`));
64
- }, 30000);
65
- this.pendingRequests.set(echo, {
66
- resolve: (data: unknown) => resolve(data as OneBot12ActionResponse['data']),
67
- reject,
68
- timeout,
69
- });
70
- this.ws!.send(JSON.stringify(req));
71
- });
72
- }
73
-
74
- async $connect(): Promise<void> {
75
- return new Promise((resolve, reject) => {
76
- const headers: Record<string, string> = {};
77
- let connectUrl = this.$config.url;
78
- if (this.$config.access_token) {
79
- headers['Authorization'] = `Bearer ${this.$config.access_token}`;
80
- const url = new URL(this.$config.url);
81
- url.searchParams.set('access_token', this.$config.access_token);
82
- connectUrl = url.toString();
83
- }
84
- this.ws = new WebSocket(connectUrl, { headers });
85
-
86
- this.ws.on('open', () => {
87
- this.$connected = true;
88
- const safeUrl = new URL(connectUrl);
89
- safeUrl.searchParams.delete('access_token');
90
- this.logger.info(formatCompact({ endpoint: this.$id, mode: 'ws' }));
91
- this.startHeartbeat();
92
- resolve();
93
- });
94
-
95
- this.ws.on('message', (data) => {
96
- try {
97
- const msg = JSON.parse(data.toString()) as OneBot12Event | OneBot12ActionResponse;
98
- if ('echo' in msg && typeof (msg as OneBot12ActionResponse).echo === 'string') {
99
- const resp = msg as OneBot12ActionResponse;
100
- const pending = this.pendingRequests.get(resp.echo!);
101
- if (pending) {
102
- this.pendingRequests.delete(resp.echo!);
103
- clearTimeout(pending.timeout);
104
- if (resp.status === 'ok') pending.resolve(resp.data);
105
- else pending.reject(new Error(`OneBot12 retcode=${resp.retcode}: ${resp.message}`));
106
- }
107
- return;
108
- }
109
- const ev = msg as OneBot12Event;
110
- if (ev.type === 'message' && isMessageEvent(ev)) {
111
- const message = this.$formatMessage(ev);
112
- this.adapter.emit('message.receive', message);
113
- this.logger.debug(`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}):${segment.raw(message.$content)}`);
114
- }
115
- } catch (error) {
116
- this.emit('error', error);
117
- }
118
- });
119
-
120
- this.ws.on('close', (code, reason) => {
121
- this.$connected = false;
122
- const reasonStr = reason?.toString?.() || String(reason);
123
- const codeHint = code === 1005 ? ' [无状态,多为服务端/代理未发 close 帧即断开]' : code === 1006 ? ' [异常关闭]' : '';
124
- this.logger.warn(formatCompact( {
125
- op: 'disconnect',
126
- endpoint: this.$config.name,
127
- code,
128
- error: `${reasonStr || 'closed'}${codeHint}`,
129
- reconnect_ms: this.$config.reconnect_interval ?? 5000,
130
- }));
131
- reject(new Error(`OneBot12 WS 关闭: ${code} ${reasonStr}`));
132
- this.scheduleReconnect();
133
- });
134
-
135
- this.ws.on('error', (err) => {
136
- this.logger.warn(formatCompact( {
137
- op: 'ws_error',
138
- endpoint: this.$config.name,
139
- ok: false,
140
- error: err instanceof Error ? err.message : String(err),
141
- }));
142
- reject(err);
143
- });
144
- });
145
- }
146
-
147
- async $disconnect(): Promise<void> {
148
- if (this.reconnectTimer) {
149
- clearTimeout(this.reconnectTimer);
150
- this.reconnectTimer = undefined;
151
- }
152
- if (this.heartbeatTimer) {
153
- clearInterval(this.heartbeatTimer);
154
- this.heartbeatTimer = undefined;
155
- }
156
- for (const [, p] of this.pendingRequests) {
157
- clearTimeout(p.timeout);
158
- p.reject(new Error('连接已关闭'));
159
- }
160
- this.pendingRequests.clear();
161
- if (this.ws) {
162
- this.ws.close();
163
- this.ws = undefined;
164
- }
165
- this.$connected = false;
166
- }
167
-
168
- $formatMessage(ev: OneBot12Event): Message<OneBot12Event> {
169
- if (!isMessageEvent(ev)) {
170
- return Message.from(ev, {
171
- $id: '',
172
- $adapter: 'onebot12',
173
- $endpoint: this.$config.name,
174
- $channel: { id: '', type: 'private' },
175
- $sender: { id: '', name: '' },
176
- $content: [],
177
- $raw: '',
178
- $timestamp: ev.time ?? 0,
179
- $recall: async () => {},
180
- $reply: async () => '',
181
- });
182
- }
183
- const payload = formatOneBot12MessagePayload(
184
- ev,
185
- this.$config.name,
186
- (id) => this.$recallMessage(id),
187
- (channel, content, _quote) =>
188
- this.adapter.sendMessage({
189
- ...channel,
190
- context: 'onebot12',
191
- endpoint: this.$config.name,
192
- content: content as import('zhin.js').SendContent,
193
- }),
194
- );
195
- return Message.from(ev, payload);
196
- }
197
-
198
- async $sendMessage(options: SendOptions): Promise<string> {
199
- const message = contentToOb12Segments(options.content);
200
- const params: Record<string, unknown> = { message };
201
- if (options.type === 'private') {
202
- params.detail_type = 'private';
203
- params.user_id = options.id;
204
- } else if (options.type === 'group') {
205
- params.detail_type = 'group';
206
- params.group_id = options.id;
207
- } else {
208
- const [guildId, channelId] = options.id.includes(':') ? options.id.split(':') : [undefined, options.id];
209
- params.detail_type = 'channel';
210
- params.channel_id = channelId ?? options.id;
211
- if (guildId) params.guild_id = guildId;
212
- }
213
- const data = await this.callAction('send_message', params) as { message_id?: string } | undefined;
214
- const msgId = data?.message_id ?? '';
215
- this.logger.debug(`${this.$config.name} send ${options.type}(${options.id}):${segment.raw(options.content)}`);
216
- return msgId;
217
- }
218
-
219
- async $recallMessage(id: string): Promise<void> {
220
- await this.callAction('delete_message', { message_id: id });
221
- }
222
- }