@zhin.js/adapter-onebot12 1.1.0 → 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 (40) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +5 -3
  3. package/adapters/{onebot12.js → onebot12/index.js} +10 -10
  4. package/adapters/{onebot12.ts → onebot12/index.ts} +17 -17
  5. package/commands/{endpoint/add/[id].js → onebot12/endpoint/add/[id]/index.js} +1 -1
  6. package/commands/onebot12/endpoint/add/[id]/index.ts +3 -0
  7. package/{lib/onebot12-endpoint-commands.js → commands/onebot12/endpoint/definition.js} +4 -3
  8. package/{src/onebot12-endpoint-commands.ts → commands/onebot12/endpoint/definition.ts} +3 -3
  9. package/commands/{endpoint/list.js → onebot12/endpoint/list/index.js} +1 -1
  10. package/commands/onebot12/endpoint/list/index.ts +3 -0
  11. package/commands/{endpoint/remove/[id].js → onebot12/endpoint/remove/[id]/index.js} +1 -1
  12. package/commands/onebot12/endpoint/remove/[id]/index.ts +3 -0
  13. package/lib/client.js +1 -1
  14. package/lib/index.d.ts +1 -1
  15. package/lib/onebot12-runtime-state.js +1 -1
  16. package/lib/protocol.d.ts +6 -22
  17. package/lib/protocol.js +44 -28
  18. package/lib/webhook.js +1 -5
  19. package/lib/ws-endpoint.js +16 -52
  20. package/lib/ws-transport.d.ts +12 -0
  21. package/lib/ws-transport.js +58 -0
  22. package/lib/ws-types.d.ts +2 -1
  23. package/lib/wss-endpoint.js +63 -92
  24. package/package.json +19 -14
  25. package/plugin.js +1 -1
  26. package/schema.json +19 -1
  27. package/src/client.ts +1 -1
  28. package/src/index.ts +0 -1
  29. package/src/onebot12-runtime-state.ts +1 -1
  30. package/src/protocol.ts +52 -51
  31. package/src/webhook.ts +1 -5
  32. package/src/ws-endpoint.ts +24 -60
  33. package/src/ws-transport.ts +88 -0
  34. package/src/ws-types.ts +3 -1
  35. package/src/wss-endpoint.ts +69 -99
  36. package/agent/skills/onebot12.md +0 -36
  37. package/commands/endpoint/add/[id].ts +0 -3
  38. package/commands/endpoint/list.ts +0 -3
  39. package/commands/endpoint/remove/[id].ts +0 -3
  40. package/lib/onebot12-endpoint-commands.d.ts +0 -1
@@ -0,0 +1,58 @@
1
+ import { clearTimeout, setTimeout } from 'node:timers';
2
+ import { formatCompact, getLogger } from '@zhin.js/logger';
3
+ import { WS_OPEN, } from './ws-types.js';
4
+ const logger = getLogger('onebot12');
5
+ export function handleOneBot12WsMessage(data, options) {
6
+ try {
7
+ const message = JSON.parse(decodeOneBot12WsPayload(data));
8
+ if ('echo' in message && typeof message.echo === 'string') {
9
+ const response = message;
10
+ const pending = options.pending.get(response.echo);
11
+ if (pending) {
12
+ options.pending.delete(response.echo);
13
+ clearTimeout(pending.timeout);
14
+ pending.resolve(response);
15
+ }
16
+ return;
17
+ }
18
+ options.ingest(message);
19
+ }
20
+ catch (error) {
21
+ logger.warn(formatCompact({
22
+ op: 'onebot12_parse_failed',
23
+ endpoint: options.endpointId,
24
+ error: error instanceof Error ? error.message : String(error),
25
+ }));
26
+ }
27
+ }
28
+ export function callOneBot12WsAction(ws, pending, requestId, action, params) {
29
+ if (!ws || ws.readyState !== WS_OPEN) {
30
+ return Promise.reject(new Error('WebSocket 未连接'));
31
+ }
32
+ const echo = `ob12_${++requestId.value}`;
33
+ const request = { action, params, echo };
34
+ return new Promise((resolve, reject) => {
35
+ const timeout = setTimeout(() => {
36
+ pending.delete(echo);
37
+ reject(new Error(`OneBot12 动作超时: ${action}`));
38
+ }, 30_000);
39
+ pending.set(echo, { resolve, reject, timeout });
40
+ ws.send(JSON.stringify(request));
41
+ });
42
+ }
43
+ export function rejectAllPending(pending, message = '连接已关闭') {
44
+ for (const [, entry] of pending) {
45
+ clearTimeout(entry.timeout);
46
+ entry.reject(new Error(message));
47
+ }
48
+ pending.clear();
49
+ }
50
+ function decodeOneBot12WsPayload(data) {
51
+ if (typeof data === 'string')
52
+ return data;
53
+ if (Buffer.isBuffer(data))
54
+ return data.toString();
55
+ if (data instanceof ArrayBuffer)
56
+ return new TextDecoder().decode(data);
57
+ return String(data ?? '');
58
+ }
package/lib/ws-types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { OneBot12ActionResponse } from './protocol.js';
1
2
  /** Minimal WS surface used by the endpoint (real `ws` or test mock). */
