@zhin.js/adapter-kook 1.0.83 → 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.
Files changed (74) hide show
  1. package/CHANGELOG.md +813 -16
  2. package/README.md +75 -374
  3. package/adapters/kook.js +41 -0
  4. package/adapters/kook.ts +56 -0
  5. package/agent/PERMITS.md +23 -0
  6. package/agent/tools/blacklist.ts +31 -0
  7. package/agent/tools/create_role.ts +23 -0
  8. package/agent/tools/delete_role.ts +19 -0
  9. package/agent/tools/grant_role.ts +20 -0
  10. package/agent/tools/list_roles.ts +25 -0
  11. package/agent/tools/revoke_role.ts +20 -0
  12. package/commands/endpoint/add/[id].js +3 -0
  13. package/commands/endpoint/add/[id].ts +3 -0
  14. package/commands/endpoint/list.js +3 -0
  15. package/commands/endpoint/list.ts +3 -0
  16. package/commands/endpoint/remove/[id].js +3 -0
  17. package/commands/endpoint/remove/[id].ts +3 -0
  18. package/lib/client.d.ts +11 -0
  19. package/lib/client.js +2 -0
  20. package/lib/endpoint.d.ts +59 -0
  21. package/lib/endpoint.js +374 -0
  22. package/lib/index.d.ts +6 -18
  23. package/lib/index.js +6 -342
  24. package/lib/kook-endpoint-commands.d.ts +1 -0
  25. package/lib/kook-endpoint-commands.js +18 -0
  26. package/lib/kook-runtime-state.d.ts +1 -0
  27. package/lib/kook-runtime-state.js +6 -0
  28. package/lib/platform-permit.d.ts +22 -0
  29. package/lib/platform-permit.js +48 -0
  30. package/lib/protocol.d.ts +144 -0
  31. package/lib/protocol.js +313 -0
  32. package/lib/side-event-dispatch.d.ts +3 -0
  33. package/lib/side-event-dispatch.js +49 -0
  34. package/lib/webhook.d.ts +16 -0
  35. package/lib/webhook.js +71 -0
  36. package/lib/ws.d.ts +56 -0
  37. package/lib/ws.js +63 -0
  38. package/package.json +60 -28
  39. package/plugin.js +19 -0
  40. package/schema.json +128 -0
  41. package/src/client.ts +15 -0
  42. package/src/endpoint.ts +433 -0
  43. package/src/index.ts +45 -322
  44. package/src/kook-endpoint-commands.ts +19 -0
  45. package/src/kook-runtime-state.ts +7 -0
  46. package/src/platform-permit.ts +75 -0
  47. package/src/protocol.ts +462 -0
  48. package/src/side-event-dispatch.ts +54 -0
  49. package/src/webhook.ts +114 -0
  50. package/src/ws.ts +131 -0
  51. package/client/Dashboard.tsx +0 -255
  52. package/client/index.tsx +0 -11
  53. package/client/tsconfig.json +0 -7
  54. package/client/utils/api.ts +0 -17
  55. package/dist/index.js +0 -31
  56. package/lib/adapter.d.ts +0 -26
  57. package/lib/adapter.d.ts.map +0 -1
  58. package/lib/adapter.js +0 -61
  59. package/lib/adapter.js.map +0 -1
  60. package/lib/bot.d.ts +0 -122
  61. package/lib/bot.d.ts.map +0 -1
  62. package/lib/bot.js +0 -608
  63. package/lib/bot.js.map +0 -1
  64. package/lib/index.d.ts.map +0 -1
  65. package/lib/index.js.map +0 -1
  66. package/lib/types.d.ts +0 -32
  67. package/lib/types.d.ts.map +0 -1
  68. package/lib/types.js +0 -8
  69. package/lib/types.js.map +0 -1
  70. package/plugin.yml +0 -3
  71. package/src/adapter.ts +0 -71
  72. package/src/bot.ts +0 -693
  73. package/src/types.ts +0 -36
  74. /package/{skills/kook/SKILL.md → agent/skills/kook.md} +0 -0
