@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.
- package/CHANGELOG.md +162 -0
- package/LICENSE +21 -0
- package/PERMITS.md +4 -4
- package/README.md +8 -4
- package/adapters/onebot11/index.js +3 -0
- package/adapters/onebot11/index.ts +3 -0
- package/agents/onebot11/conversation.js +15 -0
- package/agents/onebot11/conversation.ts +17 -0
- package/agents/onebot11/messages.js +4 -0
- package/agents/onebot11/messages.ts +4 -0
- package/agents/onebot11/skills/onebot11/tools/set_title/index.js +5 -4
- package/agents/onebot11/skills/onebot11/tools/set_title/index.ts +6 -5
- package/lib/console-spec.js +32 -3
- package/lib/endpoint-management.js +37 -0
- package/lib/protocol.d.ts +6 -1
- package/lib/protocol.js +10 -0
- package/lib/ws-endpoint.js +3 -2
- package/lib/ws-types.d.ts +6 -0
- package/lib/ws-types.js +31 -0
- package/lib/wss-auth.d.ts +2 -2
- package/lib/wss-auth.js +3 -12
- package/lib/wss-endpoint.d.ts +1 -1
- package/lib/wss-endpoint.js +15 -23
- package/package.json +24 -28
- package/plugin.js +0 -5
- package/schema.json +0 -56
- package/src/console-spec.ts +32 -3
- package/src/endpoint-management.ts +33 -0
- package/src/protocol.ts +11 -1
- package/src/ws-endpoint.ts +3 -1
- package/src/ws-types.ts +33 -0
- package/src/wss-auth.ts +4 -11
- package/src/wss-endpoint.ts +15 -24
package/lib/wss-endpoint.d.ts
CHANGED
|
@@ -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
|
|
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';
|
package/lib/wss-endpoint.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
...(
|
|
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.
|
|
209
|
+
connection.close();
|
|
207
210
|
return;
|
|
208
211
|
}
|
|
209
|
-
if (!
|
|
210
|
-
connection.
|
|
212
|
+
if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
|
|
213
|
+
connection.close(4003, 'Unauthorized');
|
|
211
214
|
return;
|
|
212
215
|
}
|
|
213
|
-
const socket = connection
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
49
|
-
"@zhin.js/adapter": "1.1.
|
|
50
|
-
"@zhin.js/
|
|
51
|
-
"@zhin.js/
|
|
52
|
-
"@zhin.js/
|
|
53
|
-
"@zhin.js/
|
|
54
|
-
"@zhin.js/
|
|
55
|
-
"@zhin.js/
|
|
56
|
-
"@zhin.js/
|
|
57
|
-
"@zhin.js/
|
|
58
|
-
"@zhin.js/
|
|
59
|
-
"@zhin.js/
|
|
60
|
-
"@zhin.js/
|
|
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
|
-
"
|
|
68
|
-
"
|
|
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
|
-
"
|
|
73
|
-
"@zhin.js/
|
|
74
|
-
"@zhin.js/
|
|
75
|
-
"
|
|
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": "
|
|
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": [
|
package/src/console-spec.ts
CHANGED
|
@@ -7,9 +7,38 @@ export const onebot11ConsoleSpec = {
|
|
|
7
7
|
adapterKey: 'onebot11',
|
|
8
8
|
adapterDisplayName: 'OneBot 11',
|
|
9
9
|
fields: [
|
|
10
|
-
{
|
|
11
|
-
|
|
12
|
-
|
|
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;
|
package/src/ws-endpoint.ts
CHANGED
|
@@ -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
|
-
...(
|
|
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
|
|
1
|
+
import { verifyProtocolAccessToken, type ProtocolAuthRequest } from '@zhin.js/host-http';
|
|
2
2
|
|
|
3
3
|
export function verifyOneBotAccessToken(
|
|
4
4
|
accessToken: string | undefined,
|
|
5
|
-
request:
|
|
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
|
-
|
|
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
|
}
|
package/src/wss-endpoint.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type PendingRpcRequest,
|
|
16
16
|
type EndpointTransportState,
|
|
17
17
|
} from 'zhin.js/adapter';
|
|
18
|
-
import type
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
...(
|
|
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.
|
|
243
|
+
connection.close();
|
|
244
244
|
return;
|
|
245
245
|
}
|
|
246
|
-
if (!
|
|
247
|
-
connection.
|
|
246
|
+
if (!verifyProtocolAccessToken(this.#options.config.access_token, connection)) {
|
|
247
|
+
connection.close(4003, 'Unauthorized');
|
|
248
248
|
return;
|
|
249
249
|
}
|
|
250
|
-
const socket = connection
|
|
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
|
-
|
|
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.
|
|
290
|
+
peer: connection.remoteAddress,
|
|
300
291
|
}));
|
|
301
292
|
}
|
|
302
293
|
}
|