@zhin.js/adapter-kook 5.0.2 → 6.0.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 +62 -0
- package/README.md +61 -453
- package/adapters/kook.ts +46 -0
- package/agent/tools/blacklist.ts +2 -2
- package/agent/tools/create_role.ts +2 -2
- package/agent/tools/delete_role.ts +2 -2
- package/agent/tools/grant_role.ts +2 -2
- package/agent/tools/list_roles.ts +2 -2
- package/agent/tools/revoke_role.ts +2 -2
- package/lib/endpoint.d.ts +84 -0
- package/lib/endpoint.js +299 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/kook-agent-deps.d.ts +29 -0
- package/lib/kook-agent-deps.js +30 -0
- package/lib/platform-permit.d.ts +23 -0
- package/lib/{src/platform-permit.js → platform-permit.js} +2 -3
- package/lib/protocol.d.ts +147 -0
- package/lib/protocol.js +279 -0
- package/lib/webhook.d.ts +15 -0
- package/lib/webhook.js +76 -0
- package/lib/ws.d.ts +42 -0
- package/lib/ws.js +63 -0
- package/package.json +44 -40
- package/plugin.ts +13 -0
- package/schema.json +92 -0
- package/src/endpoint.ts +287 -879
- package/src/index.ts +55 -177
- package/src/kook-agent-deps.ts +44 -11
- package/src/platform-permit.ts +13 -3
- package/src/protocol.ts +429 -0
- package/src/webhook.ts +116 -0
- package/src/ws.ts +120 -0
- package/client/Dashboard.tsx +0 -255
- package/client/index.tsx +0 -11
- package/client/tsconfig.json +0 -7
- package/client/utils/api.ts +0 -30
- package/dist/index.js +0 -31
- package/lib/agent/tools/blacklist.js +0 -33
- package/lib/agent/tools/blacklist.js.map +0 -1
- package/lib/agent/tools/create_role.js +0 -25
- package/lib/agent/tools/create_role.js.map +0 -1
- package/lib/agent/tools/delete_role.js +0 -21
- package/lib/agent/tools/delete_role.js.map +0 -1
- package/lib/agent/tools/grant_role.js +0 -22
- package/lib/agent/tools/grant_role.js.map +0 -1
- package/lib/agent/tools/list_roles.js +0 -27
- package/lib/agent/tools/list_roles.js.map +0 -1
- package/lib/agent/tools/revoke_role.js +0 -22
- package/lib/agent/tools/revoke_role.js.map +0 -1
- package/lib/src/adapter.js +0 -67
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/endpoint.js +0 -796
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/index.js +0 -197
- package/lib/src/index.js.map +0 -1
- package/lib/src/kook-agent-deps.js +0 -10
- package/lib/src/kook-agent-deps.js.map +0 -1
- package/lib/src/kook-asset-upload.js +0 -46
- package/lib/src/kook-asset-upload.js.map +0 -1
- package/lib/src/kook-inbound.js +0 -22
- package/lib/src/kook-inbound.js.map +0 -1
- package/lib/src/kook-msg-route.js +0 -106
- package/lib/src/kook-msg-route.js.map +0 -1
- package/lib/src/kook-side-events.js +0 -166
- package/lib/src/kook-side-events.js.map +0 -1
- package/lib/src/outbound-keyboard.js +0 -66
- package/lib/src/outbound-keyboard.js.map +0 -1
- package/lib/src/outbound-media.js +0 -45
- package/lib/src/outbound-media.js.map +0 -1
- package/lib/src/outbound-sendable.js +0 -64
- package/lib/src/outbound-sendable.js.map +0 -1
- package/lib/src/platform-permit.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/types.js +0 -8
- package/lib/src/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -75
- package/src/kook-asset-upload.ts +0 -55
- package/src/kook-inbound.ts +0 -22
- package/src/kook-msg-route.ts +0 -122
- package/src/kook-side-events.ts +0 -230
- package/src/outbound-keyboard.ts +0 -77
- package/src/outbound-media.ts +0 -60
- package/src/outbound-sendable.ts +0 -73
- package/src/segment-mapper.ts +0 -1
- package/src/types.ts +0 -58
package/src/endpoint.ts
CHANGED
|
@@ -1,953 +1,361 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* KookEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
|
|
3
3
|
*/
|
|
4
|
+
import { Client } from 'kook-client';
|
|
5
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
6
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
|
+
import type { CapabilityId } from '@zhin.js/plugin-runtime';
|
|
10
|
+
import { registerKookAgentEndpoint } from './kook-agent-deps.js';
|
|
4
11
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
12
|
+
formatInboundContent,
|
|
13
|
+
formatInboundTarget,
|
|
14
|
+
formatOutboundKmarkdown,
|
|
15
|
+
isKookBotMentioned,
|
|
16
|
+
parseSendTarget,
|
|
17
|
+
senderDisplayName,
|
|
18
|
+
type KookInboundMessage,
|
|
19
|
+
type ResolvedKookWebhookConfig,
|
|
20
|
+
type ResolvedKookWebsocketConfig,
|
|
21
|
+
} from './protocol.js';
|
|
22
|
+
import { registerKookWebhookRoutes } from './webhook.js';
|
|
16
23
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
formatKookNotice,
|
|
33
|
-
formatKookNoticeLog,
|
|
34
|
-
formatKookButtonClickMessage,
|
|
35
|
-
isKookButtonClickEvent,
|
|
36
|
-
isKookNoticeGatewayEvent,
|
|
37
|
-
resolveKookSideEventDedupeKey,
|
|
38
|
-
type KookGatewayEvent,
|
|
39
|
-
} from "./kook-side-events.js";
|
|
40
|
-
import {
|
|
41
|
-
encodeKookMsgRef,
|
|
42
|
-
encodeKookReactionId,
|
|
43
|
-
isKookApiGoneResult,
|
|
44
|
-
isKookApiSuccess,
|
|
45
|
-
isKookMsgGoneError,
|
|
46
|
-
shouldStopDeleteAfterResponse,
|
|
47
|
-
kookDeleteApiPath,
|
|
48
|
-
kookReactionApiPath,
|
|
49
|
-
parseKookMsgRef,
|
|
50
|
-
parseKookReactionId,
|
|
51
|
-
plainKookMsgId,
|
|
52
|
-
resolveKookRoutes,
|
|
53
|
-
routeFromSceneType,
|
|
54
|
-
routeFromSendType,
|
|
55
|
-
type KookMsgRoute,
|
|
56
|
-
} from "./kook-msg-route.js";
|
|
57
|
-
import { materializeOutboundMedia } from "./outbound-media.js";
|
|
58
|
-
import { uploadKookAsset } from "./kook-asset-upload.js";
|
|
59
|
-
import { convertToKookSendable } from "./outbound-sendable.js";
|
|
60
|
-
import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
|
|
61
|
-
|
|
62
|
-
export class KookEndpoint extends Client implements Endpoint<KookEndpointConfig, KookRawMessage> {
|
|
63
|
-
$connected: boolean = false;
|
|
64
|
-
/** KOOK 平台 user_id,用于 @ 触发匹配(resolveEndpointAtIds) */
|
|
65
|
-
$platformUserId?: string;
|
|
66
|
-
adapter: KookAdapter;
|
|
67
|
-
private readonly inboundDeduper = new InboundMessageDeduper();
|
|
68
|
-
private readonly onGatewayEvent = (raw: KookGatewayEvent) => {
|
|
69
|
-
this.handleGatewayEvent(raw);
|
|
70
|
-
};
|
|
71
|
-
/** kook-client 构造函数注入的 process 监听器,disconnect 时需移除 */
|
|
72
|
-
private readonly _kookProcessListeners: Array<{ event: string; fn: (...args: any[]) => void }> = [];
|
|
73
|
-
|
|
74
|
-
get $id(): string {
|
|
75
|
-
return this.$config.name;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
get pluginLogger() {
|
|
79
|
-
return this.adapter.plugin.logger;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
constructor(adapter: KookAdapter, public $config: KookEndpointConfig) {
|
|
83
|
-
// 记录构造前 process 上的 uncaughtException 监听器数量
|
|
84
|
-
const beforeCount = process.listenerCount('uncaughtException');
|
|
85
|
-
super({
|
|
86
|
-
token: $config.token,
|
|
87
|
-
mode: "websocket", // KOOK 默认使用 WebSocket 模式
|
|
88
|
-
data_dir: $config.data_dir || path.join(process.cwd(), "data", "kook"),
|
|
89
|
-
timeout: $config.timeout || 10000,
|
|
90
|
-
max_retry: $config.max_retry || 3,
|
|
91
|
-
ignore: $config.ignore || "bot",
|
|
92
|
-
logLevel: $config.logLevel || "info",
|
|
93
|
-
});
|
|
94
|
-
// 捕获 kook-client 构造函数注入的 process 监听器
|
|
95
|
-
const afterListeners = process.listeners('uncaughtException');
|
|
96
|
-
for (let i = beforeCount; i < afterListeners.length; i++) {
|
|
97
|
-
this._kookProcessListeners.push({ event: 'uncaughtException', fn: afterListeners[i] });
|
|
98
|
-
}
|
|
99
|
-
this.adapter = adapter;
|
|
100
|
-
this.setupEventListeners();
|
|
101
|
-
this.hookGatewayReceiver();
|
|
102
|
-
}
|
|
24
|
+
defaultCreateClient,
|
|
25
|
+
defaultCreateWebhookClient,
|
|
26
|
+
normalizeKookMessage,
|
|
27
|
+
type CreateKookClient,
|
|
28
|
+
type KookClientTransport,
|
|
29
|
+
} from './ws.js';
|
|
30
|
+
|
|
31
|
+
const logger = getLogger('kook');
|
|
32
|
+
|
|
33
|
+
export interface KookEndpointOptions {
|
|
34
|
+
readonly id: CapabilityId;
|
|
35
|
+
readonly gateway: MessageGateway;
|
|
36
|
+
readonly config: ResolvedKookWebsocketConfig;
|
|
37
|
+
readonly createClient?: CreateKookClient;
|
|
38
|
+
}
|
|
103
39
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
40
|
+
export class KookWebsocketEndpoint implements EndpointInstance {
|
|
41
|
+
readonly #options: KookEndpointOptions;
|
|
42
|
+
readonly #createClient: CreateKookClient;
|
|
43
|
+
#client: KookClientTransport | null = null;
|
|
44
|
+
#open = false;
|
|
45
|
+
#started = false;
|
|
46
|
+
#unregisterAgent?: () => void;
|
|
47
|
+
|
|
48
|
+
constructor(options: KookEndpointOptions) {
|
|
49
|
+
this.#options = options;
|
|
50
|
+
this.#createClient = options.createClient ?? (defaultCreateClient as CreateKookClient);
|
|
110
51
|
}
|
|
111
52
|
|
|
112
|
-
|
|
53
|
+
async start(): Promise<void> {
|
|
54
|
+
if (this.#started) return;
|
|
55
|
+
this.#started = true;
|
|
113
56
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.
|
|
123
|
-
|
|
124
|
-
);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (!isKookNoticeGatewayEvent(raw)) return;
|
|
129
|
-
|
|
130
|
-
const dedupeKey = resolveKookSideEventDedupeKey(raw, "notice");
|
|
131
|
-
if (!this.inboundDeduper.shouldProcess(dedupeKey)) return;
|
|
132
|
-
|
|
133
|
-
const notice = formatKookNotice(raw, this.$config.name);
|
|
134
|
-
this.emitSideEvent("notice.receive", notice);
|
|
135
|
-
this.pluginLogger.info(formatKookNoticeLog(notice));
|
|
57
|
+
this.#unregisterAgent = registerKookAgentEndpoint(this.#options.config.name, this);
|
|
58
|
+
this.#client = this.#createClient(this.#options.config);
|
|
59
|
+
this.#bindClient(this.#client);
|
|
60
|
+
await this.#client.connect();
|
|
61
|
+
logger.info(formatCompact({
|
|
62
|
+
op: 'connect',
|
|
63
|
+
endpoint: this.#options.config.name,
|
|
64
|
+
mode: 'websocket',
|
|
65
|
+
self_id: this.#client.self_id != null ? String(this.#client.self_id) : undefined,
|
|
66
|
+
}));
|
|
136
67
|
} catch (error) {
|
|
137
|
-
this.
|
|
68
|
+
await this.stop();
|
|
69
|
+
logger.error('Failed to connect KOOK websocket:', error);
|
|
70
|
+
throw error;
|
|
138
71
|
}
|
|
139
72
|
}
|
|
140
73
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
payload: unknown,
|
|
144
|
-
): void {
|
|
145
|
-
this.adapter.emit(event, payload as never);
|
|
74
|
+
open(): void {
|
|
75
|
+
this.#open = true;
|
|
146
76
|
}
|
|
147
77
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
*/
|
|
151
|
-
private setupEventListeners(): void {
|
|
152
|
-
// 监听消息事件
|
|
153
|
-
this.on("message", (msg: KookRawMessage) => {
|
|
154
|
-
try {
|
|
155
|
-
const message = this.$formatMessage(msg);
|
|
156
|
-
const atSegs = message.$content.filter((s) => s.type === 'at');
|
|
157
|
-
if (atSegs.length > 0) {
|
|
158
|
-
this.pluginLogger.debug(
|
|
159
|
-
`KOOK @解析: self_id=${this.$platformUserId ?? (this as { self_id?: string }).self_id ?? '?'}`
|
|
160
|
-
+ ` at=${JSON.stringify(atSegs.map((s) => s.data))}`
|
|
161
|
-
+ ` preview=${segment.raw(message.$content)}`,
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
this.pluginLogger.debug(`KOOK 格式化消息: $content=${JSON.stringify(message.$content)}, $raw=${message.$raw}`);
|
|
165
|
-
this.adapter.emit("message.receive", message);
|
|
166
|
-
|
|
167
|
-
// 根据消息类型触发特定事件
|
|
168
|
-
const eventMap: Record<MessageType, string> = {
|
|
169
|
-
private: "message.private.receive",
|
|
170
|
-
group: "message.group.receive",
|
|
171
|
-
channel: "message.channel.receive",
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const specificEvent = eventMap[msg.message_type];
|
|
175
|
-
if (specificEvent) {
|
|
176
|
-
this.adapter.emit(specificEvent as any, message);
|
|
177
|
-
}
|
|
178
|
-
} catch (error) {
|
|
179
|
-
this.pluginLogger.error(`处理 KOOK 消息失败:`, error);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// 监听连接事件
|
|
184
|
-
this.on("connect" as any, () => {
|
|
185
|
-
this.$connected = true;
|
|
186
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 已连接`);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// 监听断开事件
|
|
190
|
-
this.on("disconnect" as any, () => {
|
|
191
|
-
this.$connected = false;
|
|
192
|
-
this.pluginLogger.warn(`KOOK Endpoint ${this.$id} 已断开`);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// 监听错误事件
|
|
196
|
-
this.on("error" as any, (error: Error) => {
|
|
197
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 错误:`, error);
|
|
198
|
-
});
|
|
78
|
+
close(): void {
|
|
79
|
+
this.#open = false;
|
|
199
80
|
}
|
|
200
81
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
? (msg as ChannelMessageEvent).channel_id
|
|
207
|
-
: msg.author_id;
|
|
208
|
-
|
|
209
|
-
// 获取发送者的权限信息
|
|
210
|
-
const senderInfo = this.getSenderInfo(msg);
|
|
211
|
-
|
|
212
|
-
// 频道消息需要获取 guild_id
|
|
213
|
-
let guildId: string | undefined;
|
|
214
|
-
if (msg.message_type === "channel") {
|
|
215
|
-
const channelMsg = msg as ChannelMessageEvent;
|
|
216
|
-
// 尝试从 channel 获取 guild_id
|
|
82
|
+
async stop(): Promise<void> {
|
|
83
|
+
this.#open = false;
|
|
84
|
+
this.#unregisterAgent?.();
|
|
85
|
+
this.#unregisterAgent = undefined;
|
|
86
|
+
if (this.#client) {
|
|
217
87
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
88
|
+
this.#client.removeAllListeners();
|
|
89
|
+
await this.#client.disconnect();
|
|
220
90
|
} catch {
|
|
221
|
-
|
|
91
|
+
/* ignore */
|
|
222
92
|
}
|
|
93
|
+
this.#client = null;
|
|
223
94
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
},
|
|
95
|
+
this.#started = false;
|
|
96
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
100
|
+
const body = formatOutboundKmarkdown(payload);
|
|
101
|
+
const parsed = parseSendTarget(target);
|
|
102
|
+
const client = this.#requireClient();
|
|
103
|
+
const result = parsed.kind === 'private'
|
|
104
|
+
? await client.sendPrivateMsg(parsed.id, body)
|
|
105
|
+
: await client.sendChannelMsg(parsed.id, body);
|
|
106
|
+
const messageId = result?.msg_id != null ? String(result.msg_id) : '';
|
|
107
|
+
logger.debug(formatCompact({
|
|
108
|
+
op: 'kook_send',
|
|
109
|
+
endpoint: this.#options.config.name,
|
|
110
|
+
target,
|
|
111
|
+
messageId,
|
|
112
|
+
}));
|
|
113
|
+
return messageId;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Test / internal: admit a message when open. */
|
|
117
|
+
admit(msg: KookInboundMessage): void {
|
|
118
|
+
if (!this.#open) return;
|
|
119
|
+
if (msg.authorBot) return;
|
|
120
|
+
const target = formatInboundTarget(msg);
|
|
121
|
+
const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
122
|
+
void this.#options.gateway.receive({
|
|
123
|
+
adapter: this.#options.id,
|
|
124
|
+
target,
|
|
125
|
+
content: formatInboundContent(msg),
|
|
126
|
+
sender: senderDisplayName(msg),
|
|
127
|
+
id: msg.id,
|
|
128
|
+
metadata: Object.freeze({
|
|
129
|
+
endpoint: this.#options.config.name,
|
|
130
|
+
channelKind: msg.channelKind,
|
|
131
|
+
userId: msg.authorId,
|
|
132
|
+
guildId: msg.guildId,
|
|
133
|
+
roles: msg.authorRoles,
|
|
134
|
+
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
135
|
+
}),
|
|
136
|
+
}).catch((err) => {
|
|
137
|
+
logger.warn(formatCompact({
|
|
138
|
+
op: 'kook_gateway_receive_failed',
|
|
139
|
+
target,
|
|
140
|
+
error: err instanceof Error ? err.message : String(err),
|
|
141
|
+
}));
|
|
273
142
|
});
|
|
274
|
-
|
|
275
|
-
return message;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* 获取发送者的详细权限信息
|
|
280
|
-
*/
|
|
281
|
-
private getSenderInfo(msg: KookRawMessage): KookSenderInfo {
|
|
282
|
-
const authorInfo = msg.author?.info;
|
|
283
|
-
const senderInfo: KookSenderInfo = {
|
|
284
|
-
id: msg.author_id.toString(),
|
|
285
|
-
name: authorInfo?.nickname || authorInfo?.username || "未知用户",
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
// 频道消息才有权限信息
|
|
289
|
-
if (msg.message_type === "channel") {
|
|
290
|
-
const channelMsg = msg as ChannelMessageEvent;
|
|
291
|
-
|
|
292
|
-
// 从 author.info 中获取权限信息(如果 kook-client 提供)
|
|
293
|
-
if (authorInfo) {
|
|
294
|
-
// 尝试获取角色列表
|
|
295
|
-
senderInfo.roles = (authorInfo as any).roles || [];
|
|
296
|
-
|
|
297
|
-
// 尝试获取 guild_id 并检查是否为服务器主人
|
|
298
|
-
try {
|
|
299
|
-
const channel = channelMsg.channel;
|
|
300
|
-
const guildId = (channel?.info as any)?.guild_id;
|
|
301
|
-
if (guildId) {
|
|
302
|
-
const guildInfo = this.guilds?.get(guildId);
|
|
303
|
-
if (guildInfo) {
|
|
304
|
-
senderInfo.isGuildOwner = guildInfo.user_id === msg.author_id;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
} catch {
|
|
308
|
-
// 忽略获取失败
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// 根据 permission 字段判断(如果有)
|
|
312
|
-
const permission = (authorInfo as any).permission as KookPermission | undefined;
|
|
313
|
-
if (permission !== undefined) {
|
|
314
|
-
senderInfo.permission = permission;
|
|
315
|
-
senderInfo.isAdmin = permission === KookPermission.Admin ||
|
|
316
|
-
permission === KookPermission.Owner ||
|
|
317
|
-
permission === KookPermission.ChannelAdmin;
|
|
318
|
-
senderInfo.isGuildOwner = senderInfo.isGuildOwner || permission === KookPermission.Owner;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const normalized = normalizeKookSenderForPermit(senderInfo, msg.message_type === "channel");
|
|
324
|
-
senderInfo.role = normalized.role;
|
|
325
|
-
senderInfo.permissions = normalized.permissions;
|
|
326
|
-
return senderInfo;
|
|
327
143
|
}
|
|
328
144
|
|
|
329
|
-
//
|
|
145
|
+
// ── Agent tool surface ──────────────────────────────────────────────
|
|
330
146
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
* @param guildId 服务器ID
|
|
334
|
-
* @param userId 用户ID
|
|
335
|
-
*/
|
|
336
|
-
async kickUser(guildId: string, userId: string): Promise<boolean> {
|
|
337
|
-
try {
|
|
338
|
-
const guild = this.pickGuild(guildId);
|
|
339
|
-
const result = await guild.kick(userId);
|
|
340
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 踢出用户 ${userId} 从服务器 ${guildId}`);
|
|
341
|
-
return result;
|
|
342
|
-
} catch (error) {
|
|
343
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 踢出用户失败:`, error);
|
|
344
|
-
throw error;
|
|
345
|
-
}
|
|
147
|
+
async getRoleList(guildId: string) {
|
|
148
|
+
return this.#requireClient().pickGuild(guildId).getRoleList();
|
|
346
149
|
}
|
|
347
150
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
* @param guildId 服务器ID
|
|
351
|
-
* @param userId 用户ID
|
|
352
|
-
* @param remark 备注
|
|
353
|
-
* @param delMsgDays 删除消息天数(0-7)
|
|
354
|
-
*/
|
|
355
|
-
async addToBlacklist(guildId: string, userId: string, remark?: string, delMsgDays?: number): Promise<boolean> {
|
|
356
|
-
try {
|
|
357
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
358
|
-
const result = await member.addToBlackList(remark, delMsgDays);
|
|
359
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 将用户 ${userId} 加入黑名单(服务器 ${guildId})`);
|
|
360
|
-
return result;
|
|
361
|
-
} catch (error) {
|
|
362
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 加入黑名单失败:`, error);
|
|
363
|
-
throw error;
|
|
364
|
-
}
|
|
151
|
+
async createRole(guildId: string, name: string) {
|
|
152
|
+
return this.#requireClient().pickGuild(guildId).createRole(name);
|
|
365
153
|
}
|
|
366
154
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
* @param guildId 服务器ID
|
|
370
|
-
* @param userId 用户ID
|
|
371
|
-
*/
|
|
372
|
-
async removeFromBlacklist(guildId: string, userId: string): Promise<boolean> {
|
|
373
|
-
try {
|
|
374
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
375
|
-
const result = await member.removeFromBlackList();
|
|
376
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 将用户 ${userId} 从黑名单移除(服务器 ${guildId})`);
|
|
377
|
-
return result;
|
|
378
|
-
} catch (error) {
|
|
379
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 移除黑名单失败:`, error);
|
|
380
|
-
throw error;
|
|
381
|
-
}
|
|
155
|
+
async deleteRole(guildId: string, roleId: string) {
|
|
156
|
+
return this.#requireClient().pickGuild(guildId).deleteRole(roleId);
|
|
382
157
|
}
|
|
383
158
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
* @param guildId 服务器ID
|
|
387
|
-
* @param userId 用户ID
|
|
388
|
-
* @param roleId 角色ID
|
|
389
|
-
*/
|
|
390
|
-
async grantRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
|
|
391
|
-
try {
|
|
392
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
393
|
-
const result = await member.grant(roleId);
|
|
394
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 授予用户 ${userId} 角色 ${roleId}(服务器 ${guildId})`);
|
|
395
|
-
return result;
|
|
396
|
-
} catch (error) {
|
|
397
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 授予角色失败:`, error);
|
|
398
|
-
throw error;
|
|
399
|
-
}
|
|
159
|
+
async grantRole(guildId: string, userId: string, roleId: string) {
|
|
160
|
+
return this.#requireClient().pickGuildMember(guildId, userId).grant(roleId);
|
|
400
161
|
}
|
|
401
162
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
* @param guildId 服务器ID
|
|
405
|
-
* @param userId 用户ID
|
|
406
|
-
* @param roleId 角色ID
|
|
407
|
-
*/
|
|
408
|
-
async revokeRole(guildId: string, userId: string, roleId: string): Promise<boolean> {
|
|
409
|
-
try {
|
|
410
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
411
|
-
const result = await member.revoke(roleId);
|
|
412
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 撤销用户 ${userId} 角色 ${roleId}(服务器 ${guildId})`);
|
|
413
|
-
return result;
|
|
414
|
-
} catch (error) {
|
|
415
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 撤销角色失败:`, error);
|
|
416
|
-
throw error;
|
|
417
|
-
}
|
|
163
|
+
async revokeRole(guildId: string, userId: string, roleId: string) {
|
|
164
|
+
return this.#requireClient().pickGuildMember(guildId, userId).revoke(roleId);
|
|
418
165
|
}
|
|
419
166
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
* @param guildId 服务器ID
|
|
423
|
-
* @param userId 用户ID
|
|
424
|
-
* @param nickname 新昵称
|
|
425
|
-
*/
|
|
426
|
-
async setNickname(guildId: string, userId: string, nickname: string): Promise<boolean> {
|
|
427
|
-
try {
|
|
428
|
-
const member = this.pickGuildMember(guildId, userId);
|
|
429
|
-
const result = await member.setNickname(nickname);
|
|
430
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 设置用户 ${userId} 昵称为 "${nickname}"(服务器 ${guildId})`);
|
|
431
|
-
return result;
|
|
432
|
-
} catch (error) {
|
|
433
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 设置昵称失败:`, error);
|
|
434
|
-
throw error;
|
|
435
|
-
}
|
|
167
|
+
async addToBlacklist(guildId: string, userId: string, remark?: string) {
|
|
168
|
+
return this.#requireClient().pickGuildMember(guildId, userId).addToBlackList(remark);
|
|
436
169
|
}
|
|
437
170
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
* @param guildId 服务器ID
|
|
441
|
-
*/
|
|
442
|
-
async getRoleList(guildId: string): Promise<Guild.Role[]> {
|
|
443
|
-
try {
|
|
444
|
-
const guild = this.pickGuild(guildId);
|
|
445
|
-
return await guild.getRoleList();
|
|
446
|
-
} catch (error) {
|
|
447
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 获取角色列表失败:`, error);
|
|
448
|
-
throw error;
|
|
449
|
-
}
|
|
171
|
+
async removeFromBlacklist(guildId: string, userId: string) {
|
|
172
|
+
return this.#requireClient().pickGuildMember(guildId, userId).removeFromBlackList();
|
|
450
173
|
}
|
|
451
174
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
async createRole(guildId: string, name: string): Promise<Guild.Role> {
|
|
458
|
-
try {
|
|
459
|
-
const guild = this.pickGuild(guildId);
|
|
460
|
-
const role = await guild.createRole(name);
|
|
461
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 创建角色 "${name}"(服务器 ${guildId})`);
|
|
462
|
-
return role;
|
|
463
|
-
} catch (error) {
|
|
464
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 创建角色失败:`, error);
|
|
465
|
-
throw error;
|
|
466
|
-
}
|
|
175
|
+
#bindClient(client: KookClientTransport): void {
|
|
176
|
+
client.on('message', (raw) => {
|
|
177
|
+
const msg = normalizeKookMessage(raw);
|
|
178
|
+
if (msg) this.admit(msg);
|
|
179
|
+
});
|
|
467
180
|
}
|
|
468
181
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
* @param roleId 角色ID
|
|
473
|
-
*/
|
|
474
|
-
async deleteRole(guildId: string, roleId: string): Promise<boolean> {
|
|
475
|
-
try {
|
|
476
|
-
const guild = this.pickGuild(guildId);
|
|
477
|
-
const result = await guild.deleteRole(roleId);
|
|
478
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 删除角色 ${roleId}(服务器 ${guildId})`);
|
|
479
|
-
return result;
|
|
480
|
-
} catch (error) {
|
|
481
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 删除角色失败:`, error);
|
|
482
|
-
throw error;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* 获取服务器成员列表
|
|
488
|
-
* @param guildId 服务器ID
|
|
489
|
-
* @param channelId 可选的频道ID
|
|
490
|
-
*/
|
|
491
|
-
async getGuildMembers(guildId: string, channelId?: string): Promise<User.Info[]> {
|
|
492
|
-
try {
|
|
493
|
-
return await this.getGuildUserList(guildId, channelId);
|
|
494
|
-
} catch (error) {
|
|
495
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 获取成员列表失败:`, error);
|
|
496
|
-
throw error;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
private buildAtElement(userId: string | number): MessageElement {
|
|
500
|
-
const id = String(userId);
|
|
501
|
-
return { type: 'at', data: { id, user_id: id, qq: id } };
|
|
182
|
+
#requireClient(): KookClientTransport {
|
|
183
|
+
if (!this.#client) throw new Error('KOOK client not connected');
|
|
184
|
+
return this.#client;
|
|
502
185
|
}
|
|
186
|
+
}
|
|
503
187
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
188
|
+
export interface KookWebhookEndpointOptions {
|
|
189
|
+
readonly id: CapabilityId;
|
|
190
|
+
readonly gateway: MessageGateway;
|
|
191
|
+
readonly http: HttpHost;
|
|
192
|
+
readonly config: ResolvedKookWebhookConfig;
|
|
193
|
+
readonly createClient?: CreateKookClient;
|
|
194
|
+
}
|
|
508
195
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
196
|
+
export class KookWebhookEndpoint implements EndpointInstance {
|
|
197
|
+
readonly #options: KookWebhookEndpointOptions;
|
|
198
|
+
readonly #createClient: CreateKookClient;
|
|
199
|
+
#client: KookClientTransport | null = null;
|
|
200
|
+
#routeReleases: HttpRouteRegistration[] = [];
|
|
201
|
+
#processedSn = new Set<number>();
|
|
202
|
+
#open = false;
|
|
203
|
+
#started = false;
|
|
204
|
+
#unregisterAgent?: () => void;
|
|
519
205
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
206
|
+
constructor(options: KookWebhookEndpointOptions) {
|
|
207
|
+
this.#options = options;
|
|
208
|
+
this.#createClient = options.createClient ?? (defaultCreateWebhookClient as CreateKookClient);
|
|
209
|
+
}
|
|
523
210
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
elements.push({ type: "text", data: { text } });
|
|
529
|
-
}
|
|
530
|
-
}
|
|
211
|
+
/** Used by webhook handler. */
|
|
212
|
+
get isOpen(): boolean {
|
|
213
|
+
return this.#open;
|
|
214
|
+
}
|
|
531
215
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
216
|
+
get config(): ResolvedKookWebhookConfig {
|
|
217
|
+
return this.#options.config;
|
|
218
|
+
}
|
|
536
219
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* 将 kook-client 的 MessageSegment[] 转换为 Zhin 的 MessageElement[]
|
|
541
|
-
*/
|
|
542
|
-
private parseMessageContent(segments: MessageSegment[]): MessageElement[] {
|
|
543
|
-
const elements: MessageElement[] = [];
|
|
544
|
-
|
|
545
|
-
for (const segment of segments) {
|
|
546
|
-
switch (segment.type) {
|
|
547
|
-
case "markdown":
|
|
548
|
-
// 检查是否包含特殊语法,如果是纯文本则直接转换
|
|
549
|
-
if (this.hasKMarkdownSyntax(segment.text)) {
|
|
550
|
-
elements.push(...this.parseMarkdown(segment.text));
|
|
551
|
-
} else {
|
|
552
|
-
elements.push({ type: "text", data: { text: segment.text } });
|
|
553
|
-
}
|
|
554
|
-
break;
|
|
555
|
-
|
|
556
|
-
case "text":
|
|
557
|
-
elements.push({ type: "text", data: { text: segment.text } });
|
|
558
|
-
break;
|
|
559
|
-
|
|
560
|
-
case "at":
|
|
561
|
-
elements.push(this.buildAtElement(segment.user_id));
|
|
562
|
-
break;
|
|
563
|
-
|
|
564
|
-
case "image":
|
|
565
|
-
elements.push({
|
|
566
|
-
type: "image",
|
|
567
|
-
data: { url: segment.url, alt: segment.title || "图片" }
|
|
568
|
-
});
|
|
569
|
-
break;
|
|
570
|
-
|
|
571
|
-
case "video":
|
|
572
|
-
elements.push({ type: "video", data: { url: segment.url } });
|
|
573
|
-
break;
|
|
574
|
-
|
|
575
|
-
case "audio":
|
|
576
|
-
elements.push({ type: "audio", data: { url: segment.url } });
|
|
577
|
-
break;
|
|
578
|
-
|
|
579
|
-
case "file":
|
|
580
|
-
elements.push({
|
|
581
|
-
type: "file",
|
|
582
|
-
data: { url: segment.url, name: segment.name }
|
|
583
|
-
});
|
|
584
|
-
break;
|
|
585
|
-
|
|
586
|
-
case "reply":
|
|
587
|
-
elements.push({ type: "reply", data: { id: segment.id } });
|
|
588
|
-
break;
|
|
589
|
-
|
|
590
|
-
case "card":
|
|
591
|
-
// Card 消息暂不支持,转为提示文本
|
|
592
|
-
elements.push({ type: "text", data: { text: "[卡片消息]" } });
|
|
593
|
-
break;
|
|
594
|
-
|
|
595
|
-
default:
|
|
596
|
-
this.pluginLogger.warn(`未知的 KOOK 消息段类型: ${(segment as any).type}`);
|
|
597
|
-
break;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return elements.length > 0 ? elements : [{ type: "text", data: { text: "" } }];
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* 检查文本是否包含 KMarkdown 特殊语法
|
|
606
|
-
*/
|
|
607
|
-
private hasKMarkdownSyntax(text: string): boolean {
|
|
608
|
-
return text.includes('![')
|
|
609
|
-
|| text.includes('(met)')
|
|
610
|
-
|| text.includes('(emj)')
|
|
611
|
-
|| text.includes('(chn)')
|
|
612
|
-
|| text.includes('@');
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
private scanKMarkdownTokens(content: string): Array<{ index: number; length: number; element: MessageElement }> {
|
|
616
|
-
const tokens: Array<{ index: number; length: number; element: MessageElement }> = [];
|
|
617
|
-
let i = 0;
|
|
618
|
-
while (i < content.length) {
|
|
619
|
-
const token = this.readKMarkdownToken(content, i);
|
|
620
|
-
if (token) {
|
|
621
|
-
tokens.push(token);
|
|
622
|
-
i = token.index + token.length;
|
|
623
|
-
} else {
|
|
624
|
-
i++;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
return tokens;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
private readKMarkdownToken(content: string, index: number): { index: number; length: number; element: MessageElement } | null {
|
|
631
|
-
if (content.startsWith('![', index)) {
|
|
632
|
-
const labelEnd = content.indexOf(']', index + 2);
|
|
633
|
-
if (labelEnd >= 0 && content[labelEnd + 1] === '(') {
|
|
634
|
-
const urlEnd = content.indexOf(')', labelEnd + 2);
|
|
635
|
-
if (urlEnd >= 0) {
|
|
636
|
-
return {
|
|
637
|
-
index,
|
|
638
|
-
length: urlEnd + 1 - index,
|
|
639
|
-
element: { type: "image", data: { url: content.slice(labelEnd + 2, urlEnd), alt: content.slice(index + 2, labelEnd) } },
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
if (content.startsWith('(met)', index)) {
|
|
645
|
-
const idStart = index + '(met)'.length;
|
|
646
|
-
const idEnd = content.indexOf('(met)', idStart);
|
|
647
|
-
if (idEnd > idStart) {
|
|
648
|
-
return { index, length: idEnd + '(met)'.length - index, element: this.buildAtElement(content.slice(idStart, idEnd)) };
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
if (content[index] === '@') {
|
|
652
|
-
let end = index + 1;
|
|
653
|
-
while (end < content.length && !/\s/.test(content[end]!)) end++;
|
|
654
|
-
if (end > index + 1) {
|
|
655
|
-
return { index, length: end - index, element: this.buildAtElement(content.slice(index + 1, end)) };
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
if (content.startsWith('(emj)', index)) {
|
|
659
|
-
const nameStart = index + '(emj)'.length;
|
|
660
|
-
const nameEnd = content.indexOf('(emj)[', nameStart);
|
|
661
|
-
const idEnd = nameEnd >= 0 ? content.indexOf(']', nameEnd + 6) : -1;
|
|
662
|
-
if (nameEnd > nameStart && idEnd > nameEnd) {
|
|
663
|
-
return {
|
|
664
|
-
index,
|
|
665
|
-
length: idEnd + 1 - index,
|
|
666
|
-
element: { type: "face", data: { id: content.slice(nameEnd + 6, idEnd), name: content.slice(nameStart, nameEnd) } },
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
if (content.startsWith('(chn)', index)) {
|
|
671
|
-
const idStart = index + '(chn)'.length;
|
|
672
|
-
const idEnd = content.indexOf('(chn)', idStart);
|
|
673
|
-
if (idEnd > idStart) {
|
|
674
|
-
return { index, length: idEnd + '(chn)'.length - index, element: { type: "text", data: { text: `#频道:${content.slice(idStart, idEnd)}` } } };
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
return null;
|
|
220
|
+
get selfId(): string | undefined {
|
|
221
|
+
return this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
678
222
|
}
|
|
679
223
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
async $connect(): Promise<void> {
|
|
224
|
+
async start(): Promise<void> {
|
|
225
|
+
if (this.#started) return;
|
|
226
|
+
this.#started = true;
|
|
684
227
|
try {
|
|
685
|
-
|
|
686
|
-
this
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
228
|
+
this.#unregisterAgent = registerKookAgentEndpoint(this.#options.config.name, this);
|
|
229
|
+
this.#client = this.#createClient(this.#options.config);
|
|
230
|
+
await (this.#client as Client).init();
|
|
231
|
+
this.#routeReleases.push(...registerKookWebhookRoutes(this.#options.http, this));
|
|
232
|
+
logger.info(formatCompact({
|
|
233
|
+
op: 'connect',
|
|
234
|
+
endpoint: this.#options.config.name,
|
|
235
|
+
mode: 'webhook',
|
|
236
|
+
path: this.#options.config.webhookPath,
|
|
237
|
+
self_id: this.#client.self_id != null ? String(this.#client.self_id) : undefined,
|
|
238
|
+
}));
|
|
695
239
|
} catch (error) {
|
|
696
|
-
this.
|
|
240
|
+
await this.stop();
|
|
241
|
+
logger.error('Failed to connect KOOK webhook:', error);
|
|
697
242
|
throw error;
|
|
698
243
|
}
|
|
699
244
|
}
|
|
700
245
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
*/
|
|
704
|
-
async $disconnect(): Promise<void> {
|
|
705
|
-
try {
|
|
706
|
-
const receiver = this.receiver as import("node:events").EventEmitter;
|
|
707
|
-
receiver.off("event", this.onGatewayEvent);
|
|
708
|
-
this.inboundDeduper.clear();
|
|
709
|
-
(this as unknown as import('node:events').EventEmitter).removeAllListeners();
|
|
710
|
-
// 移除 kook-client 构造函数注入的 process 监听器
|
|
711
|
-
for (const { event, fn } of this._kookProcessListeners) {
|
|
712
|
-
process.removeListener(event, fn);
|
|
713
|
-
}
|
|
714
|
-
this._kookProcessListeners.length = 0;
|
|
715
|
-
await this.disconnect();
|
|
716
|
-
this.$connected = false;
|
|
717
|
-
this.pluginLogger.info(`KOOK Endpoint ${this.$id} 已断开连接`);
|
|
718
|
-
} catch (error) {
|
|
719
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 断开连接失败:`, error);
|
|
720
|
-
throw error;
|
|
721
|
-
}
|
|
246
|
+
open(): void {
|
|
247
|
+
this.#open = true;
|
|
722
248
|
}
|
|
723
249
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
*/
|
|
727
|
-
override async uploadMedia(data: string | Buffer): Promise<string> {
|
|
728
|
-
return uploadKookAsset(this.request, data);
|
|
250
|
+
close(): void {
|
|
251
|
+
this.#open = false;
|
|
729
252
|
}
|
|
730
253
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
elements,
|
|
744
|
-
(els) => this.convertToKookFormat(els),
|
|
745
|
-
);
|
|
746
|
-
|
|
747
|
-
// 根据消息类型发送
|
|
748
|
-
let result: any;
|
|
749
|
-
if (type === "private") {
|
|
750
|
-
result = await (this as any).sendPrivateMsg(id, kookContent);
|
|
751
|
-
} else {
|
|
752
|
-
result = await (this as any).sendChannelMsg(id, kookContent);
|
|
254
|
+
async stop(): Promise<void> {
|
|
255
|
+
this.#open = false;
|
|
256
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
257
|
+
this.#unregisterAgent?.();
|
|
258
|
+
this.#unregisterAgent = undefined;
|
|
259
|
+
this.#processedSn.clear();
|
|
260
|
+
if (this.#client) {
|
|
261
|
+
try {
|
|
262
|
+
this.#client.removeAllListeners();
|
|
263
|
+
await this.#client.disconnect();
|
|
264
|
+
} catch {
|
|
265
|
+
/* ignore */
|
|
753
266
|
}
|
|
754
|
-
|
|
755
|
-
const route = routeFromSendType(type);
|
|
756
|
-
return encodeKookMsgRef(route, String(result?.msg_id ?? ''));
|
|
757
|
-
} catch (error) {
|
|
758
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 发送消息失败:`, error);
|
|
759
|
-
throw error;
|
|
267
|
+
this.#client = null;
|
|
760
268
|
}
|
|
269
|
+
this.#started = false;
|
|
270
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.#options.config.name }));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
274
|
+
const body = formatOutboundKmarkdown(payload);
|
|
275
|
+
const parsed = parseSendTarget(target);
|
|
276
|
+
const client = this.#requireClient();
|
|
277
|
+
const result = parsed.kind === 'private'
|
|
278
|
+
? await client.sendPrivateMsg(parsed.id, body)
|
|
279
|
+
: await client.sendChannelMsg(parsed.id, body);
|
|
280
|
+
const messageId = result?.msg_id != null ? String(result.msg_id) : '';
|
|
281
|
+
logger.debug(formatCompact({
|
|
282
|
+
op: 'kook_send',
|
|
283
|
+
endpoint: this.#options.config.name,
|
|
284
|
+
target,
|
|
285
|
+
messageId,
|
|
286
|
+
}));
|
|
287
|
+
return messageId;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Test / internal: admit a message when open. */
|
|
291
|
+
admit(msg: KookInboundMessage): void {
|
|
292
|
+
if (!this.#open) return;
|
|
293
|
+
if (msg.authorBot) return;
|
|
294
|
+
const target = formatInboundTarget(msg);
|
|
295
|
+
const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
|
|
296
|
+
void this.#options.gateway.receive({
|
|
297
|
+
adapter: this.#options.id,
|
|
298
|
+
target,
|
|
299
|
+
content: formatInboundContent(msg),
|
|
300
|
+
sender: senderDisplayName(msg),
|
|
301
|
+
id: msg.id,
|
|
302
|
+
metadata: Object.freeze({
|
|
303
|
+
endpoint: this.#options.config.name,
|
|
304
|
+
channelKind: msg.channelKind,
|
|
305
|
+
userId: msg.authorId,
|
|
306
|
+
guildId: msg.guildId,
|
|
307
|
+
roles: msg.authorRoles,
|
|
308
|
+
...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
|
|
309
|
+
}),
|
|
310
|
+
}).catch((err) => {
|
|
311
|
+
logger.warn(formatCompact({
|
|
312
|
+
op: 'kook_gateway_receive_failed',
|
|
313
|
+
target,
|
|
314
|
+
error: err instanceof Error ? err.message : String(err),
|
|
315
|
+
}));
|
|
316
|
+
});
|
|
761
317
|
}
|
|
762
318
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
): Promise<void> {
|
|
770
|
-
const { route: encodedRoute, msgId } = parseKookMsgRef(messageIdOrRef);
|
|
771
|
-
const routeHint = encodedRoute ?? hint?.route ?? routeFromSceneType(hint?.sceneType);
|
|
772
|
-
try {
|
|
773
|
-
await this.deleteKookMsg(msgId, routeHint);
|
|
774
|
-
this.pluginLogger.debug(
|
|
775
|
-
`KOOK Endpoint ${this.$id} 撤回消息 (${routeHint ?? 'auto'}): ${msgId}`,
|
|
776
|
-
);
|
|
777
|
-
} catch (error) {
|
|
778
|
-
this.pluginLogger.error(`KOOK Endpoint ${this.$id} 撤回消息失败:`, error);
|
|
779
|
-
throw error;
|
|
319
|
+
checkAndRememberSn(sn: number): boolean {
|
|
320
|
+
if (this.#processedSn.has(sn)) return false;
|
|
321
|
+
this.#processedSn.add(sn);
|
|
322
|
+
if (this.#processedSn.size > 1024) {
|
|
323
|
+
const first = this.#processedSn.values().next().value;
|
|
324
|
+
if (first != null) this.#processedSn.delete(first);
|
|
780
325
|
}
|
|
326
|
+
return true;
|
|
781
327
|
}
|
|
782
328
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
329
|
+
async getRoleList(guildId: string) {
|
|
330
|
+
return this.#requireClient().pickGuild(guildId).getRoleList();
|
|
331
|
+
}
|
|
786
332
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
kookDeleteApiPath(route),
|
|
791
|
-
{ msg_id: msgId },
|
|
792
|
-
) as { code?: number };
|
|
793
|
-
if (shouldStopDeleteAfterResponse(result, routes.length)) return;
|
|
794
|
-
} catch (err) {
|
|
795
|
-
if (isKookMsgGoneError(err)) return;
|
|
796
|
-
lastError = err;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
333
|
+
async createRole(guildId: string, name: string) {
|
|
334
|
+
return this.#requireClient().pickGuild(guildId).createRole(name);
|
|
335
|
+
}
|
|
799
336
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
/**
|
|
804
|
-
* 为消息添加表情回应(TypingIndicator reaction 模式)
|
|
805
|
-
* @returns reactionId(含 channel/direct 路由),供 $removeReaction 使用
|
|
806
|
-
*/
|
|
807
|
-
async $addReaction(
|
|
808
|
-
messageId: string,
|
|
809
|
-
emoji: string,
|
|
810
|
-
hint?: { sceneType?: 'private' | 'group' | 'channel' },
|
|
811
|
-
): Promise<string> {
|
|
812
|
-
const msgId = plainKookMsgId(messageId);
|
|
813
|
-
const route = await this.mutateMsgReaction(
|
|
814
|
-
msgId,
|
|
815
|
-
emoji,
|
|
816
|
-
'add',
|
|
817
|
-
routeFromSceneType(hint?.sceneType),
|
|
818
|
-
);
|
|
819
|
-
return encodeKookReactionId(route, msgId, emoji);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/** 移除本 Endpoint 在消息上的表情回应 */
|
|
823
|
-
async $removeReaction(messageId: string, reactionId: string): Promise<void> {
|
|
824
|
-
const { route, emoji } = parseKookReactionId(reactionId);
|
|
825
|
-
await this.mutateMsgReaction(plainKookMsgId(messageId), emoji, 'delete', route);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
private async mutateMsgReaction(
|
|
829
|
-
msgId: string,
|
|
830
|
-
emoji: string,
|
|
831
|
-
action: 'add' | 'delete',
|
|
832
|
-
routeHint?: KookMsgRoute,
|
|
833
|
-
): Promise<KookMsgRoute> {
|
|
834
|
-
const routes = resolveKookRoutes(action, routeHint);
|
|
835
|
-
const body = { msg_id: msgId, emoji };
|
|
836
|
-
let lastError: unknown;
|
|
837
|
-
|
|
838
|
-
for (const route of routes) {
|
|
839
|
-
try {
|
|
840
|
-
const result = await this.request.post(
|
|
841
|
-
kookReactionApiPath(route, action),
|
|
842
|
-
body,
|
|
843
|
-
) as { code?: number };
|
|
844
|
-
if (isKookApiSuccess(result)) {
|
|
845
|
-
this.pluginLogger.debug(
|
|
846
|
-
`KOOK Endpoint ${this.$id} ${action} reaction (${route}) on ${msgId}`,
|
|
847
|
-
);
|
|
848
|
-
return route;
|
|
849
|
-
}
|
|
850
|
-
if (action === 'delete' && shouldStopDeleteAfterResponse(result, routes.length)) {
|
|
851
|
-
const label = isKookApiGoneResult(result) ? 'already gone' : 'done';
|
|
852
|
-
this.pluginLogger.debug(
|
|
853
|
-
`KOOK Endpoint ${this.$id} delete reaction ${label} (${route}) on ${msgId}`,
|
|
854
|
-
);
|
|
855
|
-
return route;
|
|
856
|
-
}
|
|
857
|
-
} catch (err) {
|
|
858
|
-
if (action === 'delete' && isKookMsgGoneError(err)) {
|
|
859
|
-
this.pluginLogger.debug(
|
|
860
|
-
`KOOK Endpoint ${this.$id} delete reaction already gone (${route}) on ${msgId}`,
|
|
861
|
-
);
|
|
862
|
-
return route;
|
|
863
|
-
}
|
|
864
|
-
lastError = err;
|
|
865
|
-
}
|
|
866
|
-
}
|
|
337
|
+
async deleteRole(guildId: string, roleId: string) {
|
|
338
|
+
return this.#requireClient().pickGuild(guildId).deleteRole(roleId);
|
|
339
|
+
}
|
|
867
340
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
);
|
|
872
|
-
return routeHint ?? 'channel';
|
|
873
|
-
}
|
|
341
|
+
async grantRole(guildId: string, userId: string, roleId: string) {
|
|
342
|
+
return this.#requireClient().pickGuildMember(guildId, userId).grant(roleId);
|
|
343
|
+
}
|
|
874
344
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
: new Error(`KOOK ${action} reaction failed (msg_id=${msgId})`);
|
|
345
|
+
async revokeRole(guildId: string, userId: string, roleId: string) {
|
|
346
|
+
return this.#requireClient().pickGuildMember(guildId, userId).revoke(roleId);
|
|
878
347
|
}
|
|
879
348
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
return
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
case "image":
|
|
893
|
-
// 图片:
|
|
894
|
-
return ``;
|
|
895
|
-
|
|
896
|
-
case "at": {
|
|
897
|
-
const atId = this.resolveAtUserId(el.data as Record<string, unknown>);
|
|
898
|
-
if (atId === 'all') {
|
|
899
|
-
return '(met)all(met)';
|
|
900
|
-
}
|
|
901
|
-
return `(met)${atId}(met)`;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
case "face":
|
|
905
|
-
// 表情:(emj)表情名(emj)[表情ID]
|
|
906
|
-
return `(emj)${el.data.name || 'emoji'}(emj)[${el.data.id}]`;
|
|
907
|
-
|
|
908
|
-
case "reply":
|
|
909
|
-
// 引用消息(KOOK 使用 quote 参数,不在消息内容中)
|
|
910
|
-
return "";
|
|
911
|
-
|
|
912
|
-
case "video":
|
|
913
|
-
// 视频:使用链接形式
|
|
914
|
-
return `[视频](${el.data.url || el.data.file})`;
|
|
915
|
-
|
|
916
|
-
case "audio":
|
|
917
|
-
// 音频:使用链接形式
|
|
918
|
-
return `[音频](${el.data.url || el.data.file})`;
|
|
919
|
-
|
|
920
|
-
case "file":
|
|
921
|
-
// 文件:使用链接形式
|
|
922
|
-
return `[文件: ${el.data.name || '未命名'}](${el.data.url || el.data.file})`;
|
|
923
|
-
|
|
924
|
-
case "link":
|
|
925
|
-
// 链接:[文本](url)
|
|
926
|
-
return `[${el.data.text || el.data.url}](${el.data.url})`;
|
|
927
|
-
|
|
928
|
-
case "bold":
|
|
929
|
-
// 粗体:**文本**
|
|
930
|
-
return `**${el.data.text}**`;
|
|
931
|
-
|
|
932
|
-
case "italic":
|
|
933
|
-
// 斜体:*文本*
|
|
934
|
-
return `*${el.data.text}*`;
|
|
935
|
-
|
|
936
|
-
case "code":
|
|
937
|
-
// 行内代码:`代码`
|
|
938
|
-
return `\`${el.data.text}\``;
|
|
939
|
-
|
|
940
|
-
case "code_block":
|
|
941
|
-
// 代码块:```语言\n代码\n```
|
|
942
|
-
return `\`\`\`${el.data.language || ''}\n${el.data.text}\n\`\`\``;
|
|
943
|
-
|
|
944
|
-
default:
|
|
945
|
-
// 未知类型,尝试转换为文本
|
|
946
|
-
this.pluginLogger.warn(`未知的消息段类型: ${el.type}`);
|
|
947
|
-
return el.data.text || JSON.stringify(el.data);
|
|
948
|
-
}
|
|
949
|
-
})
|
|
950
|
-
.filter(Boolean)
|
|
951
|
-
.join("");
|
|
349
|
+
async addToBlacklist(guildId: string, userId: string, remark?: string) {
|
|
350
|
+
return this.#requireClient().pickGuildMember(guildId, userId).addToBlackList(remark);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async removeFromBlacklist(guildId: string, userId: string) {
|
|
354
|
+
return this.#requireClient().pickGuildMember(guildId, userId).removeFromBlackList();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#requireClient(): KookClientTransport {
|
|
358
|
+
if (!this.#client) throw new Error('KOOK client not initialized');
|
|
359
|
+
return this.#client;
|
|
952
360
|
}
|
|
953
361
|
}
|