@@ -0,0 +1,433 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
2
+ /**
3
+ * KookEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
4
+ */
5
+ import { Client } from 'kook-client';
6
+ import {
7
+ createRecallEndpointControl,
8
+ type EndpointControl,
9
+ type EndpointChannel,
10
+ type EndpointGroup,
11
+ type EndpointManagement,
12
+ type EndpointSendRequest,
13
+ } from 'zhin.js/adapter';
14
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
15
+ import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
16
+ import type { CapabilityId } from 'zhin.js';
17
+ import {
18
+ formatInboundContent,
19
+ formatOutboundKmarkdown,
20
+ isKookBotMentioned,
21
+ kookInboundConversation,
22
+ normalizeKookWebhookEvent,
23
+ senderDisplayName,
24
+ type KookInboundMessage,
25
+ type KookWebhookEventData,
26
+ type ResolvedKookWebhookConfig,
27
+ type ResolvedKookWebsocketConfig,
28
+ } from './protocol.js';
29
+ import { registerKookWebhookRoutes } from './webhook.js';
30
+ import {
31
+ defaultCreateClient,
32
+ defaultCreateWebhookClient,
33
+ normalizeKookMessage,
34
+ type CreateKookClient,
35
+ type KookClientTransport,
36
+ } from './ws.js';
37
+ import { receiveKookSideEvent } from './side-event-dispatch.js';
38
+
39
+ export interface KookEndpointOptions {
40
+ readonly id: CapabilityId;
41
+ readonly config: ResolvedKookWebsocketConfig;
42
+ readonly createClient?: CreateKookClient;
43
+ }
44
+
45
+ export class KookWebsocketEndpoint extends Endpoint<KookClientTransport> {
46
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
47
+
48
+ readonly #options: KookEndpointOptions;
49
+ readonly #createClient: CreateKookClient;
50
+ #client: KookClientTransport | null = null;
51
+ #open = false;
52
+ #started = false;
53
+ readonly management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient());
54
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
55
+
56
+ constructor(options: KookEndpointOptions) {
57
+ super();
58
+ this.#logger = getAdapterLogger('kook', options.config.id);
59
+ this.#options = options;
60
+ this.#createClient = options.createClient ?? (defaultCreateClient as CreateKookClient);
61
+ }
62
+
63
+ get client(): KookClientTransport {
64
+ return this.#requireClient();
65
+ }
66
+
67
+ async start(): Promise<void> {
68
+ if (this.#started) return;
69
+ this.#started = true;
70
+ try {
71
+ this.#client = this.#createClient(this.#options.config);
72
+ this.#bindClient(this.#client);
73
+ await this.#client.connect();
74
+ this.#logger.info(formatCompact({
75
+ op: 'connect',
76
+ endpoint: this.#options.config.id,
77
+ mode: 'websocket',
78
+ self_id: this.#client.self_id != null ? String(this.#client.self_id) : undefined,
79
+ }));
80
+ } catch (error) {
81
+ await this.stop();
82
+ this.#logger.error('Failed to connect KOOK websocket:', error);
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ open(): void {
88
+ this.#open = true;
89
+ }
90
+
91
+ close(): void {
92
+ this.#open = false;
93
+ }
94
+
95
+ async stop(): Promise<void> {
96
+ this.#open = false;
97
+ if (this.#client) {
98
+ try {
99
+ this.#client.removeAllListeners();
100
+ await this.#client.disconnect();
101
+ } catch {
102
+ /* ignore */
103
+ }
104
+ this.#client = null;
105
+ }
106
+ this.#started = false;
107
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
108
+ }
109
+
110
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
111
+ const body = formatOutboundKmarkdown(payload);
112
+ const client = this.#requireClient();
113
+ const result = conversation.kind === 'private'
114
+ ? await client.sendPrivateMsg(conversation.id, body)
115
+ : await client.sendChannelMsg(conversation.id, body);
116
+ const messageId = result?.msg_id != null ? String(result.msg_id) : '';
117
+ this.#logger.debug(formatCompact({
118
+ op: 'kook_send',
119
+ endpoint: this.#options.config.id,
120
+ target: `${conversation.kind}:${conversation.id}`,
121
+ messageId,
122
+ }));
123
+ return messageId;
124
+ }
125
+
126
+ /** Test / internal: admit a normalized message when open. */
127
+ admit(msg: KookInboundMessage): void {
128
+ if (!this.#open) return;
129
+ if (msg.authorBot) return;
130
+ const conversation = kookInboundConversation(String(this.#options.id), msg);
131
+ const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
132
+ void this.emit('message.receive', {
133
+ conversation,
134
+ message: { conversation, id: msg.id },
135
+ content: formatInboundContent(msg),
136
+ sender: {
137
+ id: msg.authorId,
138
+ name: senderDisplayName(msg) || undefined,
139
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles.map(String) } : {}),
140
+ },
141
+ endpointId: this.#options.config.id,
142
+ ...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
143
+ metadata: Object.freeze({
144
+ channelKind: msg.channelKind,
145
+ userId: msg.authorId,
146
+ guildId: msg.guildId,
147
+ roles: msg.authorRoles,
148
+ }),
149
+ }).catch((err) => {
150
+ this.#logger.warn(formatCompact({
151
+ op: 'kook_gateway_receive_failed',
152
+ target: `${conversation.kind}:${conversation.id}`,
153
+ error: err instanceof Error ? err.message : String(err),
154
+ }));
155
+ });
156
+ }
157
+
158
+ async recallMessage(messageId: string): Promise<void> {
159
+ if (!messageId) return;
160
+ await this.#requireClient().recallMsg?.(messageId);
161
+ }
162
+
163
+ #bindClient(client: KookClientTransport): void {
164
+ client.on('message', (raw) => {
165
+ this.#emitPlatformEvent('message', raw);
166
+ const msg = normalizeKookMessage(raw);
167
+ if (msg) this.admit(msg);
168
+ });
169
+ const receiver = (client as { receiver?: { on(event: string, listener: (...args: unknown[]) => void): void } }).receiver;
170
+ receiver?.on('event', (raw) => {
171
+ this.#emitPlatformEvent('event', raw);
172
+ receiveKookSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, raw, this.#logger);
173
+ });
174
+ }
175
+
176
+ #emitPlatformEvent(name: string, event: unknown): void {
177
+ void this.emitPlatform(name, event).catch((error) => {
178
+ this.#logger.warn(formatCompact({
179
+ op: 'kook_platform_event_failed',
180
+ event: name,
181
+ error: error instanceof Error ? error.message : String(error),
182
+ }));
183
+ });
184
+ }
185
+
186
+ #requireClient(): KookClientTransport {
187
+ if (!this.#client) throw new Error('KOOK client not connected');
188
+ return this.#client;
189
+ }
190
+ }
191
+
192
+ export interface KookWebhookEndpointOptions {
193
+ readonly id: CapabilityId;
194
+ readonly http: HttpHost;
195
+ readonly config: ResolvedKookWebhookConfig;
196
+ readonly createClient?: CreateKookClient;
197
+ }
198
+
199
+ export class KookWebhookEndpoint extends Endpoint<KookClientTransport> {
200
+ readonly #logger!: ReturnType<typeof getAdapterLogger>;
201
+
202
+ readonly #options: KookWebhookEndpointOptions;
203
+ readonly #createClient: CreateKookClient;
204
+ #client: KookClientTransport | null = null;
205
+ #routeReleases: HttpRouteRegistration[] = [];
206
+ #processedSn = new Set<number>();
207
+ #open = false;
208
+ #started = false;
209
+ readonly management: EndpointManagement = createKookEndpointManagement(() => this.#requireClient());
210
+ readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
211
+
212
+ constructor(options: KookWebhookEndpointOptions) {
213
+ super();
214
+ this.#logger = getAdapterLogger('kook', options.config.id);
215
+ this.#options = options;
216
+ this.#createClient = options.createClient ?? (defaultCreateWebhookClient as CreateKookClient);
217
+ }
218
+
219
+ get client(): KookClientTransport {
220
+ return this.#requireClient();
221
+ }
222
+
223
+ /** Used by webhook handler. */
224
+ get isOpen(): boolean {
225
+ return this.#open;
226
+ }
227
+
228
+ get config(): ResolvedKookWebhookConfig {
229
+ return this.#options.config;
230
+ }
231
+
232
+ get selfId(): string | undefined {
233
+ return this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
234
+ }
235
+
236
+ async start(): Promise<void> {
237
+ if (this.#started) return;
238
+ this.#started = true;
239
+ try {
240
+ this.#client = this.#createClient(this.#options.config);
241
+ await (this.#client as Client).init();
242
+ this.#routeReleases.push(...registerKookWebhookRoutes(this.#options.http, this));
243
+ this.#logger.info(formatCompact({
244
+ op: 'connect',
245
+ endpoint: this.#options.config.id,
246
+ mode: 'webhook',
247
+ path: this.#options.config.webhookPath,
248
+ self_id: this.#client.self_id != null ? String(this.#client.self_id) : undefined,
249
+ }));
250
+ } catch (error) {
251
+ await this.stop();
252
+ this.#logger.error('Failed to connect KOOK webhook:', error);
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ open(): void {
258
+ this.#open = true;
259
+ }
260
+
261
+ close(): void {
262
+ this.#open = false;
263
+ }
264
+
265
+ async stop(): Promise<void> {
266
+ this.#open = false;
267
+ for (const release of this.#routeReleases.splice(0)) release();
268
+ this.#processedSn.clear();
269
+ if (this.#client) {
270
+ try {
271
+ this.#client.removeAllListeners();
272
+ await this.#client.disconnect();
273
+ } catch {
274
+ /* ignore */
275
+ }
276
+ this.#client = null;
277
+ }
278
+ this.#started = false;
279
+ this.#logger.debug(formatCompact({ op: 'disconnect' }));
280
+ }
281
+
282
+ async send({ conversation, payload }: EndpointSendRequest): Promise<string> {
283
+ const body = formatOutboundKmarkdown(payload);
284
+ const client = this.#requireClient();
285
+ const result = conversation.kind === 'private'
286
+ ? await client.sendPrivateMsg(conversation.id, body)
287
+ : await client.sendChannelMsg(conversation.id, body);
288
+ const messageId = result?.msg_id != null ? String(result.msg_id) : '';
289
+ this.#logger.debug(formatCompact({
290
+ op: 'kook_send',
291
+ endpoint: this.#options.config.id,
292
+ target: `${conversation.kind}:${conversation.id}`,
293
+ messageId,
294
+ }));
295
+ return messageId;
296
+ }
297
+
298
+ admitEvent(event: KookWebhookEventData): void {
299
+ void this.emitPlatform(String(event.type ?? 'event'), event).catch((error) => {
300
+ this.#logger.warn(formatCompact({
301
+ op: 'kook_platform_event_failed',
302
+ event: String(event.type ?? 'event'),
303
+ error: error instanceof Error ? error.message : String(error),
304
+ }));
305
+ });
306
+ if (receiveKookSideEvent(
307
+ (name, payload) => this.emit(name, payload),
308
+ this.#options.config.id,
309
+ event,
310
+ this.#logger,
311
+ )) return;
312
+ const message = normalizeKookWebhookEvent(event, {
313
+ ignore: this.#options.config.ignore,
314
+ selfId: this.selfId,
315
+ });
316
+ if (message) this.admit(message);
317
+ }
318
+
319
+ /** Test / internal: admit a message when open. */
320
+ admit(msg: KookInboundMessage): void {
321
+ if (!this.#open) return;
322
+ if (msg.authorBot) return;
323
+ const conversation = kookInboundConversation(String(this.#options.id), msg);
324
+ const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
325
+ void this.emit('message.receive', {
326
+ conversation,
327
+ message: { conversation, id: msg.id },
328
+ content: formatInboundContent(msg),
329
+ sender: {
330
+ id: msg.authorId,
331
+ name: senderDisplayName(msg) || undefined,
332
+ ...(msg.authorRoles?.length ? { roles: msg.authorRoles.map(String) } : {}),
333
+ },
334
+ endpointId: this.#options.config.id,
335
+ ...(isKookBotMentioned(msg, selfId) ? { mentioned: true } : {}),
336
+ metadata: Object.freeze({
337
+ channelKind: msg.channelKind,
338
+ userId: msg.authorId,
339
+ guildId: msg.guildId,
340
+ roles: msg.authorRoles,
341
+ }),
342
+ }).catch((err) => {
343
+ this.#logger.warn(formatCompact({
344
+ op: 'kook_gateway_receive_failed',
345
+ target: `${conversation.kind}:${conversation.id}`,
346
+ error: err instanceof Error ? err.message : String(err),
347
+ }));
348
+ });
349
+ }
350
+
351
+ async recallMessage(messageId: string): Promise<void> {
352
+ if (!messageId) return;
353
+ await this.#requireClient().recallMsg?.(messageId);
354
+ }
355
+
356
+ checkAndRememberSn(sn: number): boolean {
357
+ if (this.#processedSn.has(sn)) return false;
358
+ this.#processedSn.add(sn);
359
+ if (this.#processedSn.size > 1024) {
360
+ const first = this.#processedSn.values().next().value;
361
+ if (first != null) this.#processedSn.delete(first);
362
+ }
363
+ return true;
364
+ }
365
+
366
+ #requireClient(): KookClientTransport {
367
+ if (!this.#client) throw new Error('KOOK client not initialized');
368
+ return this.#client;
369
+ }
370
+ }
371
+
372
+ /**
373
+ * KOOK guild/channel/user id 是字符串形式的雪花号,超出
374
+ * Number.MAX_SAFE_INTEGER,Number() 会丢精度。Console 社交面只把 group_id
375
+ * 当 JSON 值透传、并以字符串回传给 listGroupMembers,因此保留原始字符串
376
+ * (仅按契约类型声明强转)是全链路最不丢信息的方案。
377
+ */
378
+ function toGroupId(id: string): number {
379
+ return id as unknown as number;
380
+ }
381
+
382
+ /** KOOK 文字频道标记:HTTP API type=1(2=语音),kook-client 消息侧用 'GROUP'。 */
383
+ function isKookTextChannelType(type: string | number | undefined): boolean {
384
+ return type === 1 || type === '1' || type === 'GROUP';
385
+ }
386
+
387
+ /**
388
+ * KOOK endpoint 的 EndpointManagement 语义端口(websocket / webhook 共用)。
389
+ * 数据走 kook-client 的 HTTP API 封装:/api/v3/guild/list、
390
+ * /api/v3/channel/list、/api/v3/guild/user-list(SDK 内部已做分页聚合)。
391
+ */
392
+ export function createKookEndpointManagement(
393
+ requireClient: () => KookClientTransport,
394
+ ): EndpointManagement {
395
+ return Object.freeze<EndpointManagement>({
396
+ async listGroups(): Promise<readonly EndpointGroup[]> {
397
+ const guilds = await requireClient().getGuildList();
398
+ const groups: EndpointGroup[] = [];
399
+ for (const guild of guilds) {
400
+ if (guild?.id == null) continue;
401
+ groups.push({
402
+ group_id: toGroupId(String(guild.id)),
403
+ name: String(guild.name ?? guild.id),
404
+ });
405
+ }
406
+ return groups;
407
+ },
408
+ async listChannels(): Promise<readonly EndpointChannel[]> {
409
+ const client = requireClient();
410
+ const channels: EndpointChannel[] = [];
411
+ for (const guild of await client.getGuildList()) {
412
+ if (guild?.id == null) continue;
413
+ const guildId = String(guild.id);
414
+ const guildName = String(guild.name ?? guildId);
415
+ for (const channel of await client.getChannelList(guildId)) {
416
+ if (channel?.id == null) continue;
417
+ if (channel.is_category) continue;
418
+ if (!isKookTextChannelType(channel.type)) continue;
419
+ channels.push({
420
+ id: String(channel.id),
421
+ name: channel.name ? String(channel.name) : undefined,
422
+ parent: { type: 'guild', id: guildId, name: guildName },
423
+ });
424
+ }
425
+ }
426
+ return channels;
427
+ },
428
+ async listGroupMembers(groupId: string): Promise<readonly unknown[]> {
429
+ // 平台形状(User.Info[])原样返回
430
+ return requireClient().getGuildUserList(groupId);
431
+ },
432
+ });
433
+ }