2
3
  export interface OneBot12WsSocket {
3
4
  readonly readyState: number;
@@ -10,7 +11,7 @@ export interface OneBot12WsCreateOptions {
10
11
  }
11
12
  export declare const WS_OPEN = 1;
12
13
  export interface OneBot12PendingAction {
13
- resolve: (value: unknown) => void;
14
+ resolve: (value: OneBot12ActionResponse) => void;
14
15
  reject: (err: Error) => void;
15
16
  timeout: NodeJS.Timeout;
16
17
  }
@@ -9,7 +9,7 @@ import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
9
9
  import { createOneBot12ContentPort } from './content-port.js';
10
10
  import { callOnebot12Client, createOnebot12EndpointClient, forwardOnebot12ClientEvents } from './client.js';
11
11
  import { verifyOneBotAccessToken } from './wss-auth.js';
12
- import { WS_OPEN } from './ws-types.js';
12
+ import { callOneBot12WsAction, handleOneBot12WsMessage, rejectAllPending, } from './ws-transport.js';
13
13
  export class OneBot12WssEndpoint extends ClientEndpoint {
14
14
  client;
15
15
  #logger;
@@ -19,17 +19,18 @@ export class OneBot12WssEndpoint extends ClientEndpoint {
19
19
  content;
20
20
  #ws;
21
21
  #wsRelease;
22
- #lifecycle;
23
- #requestId = 0;
22
+ #connectionLifecycle;
23
+ #connectionTask = Promise.resolve();
24
+ #requestId = { value: 0 };
24
25
  #pending = new Map();
26
+ #started = false;
25
27
  constructor(options) {
26
28
  super();
27
29
  this.#logger = getAdapterLogger('onebot12', options.config.id);
28
30
  this.#options = options;
29
- this.#lifecycle = createEndpointLifecycle({
30
- name: options.config.id,
31
+ this.#connectionLifecycle = createEndpointLifecycle({
32
+ name: `${options.config.id}:inbound`,
31
33
  reconnect: false,
32
- heartbeat: { intervalMs: options.config.heartbeat_interval },
33
34
  });
34
35
  this.client = createOnebot12EndpointClient(options.config, (action, params) => this.#callAction(action, params ?? {}));
35
36
  const callApi = (action, params) => callOnebot12Client(this.client, action, params);
@@ -41,6 +42,8 @@ export class OneBot12WssEndpoint extends ClientEndpoint {
41
42
  }, (_name, error) => this.#warnPlatformEvent(error));
42
43
  }
43
44
  async start() {
45
+ if (this.#started)
46
+ return;
44
47
  if (!this.#options.config.access_token) {
45
48
  // wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
46
49
  this.#logger.warn(formatCompact({
@@ -50,23 +53,19 @@ export class OneBot12WssEndpoint extends ClientEndpoint {
50
53
  error: 'missing access_token',
51
54
  }));
52
55
  }
53
- await this.#lifecycle.start(async (lifecycleHandle) => {
54
- const handle = this.#options.http.ws(this.#options.config.path);
55
- this.#wsRelease = handle.onConnection((connection) => {
56
- this.#acceptConnection(connection);
57
- });
58
- lifecycleHandle.onForceClose(() => {
59
- this.#wsRelease?.();
60
- this.#wsRelease = undefined;
61
- try {
62
- this.#ws?.close();
63
- }
64
- catch {
65
- /* ignore */
66
- }
67
- this.#ws = undefined;
56
+ const handle = this.#options.http.ws(this.#options.config.path);
57
+ this.#wsRelease = handle.onConnection((connection) => {
58
+ this.#connectionTask = this.#connectionTask
59
+ .then(() => this.#acceptConnection(connection))
60
+ .catch((error) => {
61
+ this.#logger.warn(formatCompact({
62
+ op: 'wss_connection_failed',
63
+ endpoint: this.#options.config.id,
64
+ error: error instanceof Error ? error.message : String(error),
65
+ }));
68
66
  });
