@zhin.js/adapter-onebot12 3.0.2 → 3.0.3
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 +440 -0
- package/README.md +36 -71
- package/adapters/onebot12.ts +50 -0
- package/lib/index.d.ts +6 -19
- package/lib/index.js +5 -27
- package/lib/protocol.d.ts +151 -0
- package/lib/protocol.js +242 -0
- package/lib/webhook.d.ts +25 -0
- package/lib/webhook.js +140 -0
- package/lib/ws-endpoint.d.ts +25 -0
- package/lib/ws-endpoint.js +240 -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 +24 -0
- package/lib/wss-endpoint.js +193 -0
- package/package.json +41 -15
- package/plugin.ts +8 -0
- package/schema.json +40 -0
- package/src/index.ts +46 -37
- package/src/protocol.ts +387 -0
- package/src/webhook.ts +180 -0
- package/src/ws-endpoint.ts +287 -0
- package/src/ws-types.ts +19 -0
- package/src/wss-auth.ts +17 -0
- package/src/wss-endpoint.ts +226 -0
- package/lib/adapter.d.ts +0 -20
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -43
- 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/endpoint-webhook.d.ts +0 -25
- package/lib/endpoint-webhook.d.ts.map +0 -1
- package/lib/endpoint-webhook.js +0 -122
- package/lib/endpoint-webhook.js.map +0 -1
- package/lib/endpoint-ws.d.ts +0 -26
- package/lib/endpoint-ws.d.ts.map +0 -1
- package/lib/endpoint-ws.js +0 -214
- package/lib/endpoint-ws.js.map +0 -1
- package/lib/endpoint-wss.d.ts +0 -26
- package/lib/endpoint-wss.d.ts.map +0 -1
- package/lib/endpoint-wss.js +0 -191
- package/lib/endpoint-wss.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.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 -62
- package/lib/utils.js.map +0 -1
- package/src/adapter.ts +0 -56
- package/src/api.ts +0 -48
- package/src/endpoint-webhook.ts +0 -136
- package/src/endpoint-ws.ts +0 -228
- package/src/endpoint-wss.ts +0 -208
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -86
- package/src/utils.ts +0 -89
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot 12 protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
* Spec: https://12.onebot.dev/
|
|
5
|
+
*/
|
|
6
|
+
/** Transitional legacy endpoint row (`endpoints[]` with `context: onebot12`). */
|
|
7
|
+
export interface OneBot12LegacyEndpointRow {
|
|
8
|
+
readonly context?: string;
|
|
9
|
+
readonly connection?: 'ws' | 'webhook' | 'wss';
|
|
10
|
+
readonly name?: string;
|
|
11
|
+
readonly access_token?: string;
|
|
12
|
+
readonly url?: string;
|
|
13
|
+
readonly path?: string;
|
|
14
|
+
readonly api_url?: string;
|
|
15
|
+
readonly reconnect_interval?: number;
|
|
16
|
+
readonly heartbeat_interval?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
19
|
+
export interface OneBot12AdapterConfig {
|
|
20
|
+
readonly connection?: 'ws' | 'webhook' | 'wss';
|
|
21
|
+
readonly name?: string;
|
|
22
|
+
readonly access_token?: string;
|
|
23
|
+
readonly url?: string;
|
|
24
|
+
readonly path?: string;
|
|
25
|
+
readonly api_url?: string;
|
|
26
|
+
readonly reconnect_interval?: number;
|
|
27
|
+
readonly heartbeat_interval?: number;
|
|
28
|
+
/** Transitional: legacy root `endpoints[]` with `context: onebot12`. */
|
|
29
|
+
readonly endpoints?: ReadonlyArray<OneBot12LegacyEndpointRow>;
|
|
30
|
+
}
|
|
31
|
+
export interface OneBot12ConfigBase {
|
|
32
|
+
readonly context: 'onebot12';
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly access_token?: string;
|
|
35
|
+
}
|
|
36
|
+
/** 正向 WebSocket:应用连 OneBot 实现的 WS 服务器 */
|
|
37
|
+
export interface OneBot12WsConfig extends OneBot12ConfigBase {
|
|
38
|
+
readonly connection: 'ws';
|
|
39
|
+
readonly url: string;
|
|
40
|
+
readonly reconnect_interval: number;
|
|
41
|
+
readonly heartbeat_interval: number;
|
|
42
|
+
}
|
|
43
|
+
/** HTTP Webhook:httpHostToken POST 入站 + api_url HTTP 出站 */
|
|
44
|
+
export interface OneBot12WebhookConfig extends OneBot12ConfigBase {
|
|
45
|
+
readonly connection: 'webhook';
|
|
46
|
+
readonly path: string;
|
|
47
|
+
readonly api_url?: string;
|
|
48
|
+
}
|
|
49
|
+
/** 反向 WebSocket:httpHostToken WS upgrade 入站/出站 */
|
|
50
|
+
export interface OneBot12WssConfig extends OneBot12ConfigBase {
|
|
51
|
+
readonly connection: 'wss';
|
|
52
|
+
readonly path: string;
|
|
53
|
+
readonly heartbeat_interval: number;
|
|
54
|
+
}
|
|
55
|
+
export type ResolvedOneBot12Config = OneBot12WsConfig | OneBot12WebhookConfig | OneBot12WssConfig;
|
|
56
|
+
export type OneBot12EndpointConfig = ResolvedOneBot12Config;
|
|
57
|
+
export interface OneBot12Self {
|
|
58
|
+
readonly platform: string;
|
|
59
|
+
readonly user_id: string;
|
|
60
|
+
}
|
|
61
|
+
export interface OneBot12Event {
|
|
62
|
+
id: string;
|
|
63
|
+
time: number;
|
|
64
|
+
type: 'meta' | 'message' | 'notice' | 'request';
|
|
65
|
+
detail_type: string;
|
|
66
|
+
sub_type: string;
|
|
67
|
+
self?: OneBot12Self;
|
|
68
|
+
message_id?: string;
|
|
69
|
+
message?: OneBot12Segment[];
|
|
70
|
+
alt_message?: string;
|
|
71
|
+
user_id?: string;
|
|
72
|
+
group_id?: string;
|
|
73
|
+
channel_id?: string;
|
|
74
|
+
guild_id?: string;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}
|
|
77
|
+
export interface OneBot12Segment {
|
|
78
|
+
type: string;
|
|
79
|
+
data?: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
export interface OneBot12ActionRequest {
|
|
82
|
+
action: string;
|
|
83
|
+
params: Record<string, unknown>;
|
|
84
|
+
echo?: string;
|
|
85
|
+
self?: OneBot12Self;
|
|
86
|
+
}
|
|
87
|
+
export interface OneBot12ActionResponse {
|
|
88
|
+
status: 'ok' | 'failed';
|
|
89
|
+
retcode: number;
|
|
90
|
+
data?: unknown;
|
|
91
|
+
message: string;
|
|
92
|
+
echo?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface OneBot12HttpOptions {
|
|
95
|
+
readonly url: string;
|
|
96
|
+
readonly access_token?: string;
|
|
97
|
+
}
|
|
98
|
+
export interface OneBot12WireSegment {
|
|
99
|
+
readonly type: string;
|
|
100
|
+
readonly data?: Record<string, unknown>;
|
|
101
|
+
}
|
|
102
|
+
export interface ParsedSendTarget {
|
|
103
|
+
readonly detail_type: 'private' | 'group' | 'channel';
|
|
104
|
+
readonly id: string;
|
|
105
|
+
readonly guild_id?: string;
|
|
106
|
+
}
|
|
107
|
+
export declare function resolveOneBot12Config(config?: OneBot12AdapterConfig): ResolvedOneBot12Config;
|
|
108
|
+
/** 判断是否为消息事件(type=message) */
|
|
109
|
+
export declare function isMessageEvent(ev: OneBot12Event): ev is OneBot12Event & {
|
|
110
|
+
message_id: string;
|
|
111
|
+
message?: OneBot12Segment[];
|
|
112
|
+
};
|
|
113
|
+
/** 从事件得到场景 id:私聊 user_id,群 group_id,频道 channel_id 或 guild_id:channel_id */
|
|
114
|
+
export declare function getChannelId(ev: OneBot12Event): string;
|
|
115
|
+
/**
|
|
116
|
+
* Gateway reply target:`detail_type:channelId`,便于 send() 还原动作参数。
|
|
117
|
+
*/
|
|
118
|
+
export declare function formatInboundTarget(ev: OneBot12Event): string;
|
|
119
|
+
export declare function parseSendTarget(target: string): ParsedSendTarget;
|
|
120
|
+
/** Build inbound text for MessageGateway.receive */
|
|
121
|
+
export declare function formatInboundContent(ev: OneBot12Event): string;
|
|
122
|
+
/**
|
|
123
|
+
* Runtime Message sender 必须是用户 ID(agent bridge 以 sender 与 endpointMaster 比对)。
|
|
124
|
+
* 显示名经 {@link senderNickname} 放入 metadata.nickname。
|
|
125
|
+
*/
|
|
126
|
+
export declare function senderUserId(ev: OneBot12Event): string;
|
|
127
|
+
/** 事件携带的发送者显示名(`user.name` / `qq.nickname`),没有则返回 undefined。 */
|
|
128
|
+
export declare function senderNickname(ev: OneBot12Event): string | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* 判断入站消息是否 @ 了本机:OneBot12 提及段为 `{type:'mention', data:{user_id}}`,
|
|
131
|
+
* 目标 user_id 等于事件 self.user_id(self 为 {platform, user_id} 对象)时视为提及。
|
|
132
|
+
* 新 Plugin Runtime 的 Message.content 为纯文本,mention 信息只能经 metadata.mentioned 传递。
|
|
133
|
+
*/
|
|
134
|
+
export declare function isBotMentioned(ev: OneBot12Event): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Wire-encode an already-rendered outbound payload into OneBot 12 message segments.
|
|
137
|
+
* Segment canonicalization is intentionally not done here.
|
|
138
|
+
*/
|
|
139
|
+
export declare function formatOutboundSegments(payload: unknown): OneBot12Segment[];
|
|
140
|
+
export declare function buildSendMessageParams(target: string, message: OneBot12Segment[]): Record<string, unknown>;
|
|
141
|
+
/**
|
|
142
|
+
* 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应。
|
|
143
|
+
* 供 webhook 出站与纯协议测试使用;WS 路径走 WebSocket echo 请求。
|
|
144
|
+
*/
|
|
145
|
+
export declare function callOneBot12Action(options: OneBot12HttpOptions, action: string, params?: Record<string, unknown>, echo?: string): Promise<OneBot12ActionResponse>;
|
|
146
|
+
/** Build WS connect URL + headers (access_token via Bearer + query). */
|
|
147
|
+
export declare function buildWsConnectOptions(config: OneBot12WsConfig): {
|
|
148
|
+
readonly url: string;
|
|
149
|
+
readonly headers: Record<string, string>;
|
|
150
|
+
readonly safeUrl: string;
|
|
151
|
+
};
|
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OneBot 12 protocol helpers (no legacy Adapter/Endpoint / segment-mapper).
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
* Spec: https://12.onebot.dev/
|
|
5
|
+
*/
|
|
6
|
+
export function resolveOneBot12Config(config = {}) {
|
|
7
|
+
const entry = config.endpoints?.find((item) => item.context === 'onebot12');
|
|
8
|
+
const connection = config.connection
|
|
9
|
+
?? entry?.connection
|
|
10
|
+
?? 'ws';
|
|
11
|
+
const name = (typeof config.name === 'string' && config.name)
|
|
12
|
+
|| (typeof entry?.name === 'string' && entry.name)
|
|
13
|
+
|| process.env.ONEBOT12_BOT_NAME
|
|
14
|
+
|| 'onebot12-bot';
|
|
15
|
+
const access_token = config.access_token ?? entry?.access_token;
|
|
16
|
+
if (connection === 'ws') {
|
|
17
|
+
const url = config.url ?? entry?.url;
|
|
18
|
+
if (!url) {
|
|
19
|
+
throw new TypeError('OneBot12 connection:ws requires url (plugins.<key>.url or endpoints with context: onebot12)');
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
context: 'onebot12',
|
|
23
|
+
connection: 'ws',
|
|
24
|
+
name,
|
|
25
|
+
access_token,
|
|
26
|
+
url,
|
|
27
|
+
reconnect_interval: config.reconnect_interval ?? entry?.reconnect_interval ?? 5000,
|
|
28
|
+
heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (connection === 'webhook') {
|
|
32
|
+
const path = config.path ?? entry?.path;
|
|
33
|
+
if (!path) {
|
|
34
|
+
throw new TypeError('OneBot12 connection:webhook requires path');
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
context: 'onebot12',
|
|
38
|
+
connection: 'webhook',
|
|
39
|
+
name,
|
|
40
|
+
access_token,
|
|
41
|
+
path,
|
|
42
|
+
api_url: config.api_url ?? entry?.api_url,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (connection === 'wss') {
|
|
46
|
+
const path = config.path ?? entry?.path;
|
|
47
|
+
if (!path) {
|
|
48
|
+
throw new TypeError('OneBot12 connection:wss requires path');
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
context: 'onebot12',
|
|
52
|
+
connection: 'wss',
|
|
53
|
+
name,
|
|
54
|
+
access_token,
|
|
55
|
+
path,
|
|
56
|
+
heartbeat_interval: config.heartbeat_interval ?? entry?.heartbeat_interval ?? 30_000,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
throw new TypeError(`Unknown OneBot12 connection: ${String(connection)}`);
|
|
60
|
+
}
|
|
61
|
+
/** 判断是否为消息事件(type=message) */
|
|
62
|
+
export function isMessageEvent(ev) {
|
|
63
|
+
return ev.type === 'message' && !!ev.message_id;
|
|
64
|
+
}
|
|
65
|
+
/** 从事件得到场景 id:私聊 user_id,群 group_id,频道 channel_id 或 guild_id:channel_id */
|
|
66
|
+
export function getChannelId(ev) {
|
|
67
|
+
if (ev.detail_type === 'private' && ev.user_id)
|
|
68
|
+
return ev.user_id;
|
|
69
|
+
if (ev.detail_type === 'group' && ev.group_id)
|
|
70
|
+
return ev.group_id;
|
|
71
|
+
if (ev.detail_type === 'channel' && ev.channel_id) {
|
|
72
|
+
return ev.guild_id ? `${ev.guild_id}:${ev.channel_id}` : ev.channel_id;
|
|
73
|
+
}
|
|
74
|
+
return ev.user_id ?? ev.group_id ?? '';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Gateway reply target:`detail_type:channelId`,便于 send() 还原动作参数。
|
|
78
|
+
*/
|
|
79
|
+
export function formatInboundTarget(ev) {
|
|
80
|
+
const detail = ev.detail_type === 'private' || ev.detail_type === 'group' || ev.detail_type === 'channel'
|
|
81
|
+
? ev.detail_type
|
|
82
|
+
: 'private';
|
|
83
|
+
return `${detail}:${getChannelId(ev)}`;
|
|
84
|
+
}
|
|
85
|
+
export function parseSendTarget(target) {
|
|
86
|
+
const sep = target.indexOf(':');
|
|
87
|
+
if (sep <= 0) {
|
|
88
|
+
return { detail_type: 'private', id: target };
|
|
89
|
+
}
|
|
90
|
+
const head = target.slice(0, sep);
|
|
91
|
+
const rest = target.slice(sep + 1);
|
|
92
|
+
if (head === 'private' || head === 'group') {
|
|
93
|
+
return { detail_type: head, id: rest };
|
|
94
|
+
}
|
|
95
|
+
if (head === 'channel') {
|
|
96
|
+
const guildSep = rest.indexOf(':');
|
|
97
|
+
if (guildSep > 0) {
|
|
98
|
+
return {
|
|
99
|
+
detail_type: 'channel',
|
|
100
|
+
guild_id: rest.slice(0, guildSep),
|
|
101
|
+
id: rest.slice(guildSep + 1),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { detail_type: 'channel', id: rest };
|
|
105
|
+
}
|
|
106
|
+
return { detail_type: 'private', id: target };
|
|
107
|
+
}
|
|
108
|
+
/** Build inbound text for MessageGateway.receive */
|
|
109
|
+
export function formatInboundContent(ev) {
|
|
110
|
+
if (typeof ev.alt_message === 'string' && ev.alt_message)
|
|
111
|
+
return ev.alt_message;
|
|
112
|
+
if (Array.isArray(ev.message)) {
|
|
113
|
+
return ev.message
|
|
114
|
+
.map((seg) => (seg.type === 'text' ? String(seg.data?.text ?? '') : ''))
|
|
115
|
+
.join('');
|
|
116
|
+
}
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Runtime Message sender 必须是用户 ID(agent bridge 以 sender 与 endpointMaster 比对)。
|
|
121
|
+
* 显示名经 {@link senderNickname} 放入 metadata.nickname。
|
|
122
|
+
*/
|
|
123
|
+
export function senderUserId(ev) {
|
|
124
|
+
return ev.user_id ?? '';
|
|
125
|
+
}
|
|
126
|
+
/** 事件携带的发送者显示名(`user.name` / `qq.nickname`),没有则返回 undefined。 */
|
|
127
|
+
export function senderNickname(ev) {
|
|
128
|
+
const record = ev;
|
|
129
|
+
const name = record['user.name'] ?? record['qq.nickname'];
|
|
130
|
+
if (typeof name === 'string' && name)
|
|
131
|
+
return name;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 判断入站消息是否 @ 了本机:OneBot12 提及段为 `{type:'mention', data:{user_id}}`,
|
|
136
|
+
* 目标 user_id 等于事件 self.user_id(self 为 {platform, user_id} 对象)时视为提及。
|
|
137
|
+
* 新 Plugin Runtime 的 Message.content 为纯文本,mention 信息只能经 metadata.mentioned 传递。
|
|
138
|
+
*/
|
|
139
|
+
export function isBotMentioned(ev) {
|
|
140
|
+
const selfId = ev.self?.user_id;
|
|
141
|
+
if (!selfId || !Array.isArray(ev.message))
|
|
142
|
+
return false;
|
|
143
|
+
return ev.message.some((seg) => seg.type === 'mention' && String(seg.data?.['user_id'] ?? '') === selfId);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Wire-encode an already-rendered outbound payload into OneBot 12 message segments.
|
|
147
|
+
* Segment canonicalization is intentionally not done here.
|
|
148
|
+
*/
|
|
149
|
+
export function formatOutboundSegments(payload) {
|
|
150
|
+
if (typeof payload === 'string') {
|
|
151
|
+
return [{ type: 'text', data: { text: payload } }];
|
|
152
|
+
}
|
|
153
|
+
const items = Array.isArray(payload)
|
|
154
|
+
? payload
|
|
155
|
+
: payload && typeof payload === 'object' && 'type' in payload
|
|
156
|
+
? [payload]
|
|
157
|
+
: [];
|
|
158
|
+
if (items.length === 0) {
|
|
159
|
+
const text = payload == null
|
|
160
|
+
? ''
|
|
161
|
+
: typeof payload === 'object'
|
|
162
|
+
? JSON.stringify(payload)
|
|
163
|
+
: String(payload);
|
|
164
|
+
return [{ type: 'text', data: { text } }];
|
|
165
|
+
}
|
|
166
|
+
const segs = [];
|
|
167
|
+
for (const item of items) {
|
|
168
|
+
if (typeof item === 'string') {
|
|
169
|
+
segs.push({ type: 'text', data: { text: item } });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
segs.push({ type: item.type, data: item.data ?? {} });
|
|
173
|
+
}
|
|
174
|
+
return segs.length ? segs : [{ type: 'text', data: { text: '' } }];
|
|
175
|
+
}
|
|
176
|
+
export function buildSendMessageParams(target, message) {
|
|
177
|
+
const parsed = parseSendTarget(target);
|
|
178
|
+
const params = {
|
|
179
|
+
message,
|
|
180
|
+
detail_type: parsed.detail_type,
|
|
181
|
+
};
|
|
182
|
+
if (parsed.detail_type === 'private') {
|
|
183
|
+
params.user_id = parsed.id;
|
|
184
|
+
}
|
|
185
|
+
else if (parsed.detail_type === 'group') {
|
|
186
|
+
params.group_id = parsed.id;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
params.channel_id = parsed.id;
|
|
190
|
+
if (parsed.guild_id)
|
|
191
|
+
params.guild_id = parsed.guild_id;
|
|
192
|
+
}
|
|
193
|
+
return params;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 向 OneBot 实现发送动作请求(HTTP POST),返回动作响应。
|
|
197
|
+
* 供 webhook 出站与纯协议测试使用;WS 路径走 WebSocket echo 请求。
|
|
198
|
+
*/
|
|
199
|
+
export async function callOneBot12Action(options, action, params = {}, echo) {
|
|
200
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
201
|
+
if (options.access_token) {
|
|
202
|
+
headers.Authorization = `Bearer ${options.access_token}`;
|
|
203
|
+
}
|
|
204
|
+
const body = { action, params };
|
|
205
|
+
if (echo)
|
|
206
|
+
body.echo = echo;
|
|
207
|
+
const res = await fetch(options.url, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers,
|
|
210
|
+
body: JSON.stringify(body),
|
|
211
|
+
});
|
|
212
|
+
const text = await res.text();
|
|
213
|
+
if (res.status === 401)
|
|
214
|
+
throw new Error(`OneBot12 鉴权失败: ${text}`);
|
|
215
|
+
if (res.status !== 200)
|
|
216
|
+
throw new Error(`OneBot12 HTTP ${res.status}: ${text}`);
|
|
217
|
+
let data;
|
|
218
|
+
try {
|
|
219
|
+
data = JSON.parse(text);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
throw new Error(`OneBot12 无效响应: ${text.slice(0, 200)}`);
|
|
223
|
+
}
|
|
224
|
+
if (data.status === 'failed' && data.retcode !== 0) {
|
|
225
|
+
throw new Error(`OneBot12 动作失败 retcode=${data.retcode}: ${data.message}`);
|
|
226
|
+
}
|
|
227
|
+
return data;
|
|
228
|
+
}
|
|
229
|
+
/** Build WS connect URL + headers (access_token via Bearer + query). */
|
|
230
|
+
export function buildWsConnectOptions(config) {
|
|
231
|
+
const headers = {};
|
|
232
|
+
let connectUrl = config.url;
|
|
233
|
+
if (config.access_token) {
|
|
234
|
+
headers.Authorization = `Bearer ${config.access_token}`;
|
|
235
|
+
const url = new URL(config.url);
|
|
236
|
+
url.searchParams.set('access_token', config.access_token);
|
|
237
|
+
connectUrl = url.toString();
|
|
238
|
+
}
|
|
239
|
+
const safeUrl = new URL(connectUrl);
|
|
240
|
+
safeUrl.searchParams.delete('access_token');
|
|
241
|
+
return { url: connectUrl, headers, safeUrl: safeUrl.toString() };
|
|
242
|
+
}
|
package/lib/webhook.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
2
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
|
+
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
5
|
+
import { callOneBot12Action, type OneBot12Event, type OneBot12WebhookConfig } from './protocol.js';
|
|
6
|
+
export interface OneBot12WebhookEndpointOptions {
|
|
7
|
+
readonly id: CapabilityId;
|
|
8
|
+
readonly gateway: MessageGateway;
|
|
9
|
+
readonly http: HttpHost;
|
|
10
|
+
readonly config: OneBot12WebhookConfig;
|
|
11
|
+
readonly callAction?: typeof callOneBot12Action;
|
|
12
|
+
}
|
|
13
|
+
export declare class OneBot12WebhookEndpoint implements EndpointInstance {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(options: OneBot12WebhookEndpointOptions);
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
open(): void;
|
|
18
|
+
close(): void;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
send({ target, payload }: {
|
|
21
|
+
readonly target: string;
|
|
22
|
+
readonly payload: unknown;
|
|
23
|
+
}): Promise<string>;
|
|
24
|
+
admit(ev: OneBot12Event): void;
|
|
25
|
+
}
|
package/lib/webhook.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
2
|
+
import { buildSendMessageParams, callOneBot12Action, formatInboundContent, formatInboundTarget, formatOutboundSegments, isBotMentioned, isMessageEvent, senderNickname, senderUserId, } from './protocol.js';
|
|
3
|
+
import { verifyOneBotAccessToken } from './wss-auth.js';
|
|
4
|
+
const logger = getLogger('onebot12');
|
|
5
|
+
export class OneBot12WebhookEndpoint {
|
|
6
|
+
#options;
|
|
7
|
+
#callAction;
|
|
8
|
+
#routeReleases = [];
|
|
9
|
+
#open = false;
|
|
10
|
+
#started = false;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.#options = options;
|
|
13
|
+
this.#callAction = options.callAction ?? callOneBot12Action;
|
|
14
|
+
}
|
|
15
|
+
async start() {
|
|
16
|
+
if (this.#started)
|
|
17
|
+
return;
|
|
18
|
+
this.#started = true;
|
|
19
|
+
this.#setupRoutes();
|
|
20
|
+
logger.info(formatCompact({
|
|
21
|
+
op: 'listen',
|
|
22
|
+
endpoint: this.#options.config.name,
|
|
23
|
+
mode: 'webhook',
|
|
24
|
+
path: this.#options.config.path,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
open() {
|
|
28
|
+
this.#open = true;
|
|
29
|
+
}
|
|
30
|
+
close() {
|
|
31
|
+
this.#open = false;
|
|
32
|
+
}
|
|
33
|
+
async stop() {
|
|
34
|
+
this.#open = false;
|
|
35
|
+
for (const release of this.#routeReleases.splice(0))
|
|
36
|
+
release();
|
|
37
|
+
this.#started = false;
|
|
38
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
39
|
+
}
|
|
40
|
+
async send({ target, payload }) {
|
|
41
|
+
const apiUrl = this.#options.config.api_url;
|
|
42
|
+
if (!apiUrl) {
|
|
43
|
+
throw new Error('OneBot12 connection:webhook requires api_url for outbound send');
|
|
44
|
+
}
|
|
45
|
+
const message = formatOutboundSegments(payload);
|
|
46
|
+
const params = buildSendMessageParams(target, message);
|
|
47
|
+
const resp = await this.#callAction({ url: apiUrl, access_token: this.#options.config.access_token }, 'send_message', params);
|
|
48
|
+
const data = resp.data;
|
|
49
|
+
const messageId = data?.message_id ?? '';
|
|
50
|
+
logger.debug(formatCompact({
|
|
51
|
+
op: 'onebot12_send',
|
|
52
|
+
endpoint: this.#options.config.name,
|
|
53
|
+
target,
|
|
54
|
+
messageId,
|
|
55
|
+
}));
|
|
56
|
+
return messageId;
|
|
57
|
+
}
|
|
58
|
+
admit(ev) {
|
|
59
|
+
if (!this.#open || !isMessageEvent(ev))
|
|
60
|
+
return;
|
|
61
|
+
const target = formatInboundTarget(ev);
|
|
62
|
+
const content = formatInboundContent(ev);
|
|
63
|
+
const nickname = senderNickname(ev);
|
|
64
|
+
const mentioned = isBotMentioned(ev);
|
|
65
|
+
void this.#options.gateway.receive({
|
|
66
|
+
adapter: this.#options.id,
|
|
67
|
+
target,
|
|
68
|
+
content,
|
|
69
|
+
sender: senderUserId(ev),
|
|
70
|
+
id: ev.message_id,
|
|
71
|
+
metadata: Object.freeze({
|
|
72
|
+
detail_type: ev.detail_type,
|
|
73
|
+
user_id: ev.user_id,
|
|
74
|
+
group_id: ev.group_id,
|
|
75
|
+
channel_id: ev.channel_id,
|
|
76
|
+
guild_id: ev.guild_id,
|
|
77
|
+
endpoint: this.#options.config.name,
|
|
78
|
+
time: ev.time,
|
|
79
|
+
...(nickname ? { nickname } : {}),
|
|
80
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
81
|
+
}),
|
|
82
|
+
}).catch((err) => {
|
|
83
|
+
logger.warn(formatCompact({
|
|
84
|
+
op: 'onebot12_gateway_receive_failed',
|
|
85
|
+
target,
|
|
86
|
+
error: err instanceof Error ? err.message : String(err),
|
|
87
|
+
}));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
#setupRoutes() {
|
|
91
|
+
const path = this.#options.config.path;
|
|
92
|
+
this.#routeReleases.push(this.#options.http.route('POST', path, async (request, response) => {
|
|
93
|
+
await this.#handleWebhook(request, response);
|
|
94
|
+
}, { summary: 'OneBot12 webhook callback', tags: ['onebot12'] }));
|
|
95
|
+
}
|
|
96
|
+
async #handleWebhook(request, response) {
|
|
97
|
+
try {
|
|
98
|
+
if (!verifyOneBotAccessToken(this.#options.config.access_token, request)) {
|
|
99
|
+
response.writeHead(403, { 'Content-Type': 'application/json' });
|
|
100
|
+
response.end(JSON.stringify({ message: 'Unauthorized' }));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const raw = await readRequestBody(request);
|
|
104
|
+
let ev;
|
|
105
|
+
try {
|
|
106
|
+
ev = JSON.parse(raw);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
response.writeHead(400, { 'Content-Type': 'application/json' });
|
|
110
|
+
response.end(JSON.stringify({ message: 'Invalid JSON' }));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (this.#open)
|
|
114
|
+
this.admit(ev);
|
|
115
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
116
|
+
response.end(JSON.stringify({ status: 'ok' }));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
logger.error('OneBot12 webhook error:', error);
|
|
120
|
+
if (!response.headersSent) {
|
|
121
|
+
response.writeHead(500, { 'Content-Type': 'application/json' });
|
|
122
|
+
response.end(JSON.stringify({ message: 'Internal Server Error' }));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function readRequestBody(request) {
|
|
128
|
+
const chunks = [];
|
|
129
|
+
let size = 0;
|
|
130
|
+
for await (const chunk of request) {
|
|
131
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
132
|
+
size += buffer.length;
|
|
133
|
+
if (size > 1_048_576) {
|
|
134
|
+
request.destroy();
|
|
135
|
+
throw new Error('Request body exceeds 1MB');
|
|
136
|
+
}
|
|
137
|
+
chunks.push(buffer);
|
|
138
|
+
}
|
|
139
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
140
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
2
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
4
|
+
import { type OneBot12Event, type OneBot12WsConfig } from './protocol.js';
|
|
5
|
+
import { type OneBot12WsCreateOptions, type OneBot12WsSocket } from './ws-types.js';
|
|
6
|
+
export interface OneBot12WsEndpointOptions {
|
|
7
|
+
readonly id: CapabilityId;
|
|
8
|
+
readonly gateway: MessageGateway;
|
|
9
|
+
readonly config: OneBot12WsConfig;
|
|
10
|
+
readonly createWebSocket?: (url: string, options: OneBot12WsCreateOptions) => OneBot12WsSocket;
|
|
11
|
+
}
|
|
12
|
+
export declare class OneBot12WsEndpoint implements EndpointInstance {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(options: OneBot12WsEndpointOptions);
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
open(): void;
|
|
17
|
+
close(): void;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
send({ target, payload }: {
|
|
20
|
+
readonly target: string;
|
|
21
|
+
readonly payload: unknown;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
/** Test / internal: admit a parsed event when the endpoint is open. */
|
|
24
|
+
admit(ev: OneBot12Event): void;
|
|
25
|
+
}
|