@zhin.js/adapter-onebot12 1.0.0 → 1.1.0
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 +855 -0
- package/README.md +59 -69
- package/adapters/onebot12.js +50 -0
- package/adapters/onebot12.ts +64 -0
- package/{skills/onebot12/SKILL.md → agent/skills/onebot12.md} +1 -1
- package/commands/endpoint/add/[id].js +3 -0
- package/commands/endpoint/add/[id].ts +3 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[id].js +3 -0
- package/commands/endpoint/remove/[id].ts +3 -0
- package/lib/client.d.ts +20 -0
- package/lib/client.js +33 -0
- package/lib/content-port.d.ts +4 -0
- package/lib/content-port.js +45 -0
- package/lib/endpoint-management.d.ts +16 -0
- package/lib/endpoint-management.js +47 -0
- package/lib/index.d.ts +10 -19
- package/lib/index.js +7 -27
- package/lib/onebot12-endpoint-commands.d.ts +1 -0
- package/lib/onebot12-endpoint-commands.js +25 -0
- package/lib/onebot12-runtime-state.d.ts +1 -0
- package/lib/onebot12-runtime-state.js +6 -0
- package/lib/protocol.d.ts +173 -0
- package/lib/protocol.js +415 -0
- package/lib/side-event-dispatch.d.ts +7 -0
- package/lib/side-event-dispatch.js +36 -0
- package/lib/webhook.d.ts +23 -0
- package/lib/webhook.js +161 -0
- package/lib/ws-endpoint.d.ts +22 -0
- package/lib/ws-endpoint.js +255 -0
- package/lib/ws-types.d.ts +16 -0
- package/lib/ws-types.js +1 -0
- package/lib/wss-auth.d.ts +2 -0
- package/lib/wss-auth.js +16 -0
- package/lib/wss-endpoint.d.ts +25 -0
- package/lib/wss-endpoint.js +222 -0
- package/package.json +63 -16
- package/plugin.js +14 -0
- package/schema.json +102 -0
- package/src/client.ts +81 -0
- package/src/content-port.ts +49 -0
- package/src/endpoint-management.ts +73 -0
- package/src/index.ts +64 -37
- package/src/onebot12-endpoint-commands.ts +24 -0
- package/src/onebot12-runtime-state.ts +7 -0
- package/src/protocol.ts +574 -0
- package/src/side-event-dispatch.ts +48 -0
- package/src/webhook.ts +223 -0
- package/src/ws-endpoint.ts +321 -0
- package/src/ws-types.ts +19 -0
- package/src/wss-auth.ts +17 -0
- package/src/wss-endpoint.ts +278 -0
- package/lib/adapter.d.ts +0 -17
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -40
- package/lib/adapter.js.map +0 -1
- package/lib/api.d.ts +0 -14
- package/lib/api.d.ts.map +0 -1
- package/lib/api.js +0 -34
- package/lib/api.js.map +0 -1
- package/lib/bot-webhook.d.ts +0 -25
- package/lib/bot-webhook.d.ts.map +0 -1
- package/lib/bot-webhook.js +0 -118
- package/lib/bot-webhook.js.map +0 -1
- package/lib/bot-ws.d.ts +0 -26
- package/lib/bot-ws.d.ts.map +0 -1
- package/lib/bot-ws.js +0 -210
- package/lib/bot-ws.js.map +0 -1
- package/lib/bot-wss.d.ts +0 -26
- package/lib/bot-wss.d.ts.map +0 -1
- package/lib/bot-wss.js +0 -187
- package/lib/bot-wss.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/types.d.ts +0 -77
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -5
- package/lib/types.js.map +0 -1
- package/lib/utils.d.ts +0 -45
- package/lib/utils.d.ts.map +0 -1
- package/lib/utils.js +0 -60
- package/lib/utils.js.map +0 -1
- package/src/adapter.ts +0 -52
- package/src/api.ts +0 -48
- package/src/bot-webhook.ts +0 -130
- package/src/bot-ws.ts +0 -222
- package/src/bot-wss.ts +0 -202
- package/src/types.ts +0 -86
- package/src/utils.ts +0 -87
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot12 reverse WSS endpoint — accepts inbound WebSocket from OneBot implementation.
|
|
3
|
+
*/
|
|
4
|
+
import { ClientEndpoint, createEndpointLifecycle, createRecallEndpointControl, } from 'zhin.js/adapter';
|
|
5
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
6
|
+
import { createOneBot12EndpointManagement } from './endpoint-management.js';
|
|
7
|
+
import { buildSendMessageParams, formatInboundContent, formatOutboundSegments, isBotMentioned, isMessageEvent, onebot12InboundConversation, senderNickname, senderUserId, uploadOneBot12MediaSegments, } from './protocol.js';
|
|
8
|
+
import { receiveOneBot12SideEvent } from './side-event-dispatch.js';
|
|
9
|
+
import { createOneBot12ContentPort } from './content-port.js';
|
|
10
|
+
import { callOnebot12Client, createOnebot12EndpointClient, forwardOnebot12ClientEvents } from './client.js';
|
|
11
|
+
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
12
|
+
import { WS_OPEN } from './ws-types.js';
|
|
13
|
+
export class OneBot12WssEndpoint extends ClientEndpoint {
|
|
14
|
+
client;
|
|
15
|
+
#logger;
|
|
16
|
+
#options;
|
|
17
|
+
management;
|
|
18
|
+
control = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
19
|
+
content;
|
|
20
|
+
#ws;
|
|
21
|
+
#wsRelease;
|
|
22
|
+
#lifecycle;
|
|
23
|
+
#requestId = 0;
|
|
24
|
+
#pending = new Map();
|
|
25
|
+
constructor(options) {
|
|
26
|
+
super();
|
|
27
|
+
this.#logger = getAdapterLogger('onebot12', options.config.id);
|
|
28
|
+
this.#options = options;
|
|
29
|
+
this.#lifecycle = createEndpointLifecycle({
|
|
30
|
+
name: options.config.id,
|
|
31
|
+
reconnect: false,
|
|
32
|
+
heartbeat: { intervalMs: options.config.heartbeat_interval },
|
|
33
|
+
});
|
|
34
|
+
this.client = createOnebot12EndpointClient(options.config, (action, params) => this.#callAction(action, params ?? {}));
|
|
35
|
+
const callApi = (action, params) => callOnebot12Client(this.client, action, params);
|
|
36
|
+
this.management = createOneBot12EndpointManagement({ callApi });
|
|
37
|
+
this.content = createOneBot12ContentPort(callApi);
|
|
38
|
+
this.bindClientEvents((receive) => forwardOnebot12ClientEvents(this.client, receive), (name, payload) => {
|
|
39
|
+
if (name === 'event')
|
|
40
|
+
this.#admitRaw(payload);
|
|
41
|
+
}, (_name, error) => this.#warnPlatformEvent(error));
|
|
42
|
+
}
|
|
43
|
+
async start() {
|
|
44
|
+
if (!this.#options.config.access_token) {
|
|
45
|
+
// wss 模式未配 access_token 时任何连接都会被放行(verifyOneBotAccessToken 直接 return true)
|
|
46
|
+
this.#logger.warn(formatCompact({
|
|
47
|
+
endpoint: this.#options.config.id,
|
|
48
|
+
mode: 'wss',
|
|
49
|
+
ok: false,
|
|
50
|
+
error: 'missing access_token',
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
await this.#lifecycle.start(async (lifecycleHandle) => {
|
|
54
|
+
const handle = this.#options.http.ws(this.#options.config.path);
|
|
55
|
+
this.#wsRelease = handle.onConnection((connection) => {
|
|
56
|
+
this.#acceptConnection(connection);
|
|
57
|
+
});
|
|
58
|
+
lifecycleHandle.onForceClose(() => {
|
|
59
|
+
this.#wsRelease?.();
|
|
60
|
+
this.#wsRelease = undefined;
|
|
61
|
+
try {
|
|
62
|
+
this.#ws?.close();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* ignore */
|
|
66
|
+
}
|
|
67
|
+
this.#ws = undefined;
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
this.#logger.info(formatCompact({
|
|
71
|
+
op: 'listen',
|
|
72
|
+
endpoint: this.#options.config.id,
|
|
73
|
+
mode: 'wss',
|
|
74
|
+
path: this.#options.config.path,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
async stop() {
|
|
78
|
+
this.close();
|
|
79
|
+
await this.#lifecycle.stop();
|
|
80
|
+
for (const [, pending] of this.#pending) {
|
|
81
|
+
clearTimeout(pending.timeout);
|
|
82
|
+
pending.reject(new Error('连接已关闭'));
|
|
83
|
+
}
|
|
84
|
+
this.#pending.clear();
|
|
85
|
+
}
|
|
86
|
+
async send({ conversation, payload }) {
|
|
87
|
+
const materialized = await uploadOneBot12MediaSegments(payload, (action, params) => callOnebot12Client(this.client, action, params), (error) => {
|
|
88
|
+
this.#logger.warn(formatCompact({
|
|
89
|
+
op: 'onebot12_upload_failed',
|
|
90
|
+
endpoint: this.#options.config.id,
|
|
91
|
+
error: error instanceof Error ? error.message : String(error),
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
const message = formatOutboundSegments(materialized);
|
|
95
|
+
const params = buildSendMessageParams(conversation, message);
|
|
96
|
+
const data = await callOnebot12Client(this.client, 'send_message', params);
|
|
97
|
+
return data?.message_id ?? '';
|
|
98
|
+
}
|
|
99
|
+
async recallMessage(messageId) {
|
|
100
|
+
if (!messageId)
|
|
101
|
+
return;
|
|
102
|
+
await callOnebot12Client(this.client, 'delete_message', { message_id: messageId });
|
|
103
|
+
}
|
|
104
|
+
#admitRaw(ev) {
|
|
105
|
+
if (!isMessageEvent(ev)) {
|
|
106
|
+
receiveOneBot12SideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, { callApi: (action, params) => callOnebot12Client(this.client, action, params) }, ev, this.#logger);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const conversation = onebot12InboundConversation(String(this.#options.id), ev);
|
|
110
|
+
const nickname = senderNickname(ev);
|
|
111
|
+
const mentioned = isBotMentioned(ev);
|
|
112
|
+
void this.emit('message.receive', {
|
|
113
|
+
conversation,
|
|
114
|
+
message: { conversation, id: ev.message_id },
|
|
115
|
+
content: formatInboundContent(ev),
|
|
116
|
+
sender: { id: senderUserId(ev), name: senderNickname(ev) },
|
|
117
|
+
endpointId: this.#options.config.id,
|
|
118
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
119
|
+
metadata: Object.freeze({
|
|
120
|
+
detail_type: ev.detail_type,
|
|
121
|
+
user_id: ev.user_id,
|
|
122
|
+
group_id: ev.group_id,
|
|
123
|
+
channel_id: ev.channel_id,
|
|
124
|
+
guild_id: ev.guild_id,
|
|
125
|
+
time: ev.time,
|
|
126
|
+
...(nickname ? { nickname } : {}),
|
|
127
|
+
}),
|
|
128
|
+
}).catch((err) => {
|
|
129
|
+
this.#logger.warn(formatCompact({
|
|
130
|
+
op: 'onebot12_gateway_receive_failed',
|
|
131
|
+
kind: conversation.kind,
|
|
132
|
+
conversationId: conversation.id,
|
|
133
|
+
error: err instanceof Error ? err.message : String(err),
|
|
134
|
+
}));
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
#warnPlatformEvent(error) {
|
|
138
|
+
this.#logger.warn(formatCompact({
|
|
139
|
+
op: 'onebot12_platform_event_failed',
|
|
140
|
+
error: error instanceof Error ? error.message : String(error),
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
#acceptConnection(connection) {
|
|
144
|
+
if (!verifyOneBotAccessToken(this.#options.config.access_token, connection.request)) {
|
|
145
|
+
connection.socket.close(4003, 'Unauthorized');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const socket = connection.socket;
|
|
149
|
+
if (this.#ws) {
|
|
150
|
+
try {
|
|
151
|
+
this.#ws.close();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* ignore */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
this.#ws = socket;
|
|
158
|
+
this.#lifecycle.startHeartbeat(() => {
|
|
159
|
+
this.#callAction('get_status', {}).catch(() => { });
|
|
160
|
+
});
|
|
161
|
+
socket.on('message', (data) => {
|
|
162
|
+
this.#lifecycle.notifyHeartbeatAck();
|
|
163
|
+
this.#onMessage(data);
|
|
164
|
+
});
|
|
165
|
+
socket.on('close', () => {
|
|
166
|
+
if (this.#ws === socket) {
|
|
167
|
+
this.#ws = undefined;
|
|
168
|
+
this.#lifecycle.stopHeartbeat();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
this.#logger.debug(formatCompact({
|
|
172
|
+
endpoint: this.#options.config.id,
|
|
173
|
+
mode: 'wss',
|
|
174
|
+
peer: connection.request.socket.remoteAddress,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
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
|
+
#callAction(action, params) {
|
|
208
|
+
if (!this.#ws || this.#ws.readyState !== WS_OPEN) {
|
|
209
|
+
return Promise.reject(new Error('WebSocket 未连接'));
|
|
210
|
+
}
|
|
211
|
+
const echo = `ob12_${++this.#requestId}`;
|
|
212
|
+
const req = { action, params, echo };
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const timeout = setTimeout(() => {
|
|
215
|
+
this.#pending.delete(echo);
|
|
216
|
+
reject(new Error(`OneBot12 动作超时: ${action}`));
|
|
217
|
+
}, 30_000);
|
|
218
|
+
this.#pending.set(echo, { resolve, reject, timeout });
|
|
219
|
+
this.#ws.send(JSON.stringify(req));
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-onebot12",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Zhin.js adapter for
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Zhin.js OneBot 12 adapter for Plugin Runtime (WebSocket client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
+
"adapters",
|
|
17
|
+
"commands",
|
|
18
|
+
"plugin.js",
|
|
19
|
+
"schema.json",
|
|
16
20
|
"src",
|
|
17
21
|
"lib",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"README.md"
|
|
22
|
+
"agent",
|
|
23
|
+
"README.md",
|
|
24
|
+
"CHANGELOG.md"
|
|
22
25
|
],
|
|
23
26
|
"keywords": [
|
|
24
27
|
"zhin",
|
|
@@ -36,27 +39,71 @@
|
|
|
36
39
|
"directory": "plugins/adapters/onebot12"
|
|
37
40
|
},
|
|
38
41
|
"dependencies": {
|
|
39
|
-
"
|
|
42
|
+
"@imhelper/onebot-v12": "1.0.7",
|
|
43
|
+
"imhelper": "1.0.7",
|
|
44
|
+
"ws": "^8.21.1",
|
|
45
|
+
"@zhin.js/adapter": "1.1.12",
|
|
46
|
+
"@zhin.js/core": "1.1.35",
|
|
47
|
+
"@zhin.js/feature-kit": "1.1.0",
|
|
48
|
+
"@zhin.js/host-http": "1.1.0",
|
|
49
|
+
"@zhin.js/im-contract": "1.1.0",
|
|
50
|
+
"@zhin.js/logger": "1.1.0"
|
|
40
51
|
},
|
|
41
52
|
"devDependencies": {
|
|
42
|
-
"@types/node": "^
|
|
53
|
+
"@types/node": "^26.1.2",
|
|
43
54
|
"@types/ws": "^8.18.1",
|
|
44
55
|
"typescript": "^6.0.3",
|
|
45
|
-
"
|
|
46
|
-
"@zhin.js/
|
|
47
|
-
"zhin.js": "
|
|
56
|
+
"vitest": "^4.1.10",
|
|
57
|
+
"@zhin.js/host-http": "1.1.0",
|
|
58
|
+
"zhin.js": "1.1.0"
|
|
48
59
|
},
|
|
49
60
|
"peerDependencies": {
|
|
50
|
-
"zhin.js": "
|
|
51
|
-
"@zhin.js/
|
|
61
|
+
"@zhin.js/adapter": "^1.1.12",
|
|
62
|
+
"@zhin.js/command": "^1.1.0",
|
|
63
|
+
"@zhin.js/core": "^1.1.35",
|
|
64
|
+
"zhin.js": "^1.1.0"
|
|
52
65
|
},
|
|
53
66
|
"peerDependenciesMeta": {
|
|
54
|
-
"@zhin.js/
|
|
67
|
+
"@zhin.js/command": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"zhin.js": {
|
|
55
71
|
"optional": true
|
|
56
72
|
}
|
|
57
73
|
},
|
|
74
|
+
"author": {
|
|
75
|
+
"name": "lc-cn",
|
|
76
|
+
"email": "admin@liucl.cn",
|
|
77
|
+
"url": "https://github.com/lc-cn"
|
|
78
|
+
},
|
|
79
|
+
"publishConfig": {
|
|
80
|
+
"access": "public",
|
|
81
|
+
"registry": "https://registry.npmjs.org"
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
85
|
+
},
|
|
86
|
+
"zhin": {
|
|
87
|
+
"protocol": 1,
|
|
88
|
+
"type": "plugin",
|
|
89
|
+
"entry": "./plugin.js",
|
|
90
|
+
"engine": "^1.0.0",
|
|
91
|
+
"runtime": "trusted",
|
|
92
|
+
"features": [
|
|
93
|
+
{
|
|
94
|
+
"package": "@zhin.js/adapter",
|
|
95
|
+
"api": "^1.0.0"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"package": "@zhin.js/command",
|
|
99
|
+
"api": "^1.0.0"
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"plugins": []
|
|
103
|
+
},
|
|
58
104
|
"scripts": {
|
|
59
|
-
"build": "
|
|
60
|
-
"clean": "rimraf lib"
|
|
105
|
+
"build": "tsc",
|
|
106
|
+
"clean": "rimraf lib",
|
|
107
|
+
"test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/onebot12/tests"
|
|
61
108
|
}
|
|
62
109
|
}
|
package/plugin.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Generated by build-plugin-runtime-entries.mjs. Do not edit.
|
|
2
|
+
import { createEndpointRuntimeState } from 'zhin.js/adapter';
|
|
3
|
+
import { definePlugin } from 'zhin.js';
|
|
4
|
+
import { onebot12RuntimeStateToken } from "./lib/onebot12-runtime-state.js";
|
|
5
|
+
export default definePlugin({
|
|
6
|
+
name: 'onebot12',
|
|
7
|
+
metadata: {
|
|
8
|
+
displayName: 'OneBot 12 Adapter',
|
|
9
|
+
},
|
|
10
|
+
setup(context) {
|
|
11
|
+
// 运行中 endpoint 注册表(onebot12.endpoint list 的"运行中"数据源)
|
|
12
|
+
context.resources.provide(onebot12RuntimeStateToken, createEndpointRuntimeState());
|
|
13
|
+
},
|
|
14
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"connection": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"enum": [
|
|
9
|
+
"ws",
|
|
10
|
+
"webhook",
|
|
11
|
+
"wss"
|
|
12
|
+
],
|
|
13
|
+
"default": "ws",
|
|
14
|
+
"description": "ws (default), webhook (httpHostToken POST), or wss (reverse WS via httpHostToken)"
|
|
15
|
+
},
|
|
16
|
+
"reconnect_interval": {
|
|
17
|
+
"type": "number",
|
|
18
|
+
"default": 5000
|
|
19
|
+
},
|
|
20
|
+
"heartbeat_interval": {
|
|
21
|
+
"type": "number",
|
|
22
|
+
"default": 30000
|
|
23
|
+
},
|
|
24
|
+
"master": {
|
|
25
|
+
"type": [
|
|
26
|
+
"string",
|
|
27
|
+
"number"
|
|
28
|
+
],
|
|
29
|
+
"description": "框架 master(platform user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
|
|
30
|
+
},
|
|
31
|
+
"trusted": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": [
|
|
35
|
+
"string",
|
|
36
|
+
"number"
|
|
37
|
+
],
|
|
38
|
+
"description": "Trusted platform user id"
|
|
39
|
+
},
|
|
40
|
+
"description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
|
|
41
|
+
},
|
|
42
|
+
"endpoints": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"description": "多账号:一个插件实例挂多个 endpoint。每项与顶层字段同构(id 必填,其余覆盖顶层)",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": true,
|
|
48
|
+
"properties": {
|
|
49
|
+
"master": {
|
|
50
|
+
"type": [
|
|
51
|
+
"string",
|
|
52
|
+
"number"
|
|
53
|
+
],
|
|
54
|
+
"description": "本 endpoint 的框架 master(platform user id);覆盖顶层 master"
|
|
55
|
+
},
|
|
56
|
+
"trusted": {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"items": {
|
|
59
|
+
"type": [
|
|
60
|
+
"string",
|
|
61
|
+
"number"
|
|
62
|
+
],
|
|
63
|
+
"description": "Trusted platform user id"
|
|
64
|
+
},
|
|
65
|
+
"description": "本 endpoint 的 trusted 列表"
|
|
66
|
+
},
|
|
67
|
+
"url": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "OneBot implementation WebSocket URL (required for connection: ws)"
|
|
70
|
+
},
|
|
71
|
+
"path": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "HTTP/WS path for webhook or reverse-wss"
|
|
74
|
+
},
|
|
75
|
+
"api_url": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "HTTP action endpoint for webhook outbound (required for connection: webhook send)"
|
|
78
|
+
},
|
|
79
|
+
"access_token": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "OneBot access token"
|
|
82
|
+
},
|
|
83
|
+
"id": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "OneBot12 bot name"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"required": [
|
|
89
|
+
"id"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"commandPrefix": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"default": "",
|
|
96
|
+
"description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"required": [
|
|
100
|
+
"endpoints"
|
|
101
|
+
]
|
|
102
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OneBotV12Client,
|
|
3
|
+
type OneBotV12Event as ImHelperOneBot12Event,
|
|
4
|
+
type OneBotV12Response,
|
|
5
|
+
} from '@imhelper/onebot-v12';
|
|
6
|
+
import { EventFactory, type EventMap, type ImHelperEventMap } from 'imhelper';
|
|
7
|
+
import {
|
|
8
|
+
defineEndpointClient,
|
|
9
|
+
forwardEndpointClientEvents,
|
|
10
|
+
type ClientEventPayloads,
|
|
11
|
+
} from 'zhin.js/adapter';
|
|
12
|
+
import type { ResolvedOneBot12Config } from './protocol.js';
|
|
13
|
+
|
|
14
|
+
export { OneBotV12Client as Onebot12Client } from '@imhelper/onebot-v12';
|
|
15
|
+
|
|
16
|
+
export type Onebot12ApiCall = (
|
|
17
|
+
action: string,
|
|
18
|
+
params?: Record<string, unknown>,
|
|
19
|
+
) => Promise<OneBotV12Response>;
|
|
20
|
+
|
|
21
|
+
type Onebot12ClientEvents = ImHelperEventMap<
|
|
22
|
+
string,
|
|
23
|
+
ImHelperOneBot12Event,
|
|
24
|
+
EventMap<string>
|
|
25
|
+
>;
|
|
26
|
+
export type Onebot12ClientEventMap = ClientEventPayloads<Onebot12ClientEvents>;
|
|
27
|
+
|
|
28
|
+
export function createOnebot12EndpointClient(
|
|
29
|
+
config: ResolvedOneBot12Config,
|
|
30
|
+
request: Onebot12ApiCall,
|
|
31
|
+
): OneBotV12Client {
|
|
32
|
+
const baseUrl = config.connection === 'ws'
|
|
33
|
+
? config.url.replace(/^ws(s?):/, 'http$1:')
|
|
34
|
+
: config.connection === 'webhook' && config.api_url
|
|
35
|
+
? config.api_url
|
|
36
|
+
: 'http://localhost';
|
|
37
|
+
return new OneBotV12Client({
|
|
38
|
+
baseUrl,
|
|
39
|
+
selfId: config.id,
|
|
40
|
+
accessToken: config.access_token,
|
|
41
|
+
receiveMode: 'manual',
|
|
42
|
+
call: request,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function callOnebot12Client<T = unknown>(
|
|
47
|
+
client: OneBotV12Client,
|
|
48
|
+
action: string,
|
|
49
|
+
params?: Record<string, unknown>,
|
|
50
|
+
): Promise<T | undefined> {
|
|
51
|
+
const response: OneBotV12Response<T> = await client.call<T>(action, params);
|
|
52
|
+
if (response.status !== 'ok') {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`OneBot 12 API ${action}: retcode=${response.retcode}${response.message ? ` ${response.message}` : ''}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return response.data;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const onebot12ClientEventNames = Object.freeze([
|
|
61
|
+
...EventFactory.getSupportedEventTypes<string>(),
|
|
62
|
+
'event',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
export function forwardOnebot12ClientEvents(
|
|
66
|
+
client: OneBotV12Client,
|
|
67
|
+
receive: (name: string, payload: unknown) => void,
|
|
68
|
+
): () => void {
|
|
69
|
+
return forwardEndpointClientEvents(client, onebot12ClientEventNames, receive);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
declare module '@zhin.js/feature-kit' {
|
|
73
|
+
interface AdapterClientRegistry {
|
|
74
|
+
readonly onebot12: {
|
|
75
|
+
readonly client: OneBotV12Client;
|
|
76
|
+
readonly events: Onebot12ClientEventMap;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const onebot12Client = defineEndpointClient<OneBotV12Client, Onebot12ClientEventMap>('onebot12');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EndpointContentPort, EndpointContentResolveContext } from 'zhin.js/adapter';
|
|
2
|
+
import type { ConversationMessage, ConversationReference } from '@zhin.js/im-contract';
|
|
3
|
+
|
|
4
|
+
type CallApi = (action: string, params?: Record<string, unknown>) => Promise<unknown>;
|
|
5
|
+
|
|
6
|
+
export function createOneBot12ContentPort(callApi: CallApi): EndpointContentPort {
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
async resolve(reference: ConversationReference, context: EndpointContentResolveContext) {
|
|
9
|
+
try {
|
|
10
|
+
context.signal.throwIfAborted();
|
|
11
|
+
if (reference.kind === 'forward') {
|
|
12
|
+
return Object.freeze({ status: 'unsupported' as const, code: 'onebot12_forward_lookup_not_standardized' });
|
|
13
|
+
}
|
|
14
|
+
if (reference.kind === 'media') {
|
|
15
|
+
return reference.media.kind === 'file'
|
|
16
|
+
? Object.freeze({ status: 'unsupported' as const, code: 'onebot12_opaque_media' })
|
|
17
|
+
: Object.freeze({ status: 'resolved' as const, reference, value: reference.media });
|
|
18
|
+
}
|
|
19
|
+
const row = await callApi('get_message', { message_id: reference.message.id }) as Record<string, unknown>;
|
|
20
|
+
context.signal.throwIfAborted();
|
|
21
|
+
const message = (row.message && typeof row.message === 'object' ? row.message : row) as Record<string, unknown>;
|
|
22
|
+
const segments = normalizeSegments(message.message ?? message.content);
|
|
23
|
+
const value: ConversationMessage = Object.freeze({
|
|
24
|
+
ref: reference.message,
|
|
25
|
+
...(message.user_id != null ? { actor: Object.freeze({ id: String(message.user_id) }) } : {}),
|
|
26
|
+
segments,
|
|
27
|
+
timestamp: toMillis(message.time),
|
|
28
|
+
});
|
|
29
|
+
return Object.freeze({ status: 'resolved' as const, reference, value });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (context.signal.aborted) return Object.freeze({ status: 'expired' as const, code: 'turn_aborted' });
|
|
32
|
+
return Object.freeze({ status: 'failed' as const, code: 'onebot12_content_resolution_failed', message: error instanceof Error ? error.message : String(error) });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeSegments(value: unknown) {
|
|
39
|
+
if (!Array.isArray(value)) return Object.freeze([{ type: 'text', data: Object.freeze({ text: String(value ?? '') }) }]);
|
|
40
|
+
return Object.freeze(value.map((item) => {
|
|
41
|
+
const row = item as Record<string, unknown>;
|
|
42
|
+
return Object.freeze({ type: String(row.type ?? 'text'), data: Object.freeze((row.data ?? {}) as Record<string, unknown>) });
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toMillis(value: unknown): number {
|
|
47
|
+
const time = Number(value);
|
|
48
|
+
return Number.isFinite(time) ? (time < 10_000_000_000 ? time * 1000 : time) : Date.now();
|
|
49
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot12 endpoint management 语义端口(Console 社交面 RPC 消费,见
|
|
3
|
+
* packages/im/adapter/src/endpoint-management.ts)。
|
|
4
|
+
*
|
|
5
|
+
* 动作名对齐 OB12 规范:get_friend_list / get_group_list / get_group_member_list。
|
|
6
|
+
* 归一化取舍(OB12 id 为字符串,收敛为数字):
|
|
7
|
+
* - friend → {user_id:number, nickname: user_name, remark: user_displayname ?? ''}
|
|
8
|
+
* - group → {group_id:number, name: group_name ?? name}
|
|
9
|
+
* - 群成员列表保持 OB12 原生形状,仅保证数组
|
|
10
|
+
*/
|
|
11
|
+
import type {
|
|
12
|
+
EndpointFriend,
|
|
13
|
+
EndpointGroup,
|
|
14
|
+
EndpointManagement,
|
|
15
|
+
} from 'zhin.js/adapter';
|
|
16
|
+
|
|
17
|
+
/** 管理面只依赖 endpoint 的 callApi(ws/wss/webhook 三种传输各自实现)。 */
|
|
18
|
+
export interface OneBot12ManagementCaller {
|
|
19
|
+
callApi(action: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createOneBot12EndpointManagement(
|
|
23
|
+
endpoint: OneBot12ManagementCaller,
|
|
24
|
+
): EndpointManagement {
|
|
25
|
+
return Object.freeze<EndpointManagement>({
|
|
26
|
+
async listFriends(): Promise<readonly EndpointFriend[]> {
|
|
27
|
+
const data = await endpoint.callApi('get_friend_list');
|
|
28
|
+
return toArray(data).map((value) => {
|
|
29
|
+
const friend = asRecord(value);
|
|
30
|
+
return {
|
|
31
|
+
user_id: toNumberId(friend.user_id, 'user_id'),
|
|
32
|
+
nickname: String(friend.user_name ?? friend.nickname ?? ''),
|
|
33
|
+
remark: String(friend.user_displayname ?? friend.remark ?? ''),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
async listGroups(): Promise<readonly EndpointGroup[]> {
|
|
38
|
+
const data = await endpoint.callApi('get_group_list');
|
|
39
|
+
return toArray(data).map((value) => {
|
|
40
|
+
const group = asRecord(value);
|
|
41
|
+
return {
|
|
42
|
+
group_id: toNumberId(group.group_id, 'group_id'),
|
|
43
|
+
name: String(group.group_name ?? group.name ?? ''),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
async listGroupMembers(groupId: string): Promise<readonly unknown[]> {
|
|
48
|
+
const data = await endpoint.callApi('get_group_member_list', {
|
|
49
|
+
group_id: groupId,
|
|
50
|
+
});
|
|
51
|
+
return toArray(data);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
57
|
+
return value !== null && typeof value === 'object'
|
|
58
|
+
? value as Record<string, unknown>
|
|
59
|
+
: {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toArray(value: unknown): readonly unknown[] {
|
|
63
|
+
return Array.isArray(value) ? value : [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** OB12 id 是字符串;QQ 系实现均为数字串,统一收敛为数字。 */
|
|
67
|
+
function toNumberId(value: unknown, label: string): number {
|
|
68
|
+
const n = Number(value);
|
|
69
|
+
if (!Number.isFinite(n) || String(value ?? '').trim() === '') {
|
|
70
|
+
throw new TypeError(`onebot12 ${label} 必须是数字: ${String(value)}`);
|
|
71
|
+
}
|
|
72
|
+
return n;
|
|
73
|
+
}
|