69
67
  });
68
+ this.#started = true;
70
69
  this.#logger.info(formatCompact({
71
70
  op: 'listen',
72
71
  endpoint: this.#options.config.id,
@@ -76,12 +75,13 @@ export class OneBot12WssEndpoint extends ClientEndpoint {
76
75
  }
77
76
  async stop() {
78
77
  this.close();
79
- await this.#lifecycle.stop();
80
- for (const [, pending] of this.#pending) {
81
- clearTimeout(pending.timeout);
82
- pending.reject(new Error('连接已关闭'));
83
- }
84
- this.#pending.clear();
78
+ this.#wsRelease?.();
79
+ this.#wsRelease = undefined;
80
+ await this.#connectionTask;
81
+ await this.#connectionLifecycle.stop();
82
+ rejectAllPending(this.#pending);
83
+ this.#ws = undefined;
84
+ this.#started = false;
85
85
  }
86
86
  async send({ conversation, payload }) {
87
87
  const materialized = await uploadOneBot12MediaSegments(payload, (action, params) => callOnebot12Client(this.client, action, params), (error) => {
@@ -140,33 +140,46 @@ export class OneBot12WssEndpoint extends ClientEndpoint {
140
140
  error: error instanceof Error ? error.message : String(error),
141
141
  }));
142
142
  }
143
- #acceptConnection(connection) {
143
+ async #acceptConnection(connection) {
144
144
  if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
145
145
  connection.socket.close(4003, 'Unauthorized');
146
146
  return;
147
147
  }
148
148
  const socket = connection.socket;
149
- if (this.#ws) {
150
- try {
151
- this.#ws.close();
152
- }
153
- catch {
154
- /* ignore */
155
- }
156
- }
157
- this.#ws = socket;
158
- this.#lifecycle.startHeartbeat(() => {
159
- this.#callAction('get_status', {}).catch(() => { });
160
- });
161
- socket.on('message', (data) => {
162
- this.#lifecycle.notifyHeartbeatAck();
163
- this.#onMessage(data);
164
- });
165
- socket.on('close', () => {
166
- if (this.#ws === socket) {
167
- this.#ws = undefined;
168
- this.#lifecycle.stopHeartbeat();
169
- }
149
+ await this.#connectionLifecycle.stop();
150
+ rejectAllPending(this.#pending, '连接已替换');
151
+ await this.#connectionLifecycle.start(async (lifecycleHandle) => {
152
+ this.#ws = socket;
153
+ lifecycleHandle.onForceClose(() => {
154
+ try {
155
+ socket.close();
156
+ }
157
+ catch {
158
+ /* ignore */
159
+ }
160
+ if (this.#ws === socket)
161
+ this.#ws = undefined;
162
+ });
163
+ this.#connectionLifecycle.startHeartbeat(() => {
164
+ this.#callAction('get_status', {}).catch(() => { });
165
+ }, this.#options.config.heartbeat_interval);
166
+ socket.on('message', (data) => {
167
+ if (this.#ws !== socket)
168
+ return;
169
+ this.#connectionLifecycle.notifyHeartbeatAck();
170
+ handleOneBot12WsMessage(data, {
171
+ endpointId: this.#options.config.id,
172
+ pending: this.#pending,
173
+ ingest: (event) => this.client.ingest(event),
174
+ });
175
+ });
176
+ socket.on('close', () => {
177
+ if (this.#ws === socket) {
178
+ this.#ws = undefined;
179
+ rejectAllPending(this.#pending);
180
+ }
181
+ lifecycleHandle.notifyClosed(new Error('OneBot12 reverse WebSocket closed'));
182
+ });
170
183
  });
171
184
  this.#logger.debug(formatCompact({
172
185
  endpoint: this.#options.config.id,
@@ -174,49 +187,7 @@ export class OneBot12WssEndpoint extends ClientEndpoint {
174
187
  peer: connection.request.socket.remoteAddress,
175
188
  }));
176
189
  }
