@zhin.js/adapter-onebot11 1.1.25 → 1.1.27

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.
@@ -2,7 +2,7 @@
2
2
  * OneBot11 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
3
3
  */
4
4
  import { endpointSpec, type Endpoint, type EndpointSpecification } from 'zhin.js/adapter';
5
- import type { HttpHost } from '@zhin.js/host-http';
5
+ import { type HttpHost } from '@zhin.js/host-http';
6
6
  import type { CapabilityId } from 'zhin.js';
7
7
  import { type OneBot11WssConfig } from './protocol.js';
8
8
  import { Onebot11Client } from './client.js';
@@ -4,13 +4,14 @@ import { EndpointDeliveryError } from '@zhin.js/im-contract';
4
4
  * OneBot11 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
5
5
  */
6
6
  import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
7
+ import { verifyProtocolAccessToken } from '@zhin.js/host-http';
7
8
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
8
9
  import { createOneBot11EndpointManagement } from './endpoint-management.js';
9
- import { buildSendAction, formatInboundContent, formatInboundMetadata, formatOutboundSegments, isMessageEvent, onebot11InboundConversation, senderDisplayName, senderUserId, } from './protocol.js';
10
+ import { buildSendAction, formatInboundContent, formatInboundMetadata, formatOutboundSegments, isMessageEvent, onebot11InboundConversation, senderDisplayName, onebot11SenderStanding, senderUserId, } from './protocol.js';
10
11
  import { receiveOneBot11SideEvent } from './side-event-dispatch.js';
11
12
  import { createOneBot11ContentPort } from './content-port.js';
12
- import { verifyOneBotAccessToken } from './wss-auth.js';
13
13
  import { createOneBot11Rpc } from './ws-transport.js';
14
+ import { socketOfConnection } from './ws-types.js';
14
15
  import { probeOnebot11SelfId, resolveOnebot11SelfId } from './self-id.js';
15
16
  import { callOnebot11Client, readOnebot11Response, onebot11ClientOptions, forwardOnebot11ClientEvents, Onebot11Client } from './client.js';
16
17
  export class OneBot11WssEndpoint extends Onebot11Client {
@@ -83,15 +84,16 @@ export class OneBot11WssEndpoint extends Onebot11Client {
83
84
  return;
84
85
  this.#stopped = false;
85
86
  if (!this.#options.config.access_token) {
86
- // wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
87
+ // 未配 access_token 时只接受本机回环连接(verifyProtocolAccessToken)。
87
88
  this.#logger.warn(formatCompact({
88
89
  endpoint: this.#options.config.id,
89
90
  mode: 'wss',
90
91
  ok: false,
91
- error: 'missing access_token',
92
+ error: 'missing access_token: only loopback peers are accepted',
92
93
  }));
93
94
  }
94
- const handle = this.#options.http.ws(this.#options.config.path);
95
+ // The peer authenticates with this endpoint's own access_token, not a Console credential.
96
+ const handle = this.#options.http.ws(this.#options.config.path, { auth: 'protocol' });
95
97
  this.#wsRelease = handle.onConnection((connection) => {
96
98
  this.#connectionTask = this.#connectionTask
