@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.
- package/CHANGELOG.md +85 -0
- package/README.md +5 -3
- package/adapters/{onebot12.js → onebot12/index.js} +10 -10
- package/adapters/{onebot12.ts → onebot12/index.ts} +17 -17
- package/commands/{endpoint/add/[id].js → onebot12/endpoint/add/[id]/index.js} +1 -1
- package/commands/onebot12/endpoint/add/[id]/index.ts +3 -0
- package/{lib/onebot12-endpoint-commands.js → commands/onebot12/endpoint/definition.js} +4 -3
- package/{src/onebot12-endpoint-commands.ts → commands/onebot12/endpoint/definition.ts} +3 -3
- package/commands/{endpoint/list.js → onebot12/endpoint/list/index.js} +1 -1
- package/commands/onebot12/endpoint/list/index.ts +3 -0
- package/commands/{endpoint/remove/[id].js → onebot12/endpoint/remove/[id]/index.js} +1 -1
- package/commands/onebot12/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.js +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/onebot12-runtime-state.js +1 -1
- package/lib/protocol.d.ts +6 -22
- package/lib/protocol.js +44 -28
- package/lib/webhook.js +1 -5
- package/lib/ws-endpoint.js +16 -52
- package/lib/ws-transport.d.ts +12 -0
- package/lib/ws-transport.js +58 -0
- package/lib/ws-types.d.ts +2 -1
- package/lib/wss-endpoint.js +63 -92
- package/package.json +19 -14
- package/plugin.js +1 -1
- package/schema.json +19 -1
- package/src/client.ts +1 -1
- package/src/index.ts +0 -1
- package/src/onebot12-runtime-state.ts +1 -1
- package/src/protocol.ts +52 -51
- package/src/webhook.ts +1 -5
- package/src/ws-endpoint.ts +24 -60
- package/src/ws-transport.ts +88 -0
- package/src/ws-types.ts +3 -1
- package/src/wss-endpoint.ts +69 -99
- package/agent/skills/onebot12.md +0 -36
- 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/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:
|
|
14
|
+
resolve: (value: OneBot12ActionResponse) => void;
|
|
14
15
|
reject: (err: Error) => void;
|
|
15
16
|
timeout: NodeJS.Timeout;
|
|
16
17
|
}
|
package/lib/wss-endpoint.js
CHANGED
|
@@ -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 {
|
|
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
|
-
#
|
|
23
|
-
#
|
|
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.#
|
|
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
|
-
|
|
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,12 +75,13 @@ export class OneBot12WssEndpoint extends ClientEndpoint {
|
|
|
76
75
|
}
|
|
77
76
|
async stop() {
|
|
78
77
|
this.close();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.#
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.#
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this.#ws
|
|
168
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
46
|
-
"@zhin.js/core": "1.1.
|
|
47
|
-
"@zhin.js/feature-kit": "1.1.
|
|
48
|
-
"@zhin.js/host-http": "1.1.
|
|
49
|
-
"@zhin.js/im-contract": "1.1.
|
|
50
|
-
"@zhin.js/logger": "1.1.
|
|
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.
|
|
58
|
-
"zhin.js": "1.1.
|
|
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.
|
|
62
|
-
"@zhin.js/command": "^1.1.
|
|
63
|
-
"@zhin.js/core": "^1.1.
|
|
64
|
-
"zhin.js": "^1.1.
|
|
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
|
|
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":
|
|
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'
|
|
34
|
+
: config.connection === 'webhook'
|
|
35
35
|
? config.api_url
|
|
36
36
|
: 'http://localhost';
|
|
37
37
|
return new OneBotV12Client({
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OneBot12 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
-
* 由 plugin.ts setup() provide,adapter create 与 `onebot12
|
|
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/
|
|
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
|
-
/**
|
|
13
|
-
export interface
|
|
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
|
|
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
|
|
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:
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
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
|
|
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 ??
|
|
147
|
-
heartbeat_interval: config.heartbeat_interval ??
|
|
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
|
|
153
|
-
|
|
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
|
|
132
|
+
api_url,
|
|
163
133
|
};
|
|
164
134
|
}
|
|
165
135
|
|
|
166
136
|
if (connection === 'wss') {
|
|
167
|
-
const path = config.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 ??
|
|
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
|
|
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:
|
|
135
|
+
{ url: this.#options.config.api_url, access_token: this.#options.config.access_token },
|
|
140
136
|
action,
|
|
141
137
|
params,
|
|
142
138
|
);
|