177
- #onMessage(data) {
178
- try {
179
- const raw = typeof data === 'string'
180
- ? data
181
- : Buffer.isBuffer(data)
182
- ? data.toString()
183
- : data instanceof ArrayBuffer
184
- ? new TextDecoder().decode(data)
185
- : String(data ?? '');
186
- const msg = JSON.parse(raw);
187
- if ('echo' in msg && typeof msg.echo === 'string') {
188
- const resp = msg;
189
- const pending = this.#pending.get(resp.echo);
190
- if (pending) {
191
- this.#pending.delete(resp.echo);
192
- clearTimeout(pending.timeout);
193
- pending.resolve(resp);
194
- }
195
- return;
196
- }
197
- this.client.ingest(msg);
198
- }
199
- catch (error) {
200
- this.#logger.warn(formatCompact({
201
- op: 'onebot12_parse_failed',
202
- endpoint: this.#options.config.id,
203
- error: error instanceof Error ? error.message : String(error),
204
- }));
205
- }
206
- }
207
190
  #callAction(action, params) {
208
- if (!this.#ws || this.#ws.readyState !== WS_OPEN) {
209
- return Promise.reject(new Error('WebSocket 未连接'));
210
- }
211
- const echo = `ob12_${++this.#requestId}`;
212
- const req = { action, params, echo };
213
- return new Promise((resolve, reject) => {
214
- const timeout = setTimeout(() => {
215
- this.#pending.delete(echo);
216
- reject(new Error(`OneBot12 动作超时: ${action}`));
217
- }, 30_000);
218
- this.#pending.set(echo, { resolve, reject, timeout });
219
- this.#ws.send(JSON.stringify(req));
220
- });
191
+ return callOneBot12WsAction(this.#ws, this.#pending, this.#requestId, action, params);
221
192
  }
222
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-onebot12",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Zhin.js OneBot 12 adapter for Plugin Runtime (WebSocket client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -19,7 +19,7 @@
19
19
  "schema.json",
20
20
  "src",
21
21
  "lib",
22
- "agent",
22
+ "skills",
23
23
  "README.md",
24
24
  "CHANGELOG.md"
25
25
  ],
@@ -42,26 +42,27 @@
42
42
  "@imhelper/onebot-v12": "1.0.7",
43
43
  "imhelper": "1.0.7",
44
44
  "ws": "^8.21.1",
45
- "@zhin.js/adapter": "1.1.12",
46
- "@zhin.js/core": "1.1.35",
47
- "@zhin.js/feature-kit": "1.1.0",
48
- "@zhin.js/host-http": "1.1.0",
49
- "@zhin.js/im-contract": "1.1.0",
50
- "@zhin.js/logger": "1.1.0"
45
+ "@zhin.js/adapter": "1.1.14",
46
+ "@zhin.js/core": "1.1.37",
47
+ "@zhin.js/feature-kit": "1.1.1",
48
+ "@zhin.js/host-http": "1.1.1",
49
+ "@zhin.js/im-contract": "1.1.1",
50
+ "@zhin.js/logger": "1.1.1",
51
+ "@zhin.js/skill": "1.1.1"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@types/node": "^26.1.2",
54
55
  "@types/ws": "^8.18.1",
55
56
  "typescript": "^6.0.3",
56
57
  "vitest": "^4.1.10",
57
- "@zhin.js/host-http": "1.1.0",
58
- "zhin.js": "1.1.0"
58
+ "@zhin.js/host-http": "1.1.1",
59
+ "zhin.js": "1.1.2"
59
60
  },