97
99
  .then(() => this.#acceptConnection(connection))
@@ -158,6 +160,7 @@ export class OneBot11WssEndpoint extends Onebot11Client {
158
160
  }
159
161
  const conversation = { ...onebot11InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
160
162
  const inbound = formatInboundMetadata(ev, this.#options.config.id);
163
+ const platform = onebot11SenderStanding(ev);
161
164
  void this[endpointSpec].events.message({
162
165
  event: ev,
163
166
  ...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
@@ -167,7 +170,7 @@ export class OneBot11WssEndpoint extends Onebot11Client {
167
170
  sender: {
168
171
  id: senderUserId(ev),
169
172
  name: senderDisplayName(ev) || undefined,
170
- ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
173
+ ...(platform ? { platform } : {}),
171
174
  },
172
175
  endpointId: inbound.endpointId,
173
176
  ...(inbound.mentioned ? { mentioned: true } : {}),
@@ -203,14 +206,14 @@ export class OneBot11WssEndpoint extends Onebot11Client {
203
206
  }
204
207
  async #acceptConnection(connection) {
205
208
  if (!this.#started || this.#stopped) {
206
- connection.socket.close();
209
+ connection.close();
207
210
  return;
208
211
  }
209
- if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
210
- connection.socket.close(4003, 'Unauthorized');
212
+ if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
213
+ connection.close(4003, 'Unauthorized');
211
214
  return;
212
215
  }
213
- const socket = connection.socket;
216
+ const socket = socketOfConnection(connection);
214
217
  await this.#connectionLifecycle.stop();
215
218
  this.#rpc.disconnect('连接已替换');
216
219
  await this.#connectionLifecycle.start(async (lifecycleHandle) => {
@@ -229,18 +232,7 @@ export class OneBot11WssEndpoint extends Onebot11Client {
229
232
  if (this.#ws === socket)
230
233
  this.#ws = undefined;
231
234
  });
232
- this.#connectionLifecycle.startHeartbeat(() => {
233
- try {
234
- socket.ping?.();
235
- }
236
- catch {
237
- /* ignore */
238
- }
239
- }, this.#options.config.heartbeat_interval);
240
- socket.on('pong', () => {
241
- if (this.#ws === socket)
242
- this.#connectionLifecycle.notifyHeartbeatAck();
243
- });
235
+ // Dead-peer detection is the Host transport's keepalive (protocol ping / pong).
244
236
  socket.on('message', (data) => {
245
237
  if (this.#ws !== socket || this.#connectionLifecycle.state === 'closed')
246
238
  return;
@@ -267,7 +259,7 @@ export class OneBot11WssEndpoint extends Onebot11Client {
267
259
  this.#logger.debug(formatCompact({
268
260
  endpoint: this.#options.config.id,
269
261
  mode: 'wss',
270
- peer: connection.request.socket.remoteAddress,
262
+ peer: connection.remoteAddress,
271
263
  }));
272
264
  }
273
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-onebot11",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
4
4
  "description": "Zhin.js OneBot 11 adapter for Plugin Runtime (WebSocket client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -19,7 +19,6 @@
19
19
  "schema.json",
20
20
  "src",
21
21
  "lib",
22
- "tools",
23
22
  "agents",
24
23
  "README.md",
25
24
  "CHANGELOG.md",
@@ -38,42 +37,41 @@
38
37
  "license": "MIT",
39
38
  "repository": {
40
39
  "type": "git",
41
- "url": "https://github.com/zhinjs/zhin",
40
+ "url": "git+https://github.com/zhinjs/zhin.git",
42
41
  "directory": "plugins/adapters/onebot11"
43
42
  },
44
43
  "dependencies": {
45
44
  "@imhelper/onebot-v11": "1.0.7",
46
45
  "imhelper": "1.0.7",
47
46
  "ws": "^8.21.1",
48
- "@zhin.js/adapter-onebot-events": "0.0.4",
49
- "@zhin.js/adapter": "1.1.31",
50
- "@zhin.js/agent-feature": "1.1.16",
51
- "@zhin.js/console-contract": "1.1.5",
52
- "@zhin.js/core": "1.1.59",
53
- "@zhin.js/feature-kit": "1.1.15",
54
- "@zhin.js/host-http": "1.1.17",
55
- "@zhin.js/im-contract": "1.1.13",
56
- "@zhin.js/logger": "1.1.2",
57
- "@zhin.js/page": "1.1.17",
58
- "@zhin.js/permission": "1.1.15",
59
- "@zhin.js/skill": "1.1.16",
60
- "@zhin.js/tool": "1.1.18"
47
+ "zod": "^4.6.7",
48
+ "@zhin.js/adapter": "1.1.33",
49
+ "@zhin.js/adapter-onebot-events": "0.0.6",
50
+ "@zhin.js/agent-feature": "1.1.17",
51
+ "@zhin.js/console-contract": "1.1.6",
52
+ "@zhin.js/core": "1.2.0",
53
+ "@zhin.js/feature-kit": "1.1.16",
54
+ "@zhin.js/host-http": "1.2.0",
55
+ "@zhin.js/im-contract": "1.1.14",
56
+ "@zhin.js/logger": "1.1.3",
57
+ "@zhin.js/page": "1.1.18",
58
+ "@zhin.js/permission": "1.2.0",
59
+ "@zhin.js/skill": "1.1.17",
60
+ "@zhin.js/tool": "1.1.19"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^26.1.2",
64
64
  "@types/ws": "^8.18.1",
65
65
  "typescript": "^6.0.3",
66
66
  "vitest": "^4.1.10",
67
- "zod": "^4.4.3",
68
- "@zhin.js/host-http": "1.1.17",
69
- "zhin.js": "1.1.24"
67
+ "@zhin.js/host-http": "1.2.0",
68
+ "zhin.js": "1.1.26"
70
69
  },
71
70
  "peerDependencies": {
72
- "zod": "^4.0.0",
73
- "@zhin.js/adapter": "^1.1.31",
74
- "@zhin.js/command": "^1.1.17",
75
- "@zhin.js/core": "^1.1.59",
76
- "zhin.js": "^1.1.24"
71
+ "@zhin.js/adapter": "^1.1.33",
72
+ "@zhin.js/command": "^1.2.0",
73
+ "@zhin.js/core": "^1.2.0",
74
+ "zhin.js": "^1.1.26"
77
75
  },
78
76
  "peerDependenciesMeta": {
79
77
  "@zhin.js/command": {
@@ -81,9 +79,6 @@
81
79
  },
82
80
  "zhin.js": {
83
81
  "optional": true
84
- },
85
- "zod": {
86
- "optional": true
87
82
  }
88
83
  },
89
84
  "author": {
@@ -135,6 +130,7 @@
135
130
  "scripts": {
136
131
  "build": "tsc",
137
132
  "clean": "rimraf lib",
138
- "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/onebot11/tests"
133
+ "test": "pnpm run test:types && vitest run --root ../../.. plugins/adapters/onebot11/tests",
134
+ "test:types": "tsc -p tsconfig.type-tests.json"
139
135
  }
140
136
  }
package/plugin.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
2
  import { definePlugin } from 'zhin.js';
3
- import { permissionHostToken, createSceneRolePlatformChecker } from '@zhin.js/permission';
4
3
  import { createOnebot11ConsoleRpc } from "./lib/console.js";
5
4
  export default definePlugin({
6
5
  name: 'onebot11',
@@ -9,12 +8,8 @@ export default definePlugin({
9
8
  displayName: 'OneBot 11 Adapter',
10
9
  },
11
10
  setup(context) {
12
- const dispose = context.resources.has(permissionHostToken)
13
- ? context.resources.use(permissionHostToken).registerPlatform('onebot11', createSceneRolePlatformChecker())
14
- : undefined;
15
11
  return {
16
12
  consoleRpc: createOnebot11ConsoleRpc(context.resources, () => context.config.get()),
17
- ...(dispose ? { dispose } : {}),
18
13
  };
19
14
  },
20
15
  });
package/schema.json CHANGED
@@ -20,34 +20,6 @@
20
20
  "type": "number",
21
21
  "default": 30000
22
22
  },
23
- "master": {
24
- "type": [
25
- "string",
26
- "number"
27
- ],
28
- "description": "框架 master(platform user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖",
29
- "x-i18n": {
30
- "en": {
31
- "description": "Framework masters (platform user id; AI/tool permissions and endpoint management). endpoints[i].master can override this value."
32
- }
33
- }
34
- },
35
- "trusted": {
36
- "type": "array",
37
- "items": {
38
- "type": [
39
- "string",
40
- "number"
41
- ],
42
- "description": "Trusted platform user id"
43
- },
44
- "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
45
- "x-i18n": {
46
- "en": {
47
- "description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
48
- }
49
- }
50
- },
51
23
  "endpoints": {
52
24
  "type": "array",
53
25
  "minItems": 1,
@@ -56,34 +28,6 @@
56
28
  "type": "object",
57
29
  "additionalProperties": false,
58
30
  "properties": {
59
- "master": {
60
- "type": [
61
- "string",
62
- "number"
63
- ],
64
- "description": "本 endpoint 的框架 master(platform user id);覆盖顶层 master",
65
- "x-i18n": {
66
- "en": {
67
- "description": "Framework masters for this endpoint (platform user id), overriding the top-level master list."
68
- }
69
- }
70
- },
71
- "trusted": {
72
- "type": "array",
73
- "items": {
74
- "type": [
75
- "string",
76
- "number"
77
- ],
78
- "description": "Trusted platform user id"
79
- },
80
- "description": "本 endpoint 的 trusted 列表",
81
- "x-i18n": {
82
- "en": {
83
- "description": "Trusted users for this endpoint."
84
- }
85
- }
86
- },
87
31
  "connection": {
88
32
  "type": "string",
89
33
  "enum": [
@@ -7,9 +7,38 @@ export const onebot11ConsoleSpec = {
7
7
  adapterKey: 'onebot11',
8
8
  adapterDisplayName: 'OneBot 11',
9
9
  fields: [
10
- { key: 'url', description: 'OneBot 实现 WebSocket URL(connection: ws 必填)', descriptionI18n: { en: 'OneBot WebSocket URL (required for connection: ws)' } },
11
- { key: 'path', description: 'reverse-wss 路径(connection: wss)', descriptionI18n: { en: 'Reverse WebSocket path (connection: wss)' } },
12
- { key: 'access_token', env: true, description: 'OneBot access token' },
10
+ {
11
+ key: 'url',
12
+ label: '正向 WebSocket 地址',
13
+ labelI18n: { en: 'Forward WebSocket URL' },
14
+ group: '连接模式',
15
+ groupI18n: { en: 'Connection Mode' },
16
+ placeholder: 'ws://127.0.0.1:3001',
17
+ description: 'OneBot 实现 WebSocket URL(connection: ws 必填)',
18
+ descriptionI18n: { en: 'OneBot WebSocket URL (required for connection: ws)' },
19
+ },
20
+ {
21
+ key: 'path',
22
+ label: '反向 WebSocket 路径',
23
+ labelI18n: { en: 'Reverse WebSocket Path' },
24
+ group: '连接模式',
25
+ groupI18n: { en: 'Connection Mode' },
26
+ placeholder: '/onebot/v11/ws',
27
+ description: 'reverse-wss 路径(connection: wss)',
28
+ descriptionI18n: { en: 'Reverse WebSocket path (connection: wss)' },
29
+ },
30
+ {
31
+ key: 'access_token',
32
+ env: true,
33
+ label: 'Access Token (访问令牌)',
34
+ labelI18n: { en: 'Access Token' },
35
+ group: '安全认证',
36
+ groupI18n: { en: 'Authentication' },
37
+ placeholder: '与 OneBot 协议端一致的 Token',
38
+ placeholderI18n: { en: 'Token matching OneBot server config' },
39
+ description: 'OneBot 11 接口鉴权令牌(Access Token)',
40
+ descriptionI18n: { en: 'OneBot access token' },
41
+ },
13
42
  ] as readonly AdapterConsoleField[],
14
43
  docsUrl: 'https://zhin.js.org/adapters/onebot11',
15
44
  readPermission: 'onebot11:read',
@@ -15,16 +15,49 @@ import {
15
15
  type EndpointGroupRequest,
16
16
  type PendingRpcRequest,
17
17
  } from 'zhin.js/adapter';
18
+ import { standingFromMemberRole, type PlatformStanding, type SelfStandingRequest } from '@zhin.js/im-contract';
18
19
 
19
20
  /** 管理面只依赖 endpoint 的 callApi(ws/wss 两种传输各自实现)。 */
20
21
  export interface OneBot11ManagementCaller {
21
22
  callApi(request: PendingRpcRequest): Promise<unknown>;
22
23
  }
23
24
 
25
+ /** How long one `get_login_info` answer is reused (bursts only; see inspectSelfStanding). */
26
+ const SELF_ID_TTL_MS = 5_000;
27
+
24
28
  export function createOneBot11EndpointManagement(
25
29
  endpoint: OneBot11ManagementCaller,
26
30
  ): EndpointManagement {
31
+ // The login account can change across reconnects (or a different account on the same
32
+ // connection), so `get_login_info` is not remembered for the endpoint's lifetime: only briefly,
33
+ // to coalesce bursts, and never across a failed lookup.
34
+ let login: { readonly id: string; readonly at: number } | undefined;
35
+ const loginId = async (signal: AbortSignal | undefined): Promise<string | undefined> => {
36
+ if (login && Date.now() - login.at < SELF_ID_TTL_MS) return login.id;
37
+ login = undefined;
38
+ const info = asRecord(await endpoint.callApi({ action: 'get_login_info', signal }));
39
+ const id = info.user_id;
40
+ const value = (typeof id === 'number' && id > 0) || (typeof id === 'string' && id.trim() !== '' && id !== '0') ? String(id) : undefined;
41
+ if (value) login = { id: value, at: Date.now() };
42
+ return value;
43
+ };
27
44
  return Object.freeze<EndpointManagement>({
45
+ /** ADR 0056 §1: the Bot's own group role (`get_login_info`, then `get_group_member_info` self). */
46
+ async inspectSelfStanding({ conversation, signal }: SelfStandingRequest): Promise<PlatformStanding | undefined> {
47
+ if (conversation.kind !== 'group') return undefined;
48
+ const selfId = await loginId(signal);
49
+ if (!selfId) return undefined;
50
+ let member: Record<string, unknown>;
51
+ try {
52
+ member = asRecord(await endpoint.callApi({ action: 'get_group_member_info', signal, params: {
53
+ group_id: toNumberId(conversation.id, 'group_id'), user_id: toNumberId(selfId, 'user_id'), no_cache: true,
54
+ } }));
55
+ } catch (error) {
56
+ login = undefined;
57
+ throw error;
58
+ }
59
+ return standingFromMemberRole(member.role);
60
+ },
28
61
  async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
29
62
  const data = await endpoint.callApi({ action: 'get_friend_list', signal: request.signal });
30
63
  return toArray(data).map((value) => {
package/src/protocol.ts CHANGED
@@ -4,7 +4,7 @@ import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
4
4
  * Canonicalization is owned by gateway/core before endpoint.send.
5
5
  * Spec: https://github.com/botuniverse/onebot-11
6
6
  */
7
- import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef } from '@zhin.js/im-contract';
7
+ import { EndpointDeliveryError, isMediaRef, type MediaRef, type ConversationRef, type PlatformStanding } from '@zhin.js/im-contract';
8
8
  import { toCanonicalSegments, type Segment } from '@zhin.js/core';
9
9
  import { formatCompact, getLogger } from '@zhin.js/logger';
10
10
 
@@ -218,6 +218,16 @@ export function senderUserId(ev: OneBot11Event): string {
218
218
  return ev.user_id != null ? String(ev.user_id) : '';
219
219
  }
220
220
 
221
+ /**
222
+ * ADR 0056 §1:群消息 `sender.role`(owner / admin / member)→ `sender.platform.standing`。
223
+ * 私聊不上报;未知角色词不上报。OneBot v11 没有平台原生角色与权限位。
224
+ */
225
+ export function onebot11SenderStanding(ev: OneBot11Event): PlatformStanding | undefined {
226
+ if (ev.message_type !== 'group') return undefined;
227
+ const role = ev.sender?.role;
228
+ return role === 'owner' || role === 'admin' || role === 'member' ? { standing: role } : undefined;
229
+ }
230
+
221
231
  /** 显示名(群名片优先)放 metadata.nickname;没有则返回 undefined,不写该字段。 */
222
232
  export function senderNickname(ev: OneBot11Event): string | undefined {
223
233
  const name = ev.sender?.card || ev.sender?.nickname;
@@ -29,6 +29,7 @@ import {
29
29
  isMessageEvent,
30
30
  onebot11InboundConversation,
31
31
  senderDisplayName,
32
+ onebot11SenderStanding,
32
33
  senderUserId,
33
34
  type OneBot11Event,
34
35
  type OneBot11WsConfig,
@@ -192,6 +193,7 @@ export class OneBot11WsEndpoint extends Onebot11Client implements Endpoint {
192
193
  const conversation = { ...onebot11InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
193
194
  const content = formatInboundContent(ev);
194
195
  const inbound = formatInboundMetadata(ev, this.#options.config.id);
196
+ const platform = onebot11SenderStanding(ev);
195
197
  void this[endpointSpec].events.message({
196
198
  event: ev,
197
199
  ...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
@@ -201,7 +203,7 @@ export class OneBot11WsEndpoint extends Onebot11Client implements Endpoint {
201
203
  sender: {
202
204
  id: senderUserId(ev),
203
205
  name: senderDisplayName(ev) || undefined,
204
- ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
206
+ ...(platform ? { platform } : {}),
205
207
  },
206
208
  endpointId: inbound.endpointId,
207
209
  ...(inbound.mentioned ? { mentioned: true } : {}),
package/src/ws-types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { WsConnection } from '@zhin.js/host-http';
2
+
1
3
  /** Minimal WS surface used by the endpoint (real `ws` or test mock). */
2
4
  export interface OneBot11WsSocket {
3
5
  readonly readyState: number;
@@ -13,3 +15,34 @@ export interface OneBot11WsCreateOptions {
13
15
  }
14
16
 
15
17
  export const WS_OPEN = 1;
18
+
19
+ /**
20
+ * A Host reverse-WebSocket connection seen through the OneBot11WsSocket surface. Protocol
21
+ * keepalive (ping / pong) belongs to the Host transport, so `ping` is absent.
22
+ */
23
+ export function socketOfConnection(connection: WsConnection): OneBot11WsSocket {
24
+ let readyState = 1;
25
+ connection.onClose(() => { readyState = 3; });
26
+ const subscriptions = new Map<(...args: unknown[]) => void, () => void>();
27
+ const off = (_event: string, listener: (...args: unknown[]) => void): void => {
28
+ subscriptions.get(listener)?.();
29
+ subscriptions.delete(listener);
30
+ };
31
+ return {
32
+ get readyState() { return readyState; },
33
+ send: (data: string) => connection.send(data),
34
+ close: (code?: number, reason?: string) => connection.close(code, reason),
35
+ on(event: string, listener: (...args: unknown[]) => void) {
36
+ if (event === 'message') {
37
+ subscriptions.set(listener, connection.onMessage((data) => {
38
+ listener(typeof data === 'string' ? data : Buffer.from(data));
39
+ }));
40
+ } else if (event === 'close') {
41
+ subscriptions.set(listener, connection.onClose((code, reason) => listener(code, reason)));
42
+ }
43
+ // 'error' surfaces as a close on a WsConnection; 'open' already happened.
44
+ },
45
+ off,
46
+ removeListener: off,
47
+ } as OneBot11WsSocket;
48
+ }
package/src/wss-auth.ts CHANGED
@@ -1,17 +1,10 @@
1
- import type { IncomingMessage } from 'node:http';
1
+ import { verifyProtocolAccessToken, type ProtocolAuthRequest } from '@zhin.js/host-http';
2
2
 
3
3
  export function verifyOneBotAccessToken(
4
4
  accessToken: string | undefined,
5
- request: IncomingMessage,
5
+ request: ProtocolAuthRequest,
6
6
  ): boolean {
7
+ // Webhook semantics: no configured token admits any caller (reverse sockets use verifyProtocolAccessToken).
7
8
  if (!accessToken) return true;
8
- const auth = request.headers.authorization ?? '';
9
- if (auth === `Bearer ${accessToken}`) return true;
10
- try {
11
- const url = new URL(request.url ?? '/', 'http://localhost');
12
- if (url.searchParams.get('access_token') === accessToken) return true;
13
- } catch {
14
- /* ignore */
15
- }
16
- return false;
9
+ return verifyProtocolAccessToken(accessToken, request);
17
10
  }
@@ -15,7 +15,7 @@ import {
15
15
  type PendingRpcRequest,
16
16
  type EndpointTransportState,
17
17
  } from 'zhin.js/adapter';
18
- import type { HttpHost, WsConnection } from '@zhin.js/host-http';
18
+ import { verifyProtocolAccessToken, type HttpHost, type WsConnection } from '@zhin.js/host-http';
19
19
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
20
20
  import type { CapabilityId } from 'zhin.js';
21
21
  import { createOneBot11EndpointManagement } from './endpoint-management.js';
@@ -27,17 +27,15 @@ import {
27
27
  isMessageEvent,
28
28
  onebot11InboundConversation,
29
29
  senderDisplayName,
30
+ onebot11SenderStanding,
30
31
  senderUserId,
31
32
  type OneBot11Event,
32
33
  type OneBot11WssConfig,
33
34
  } from './protocol.js';
34
35
  import { receiveOneBot11SideEvent } from './side-event-dispatch.js';
35
36
  import { createOneBot11ContentPort } from './content-port.js';
36
- import { verifyOneBotAccessToken } from './wss-auth.js';
37
37
  import { createOneBot11Rpc, type OneBot11Rpc } from './ws-transport.js';
38
- import {
39
- type OneBot11WsSocket,
40
- } from './ws-types.js';
38
+ import { socketOfConnection, type OneBot11WsSocket } from './ws-types.js';
41
39
  import { probeOnebot11SelfId, resolveOnebot11SelfId } from './self-id.js';
42
40
  import { callOnebot11Client, readOnebot11Response, onebot11ClientOptions, forwardOnebot11ClientEvents, Onebot11Client } from './client.js';
43
41
 
@@ -117,15 +115,16 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
117
115
  if (this.#started) return;
118
116
  this.#stopped = false;
119
117
  if (!this.#options.config.access_token) {
120
- // wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
118
+ // 未配 access_token 时只接受本机回环连接(verifyProtocolAccessToken)。
121
119
  this.#logger.warn(formatCompact({
122
120
  endpoint: this.#options.config.id,
123
121
  mode: 'wss',
124
122
  ok: false,
125
- error: 'missing access_token',
123
+ error: 'missing access_token: only loopback peers are accepted',
126
124
  }));
127
125
  }
128
- const handle = this.#options.http.ws(this.#options.config.path);
126
+ // The peer authenticates with this endpoint's own access_token, not a Console credential.
127
+ const handle = this.#options.http.ws(this.#options.config.path, { auth: 'protocol' });
129
128
  this.#wsRelease = handle.onConnection((connection) => {
130
129
  this.#connectionTask = this.#connectionTask
131
130
  .then(() => this.#acceptConnection(connection))
@@ -194,6 +193,7 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
194
193
  }
195
194
  const conversation = { ...onebot11InboundConversation(String(this.#options.id), ev), endpoint: this[endpointSpec].identity };
196
195
  const inbound = formatInboundMetadata(ev, this.#options.config.id);
196
+ const platform = onebot11SenderStanding(ev);
197
197
  void this[endpointSpec].events.message({
198
198
  event: ev,
199
199
  ...(typeof ev.time === 'number' ? { platformSentAt: ev.time * 1000 } : {}),
@@ -203,7 +203,7 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
203
203
  sender: {
204
204
  id: senderUserId(ev),
205
205
  name: senderDisplayName(ev) || undefined,
206
- ...(ev.sender?.role ? { roles: [ev.sender.role] } : {}),
206
+ ...(platform ? { platform } : {}),
207
207
  },
208
208
  endpointId: inbound.endpointId,
209
209
  ...(inbound.mentioned ? { mentioned: true } : {}),
@@ -240,14 +240,14 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
240
240
 
241
241
  async #acceptConnection(connection: WsConnection): Promise<void> {
242
242
  if (!this.#started || this.#stopped) {
243
- connection.socket.close();
243
+ connection.close();
244
244
  return;
245
245
  }
246
- if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
247
- connection.socket.close(4003, 'Unauthorized');
246
+ if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
247
+ connection.close(4003, 'Unauthorized');
248
248
  return;
249
249
  }
250
- const socket = connection.socket as unknown as OneBot11WsSocket;
250
+ const socket = socketOfConnection(connection);
251
251
  await this.#connectionLifecycle.stop();
252
252
  this.#rpc.disconnect('连接已替换');
253
253
  await this.#connectionLifecycle.start(async (lifecycleHandle) => {
@@ -264,16 +264,7 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
264
264
  }
265
265
  if (this.#ws === socket) this.#ws = undefined;
266
266
  });
267
- this.#connectionLifecycle.startHeartbeat(() => {
268
- try {
269
- socket.ping?.();
270
- } catch {
271
- /* ignore */
272
- }
273
- }, this.#options.config.heartbeat_interval);
274
- socket.on('pong', () => {
275
- if (this.#ws === socket) this.#connectionLifecycle.notifyHeartbeatAck();
276
- });
267
+ // Dead-peer detection is the Host transport's keepalive (protocol ping / pong).
277
268
  socket.on('message', (data) => {
278
269
  if (this.#ws !== socket || this.#connectionLifecycle.state === 'closed') return;
279
270
  this.#connectionLifecycle.notifyHeartbeatAck();
@@ -296,7 +287,7 @@ export class OneBot11WssEndpoint extends Onebot11Client implements Endpoint {
296
287
  this.#logger.debug(formatCompact({
297
288
  endpoint: this.#options.config.id,
298
289
  mode: 'wss',
299
- peer: connection.request.socket.remoteAddress,
290
+ peer: connection.remoteAddress,
300
291
  }));
301
292
  }
302
293
  }