@zhin.js/adapter-onebot11 4.0.1 → 4.0.3

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 (66) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +40 -370
  3. package/adapters/onebot11.ts +39 -0
  4. package/agent/tools/set_title.ts +21 -0
  5. package/lib/index.d.ts +7 -16
  6. package/lib/index.js +6 -56
  7. package/lib/onebot11-agent-deps.d.ts +15 -0
  8. package/lib/onebot11-agent-deps.js +30 -0
  9. package/lib/protocol.d.ts +144 -0
  10. package/lib/protocol.js +235 -0
  11. package/lib/ws-endpoint.d.ts +28 -0
  12. package/lib/ws-endpoint.js +210 -0
  13. package/lib/ws-transport.d.ts +13 -0
  14. package/lib/ws-transport.js +74 -0
  15. package/lib/ws-types.d.ts +17 -0
  16. package/lib/ws-types.js +1 -0
  17. package/lib/wss-auth.d.ts +2 -0
  18. package/lib/wss-auth.js +16 -0
  19. package/lib/wss-endpoint.d.ts +26 -0
  20. package/lib/wss-endpoint.js +143 -0
  21. package/package.json +52 -22
  22. package/plugin.ts +13 -0
  23. package/schema.json +36 -0
  24. package/src/index.ts +54 -68
  25. package/src/onebot11-agent-deps.ts +49 -0
  26. package/src/protocol.ts +370 -0
  27. package/src/ws-endpoint.ts +266 -0
  28. package/src/ws-transport.ts +101 -0
  29. package/src/ws-types.ts +20 -0
  30. package/src/wss-auth.ts +17 -0
  31. package/src/wss-endpoint.ts +189 -0
  32. package/lib/adapter.d.ts +0 -41
  33. package/lib/adapter.d.ts.map +0 -1
  34. package/lib/adapter.js +0 -101
  35. package/lib/adapter.js.map +0 -1
  36. package/lib/endpoint-ws-client.d.ts +0 -41
  37. package/lib/endpoint-ws-client.d.ts.map +0 -1
  38. package/lib/endpoint-ws-client.js +0 -408
  39. package/lib/endpoint-ws-client.js.map +0 -1
  40. package/lib/endpoint-ws-server.d.ts +0 -43
  41. package/lib/endpoint-ws-server.d.ts.map +0 -1
  42. package/lib/endpoint-ws-server.js +0 -355
  43. package/lib/endpoint-ws-server.js.map +0 -1
  44. package/lib/index.d.ts.map +0 -1
  45. package/lib/index.js.map +0 -1
  46. package/lib/onebot-get-msg.d.ts +0 -3
  47. package/lib/onebot-get-msg.d.ts.map +0 -1
  48. package/lib/onebot-get-msg.js +0 -27
  49. package/lib/onebot-get-msg.js.map +0 -1
  50. package/lib/segment-mapper.d.ts +0 -2
  51. package/lib/segment-mapper.d.ts.map +0 -1
  52. package/lib/segment-mapper.js +0 -2
  53. package/lib/segment-mapper.js.map +0 -1
  54. package/lib/types.d.ts +0 -61
  55. package/lib/types.d.ts.map +0 -1
  56. package/lib/types.js +0 -5
  57. package/lib/types.js.map +0 -1
  58. package/plugin.yml +0 -3
  59. package/src/adapter.ts +0 -114
  60. package/src/endpoint-ws-client.ts +0 -435
  61. package/src/endpoint-ws-server.ts +0 -396
  62. package/src/onebot-get-msg.ts +0 -33
  63. package/src/segment-mapper.ts +0 -1
  64. package/src/types.ts +0 -60
  65. /package/{skills/onebot11 → agent}/PERMITS.md +0 -0
  66. /package/{skills/onebot11/SKILL.md → agent/skills/onebot11.md} +0 -0
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Agent tool deps for onebot11 (set_title etc.).
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
+ */
5
+ const endpoints = new Map();
6
+ let override = null;
7
+ export function registerOnebot11AgentEndpoint(endpointId, endpoint) {
8
+ endpoints.set(endpointId, endpoint);
9
+ return () => {
10
+ if (endpoints.get(endpointId) === endpoint) {
11
+ endpoints.delete(endpointId);
12
+ }
13
+ };
14
+ }
15
+ /** Optional override used by tests / transitional callers. */
16
+ export function setOnebot11AgentDeps(deps) {
17
+ override = deps;
18
+ }
19
+ export function getOnebot11AgentDeps() {
20
+ if (override)
21
+ return override;
22
+ return {
23
+ getEndpoint(endpointId) {
24
+ const registered = endpoints.get(endpointId);
25
+ if (!registered)
26
+ throw new Error(`Endpoint ${endpointId} 不存在`);
27
+ return registered;
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * OneBot 11 protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ * Spec: https://github.com/botuniverse/onebot-11
5
+ */
6
+ /** Transitional legacy endpoint row (`endpoints[]` with `context: onebot11`). */
7
+ export interface OneBot11LegacyEndpointRow {
8
+ readonly context?: string;
9
+ readonly connection?: 'ws' | 'wss';
10
+ /** Legacy alias: `type: 'ws' | 'ws_reverse'` */
11
+ readonly type?: string;
12
+ readonly name?: string;
13
+ readonly access_token?: string;
14
+ readonly url?: string;
15
+ readonly path?: string;
16
+ readonly reconnect_interval?: number;
17
+ readonly heartbeat_interval?: number;
18
+ }
19
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
20
+ export interface OneBot11AdapterConfig {
21
+ readonly connection?: 'ws' | 'wss';
22
+ readonly name?: string;
23
+ readonly access_token?: string;
24
+ readonly url?: string;
25
+ readonly path?: string;
26
+ readonly reconnect_interval?: number;
27
+ readonly heartbeat_interval?: number;
28
+ /** Transitional: legacy root `endpoints[]` with `context: onebot11`. */
29
+ readonly endpoints?: ReadonlyArray<OneBot11LegacyEndpointRow>;
30
+ }
31
+ export interface OneBot11ConfigBase {
32
+ readonly context: 'onebot11';
33
+ readonly name: string;
34
+ readonly access_token?: string;
35
+ }
36
+ /** 正向 WebSocket:应用连 OneBot 实现的 WS 服务器 */
37
+ export interface OneBot11WsConfig extends OneBot11ConfigBase {
38
+ readonly connection: 'ws';
39
+ readonly url: string;
40
+ readonly reconnect_interval: number;
41
+ readonly heartbeat_interval: number;
42
+ }
43
+ /** 反向 WebSocket(slice 1 deferred — needs httpHostToken) */
44
+ export interface OneBot11WssConfig extends OneBot11ConfigBase {
45
+ readonly connection: 'wss';
46
+ readonly path: string;
47
+ readonly heartbeat_interval: number;
48
+ }
49
+ export type ResolvedOneBot11Config = OneBot11WsConfig | OneBot11WssConfig;
50
+ export type OneBot11EndpointConfig = ResolvedOneBot11Config;
51
+ export interface OneBot11Sender {
52
+ readonly role?: string;
53
+ readonly nickname?: string;
54
+ readonly card?: string;
55
+ readonly title?: string;
56
+ }
57
+ export interface OneBot11Segment {
58
+ type: string;
59
+ data?: Record<string, unknown>;
60
+ }
61
+ export interface OneBot11Event {
62
+ post_type: string;
63
+ self_id?: number | string;
64
+ message_type?: string;
65
+ sub_type?: string;
66
+ message_id?: number | string;
67
+ user_id?: number | string;
68
+ group_id?: number | string;
69
+ sender?: OneBot11Sender;
70
+ message?: OneBot11Segment[];
71
+ raw_message?: string;
72
+ time?: number;
73
+ notice_type?: string;
74
+ request_type?: string;
75
+ [key: string]: unknown;
76
+ }
77
+ export interface OneBot11ActionRequest {
78
+ action: string;
79
+ params: Record<string, unknown>;
80
+ echo?: string;
81
+ }
82
+ export interface OneBot11ActionResponse {
83
+ status: string;
84
+ retcode: number;
85
+ data?: unknown;
86
+ echo?: string;
87
+ }
88
+ export interface OneBot11WireSegment {
89
+ readonly type: string;
90
+ readonly data?: Record<string, unknown>;
91
+ }
92
+ export interface ParsedSendTarget {
93
+ readonly message_type: 'private' | 'group';
94
+ readonly id: string;
95
+ }
96
+ export declare function resolveOneBot11Config(config?: OneBot11AdapterConfig): ResolvedOneBot11Config;
97
+ /** 判断是否为消息事件(post_type=message) */
98
+ export declare function isMessageEvent(ev: OneBot11Event): ev is OneBot11Event & {
99
+ message_id: number | string;
100
+ };
101
+ /** 从事件得到场景 id:私聊 user_id,群 group_id */
102
+ export declare function getChannelId(ev: OneBot11Event): string;
103
+ /**
104
+ * Gateway reply target:`private:uid` / `group:gid`,便于 send() 还原动作参数。
105
+ */
106
+ export declare function formatInboundTarget(ev: OneBot11Event): string;
107
+ export declare function parseSendTarget(target: string): ParsedSendTarget;
108
+ /** Build inbound text for MessageGateway.receive */
109
+ export declare function formatInboundContent(ev: OneBot11Event): string;
110
+ export declare function senderDisplayName(ev: OneBot11Event): string;
111
+ /**
112
+ * Runtime Message.sender 必须是用户 ID(agent bridge 用它与 endpointMaster 比对)。
113
+ * user_id 缺失时兜底为空串,绝不回退到显示名。
114
+ */
115
+ export declare function senderUserId(ev: OneBot11Event): string;
116
+ /** 显示名(群名片优先)放 metadata.nickname;没有则返回 undefined,不写该字段。 */
117
+ export declare function senderNickname(ev: OneBot11Event): string | undefined;
118
+ /**
119
+ * 回复引用 id:优先取 message 数组中的 {type:'reply', data:{id}} 段;
120
+ * 兼容部分实现把 reply 放在事件顶层(标量或带 message_id 的对象)。
121
+ */
122
+ export declare function extractQuoteId(ev: OneBot11Event): string | undefined;
123
+ /** 扫描 message 段中的 at,qq 等于本机 uin(self_id)即视为被 @;`qq:'all'`(@全体)不算。 */
124
+ export declare function isOneBot11BotMentioned(input: {
125
+ readonly selfId: string | undefined;
126
+ readonly message?: readonly OneBot11Segment[];
127
+ }): boolean;
128
+ /** 构造 gateway.receive 的 metadata(ws / wss 两个 endpoint 共用)。 */
129
+ export declare function formatInboundMetadata(ev: OneBot11Event, endpoint: string): Readonly<Record<string, unknown>>;
130
+ /**
131
+ * Wire-encode an already-rendered outbound payload into OneBot 11 message segments.
132
+ * Segment canonicalization is intentionally not done here.
133
+ */
134
+ export declare function formatOutboundSegments(payload: unknown): OneBot11Segment[];
135
+ export declare function buildSendAction(target: string, message: OneBot11Segment[]): {
136
+ action: string;
137
+ params: Record<string, unknown>;
138
+ };
139
+ /** Build WS connect URL + headers (access_token via Bearer + query). */
140
+ export declare function buildWsConnectOptions(config: OneBot11WsConfig): {
141
+ readonly url: string;
142
+ readonly headers: Record<string, string>;
143
+ readonly safeUrl: string;
144
+ };
@@ -0,0 +1,235 @@
1
+ /**
2
+ * OneBot 11 protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ * Spec: https://github.com/botuniverse/onebot-11
5
+ */
6
+ function normalizeConnection(connection, legacyType) {
7
+ if (connection === 'ws' || connection === 'wss')
8
+ return connection;
9
+ if (legacyType === 'ws_reverse' || legacyType === 'wss')
10
+ return 'wss';
11
+ if (legacyType === 'ws')
12
+ return 'ws';
13
+ return 'ws';
14
+ }
15
+ export function resolveOneBot11Config(config = {}) {
16
+ const entry = config.endpoints?.find((item) => item.context === 'onebot11');
17
+ const connection = normalizeConnection(config.connection ?? entry?.connection, entry?.type);
18
+ const name = (typeof config.name === 'string' && config.name)
19
+ || (typeof entry?.name === 'string' && entry.name)
20
+ || process.env.ONEBOT11_BOT_NAME
21
+ || 'onebot11-bot';
22
+ const access_token = config.access_token ?? entry?.access_token;
23
+ if (connection === 'ws') {
24
+ const url = config.url ?? entry?.url;
25
+ if (!url) {
26
+ throw new TypeError('OneBot11 connection:ws requires url (plugins.<key>.url or endpoints with context: onebot11)');
27
+ }
28
+ return {
29
+ context: 'onebot11',
30
+ connection: 'ws',
31
+ name,
32
+ access_token,
33
+ url,
34
+ reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
35
+ heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
36
+ };
37
+ }
38
+ if (connection === 'wss') {
39
+ const path = config.path ?? entry?.path;
40
+ if (!path) {
41
+ throw new TypeError('OneBot11 connection:wss requires path');
42
+ }
43
+ return {
44
+ context: 'onebot11',
45
+ connection: 'wss',
46
+ name,
47
+ access_token,
48
+ path,
49
+ heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
50
+ };
51
+ }
52
+ throw new TypeError(`Unknown OneBot11 connection: ${String(connection)}`);
53
+ }
54
+ /** 判断是否为消息事件(post_type=message) */
55
+ export function isMessageEvent(ev) {
56
+ return ev.post_type === 'message' && ev.message_id != null;
57
+ }
58
+ /** 从事件得到场景 id:私聊 user_id,群 group_id */
59
+ export function getChannelId(ev) {
60
+ if (ev.message_type === 'group' && ev.group_id != null)
61
+ return String(ev.group_id);
62
+ if (ev.group_id != null && ev.message_type !== 'private')
63
+ return String(ev.group_id);
64
+ if (ev.user_id != null)
65
+ return String(ev.user_id);
66
+ return '';
67
+ }
68
+ /**
69
+ * Gateway reply target:`private:uid` / `group:gid`,便于 send() 还原动作参数。
70
+ */
71
+ export function formatInboundTarget(ev) {
72
+ const messageType = ev.message_type === 'group' || (ev.group_id != null && ev.message_type !== 'private')
73
+ ? 'group'
74
+ : 'private';
75
+ return `${messageType}:${getChannelId(ev)}`;
76
+ }
77
+ export function parseSendTarget(target) {
78
+ const sep = target.indexOf(':');
79
+ if (sep <= 0) {
80
+ return { message_type: 'private', id: target };
81
+ }
82
+ const head = target.slice(0, sep);
83
+ const rest = target.slice(sep + 1);
84
+ if (head === 'group') {
85
+ return { message_type: 'group', id: rest };
86
+ }
87
+ if (head === 'private') {
88
+ return { message_type: 'private', id: rest };
89
+ }
90
+ return { message_type: 'private', id: target };
91
+ }
92
+ /** Build inbound text for MessageGateway.receive */
93
+ export function formatInboundContent(ev) {
94
+ if (typeof ev.raw_message === 'string' && ev.raw_message)
95
+ return ev.raw_message;
96
+ if (Array.isArray(ev.message)) {
97
+ return ev.message
98
+ .map((seg) => (seg.type === 'text' ? String(seg.data?.text ?? '') : ''))
99
+ .join('');
100
+ }
101
+ return '';
102
+ }
103
+ export function senderDisplayName(ev) {
104
+ const name = ev.sender?.card || ev.sender?.nickname;
105
+ if (typeof name === 'string' && name)
106
+ return name;
107
+ return ev.user_id != null ? String(ev.user_id) : '';
108
+ }
109
+ /**
110
+ * Runtime Message.sender 必须是用户 ID(agent bridge 用它与 endpointMaster 比对)。
111
+ * user_id 缺失时兜底为空串,绝不回退到显示名。
112
+ */
113
+ export function senderUserId(ev) {
114
+ return ev.user_id != null ? String(ev.user_id) : '';
115
+ }
116
+ /** 显示名(群名片优先)放 metadata.nickname;没有则返回 undefined,不写该字段。 */
117
+ export function senderNickname(ev) {
118
+ const name = ev.sender?.card || ev.sender?.nickname;
119
+ return typeof name === 'string' && name ? name : undefined;
120
+ }
121
+ /**
122
+ * 回复引用 id:优先取 message 数组中的 {type:'reply', data:{id}} 段;
123
+ * 兼容部分实现把 reply 放在事件顶层(标量或带 message_id 的对象)。
124
+ */
125
+ export function extractQuoteId(ev) {
126
+ if (Array.isArray(ev.message)) {
127
+ for (const seg of ev.message) {
128
+ if (seg?.type === 'reply' && seg.data?.id != null)
129
+ return String(seg.data.id);
130
+ }
131
+ }
132
+ const reply = ev.reply;
133
+ if (typeof reply === 'number' || typeof reply === 'string')
134
+ return String(reply);
135
+ if (reply && typeof reply === 'object' && 'message_id' in reply) {
136
+ const id = reply.message_id;
137
+ if (id != null)
138
+ return String(id);
139
+ }
140
+ return undefined;
141
+ }
142
+ /** 扫描 message 段中的 at,qq 等于本机 uin(self_id)即视为被 @;`qq:'all'`(@全体)不算。 */
143
+ export function isOneBot11BotMentioned(input) {
144
+ if (!input.selfId || !Array.isArray(input.message))
145
+ return false;
146
+ return input.message.some((seg) => {
147
+ if (seg?.type !== 'at' || seg.data?.qq == null)
148
+ return false;
149
+ const qq = String(seg.data.qq);
150
+ return qq !== 'all' && qq === input.selfId;
151
+ });
152
+ }
153
+ /** 构造 gateway.receive 的 metadata(ws / wss 两个 endpoint 共用)。 */
154
+ export function formatInboundMetadata(ev, endpoint) {
155
+ const selfId = ev.self_id != null ? String(ev.self_id) : undefined;
156
+ const nickname = senderNickname(ev);
157
+ const quoteId = extractQuoteId(ev);
158
+ const mentioned = isOneBot11BotMentioned({ selfId, message: ev.message });
159
+ return Object.freeze({
160
+ message_type: ev.message_type,
161
+ user_id: ev.user_id != null ? String(ev.user_id) : undefined,
162
+ group_id: ev.group_id != null ? String(ev.group_id) : undefined,
163
+ endpoint,
164
+ time: ev.time,
165
+ self_id: selfId,
166
+ role: ev.sender?.role,
167
+ ...(nickname ? { nickname } : {}),
168
+ ...(quoteId ? { quote_id: quoteId } : {}),
169
+ ...(mentioned ? { mentioned: true } : {}),
170
+ });
171
+ }
172
+ /**
173
+ * Wire-encode an already-rendered outbound payload into OneBot 11 message segments.
174
+ * Segment canonicalization is intentionally not done here.
175
+ */
176
+ export function formatOutboundSegments(payload) {
177
+ if (typeof payload === 'string') {
178
+ return [{ type: 'text', data: { text: payload } }];
179
+ }
180
+ const items = Array.isArray(payload)
181
+ ? payload
182
+ : payload && typeof payload === 'object' && 'type' in payload
183
+ ? [payload]
184
+ : [];
185
+ if (items.length === 0) {
186
+ const text = payload == null
187
+ ? ''
188
+ : typeof payload === 'object'
189
+ ? JSON.stringify(payload)
190
+ : String(payload);
191
+ return [{ type: 'text', data: { text } }];
192
+ }
193
+ const segs = [];
194
+ for (const item of items) {
195
+ if (typeof item === 'string') {
196
+ segs.push({ type: 'text', data: { text: item } });
197
+ continue;
198
+ }
199
+ segs.push({ type: item.type, data: item.data ?? {} });
200
+ }
201
+ return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
202
+ }
203
+ export function buildSendAction(target, message) {
204
+ const parsed = parseSendTarget(target);
205
+ if (parsed.message_type === 'group') {
206
+ return {
207
+ action: 'send_group_msg',
208
+ params: {
209
+ group_id: Number(parsed.id) || parsed.id,
210
+ message,
211
+ },
212
+ };
213
+ }
214
+ return {
215
+ action: 'send_private_msg',
216
+ params: {
217
+ user_id: Number(parsed.id) || parsed.id,
218
+ message,
219
+ },
220
+ };
221
+ }
222
+ /** Build WS connect URL + headers (access_token via Bearer + query). */
223
+ export function buildWsConnectOptions(config) {
224
+ const headers = {};
225
+ let connectUrl = config.url;
226
+ if (config.access_token) {
227
+ headers.Authorization = `Bearer ${config.access_token}`;
228
+ const url = new URL(config.url);
229
+ url.searchParams.set('access_token', config.access_token);
230
+ connectUrl = url.toString();
231
+ }
232
+ const safeUrl = new URL(connectUrl);
233
+ safeUrl.searchParams.delete('access_token');
234
+ return { url: connectUrl, headers, safeUrl: safeUrl.toString() };
235
+ }
@@ -0,0 +1,28 @@
1
+ import type { EndpointInstance } from '@zhin.js/adapter';
2
+ import type { MessageGateway } from '@zhin.js/core/runtime';
3
+ import type { CapabilityId } from '@zhin.js/plugin-runtime';
4
+ import { type OneBot11Event, type OneBot11WsConfig } from './protocol.js';
5
+ import { type OneBot11WsCreateOptions, type OneBot11WsSocket } from './ws-types.js';
6
+ export interface OneBot11WsEndpointOptions {
7
+ readonly id: CapabilityId;
8
+ readonly gateway: MessageGateway;
9
+ readonly config: OneBot11WsConfig;
10
+ readonly createWebSocket?: (url: string, options: OneBot11WsCreateOptions) => OneBot11WsSocket;
11
+ }
12
+ export declare class OneBot11WsEndpoint implements EndpointInstance {
13
+ #private;
14
+ constructor(options: OneBot11WsEndpointOptions);
15
+ start(): Promise<void>;
16
+ open(): void;
17
+ close(): void;
18
+ stop(): Promise<void>;
19
+ send({ target, payload }: {
20
+ readonly target: string;
21
+ readonly payload: unknown;
22
+ }): Promise<string>;
23
+ /** Public API for agent tools / callers. */
24
+ callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
25
+ setTitle(groupId: number, userId: number, title: string, duration?: number): Promise<boolean>;
26
+ /** Test / internal: admit a parsed event when the endpoint is open. */
27
+ admit(ev: OneBot11Event): void;
28
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * OneBot11 WS client endpoint — outbound connect to OneBot implementation.
3
+ */
4
+ import WebSocket from 'ws';
5
+ import { clearInterval, clearTimeout, setTimeout } from 'node:timers';
6
+ import { formatCompact, getLogger } from '@zhin.js/logger';
7
+ import { registerOnebot11AgentEndpoint } from './onebot11-agent-deps.js';
8
+ import { buildSendAction, buildWsConnectOptions, formatInboundContent, formatInboundMetadata, formatInboundTarget, formatOutboundSegments, isMessageEvent, senderUserId, } from './protocol.js';
9
+ import { callOneBot11WsAction, handleOneBot11WsMessage, rejectAllPending, startOneBot11Heartbeat, } from './ws-transport.js';
10
+ const logger = getLogger('onebot11');
11
+ export class OneBot11WsEndpoint {
12
+ #options;
13
+ #ws;
14
+ #reconnectTimer;
15
+ #heartbeatTimer;
16
+ #requestId = { value: 0 };
17
+ #pending = new Map();
18
+ #open = false;
19
+ #started = false;
20
+ #stopping = false;
21
+ #unregisterAgent;
22
+ constructor(options) {
23
+ this.#options = options;
24
+ }
25
+ async start() {
26
+ if (this.#started)
27
+ return;
28
+ this.#started = true;
29
+ this.#stopping = false;
30
+ this.#unregisterAgent = registerOnebot11AgentEndpoint(this.#options.config.name, this);
31
+ try {
32
+ await this.#connect();
33
+ }
34
+ catch (err) {
35
+ this.#started = false;
36
+ throw err;
37
+ }
38
+ }
39
+ open() {
40
+ this.#open = true;
41
+ }
42
+ close() {
43
+ this.#open = false;
44
+ }
45
+ async stop() {
46
+ this.#open = false;
47
+ this.#stopping = true;
48
+ this.#started = false;
49
+ this.#unregisterAgent?.();
50
+ this.#unregisterAgent = undefined;
51
+ if (this.#reconnectTimer) {
52
+ clearTimeout(this.#reconnectTimer);
53
+ this.#reconnectTimer = undefined;
54
+ }
55
+ if (this.#heartbeatTimer) {
56
+ clearInterval(this.#heartbeatTimer);
57
+ this.#heartbeatTimer = undefined;
58
+ }
59
+ rejectAllPending(this.#pending);
60
+ if (this.#ws) {
61
+ try {
62
+ this.#ws.close();
63
+ }
64
+ catch {
65
+ /* ignore */
66
+ }
67
+ this.#ws = undefined;
68
+ }
69
+ logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
70
+ }
71
+ async send({ target, payload }) {
72
+ const message = formatOutboundSegments(payload);
73
+ const { action, params } = buildSendAction(target, message);
74
+ const data = await this.callApi(action, params);
75
+ const messageId = data?.message_id != null ? String(data.message_id) : '';
76
+ logger.debug(formatCompact({
77
+ op: 'onebot11_send',
78
+ endpoint: this.#options.config.name,
79
+ target,
80
+ messageId,
81
+ }));
82
+ return messageId;
83
+ }
84
+ /** Public API for agent tools / callers. */
85
+ callApi(action, params = {}) {
86
+ return callOneBot11WsAction(this.#ws, this.#pending, this.#requestId, action, params);
87
+ }
88
+ async setTitle(groupId, userId, title, duration = -1) {
89
+ await this.callApi('set_group_special_title', {
90
+ group_id: groupId,
91
+ user_id: userId,
92
+ special_title: title,
93
+ duration,
94
+ });
95
+ return true;
96
+ }
97
+ /** Test / internal: admit a parsed event when the endpoint is open. */
98
+ admit(ev) {
99
+ if (!this.#open || !isMessageEvent(ev))
100
+ return;
101
+ const target = formatInboundTarget(ev);
102
+ const content = formatInboundContent(ev);
103
+ void this.#options.gateway.receive({
104
+ adapter: this.#options.id,
105
+ target,
106
+ content,
107
+ sender: senderUserId(ev),
108
+ id: String(ev.message_id),
109
+ metadata: formatInboundMetadata(ev, this.#options.config.name),
110
+ }).catch((err) => {
111
+ logger.warn(formatCompact({
112
+ op: 'onebot11_gateway_receive_failed',
113
+ target,
114
+ error: err instanceof Error ? err.message : String(err),
115
+ }));
116
+ });
117
+ }
118
+ async #connect() {
119
+ const { url, headers, safeUrl } = buildWsConnectOptions(this.#options.config);
120
+ const create = this.#options.createWebSocket
121
+ ?? ((connectUrl, options) => new WebSocket(connectUrl, { headers: options.headers }));
122
+ await new Promise((resolve, reject) => {
123
+ let settled = false;
124
+ const ws = create(url, { headers });
125
+ this.#ws = ws;
126
+ ws.on('open', () => {
127
+ if (settled)
128
+ return;
129
+ settled = true;
130
+ if (!this.#options.config.access_token) {
131
+ logger.warn(formatCompact({
132
+ endpoint: this.#options.config.name,
133
+ ok: false,
134
+ error: 'missing access_token',
135
+ }));
136
+ }
137
+ logger.debug(formatCompact({
138
+ endpoint: this.#options.config.name,
139
+ mode: 'ws',
140
+ url: safeUrl,
141
+ }));
142
+ this.#heartbeatTimer = this.#stopping
143
+ ? this.#heartbeatTimer
144
+ : startOneBot11Heartbeat(this.#ws, this.#options.config.heartbeat_interval, this.#heartbeatTimer);
145
+ resolve();
146
+ });
147
+ ws.on('message', (data) => {
148
+ handleOneBot11WsMessage(data, {
149
+ endpointName: this.#options.config.name,
150
+ pending: this.#pending,
151
+ admit: (ev) => this.admit(ev),
152
+ });
153
+ });
154
+ ws.on('close', (code, reason) => {
155
+ const reasonStr = typeof reason === 'string'
156
+ ? reason
157
+ : Buffer.isBuffer(reason)
158
+ ? reason.toString()
159
+ : String(reason ?? '');
160
+ const codeNum = typeof code === 'number' ? code : Number(code ?? 0);
161
+ const codeHint = codeNum === 1005
162
+ ? ' [无状态,多为服务端/代理未发 close 帧即断开]'
163
+ : codeNum === 1006
164
+ ? ' [异常关闭]'
165
+ : '';
166
+ logger.warn(formatCompact({
167
+ op: 'disconnect',
168
+ endpoint: this.#options.config.name,
169
+ code: codeNum,
170
+ error: `${reasonStr || 'closed'}${codeHint}`,
171
+ reconnect_ms: this.#options.config.reconnect_interval,
172
+ }));
173
+ if (!settled) {
174
+ settled = true;
175
+ reject(new Error(`OneBot11 WS 关闭: ${codeNum} ${reasonStr}`));
176
+ }
177
+ this.#scheduleReconnect();
178
+ });
179
+ ws.on('error', (err) => {
180
+ const error = err instanceof Error ? err : new Error(String(err));
181
+ logger.warn(formatCompact({
182
+ op: 'ws_error',
183
+ endpoint: this.#options.config.name,
184
+ ok: false,
185
+ error: error.message,
186
+ }));
187
+ if (!settled) {
188
+ settled = true;
189
+ reject(error);
190
+ }
191
+ });
192
+ });
193
+ }
194
+ #scheduleReconnect() {
195
+ if (this.#stopping || !this.#started || this.#reconnectTimer)
196
+ return;
197
+ const delay = this.#options.config.reconnect_interval;
198
+ this.#reconnectTimer = setTimeout(() => {
199
+ this.#reconnectTimer = undefined;
200
+ void this.#connect().catch((err) => {
201
+ logger.warn(formatCompact({
202
+ op: 'reconnect',
203
+ endpoint: this.#options.config.name,
204
+ ok: false,
205
+ error: err instanceof Error ? err.message : String(err),
206
+ }));
207
+ });
208
+ }, delay);
209
+ }
210
+ }
@@ -0,0 +1,13 @@
1
+ import { type OneBot11Event } from './protocol.js';
2
+ import { type OneBot11PendingAction, type OneBot11WsSocket } from './ws-types.js';
3
+ export declare function decodeWsPayload(data: unknown): string;
4
+ export declare function handleOneBot11WsMessage(data: unknown, options: {
5
+ readonly endpointName: string;
6
+ readonly pending: Map<string, OneBot11PendingAction>;
7
+ readonly admit: (ev: OneBot11Event) => void;
8
+ }): void;
9
+ export declare function callOneBot11WsAction(ws: OneBot11WsSocket | undefined, pending: Map<string, OneBot11PendingAction>, requestId: {
10
+ value: number;
11
+ }, action: string, params: Record<string, unknown>): Promise<unknown>;
12
+ export declare function startOneBot11Heartbeat(ws: OneBot11WsSocket | undefined, intervalMs: number, existingTimer?: NodeJS.Timeout): NodeJS.Timeout;
13
+ export declare function rejectAllPending(pending: Map<string, OneBot11PendingAction>, message?: string): void;