60
61
  "peerDependencies": {
61
- "@zhin.js/adapter": "^1.1.12",
62
- "@zhin.js/command": "^1.1.0",
63
- "@zhin.js/core": "^1.1.35",
64
- "zhin.js": "^1.1.0"
62
+ "@zhin.js/adapter": "^1.1.14",
63
+ "@zhin.js/command": "^1.1.1",
64
+ "@zhin.js/core": "^1.1.37",
65
+ "zhin.js": "^1.1.2"
65
66
  },
66
67
  "peerDependenciesMeta": {
67
68
  "@zhin.js/command": {
@@ -97,6 +98,10 @@
97
98
  {
98
99
  "package": "@zhin.js/command",
99
100
  "api": "^1.0.0"
101
+ },
102
+ {
103
+ "package": "@zhin.js/skill",
104
+ "api": "^1.0.0"
100
105
  }
101
106
  ],
102
107
  "plugins": []
package/plugin.js CHANGED
@@ -8,7 +8,7 @@ export default definePlugin({
8
8
  displayName: 'OneBot 12 Adapter',
9
9
  },
10
10
  setup(context) {
11
- // 运行中 endpoint 注册表(onebot12.endpoint list 的"运行中"数据源)
11
+ // 运行中 endpoint 注册表(onebot12 endpoint list 的"运行中"数据源)
12
12
  context.resources.provide(onebot12RuntimeStateToken, createEndpointRuntimeState());
13
13
  },
14
14
  });
package/schema.json CHANGED
@@ -41,10 +41,11 @@
41
41
  },
