@zhin.js/adapter-onebot11 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.
- package/CHANGELOG.md +95 -0
- package/README.md +8 -6
- package/adapters/{onebot11.js → onebot11/index.js} +8 -8
- package/adapters/{onebot11.ts → onebot11/index.ts} +14 -14
- package/agents/onebot11/agent.json +20 -0
- package/agents/onebot11/boundaries.md +3 -0
- package/agents/onebot11/conventions.md +3 -0
- package/agents/onebot11/skills/onebot11/SKILL.md +15 -0
- package/agents/onebot11/skills/onebot11/tools/set_title/index.js +25 -0
- package/{agent/tools/set_title.ts → agents/onebot11/skills/onebot11/tools/set_title/index.ts} +1 -1
- package/agents/onebot11/system.md +3 -0
- package/commands/{endpoint/add/[id].js → onebot11/endpoint/add/[id]/index.js} +1 -1
- package/commands/onebot11/endpoint/add/[id]/index.ts +3 -0
- package/{lib/onebot11-endpoint-commands.js → commands/onebot11/endpoint/definition.js} +4 -3
- package/{src/onebot11-endpoint-commands.ts → commands/onebot11/endpoint/definition.ts} +3 -3
- package/commands/{endpoint/list.js → onebot11/endpoint/list/index.js} +1 -1
- package/commands/onebot11/endpoint/list/index.ts +3 -0
- package/commands/{endpoint/remove/[id].js → onebot11/endpoint/remove/[id]/index.js} +1 -1
- package/commands/onebot11/endpoint/remove/[id]/index.ts +3 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +1 -1
- package/lib/onebot11-runtime-state.js +1 -1
- package/lib/protocol.d.ts +5 -22
- package/lib/protocol.js +37 -26
- package/lib/ws-transport.d.ts +0 -1
- package/lib/ws-transport.js +1 -13
- package/lib/wss-endpoint.js +63 -50
- package/package.json +33 -21
- package/schema.json +18 -1
- package/src/index.ts +0 -2
- package/src/onebot11-runtime-state.ts +1 -1
- package/src/protocol.ts +43 -50
- package/src/ws-transport.ts +1 -16
- package/src/wss-endpoint.ts +59 -48
- package/agent/skills/onebot11.md +0 -80
- package/commands/endpoint/add/[id].ts +0 -3
- package/commands/endpoint/list.ts +0 -3
- package/commands/endpoint/remove/[id].ts +0 -3
- package/lib/onebot11-endpoint-commands.d.ts +0 -1
- /package/{agent/PERMITS.md → PERMITS.md} +0 -0
package/lib/wss-endpoint.js
CHANGED
|
@@ -19,17 +19,18 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
|
|
|
19
19
|
content;
|
|
20
20
|
#ws;
|
|
21
21
|
#wsRelease;
|
|
22
|
-
#
|
|
22
|
+
#connectionLifecycle;
|
|
23
|
+
#connectionTask = Promise.resolve();
|
|
23
24
|
#requestId = { value: 0 };
|
|
24
25
|
#pending = new Map();
|
|
26
|
+
#started = false;
|
|
25
27
|
constructor(options) {
|
|
26
28
|
super();
|
|
27
29
|
this.#logger = getAdapterLogger('onebot11', options.config.id);
|
|
28
30
|
this.#options = options;
|
|
29
|
-
this.#
|
|
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 = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
|
|
35
36
|
const callApi = (action, params) => callOnebot11Client(this.client, action, params);
|
|
@@ -41,6 +42,8 @@ export class OneBot11WssEndpoint 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 OneBot11WssEndpoint extends ClientEndpoint {
|
|
|
50
53
|
error: 'missing access_token',
|
|
51
54
|
}));
|
|
52
55
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.#
|
|
56
|
-
this.#acceptConnection(connection)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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,8 +75,13 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
|
|
|
76
75
|
}
|
|
77
76
|
async stop() {
|
|
78
77
|
this.close();
|
|
79
|
-
|
|
78
|
+
this.#wsRelease?.();
|
|
79
|
+
this.#wsRelease = undefined;
|
|
80
|
+
await this.#connectionTask;
|
|
81
|
+
await this.#connectionLifecycle.stop();
|
|
80
82
|
rejectAllPending(this.#pending);
|
|
83
|
+
this.#ws = undefined;
|
|
84
|
+
this.#started = false;
|
|
81
85
|
}
|
|
82
86
|
async send({ conversation, payload }) {
|
|
83
87
|
const message = formatOutboundSegments(payload);
|
|
@@ -127,42 +131,51 @@ export class OneBot11WssEndpoint extends ClientEndpoint {
|
|
|
127
131
|
error: error instanceof Error ? error.message : String(error),
|
|
128
132
|
}));
|
|
129
133
|
}
|
|
130
|
-
#acceptConnection(connection) {
|
|
134
|
+
async #acceptConnection(connection) {
|
|
131
135
|
if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
|
|
132
136
|
connection.socket.close(4003, 'Unauthorized');
|
|
133
137
|
return;
|
|
134
138
|
}
|
|
135
139
|
const socket = connection.socket;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.#ws
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
140
|
+
await this.#connectionLifecycle.stop();
|
|
141
|
+
rejectAllPending(this.#pending, '连接已替换');
|
|
142
|
+
await this.#connectionLifecycle.start(async (lifecycleHandle) => {
|
|
143
|
+
this.#ws = socket;
|
|
144
|
+
lifecycleHandle.onForceClose(() => {
|
|
145
|
+
try {
|
|
146
|
+
socket.close();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
/* ignore */
|
|
150
|
+
}
|
|
151
|
+
if (this.#ws === socket)
|
|
152
|
+
this.#ws = undefined;
|
|
153
|
+
});
|
|
154
|
+
this.#connectionLifecycle.startHeartbeat(() => {
|
|
155
|
+
try {
|
|
156
|
+
socket.ping?.();
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
/* ignore */
|
|
160
|
+
}
|
|
161
|
+
}, this.#options.config.heartbeat_interval);
|
|
162
|
+
socket.on('message', (data) => {
|
|
163
|
+
if (this.#ws !== socket)
|
|
164
|
+
return;
|
|
165
|
+
this.#connectionLifecycle.notifyHeartbeatAck();
|
|
166
|
+
handleOneBot11WsMessage(data, {
|
|
167
|
+
endpointId: this.#options.config.id,
|
|
168
|
+
pending: this.#pending,
|
|
169
|
+
ingest: (ev) => this.client.ingest(ev),
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
socket.on('close', () => {
|
|
173
|
+
if (this.#ws === socket) {
|
|
174
|
+
this.#ws = undefined;
|
|
175
|
+
rejectAllPending(this.#pending);
|
|
176
|
+
}
|
|
177
|
+
lifecycleHandle.notifyClosed(new Error('OneBot11 reverse WebSocket closed'));
|
|
159
178
|
});
|
|
160
|
-
});
|
|
161
|
-
socket.on('close', () => {
|
|
162
|
-
if (this.#ws === socket) {
|
|
163
|
-
this.#ws = undefined;
|
|
164
|
-
this.#lifecycle.stopHeartbeat();
|
|
165
|
-
}
|
|
166
179
|
});
|
|
167
180
|
this.#logger.debug(formatCompact({
|
|
168
181
|
endpoint: this.#options.config.id,
|
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.2",
|
|
4
4
|
"description": "Zhin.js OneBot 11 adapter for Plugin Runtime (WebSocket client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
"schema.json",
|
|
20
20
|
"src",
|
|
21
21
|
"lib",
|
|
22
|
-
"
|
|
22
|
+
"tools",
|
|
23
|
+
"agents",
|
|
23
24
|
"README.md",
|
|
24
|
-
"CHANGELOG.md"
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"PERMITS.md"
|
|
25
27
|
],
|
|
26
28
|
"keywords": [
|
|
27
29
|
"zhin",
|
|
@@ -43,13 +45,16 @@
|
|
|
43
45
|
"@imhelper/onebot-v11": "1.0.7",
|
|
44
46
|
"imhelper": "1.0.7",
|
|
45
47
|
"ws": "^8.21.1",
|
|
46
|
-
"@zhin.js/adapter": "1.1.
|
|
47
|
-
"@zhin.js/
|
|
48
|
-
"@zhin.js/
|
|
49
|
-
"@zhin.js/
|
|
50
|
-
"@zhin.js/
|
|
51
|
-
"@zhin.js/
|
|
52
|
-
"@zhin.js/
|
|
48
|
+
"@zhin.js/adapter": "1.1.14",
|
|
49
|
+
"@zhin.js/agent-feature": "1.1.1",
|
|
50
|
+
"@zhin.js/core": "1.1.37",
|
|
51
|
+
"@zhin.js/feature-kit": "1.1.1",
|
|
52
|
+
"@zhin.js/host-http": "1.1.1",
|
|
53
|
+
"@zhin.js/im-contract": "1.1.1",
|
|
54
|
+
"@zhin.js/logger": "1.1.1",
|
|
55
|
+
"@zhin.js/permission": "1.1.1",
|
|
56
|
+
"@zhin.js/skill": "1.1.1",
|
|
57
|
+
"@zhin.js/tool": "1.1.1"
|
|
53
58
|
},
|
|
54
59
|
"devDependencies": {
|
|
55
60
|
"@types/node": "^26.1.2",
|
|
@@ -57,22 +62,17 @@
|
|
|
57
62
|
"typescript": "^6.0.3",
|
|
58
63
|
"vitest": "^4.1.10",
|
|
59
64
|
"zod": "^4.4.3",
|
|
60
|
-
"@zhin.js/
|
|
61
|
-
"
|
|
62
|
-
"zhin.js": "1.1.0"
|
|
65
|
+
"@zhin.js/host-http": "1.1.1",
|
|
66
|
+
"zhin.js": "1.1.2"
|
|
63
67
|
},
|
|
64
68
|
"peerDependencies": {
|
|
65
69
|
"zod": "^4.0.0",
|
|
66
|
-
"@zhin.js/adapter": "^1.1.
|
|
67
|
-
"@zhin.js/
|
|
68
|
-
"@zhin.js/
|
|
69
|
-
"
|
|
70
|
-
"zhin.js": "^1.1.0"
|
|
70
|
+
"@zhin.js/adapter": "^1.1.14",
|
|
71
|
+
"@zhin.js/command": "^1.1.1",
|
|
72
|
+
"@zhin.js/core": "^1.1.37",
|
|
73
|
+
"zhin.js": "^1.1.2"
|
|
71
74
|
},
|
|
72
75
|
"peerDependenciesMeta": {
|
|
73
|
-
"@zhin.js/agent": {
|
|
74
|
-
"optional": true
|
|
75
|
-
},
|
|
76
76
|
"@zhin.js/command": {
|
|
77
77
|
"optional": true
|
|
78
78
|
},
|
|
@@ -109,6 +109,18 @@
|
|
|
109
109
|
{
|
|
110
110
|
"package": "@zhin.js/command",
|
|
111
111
|
"api": "^1.0.0"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"package": "@zhin.js/tool",
|
|
115
|
+
"api": "^1.0.0"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"package": "@zhin.js/skill",
|
|
119
|
+
"api": "^1.0.0"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"package": "@zhin.js/agent-feature",
|
|
123
|
+
"api": "^1.0.0"
|
|
112
124
|
}
|
|
113
125
|
],
|
|
114
126
|
"plugins": []
|
package/schema.json
CHANGED
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
},
|
|
41
41
|
"endpoints": {
|
|
42
42
|
"type": "array",
|
|
43
|
+
"minItems": 1,
|
|
43
44
|
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
44
45
|
"items": {
|
|
45
46
|
"type": "object",
|
|
46
|
-
"additionalProperties":
|
|
47
|
+
"additionalProperties": false,
|
|
47
48
|
"properties": {
|
|
48
49
|
"master": {
|
|
49
50
|
"type": [
|
|
@@ -63,6 +64,13 @@
|
|
|
63
64
|
},
|
|
64
65
|
"description": "本 endpoint 的 trusted 列表"
|
|
65
66
|
},
|
|
67
|
+
"connection": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"enum": [
|
|
70
|
+
"ws",
|
|
71
|
+
"wss"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
66
74
|
"url": {
|
|
67
75
|
"type": "string",
|
|
68
76
|
"description": "OneBot implementation WebSocket URL (required for connection: ws)"
|
|
@@ -75,6 +83,15 @@
|
|
|
75
83
|
"type": "string",
|
|
76
84
|
"description": "OneBot access token"
|
|
77
85
|
},
|
|
86
|
+
"reconnect_interval": {
|
|
87
|
+
"type": "number"
|
|
88
|
+
},
|
|
89
|
+
"heartbeat_interval": {
|
|
90
|
+
"type": "number"
|
|
91
|
+
},
|
|
92
|
+
"commandPrefix": {
|
|
93
|
+
"type": "string"
|
|
94
|
+
},
|
|
78
95
|
"id": {
|
|
79
96
|
"type": "string",
|
|
80
97
|
"description": "OneBot11 bot name"
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,6 @@ export {
|
|
|
16
16
|
senderUserId,
|
|
17
17
|
type OneBot11ActionRequest,
|
|
18
18
|
type OneBot11ActionResponse,
|
|
19
|
-
type OneBot11AdapterConfig,
|
|
20
19
|
type OneBot11ConfigBase,
|
|
21
20
|
type OneBot11EndpointConfig,
|
|
22
21
|
type OneBot11Event,
|
|
@@ -45,7 +44,6 @@ export {
|
|
|
45
44
|
decodeWsPayload,
|
|
46
45
|
handleOneBot11WsMessage,
|
|
47
46
|
rejectAllPending,
|
|
48
|
-
startOneBot11Heartbeat,
|
|
49
47
|
} from './ws-transport.js';
|
|
50
48
|
|
|
51
49
|
export { verifyOneBotAccessToken } from './wss-auth.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OneBot11 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
-
* 由 plugin.ts setup() provide,adapter create 与 `onebot11
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `onebot11 endpoint` 命令共享(同一 owner generation)。
|
|
4
4
|
*/
|
|
5
5
|
import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
|
|
6
6
|
|
package/src/protocol.ts
CHANGED
|
@@ -3,37 +3,20 @@
|
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
* Spec: https://github.com/botuniverse/onebot-11
|
|
5
5
|
*/
|
|
6
|
-
import { isMediaRef, type MediaRef } from '@zhin.js/
|
|
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('onebot11');
|
|
11
10
|
|
|
12
|
-
/**
|
|
13
|
-
export interface
|
|
14
|
-
readonly context?: string;
|
|
11
|
+
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
12
|
+
export interface OneBot11EndpointConfig {
|
|
15
13
|
readonly connection?: 'ws' | 'wss';
|
|
16
|
-
|
|
17
|
-
readonly type?: string;
|
|
18
|
-
readonly id?: string;
|
|
19
|
-
readonly access_token?: string;
|
|
20
|
-
readonly url?: string;
|
|
21
|
-
readonly path?: string;
|
|
22
|
-
readonly reconnect_interval?: number;
|
|
23
|
-
readonly heartbeat_interval?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
27
|
-
export interface OneBot11AdapterConfig {
|
|
28
|
-
readonly connection?: 'ws' | 'wss';
|
|
29
|
-
readonly id?: string;
|
|
14
|
+
readonly id: string;
|
|
30
15
|
readonly access_token?: string;
|
|
31
16
|
readonly url?: string;
|
|
32
17
|
readonly path?: string;
|
|
33
18
|
readonly reconnect_interval?: number;
|
|
34
19
|
readonly heartbeat_interval?: number;
|
|
35
|
-
/** Transitional: legacy root `endpoints[]` with `context: onebot11`. */
|
|
36
|
-
readonly endpoints?: ReadonlyArray<OneBot11LegacyEndpointRow>;
|
|
37
20
|
}
|
|
38
21
|
|
|
39
22
|
export interface OneBot11ConfigBase {
|
|
@@ -58,7 +41,6 @@ export interface OneBot11WssConfig extends OneBot11ConfigBase {
|
|
|
58
41
|
}
|
|
59
42
|
|
|
60
43
|
export type ResolvedOneBot11Config = OneBot11WsConfig | OneBot11WssConfig;
|
|
61
|
-
export type OneBot11EndpointConfig = ResolvedOneBot11Config;
|
|
62
44
|
|
|
63
45
|
export interface OneBot11Sender {
|
|
64
46
|
readonly role?: string;
|
|
@@ -110,62 +92,59 @@ export interface OneBot11WireSegment {
|
|
|
110
92
|
|
|
111
93
|
function normalizeConnection(
|
|
112
94
|
connection: string | undefined,
|
|
113
|
-
legacyType: string | undefined,
|
|
114
95
|
): 'ws' | 'wss' {
|
|
115
96
|
if (connection === 'ws' || connection === 'wss') return connection;
|
|
116
|
-
if (legacyType === 'ws_reverse' || legacyType === 'wss') return 'wss';
|
|
117
|
-
if (legacyType === 'ws') return 'ws';
|
|
118
97
|
return 'ws';
|
|
119
98
|
}
|
|
120
99
|
|
|
121
|
-
export function resolveOneBot11Config(config:
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
entry?.type,
|
|
126
|
-
);
|
|
127
|
-
const id = (typeof config.id === 'string' && config.id)
|
|
128
|
-
|| (typeof entry?.id === 'string' && entry.id)
|
|
129
|
-
|| process.env.ONEBOT11_BOT_NAME
|
|
130
|
-
|| 'onebot11-bot';
|
|
131
|
-
const access_token = config.access_token ?? entry?.access_token;
|
|
100
|
+
export function resolveOneBot11Config(config: OneBot11EndpointConfig): ResolvedOneBot11Config {
|
|
101
|
+
const connection = normalizeConnection(config.connection);
|
|
102
|
+
const id = requiredEndpointField(config.id, 'id');
|
|
103
|
+
const access_token = optionalEndpointField(config.access_token);
|
|
132
104
|
|
|
133
105
|
if (connection === 'ws') {
|
|
134
|
-
const url = config.url
|
|
135
|
-
if (!url) {
|
|
136
|
-
throw new TypeError(
|
|
137
|
-
'OneBot11 connection:ws requires url (plugins.<key>.url or endpoints with context: onebot11)',
|
|
138
|
-
);
|
|
139
|
-
}
|
|
106
|
+
const url = requiredEndpointField(config.url, 'url');
|
|
140
107
|
return {
|
|
141
108
|
context: 'onebot11',
|
|
142
109
|
connection: 'ws',
|
|
143
110
|
id,
|
|
144
111
|
access_token,
|
|
145
112
|
url,
|
|
146
|
-
reconnect_interval: config.reconnect_interval ??
|
|
147
|
-
heartbeat_interval: config.heartbeat_interval ??
|
|
113
|
+
reconnect_interval: config.reconnect_interval ?? 5000,
|
|
114
|
+
heartbeat_interval: config.heartbeat_interval ?? 30_000,
|
|
148
115
|
};
|
|
149
116
|
}
|
|
150
117
|
|
|
151
118
|
if (connection === 'wss') {
|
|
152
|
-
const path = config.path
|
|
153
|
-
if (!path) {
|
|
154
|
-
throw new TypeError('OneBot11 connection:wss requires path');
|
|
155
|
-
}
|
|
119
|
+
const path = requiredEndpointField(config.path, 'path');
|
|
156
120
|
return {
|
|
157
121
|
context: 'onebot11',
|
|
158
122
|
connection: 'wss',
|
|
159
123
|
id,
|
|
160
124
|
access_token,
|
|
161
125
|
path,
|
|
162
|
-
heartbeat_interval: config.heartbeat_interval ??
|
|
126
|
+
heartbeat_interval: config.heartbeat_interval ?? 30_000,
|
|
163
127
|
};
|
|
164
128
|
}
|
|
165
129
|
|
|
166
130
|
throw new TypeError(`Unknown OneBot11 connection: ${String(connection)}`);
|
|
167
131
|
}
|
|
168
132
|
|
|
133
|
+
function requiredEndpointField(
|
|
134
|
+
value: unknown,
|
|
135
|
+
field: 'id' | 'url' | 'path',
|
|
136
|
+
): string {
|
|
137
|
+
const resolved = optionalEndpointField(value);
|
|
138
|
+
if (!resolved) {
|
|
139
|
+
throw new TypeError(`OneBot11 endpoint requires a non-empty ${field}`);
|
|
140
|
+
}
|
|
141
|
+
return resolved;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function optionalEndpointField(value: unknown): string | undefined {
|
|
145
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
169
148
|
/** 判断是否为消息事件(post_type=message) */
|
|
170
149
|
export function isMessageEvent(
|
|
171
150
|
ev: OneBot11Event,
|
|
@@ -205,10 +184,24 @@ export function formatInboundContent(ev: OneBot11Event): string {
|
|
|
205
184
|
.join('');
|
|
206
185
|
}
|
|
207
186
|
return typeof ev.raw_message === 'string'
|
|
208
|
-
? ev.raw_message
|
|
187
|
+
? stripCqCodes(ev.raw_message).trim()
|
|
209
188
|
: '';
|
|
210
189
|
}
|
|
211
190
|
|
|
191
|
+
function stripCqCodes(input: string): string {
|
|
192
|
+
let result = '';
|
|
193
|
+
let cursor = 0;
|
|
194
|
+
while (cursor < input.length) {
|
|
195
|
+
const start = input.indexOf('[CQ:', cursor);
|
|
196
|
+
if (start < 0) return result + input.slice(cursor);
|
|
197
|
+
result += input.slice(cursor, start);
|
|
198
|
+
const end = input.indexOf(']', start + 4);
|
|
199
|
+
if (end < 0) return result + input.slice(start);
|
|
200
|
+
cursor = end + 1;
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
212
205
|
export function senderDisplayName(ev: OneBot11Event): string {
|
|
213
206
|
const name = ev.sender?.card || ev.sender?.nickname;
|
|
214
207
|
if (typeof name === 'string' && name) return name;
|
package/src/ws-transport.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { clearTimeout, setTimeout } from 'node:timers';
|
|
2
2
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
3
3
|
import {
|
|
4
4
|
type OneBot11ActionRequest,
|
|
@@ -73,21 +73,6 @@ export function callOneBot11WsAction(
|
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function startOneBot11Heartbeat(
|
|
77
|
-
ws: OneBot11WsSocket | undefined,
|
|
78
|
-
intervalMs: number,
|
|
79
|
-
existingTimer?: NodeJS.Timeout,
|
|
80
|
-
): NodeJS.Timeout {
|
|
81
|
-
if (existingTimer) clearInterval(existingTimer);
|
|
82
|
-
return setInterval(() => {
|
|
83
|
-
try {
|
|
84
|
-
ws?.ping?.();
|
|
85
|
-
} catch {
|
|
86
|
-
/* ignore */
|
|
87
|
-
}
|
|
88
|
-
}, intervalMs);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
76
|
export function rejectAllPending(
|
|
92
77
|
pending: Map<string, OneBot11PendingAction>,
|
|
93
78
|
message = '连接已关闭',
|
package/src/wss-endpoint.ts
CHANGED
|
@@ -57,18 +57,19 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
|
|
|
57
57
|
readonly content;
|
|
58
58
|
#ws?: OneBot11WsSocket;
|
|
59
59
|
#wsRelease?: () => void;
|
|
60
|
-
readonly #
|
|
60
|
+
readonly #connectionLifecycle: EndpointLifecycle;
|
|
61
|
+
#connectionTask = Promise.resolve();
|
|
61
62
|
#requestId = { value: 0 };
|
|
62
63
|
#pending = new Map<string, OneBot11PendingAction>();
|
|
64
|
+
#started = false;
|
|
63
65
|
|
|
64
66
|
constructor(options: OneBot11WssEndpointOptions) {
|
|
65
67
|
super();
|
|
66
68
|
this.#logger = getAdapterLogger('onebot11', options.config.id);
|
|
67
69
|
this.#options = options;
|
|
68
|
-
this.#
|
|
69
|
-
name: options.config.id
|
|
70
|
+
this.#connectionLifecycle = createEndpointLifecycle({
|
|
71
|
+
name: `${options.config.id}:inbound`,
|
|
70
72
|
reconnect: false,
|
|
71
|
-
heartbeat: { intervalMs: options.config.heartbeat_interval },
|
|
72
73
|
});
|
|
73
74
|
this.client = createOnebot11EndpointClient(options.config, (action, params) => this.#callApi(action, params));
|
|
74
75
|
const callApi = (action: string, params?: Record<string, unknown>) => callOnebot11Client(this.client, action, params);
|
|
@@ -84,6 +85,7 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
async start(): Promise<void> {
|
|
88
|
+
if (this.#started) return;
|
|
87
89
|
if (!this.#options.config.access_token) {
|
|
88
90
|
// wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
|
|
89
91
|
this.#logger.warn(formatCompact({
|
|
@@ -93,22 +95,19 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
|
|
|
93
95
|
error: 'missing access_token',
|
|
94
96
|
}));
|
|
95
97
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
this.#
|
|
99
|
-
this.#acceptConnection(connection)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
/* ignore */
|
|
108
|
-
}
|
|
109
|
-
this.#ws = undefined;
|
|
110
|
-
});
|
|
98
|
+
const handle = this.#options.http.ws(this.#options.config.path);
|
|
99
|
+
this.#wsRelease = handle.onConnection((connection) => {
|
|
100
|
+
this.#connectionTask = this.#connectionTask
|
|
101
|
+
.then(() => this.#acceptConnection(connection))
|
|
102
|
+
.catch((error) => {
|
|
103
|
+
this.#logger.warn(formatCompact({
|
|
104
|
+
op: 'wss_connection_failed',
|
|
105
|
+
endpoint: this.#options.config.id,
|
|
106
|
+
error: error instanceof Error ? error.message : String(error),
|
|
107
|
+
}));
|
|
108
|
+
});
|
|
111
109
|
});
|
|
110
|
+
this.#started = true;
|
|
112
111
|
this.#logger.info(formatCompact({
|
|
113
112
|
op: 'listen',
|
|
114
113
|
endpoint: this.#options.config.id,
|
|
@@ -119,8 +118,13 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
|
|
|
119
118
|
|
|
120
119
|
async stop(): Promise<void> {
|
|
121
120
|
this.close();
|
|
122
|
-
|
|
121
|
+
this.#wsRelease?.();
|
|
122
|
+
this.#wsRelease = undefined;
|
|
123
|
+
await this.#connectionTask;
|
|
124
|
+
await this.#connectionLifecycle.stop();
|
|
123
125
|
rejectAllPending(this.#pending);
|
|
126
|
+
this.#ws = undefined;
|
|
127
|
+
this.#started = false;
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
|
|
@@ -184,40 +188,47 @@ export class OneBot11WssEndpoint extends ClientEndpoint<Onebot11Client> {
|
|
|
184
188
|
}));
|
|
185
189
|
}
|
|
186
190
|
|
|
187
|
-
#acceptConnection(connection: WsConnection): void {
|
|
191
|
+
async #acceptConnection(connection: WsConnection): Promise<void> {
|
|
188
192
|
if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
|
|
189
193
|
connection.socket.close(4003, 'Unauthorized');
|
|
190
194
|
return;
|
|
191
195
|
}
|
|
192
196
|
const socket = connection.socket as unknown as OneBot11WsSocket;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.#ws
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
197
|
+
await this.#connectionLifecycle.stop();
|
|
198
|
+
rejectAllPending(this.#pending, '连接已替换');
|
|
199
|
+
await this.#connectionLifecycle.start(async (lifecycleHandle) => {
|
|
200
|
+
this.#ws = socket;
|
|
201
|
+
lifecycleHandle.onForceClose(() => {
|
|
202
|
+
try {
|
|
203
|
+
socket.close();
|
|
204
|
+
} catch {
|
|
205
|
+
/* ignore */
|
|
206
|
+
}
|
|
207
|
+
if (this.#ws === socket) this.#ws = undefined;
|
|
208
|
+
});
|
|
209
|
+
this.#connectionLifecycle.startHeartbeat(() => {
|
|
210
|
+
try {
|
|
211
|
+
socket.ping?.();
|
|
212
|
+
} catch {
|
|
213
|
+
/* ignore */
|
|
214
|
+
}
|
|
215
|
+
}, this.#options.config.heartbeat_interval);
|
|
216
|
+
socket.on('message', (data) => {
|
|
217
|
+
if (this.#ws !== socket) return;
|
|
218
|
+
this.#connectionLifecycle.notifyHeartbeatAck();
|
|
219
|
+
handleOneBot11WsMessage(data, {
|
|
220
|
+
endpointId: this.#options.config.id,
|
|
221
|
+
pending: this.#pending,
|
|
222
|
+
ingest: (ev) => this.client.ingest(ev as Parameters<Onebot11Client['ingest']>[0]),
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
socket.on('close', () => {
|
|
226
|
+
if (this.#ws === socket) {
|
|
227
|
+
this.#ws = undefined;
|
|
228
|
+
rejectAllPending(this.#pending);
|
|
229
|
+
}
|
|
230
|
+
lifecycleHandle.notifyClosed(new Error('OneBot11 reverse WebSocket closed'));
|
|
214
231
|
});
|
|
215
|
-
});
|
|
216
|
-
socket.on('close', () => {
|
|
217
|
-
if (this.#ws === socket) {
|
|
218
|
-
this.#ws = undefined;
|
|
219
|
-
this.#lifecycle.stopHeartbeat();
|
|
220
|
-
}
|
|
221
232
|
});
|
|
222
233
|
this.#logger.debug(formatCompact({
|
|
223
234
|
endpoint: this.#options.config.id,
|