42
42
  "endpoints": {
43
43
  "type": "array",
44
+ "minItems": 1,
44
45
  "description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
45
46
  "items": {
46
47
  "type": "object",
47
- "additionalProperties": true,
48
+ "additionalProperties": false,
48
49
  "properties": {
49
50
  "master": {
50
51
  "type": [
@@ -64,6 +65,14 @@
64
65
  },
65
66
  "description": "本 endpoint 的 trusted 列表"
66
67
  },
68
+ "connection": {
69
+ "type": "string",
70
+ "enum": [
71
+ "ws",
72
+ "webhook",
73
+ "wss"
74
+ ]
75
+ },
67
76
  "url": {
68
77
  "type": "string",
69
78
  "description": "OneBot implementation WebSocket URL (required for connection: ws)"
@@ -80,6 +89,15 @@
80
89
  "type": "string",
81
90
  "description": "OneBot access token"
82
91
  },
92
+ "reconnect_interval": {
93
+ "type": "number"
94
+ },
95
+ "heartbeat_interval": {
96
+ "type": "number"
97
+ },
98
+ "commandPrefix": {
99
+ "type": "string"
100
+ },
83
101
  "id": {
84
102
  "type": "string",
85
103
  "description": "OneBot12 bot name"
package/src/client.ts CHANGED
@@ -31,7 +31,7 @@ export function createOnebot12EndpointClient(
31
31
  ): OneBotV12Client {
32
32
  const baseUrl = config.connection === 'ws'
33
33
  ? config.url.replace(/^ws(s?):/, 'http$1:')
34
- : config.connection === 'webhook' && config.api_url
34
+ : config.connection === 'webhook'
35
35
  ? config.api_url
36
36
  : 'http://localhost';
37
37
  return new OneBotV12Client({
package/src/index.ts CHANGED
@@ -16,7 +16,6 @@ export {
16
16
  uploadOneBot12MediaSegments,
17
17
  type OneBot12ActionRequest,
18
18
  type OneBot12ActionResponse,
19
- type OneBot12AdapterConfig,
20
19
  type OneBot12CallAction,
21
20
  type OneBot12ConfigBase,
22
21
  type OneBot12EndpointConfig,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * OneBot12 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `onebot12.endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `onebot12 endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
6
 
package/src/protocol.ts CHANGED
@@ -3,37 +3,21 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  * Spec: https://12.onebot.dev/
5
5
  */
6
- import { isMediaRef, type MediaRef } from '@zhin.js/core';
7
- import type { ConversationRef } from '@zhin.js/im-contract';
6
+ import { isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
8
7
  import { formatCompact, getLogger } from '@zhin.js/logger';
9
8
 
10
9
  const logger = getLogger('onebot12');
11
10
 
12
- /** Transitional legacy endpoint row (`endpoints[]` with `context: onebot12`). */
13
- export interface OneBot12LegacyEndpointRow {
14
- readonly context?: string;
11
+ /** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
12
+ export interface OneBot12EndpointConfig {
15
13
  readonly connection?: 'ws' | 'webhook' | 'wss';
16
- readonly id?: string;
17
- readonly access_token?: string;
18
- readonly url?: string;
19
- readonly path?: string;
20
- readonly api_url?: string;
21
- readonly reconnect_interval?: number;
22
- readonly heartbeat_interval?: number;
23
- }
24
-
25
- /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
26
- export interface OneBot12AdapterConfig {
27
- readonly connection?: 'ws' | 'webhook' | 'wss';
28
- readonly id?: string;
14
+ readonly id: string;
29
15
  readonly access_token?: string;
30
16
  readonly url?: string;
31
17
  readonly path?: string;
32
18
  readonly api_url?: string;
33
19
  readonly reconnect_interval?: number;
34
20
  readonly heartbeat_interval?: number;
35
- /** Transitional: legacy root `endpoints[]` with `context: onebot12`. */
36
- readonly endpoints?: ReadonlyArray<OneBot12LegacyEndpointRow>;
37
21
  }
38
22
 
39
23
  export interface OneBot12ConfigBase {
@@ -54,7 +38,7 @@ export interface OneBot12WsConfig extends OneBot12ConfigBase {
54
38
  export interface OneBot12WebhookConfig extends OneBot12ConfigBase {
55
39
  readonly connection: 'webhook';
56
40
  readonly path: string;
57
- readonly api_url?: string;
41
+ readonly api_url: string;
58
42
  }
59
43
 
60
44
  /** 反向 WebSocket:httpHostToken WS upgrade 入站/出站 */
@@ -65,7 +49,6 @@ export interface OneBot12WssConfig extends OneBot12ConfigBase {
65
49
  }
66
50
 
67
51
  export type ResolvedOneBot12Config = OneBot12WsConfig | OneBot12WebhookConfig | OneBot12WssConfig;
68
- export type OneBot12EndpointConfig = ResolvedOneBot12Config;
69
52
 
70
53
  export interface OneBot12Self {
71
54
  readonly platform: string;
@@ -119,68 +102,67 @@ export interface OneBot12WireSegment {
119
102
  readonly data?: Record<string, unknown>;
120
103
  }
121
104
 
122
- export function resolveOneBot12Config(config: OneBot12AdapterConfig = {}): ResolvedOneBot12Config {
123
- const entry = config.endpoints?.find((item) => item.context === 'onebot12');
124
- const connection = config.connection
125
- ?? entry?.connection
126
- ?? 'ws';
127
- const id = (typeof config.id === 'string' && config.id)
128
- || (typeof entry?.id === 'string' && entry.id)
129
- || process.env.ONEBOT12_BOT_NAME
130
- || 'onebot12-bot';
131
- const access_token = config.access_token ?? entry?.access_token;
105
+ export function resolveOneBot12Config(config: OneBot12EndpointConfig): ResolvedOneBot12Config {
106
+ const connection = config.connection ?? 'ws';
107
+ const id = requiredEndpointField(config.id, 'id');
108
+ const access_token = optionalEndpointField(config.access_token);
132
109
 
133
110
  if (connection === 'ws') {
134
- const url = config.url ?? entry?.url;
135
- if (!url) {
136
- throw new TypeError(
137
- 'OneBot12 connection:ws requires url (plugins.<key>.url or endpoints with context: onebot12)',
138
- );
139
- }
111
+ const url = requiredEndpointField(config.url, 'url');
140
112
  return {
141
113
  context: 'onebot12',
142
114
  connection: 'ws',
143
115
  id,
144
116
  access_token,
145
117
  url,
146
- reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
147
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
118
+ reconnect_interval: config.reconnect_interval ?? 5000,
119
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
148
120
  };
149
121
  }
150
122
 
151
123
  if (connection === 'webhook') {
152
- const path = config.path ?? entry?.path;
153
- if (!path) {
154
- throw new TypeError('OneBot12 connection:webhook requires path');
155
- }
124
+ const path = requiredEndpointField(config.path, 'path');
125
+ const api_url = requiredEndpointField(config.api_url, 'api_url');
156
126
  return {
157
127
  context: 'onebot12',
158
128
  connection: 'webhook',
159
129
  id,
160
130
  access_token,
161
131
  path,
162
- api_url: config.api_url ?? entry?.api_url,
132
+ api_url,
163
133
  };
164
134
  }
165
135
 
166
136
  if (connection === 'wss') {
167
- const path = config.path ?? entry?.path;
168
- if (!path) {
169
- throw new TypeError('OneBot12 connection:wss requires path');
170
- }
137
+ const path = requiredEndpointField(config.path, 'path');
171
138
  return {
172
139
  context: 'onebot12',
173
140
  connection: 'wss',
174
141
  id,
175
142
  access_token,
176
143
  path,
177
- heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
144
+ heartbeat_interval: config.heartbeat_interval ?? 30_000,
178
145
  };
179
146
  }
180
147
 
181
148
  throw new TypeError(`Unknown OneBot12 connection: ${String(connection)}`);
182
149
  }
183
150
 
151
+ function requiredEndpointField(
152
+ value: unknown,
153
+ field: 'id' | 'url' | 'path' | 'api_url',
154
+ ): string {
155
+ const resolved = optionalEndpointField(value);
156
+ if (!resolved) {
157
+ throw new TypeError(`OneBot12 endpoint requires a non-empty ${field}`);
158
+ }
159
+ return resolved;
160
+ }
161
+
162
+ function optionalEndpointField(value: unknown): string | undefined {
163
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
164
+ }
165
+
184
166
  /** 判断是否为消息事件(type=message) */
185
167
  export function isMessageEvent(
186
168
  ev: OneBot12Event,
@@ -234,10 +216,29 @@ export function formatInboundContent(ev: OneBot12Event): string {
234
216
  .join('');
235
217
  }
236
218
  return typeof ev.alt_message === 'string'
237
- ? ev.alt_message.replace(/\[[^\]]*(?:image|audio|video|file)[^\]]*\]/gi, '').trim()
219
+ ? stripMediaPlaceholders(ev.alt_message).trim()
238
220
  : '';
239
221
  }
240
222
 
223
+ function stripMediaPlaceholders(input: string): string {
224
+ const mediaNames = ['image', 'audio', 'video', 'file'];
225
+ let result = '';
226
+ let cursor = 0;
227
+ while (cursor < input.length) {
228
+ const start = input.indexOf('[', cursor);
229
+ if (start < 0) return result + input.slice(cursor);
230
+ const end = input.indexOf(']', start + 1);
231
+ if (end < 0) return result + input.slice(cursor);
232
+ result += input.slice(cursor, start);
233
+ const placeholder = input.slice(start + 1, end).toLowerCase();
234
+ if (!mediaNames.some((name) => placeholder.includes(name))) {
235
+ result += input.slice(start, end + 1);
236
+ }
237
+ cursor = end + 1;
238
+ }
239
+ return result;
240
+ }
241
+
241
242
  /**
242
243
  * Runtime Message sender 必须是用户 ID(agent bridge 以 sender 与 endpointMaster 比对)。
243
244
  * 显示名经 {@link senderNickname} 放入 metadata.nickname。
package/src/webhook.ts CHANGED
@@ -131,12 +131,8 @@ export class OneBot12WebhookEndpoint extends ClientEndpoint<Onebot12Client> {
131
131
  action: string,
132
132
  params: Record<string, unknown> = {},
133
133
  ): Promise<import('@imhelper/onebot-v12').OneBotV12Response> {
134
- const apiUrl = this.#options.config.api_url;
135
- if (!apiUrl) {
136
- throw new Error('OneBot12 connection:webhook requires api_url for outbound api');
137
- }
138
134
  return this.#callAction(
139
- { url: apiUrl, access_token: this.#options.config.access_token },
135
+ { url: this.#options.config.api_url, access_token: this.#options.config.access_token },
140
136
  action,
141
137
  params,
142
138
  );