@zhin.js/adapter-kook 1.0.84 → 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 +804 -17
  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,313 @@
1
+ /**
2
+ * KOOK protocol helpers — no legacy Adapter/Endpoint / segment-mapper.
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+ import { createDecipheriv, timingSafeEqual } from 'node:crypto';
6
+ import { inflateSync } from 'node:zlib';
7
+ import { pickCredential } from 'zhin.js/adapter';
8
+ import { isMediaRef } from '@zhin.js/core';
9
+ import { formatCompact, getLogger } from '@zhin.js/logger';
10
+ const logger = getLogger('kook');
11
+ export var KookPermission;
12
+ (function (KookPermission) {
13
+ KookPermission[KookPermission["Normal"] = 1] = "Normal";
14
+ KookPermission[KookPermission["Admin"] = 2] = "Admin";
15
+ KookPermission[KookPermission["Owner"] = 4] = "Owner";
16
+ KookPermission[KookPermission["ChannelAdmin"] = 5] = "ChannelAdmin";
17
+ })(KookPermission || (KookPermission = {}));
18
+ export function resolveKookConfig(config = {}) {
19
+ const entry = config.endpoints?.find((item) => item.context === 'kook' || !item.context);
20
+ const token = pickCredential(config.token, entry?.token, process.env.KOOK_TOKEN, process.env.KOOK_BOT_TOKEN);
21
+ if (!token) {
22
+ throw new TypeError('KOOK adapter requires token (plugins.<key>.token or endpoints with context: kook)');
23
+ }
24
+ const id = (typeof config.id === 'string' && config.id)
25
+ || (typeof entry?.id === 'string' && entry.id)
26
+ || process.env.KOOK_BOT_NAME
27
+ || 'kook-bot';
28
+ const connection = config.connection
29
+ ?? entry?.connection
30
+ ?? 'websocket';
31
+ if (connection === 'webhook') {
32
+ const verifyToken = (typeof config.verify_token === 'string' && config.verify_token)
33
+ || (typeof entry?.verify_token === 'string' && entry.verify_token)
34
+ || process.env.KOOK_VERIFY_TOKEN
35
+ || '';
36
+ if (!verifyToken) {
37
+ throw new TypeError('KOOK webhook mode requires verify_token (plugins.<key>.verify_token or KOOK_VERIFY_TOKEN)');
38
+ }
39
+ const encryptKey = config.encrypt_key ?? entry?.encrypt_key ?? process.env.KOOK_ENCRYPT_KEY;
40
+ return {
41
+ context: 'kook',
42
+ connection: 'webhook',
43
+ id,
44
+ token,
45
+ webhookPath: normalizeWebhookPath(config.webhookPath ?? entry?.webhookPath ?? process.env.KOOK_WEBHOOK_PATH ?? '/kook/webhook'),
46
+ verifyToken,
47
+ encryptKey: typeof encryptKey === 'string' && encryptKey ? encryptKey : undefined,
48
+ ignore: config.ignore ?? entry?.ignore ?? 'bot',
49
+ logLevel: config.logLevel ?? entry?.logLevel ?? 'info',
50
+ };
51
+ }
52
+ return {
53
+ context: 'kook',
54
+ connection: 'websocket',
55
+ id,
56
+ token,
57
+ data_dir: config.data_dir ?? entry?.data_dir,
58
+ timeout: config.timeout ?? entry?.timeout ?? 10_000,
59
+ max_retry: config.max_retry ?? entry?.max_retry ?? 3,
60
+ ignore: config.ignore ?? entry?.ignore ?? 'bot',
61
+ logLevel: config.logLevel ?? entry?.logLevel ?? 'info',
62
+ };
63
+ }
64
+ /**
65
+ * 入站归一化 → ConversationRef:频道消息的所属 guild 进 `parent`(guild 容器
66
+ * 映射为 channel 容器);私聊(PERSON)无容器,直接 kind 'private'。
67
+ */
68
+ export function kookInboundConversation(endpointKey, msg) {
69
+ return {
70
+ endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
71
+ kind: msg.channelKind,
72
+ id: msg.channelId,
73
+ ...(msg.guildId && msg.channelKind === 'channel'
74
+ ? { parent: { kind: 'channel', id: msg.guildId } }
75
+ : {}),
76
+ };
77
+ }
78
+ export function senderDisplayName(msg) {
79
+ return msg.authorName || msg.authorId;
80
+ }
81
+ /** Build inbound text for OutboundMessageService.receive */
82
+ export function formatInboundContent(msg) {
83
+ const text = (msg.content || msg.rawMessage || '').trim();
84
+ return text || '(Empty message)';
85
+ }
86
+ /**
87
+ * KOOK @ 用户标记为 `(met)<userId>(met)`;content 含 `(met)<botId>(met)` 时视为 @ 机器人。
88
+ * botId 取 client 缓存的 self_id(/me 类接口),拿不到则不标注。
89
+ */
90
+ export function isKookBotMentioned(msg, selfId) {
91
+ if (!selfId)
92
+ return false;
93
+ const marker = `(met)${selfId}(met)`;
94
+ return (msg.content || '').includes(marker) || (msg.rawMessage || '').includes(marker);
95
+ }
96
+ /**
97
+ * 解析 image/audio/video/file 段的投递 URL(canonical MediaRef 唯一来源)。
98
+ * KOOK KMarkdown 媒体只能消费远程 URL;base64 / path / file 引用无法经
99
+ * KMarkdown 投递,warn 后丢弃(返回 undefined)。
100
+ */
101
+ function resolveOutboundMediaUrl(seg) {
102
+ const media = (seg.data ?? {}).media;
103
+ if (!isMediaRef(media)) {
104
+ logger.warn(formatCompact({
105
+ op: 'kook_outbound_media_dropped',
106
+ type: seg.type,
107
+ reason: 'missing_media_ref',
108
+ }));
109
+ return undefined;
110
+ }
111
+ if (media.kind !== 'url') {
112
+ logger.warn(formatCompact({
113
+ op: 'kook_outbound_media_dropped',
114
+ type: seg.type,
115
+ reason: 'unsupported_media_kind',
116
+ kind: media.kind,
117
+ }));
118
+ return undefined;
119
+ }
120
+ return media.value;
121
+ }
122
+ /**
123
+ * Wire-encode an already-rendered outbound payload into KOOK KMarkdown.
124
+ * Segment canonicalization is intentionally not done here.
125
+ */
126
+ export function formatOutboundKmarkdown(payload) {
127
+ if (typeof payload === 'string')
128
+ return payload;
129
+ const segments = Array.isArray(payload)
130
+ ? payload
131
+ : payload && typeof payload === 'object' && 'type' in payload
132
+ ? [payload]
133
+ : [];
134
+ if (segments.length === 0) {
135
+ return payload == null
136
+ ? ''
137
+ : typeof payload === 'object'
138
+ ? JSON.stringify(payload)
139
+ : String(payload);
140
+ }
141
+ return segments
142
+ .map((item) => {
143
+ if (typeof item === 'string')
144
+ return item;
145
+ const data = item.data ?? {};
146
+ switch (item.type) {
147
+ case 'text':
148
+ return String(data.text ?? data.content ?? '');
149
+ case 'markdown':
150
+ return String(data.content ?? data.text ?? '');
151
+ case 'at': {
152
+ const id = String(data.user_id ?? data.qq ?? data.id ?? '');
153
+ return id === 'all' ? '(met)all(met)' : `(met)${id}(met)`;
154
+ }
155
+ case 'image': {
156
+ const url = resolveOutboundMediaUrl(item);
157
+ return url ? `![${String(data.alt || '图片')}](${url})` : '';
158
+ }
159
+ case 'face':
160
+ return `(emj)${String(data.name || 'emoji')}(emj)[${String(data.id ?? '')}]`;
161
+ case 'link':
162
+ return `[${String(data.text || data.url || '')}](${String(data.url || '')})`;
163
+ case 'video': {
164
+ const url = resolveOutboundMediaUrl(item);
165
+ return url ? `[视频](${url})` : '';
166
+ }
167
+ case 'audio': {
168
+ const url = resolveOutboundMediaUrl(item);
169
+ return url ? `[音频](${url})` : '';
170
+ }
171
+ case 'file': {
172
+ const url = resolveOutboundMediaUrl(item);
173
+ return url ? `[文件: ${String(data.name || '未命名')}](${url})` : '';
174
+ }
175
+ case 'bold':
176
+ return `**${String(data.text ?? '')}**`;
177
+ case 'italic':
178
+ return `*${String(data.text ?? '')}*`;
179
+ case 'code':
180
+ return `\`${String(data.text ?? '')}\``;
181
+ case 'code_block':
182
+ return `\`\`\`${String(data.language || '')}\n${String(data.text ?? '')}\n\`\`\``;
183
+ case 'reply':
184
+ return '';
185
+ default:
186
+ return data.text != null ? String(data.text) : '';
187
+ }
188
+ })
189
+ .filter(Boolean)
190
+ .join('');
191
+ }
192
+ export function normalizeWebhookPath(path) {
193
+ const trimmed = path.trim() || '/kook/webhook';
194
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
195
+ }
196
+ export async function readTextBody(request, options = {}) {
197
+ const limit = options.limit ?? 1_048_576;
198
+ const chunks = [];
199
+ let size = 0;
200
+ for await (const chunk of request) {
201
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
202
+ size += buffer.length;
203
+ if (size > limit) {
204
+ request.destroy();
205
+ throw new Error(`Request body exceeds ${limit} bytes`);
206
+ }
207
+ chunks.push(buffer);
208
+ }
209
+ return Buffer.concat(chunks).toString('utf8');
210
+ }
211
+ export async function readRequestBody(request, options = {}) {
212
+ const limit = options.limit ?? 1_048_576;
213
+ const chunks = [];
214
+ let size = 0;
215
+ for await (const chunk of request) {
216
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
217
+ size += buffer.length;
218
+ if (size > limit) {
219
+ request.destroy();
220
+ throw new Error(`Request body exceeds ${limit} bytes`);
221
+ }
222
+ chunks.push(buffer);
223
+ }
224
+ return Buffer.concat(chunks);
225
+ }
226
+ export function padKookEncryptKey(key) {
227
+ const buf = Buffer.alloc(32, 0);
228
+ Buffer.from(key, 'utf8').copy(buf, 0, 0, Math.min(32, Buffer.byteLength(key, 'utf8')));
229
+ return buf;
230
+ }
231
+ export function decryptKookWebhookPayload(encrypted, encryptKey) {
232
+ const decoded = Buffer.from(encrypted, 'base64');
233
+ const iv = decoded.subarray(0, 16);
234
+ const ciphertext = decoded.subarray(16);
235
+ const decipher = createDecipheriv('aes-256-cbc', padKookEncryptKey(encryptKey), iv);
236
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
237
+ }
238
+ export function parseKookWebhookBody(rawBody, encryptKey) {
239
+ let text = rawBody.toString('utf8').trim();
240
+ if (!text.startsWith('{')) {
241
+ text = inflateSync(rawBody).toString('utf8');
242
+ }
243
+ let parsed;
244
+ try {
245
+ parsed = JSON.parse(text);
246
+ }
247
+ catch {
248
+ throw new TypeError('Invalid KOOK webhook JSON body');
249
+ }
250
+ if (parsed.encrypt) {
251
+ if (!encryptKey) {
252
+ throw new TypeError('KOOK webhook payload is encrypted but encrypt_key is not configured');
253
+ }
254
+ const decrypted = decryptKookWebhookPayload(parsed.encrypt, encryptKey);
255
+ // 在字节层面判断:明文 JSON 以 '{' 开头,否则按 zlib 压缩数据处理。
256
+ // 不能先 toString('utf8') 再转回 Buffer —— 压缩二进制会被替换字符损坏。
257
+ let offset = 0;
258
+ while (offset < decrypted.length
259
+ && (decrypted[offset] === 0x20 || decrypted[offset] === 0x09
260
+ || decrypted[offset] === 0x0a || decrypted[offset] === 0x0d)) {
261
+ offset += 1;
262
+ }
263
+ const decryptedText = decrypted[offset] === 0x7b // '{'
264
+ ? decrypted.toString('utf8')
265
+ : inflateSync(decrypted).toString('utf8');
266
+ parsed = JSON.parse(decryptedText);
267
+ }
268
+ return parsed;
269
+ }
270
+ export function isKookWebhookChallenge(event) {
271
+ return event.type === 255 && event.channel_type === 'WEBHOOK_CHALLENGE';
272
+ }
273
+ export function verifyKookWebhookToken(expected, actual) {
274
+ if (!actual)
275
+ return false;
276
+ const left = Buffer.from(expected, 'utf8');
277
+ const right = Buffer.from(actual, 'utf8');
278
+ if (left.length !== right.length)
279
+ return false;
280
+ return timingSafeEqual(left, right);
281
+ }
282
+ export function normalizeKookWebhookEvent(event, options = {}) {
283
+ if (isKookWebhookChallenge(event))
284
+ return null;
285
+ if (event.msg_id == null || event.author_id == null)
286
+ return null;
287
+ const authorBot = event.extra?.author?.bot === true;
288
+ if (authorBot && options.ignore === 'bot')
289
+ return null;
290
+ if (options.selfId && String(event.author_id) === options.selfId && options.ignore === 'self') {
291
+ return null;
292
+ }
293
+ const channelKind = event.channel_type === 'GROUP' ? 'channel' : 'private';
294
+ const channelId = channelKind === 'channel'
295
+ ? String(event.target_id ?? '')
296
+ : String(event.author_id);
297
+ if (!channelId)
298
+ return null;
299
+ return {
300
+ id: String(event.msg_id),
301
+ content: event.content ?? '',
302
+ channelKind,
303
+ channelId,
304
+ authorId: String(event.author_id),
305
+ authorName: event.extra?.author?.username
306
+ || event.extra?.author?.id
307
+ || String(event.author_id),
308
+ authorBot,
309
+ authorRoles: event.extra?.author?.roles,
310
+ timestamp: event.msg_timestamp ?? Date.now(),
311
+ guildId: event.extra?.guild_id,
312
+ };
313
+ }
@@ -0,0 +1,3 @@
1
+ import type { EndpointEventEmitter } from 'zhin.js/adapter';
2
+ import { type getAdapterLogger } from '@zhin.js/logger';
3
+ export declare function receiveKookSideEvent(emit: EndpointEventEmitter, configId: string, raw: unknown, logger: ReturnType<typeof getAdapterLogger>): boolean;
@@ -0,0 +1,49 @@
1
+ import { buildNotice, mapNoticeParts, senderFromId, KOOK_NOTICE_PARTS_MAP } from '@zhin.js/core';
2
+ import { formatCompact } from '@zhin.js/logger';
3
+ function extractKookNoticeType(raw) {
4
+ if (!raw || typeof raw !== 'object')
5
+ return null;
6
+ const event = raw;
7
+ const extraType = event.extra?.type;
8
+ if (typeof extraType === 'string' && extraType)
9
+ return extraType;
10
+ if (event.channel_type === 'NOTICE' && event.type != null) {
11
+ return String(event.type);
12
+ }
13
+ return null;
14
+ }
15
+ export function receiveKookSideEvent(emit, configId, raw, logger) {
16
+ const noticeType = extractKookNoticeType(raw);
17
+ if (!noticeType)
18
+ return false;
19
+ if (!Object.prototype.hasOwnProperty.call(KOOK_NOTICE_PARTS_MAP, noticeType))
20
+ return false;
21
+ const event = raw;
22
+ const parts = mapNoticeParts('kook', noticeType);
23
+ const guildId = event.extra?.guild_id;
24
+ const userId = event.author_id ?? event.extra?.author?.id;
25
+ const sceneId = guildId != null
26
+ ? String(guildId)
27
+ : userId != null
28
+ ? String(userId)
29
+ : configId;
30
+ void emit('notice.receive', buildNotice(event, {
31
+ $id: `kook:${noticeType}:${event.msg_timestamp ?? Date.now()}:${sceneId}:${String(userId ?? '')}`,
32
+ $adapter: 'kook',
33
+ $endpoint: configId,
34
+ $type: 'notice',
35
+ $scene_id: sceneId,
36
+ $scene_type: parts.scene_type,
37
+ $sub_type: parts.sub_type,
38
+ $actor: senderFromId(userId, event.extra?.author?.username),
39
+ $timestamp: event.msg_timestamp ?? Date.now(),
40
+ })).catch((err) => {
41
+ logger.warn(formatCompact({
42
+ op: 'kook_side_event_failed',
43
+ endpoint: configId,
44
+ event: noticeType,
45
+ error: err instanceof Error ? err.message : String(err),
46
+ }));
47
+ });
48
+ return true;
49
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * KOOK webhook HTTP: verify → parse → admit.
3
+ */
4
+ import type { IncomingMessage, ServerResponse } from 'node:http';
5
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
6
+ import { type KookInboundMessage, type KookWebhookEventData, type ResolvedKookWebhookConfig } from './protocol.js';
7
+ export interface KookWebhookHandler {
8
+ readonly config: ResolvedKookWebhookConfig;
9
+ readonly isOpen: boolean;
10
+ readonly selfId?: string;
11
+ admit(msg: KookInboundMessage): void;
12
+ admitEvent(event: KookWebhookEventData): void;
13
+ checkAndRememberSn(sn: number): boolean;
14
+ }
15
+ export declare function registerKookWebhookRoutes(http: HttpHost, handler: KookWebhookHandler): HttpRouteRegistration[];
16
+ export declare function handleKookWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: KookWebhookHandler): Promise<void>;
package/lib/webhook.js ADDED
@@ -0,0 +1,71 @@
1
+ import { formatCompact, getLogger } from '@zhin.js/logger';
2
+ import { isKookWebhookChallenge, parseKookWebhookBody, readRequestBody, verifyKookWebhookToken, } from './protocol.js';
3
+ const logger = getLogger('kook');
4
+ export function registerKookWebhookRoutes(http, handler) {
5
+ const path = handler.config.webhookPath;
6
+ return [
7
+ http.route('POST', path, async (request, response) => {
8
+ await handleKookWebhookRequest(request, response, handler);
9
+ }, { summary: 'KOOK webhook callback', tags: ['kook'] }),
10
+ ];
11
+ }
12
+ export async function handleKookWebhookRequest(request, response, handler) {
13
+ try {
14
+ const rawBody = await readRequestBody(request);
15
+ let frame;
16
+ try {
17
+ frame = parseKookWebhookBody(rawBody, handler.config.encryptKey);
18
+ }
19
+ catch (error) {
20
+ logger.warn(formatCompact({
21
+ op: 'kook_webhook',
22
+ ok: false,
23
+ error: error instanceof Error ? error.message : String(error),
24
+ }));
25
+ response.writeHead(400, { 'Content-Type': 'application/json' });
26
+ response.end(JSON.stringify({ message: 'Invalid webhook payload' }));
27
+ return;
28
+ }
29
+ const event = frame.d;
30
+ if (!event || typeof event !== 'object') {
31
+ response.writeHead(200, { 'Content-Type': 'application/json' });
32
+ response.end(JSON.stringify({ message: 'OK' }));
33
+ return;
34
+ }
35
+ if (isKookWebhookChallenge(event)) {
36
+ if (!verifyKookWebhookToken(handler.config.verifyToken, event.verify_token)) {
37
+ response.writeHead(403, { 'Content-Type': 'application/json' });
38
+ response.end(JSON.stringify({ message: 'Invalid verify_token' }));
39
+ return;
40
+ }
41
+ response.writeHead(200, { 'Content-Type': 'application/json' });
42
+ response.end(JSON.stringify({ challenge: event.challenge ?? '' }));
43
+ return;
44
+ }
45
+ if (!verifyKookWebhookToken(handler.config.verifyToken, event.verify_token)) {
46
+ response.writeHead(403, { 'Content-Type': 'application/json' });
47
+ response.end(JSON.stringify({ message: 'Invalid verify_token' }));
48
+ return;
49
+ }
50
+ if (frame.sn != null) {
51
+ if (!handler.checkAndRememberSn(frame.sn)) {
52
+ response.writeHead(200, { 'Content-Type': 'application/json' });
53
+ response.end(JSON.stringify({ message: 'OK' }));
54
+ return;
55
+ }
56
+ }
57
+ if (handler.isOpen) {
58
+ handleKookWebhookEvent(event, handler);
59
+ }
60
+ response.writeHead(200, { 'Content-Type': 'application/json' });
61
+ response.end(JSON.stringify({ message: 'OK' }));
62
+ }
63
+ catch (error) {
64
+ logger.error('KOOK webhook error:', error);
65
+ response.writeHead(200, { 'Content-Type': 'application/json' });
66
+ response.end(JSON.stringify({ message: 'OK' }));
67
+ }
68
+ }
69
+ function handleKookWebhookEvent(event, handler) {
70
+ handler.admitEvent(event);
71
+ }
package/lib/ws.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { type KookInboundMessage, type ResolvedKookConfig, type ResolvedKookWebhookConfig, type ResolvedKookWebsocketConfig } from './protocol.js';
2
+ /** Minimal client surface used by the endpoint (real kook-client or test mock). */
3
+ export interface KookClientTransport {
4
+ connect(): Promise<void>;
5
+ disconnect(): Promise<void>;
6
+ on(event: string, listener: (...args: unknown[]) => void): void;
7
+ removeAllListeners(): void;
8
+ sendChannelMsg(channelId: string, message: string): Promise<{
9
+ msg_id?: string | number;
10
+ }>;
11
+ sendPrivateMsg(userId: string, message: string): Promise<{
12
+ msg_id?: string | number;
13
+ }>;
14
+ recallMsg?(messageId: string): Promise<boolean>;
15
+ pickGuild(guildId: string): {
16
+ kick(userId: string): Promise<boolean>;
17
+ getRoleList(): Promise<Array<{
18
+ role_id: string | number;
19
+ name: string;
20
+ color?: number;
21
+ position?: number;
22
+ permissions?: unknown;
23
+ }>>;
24
+ createRole(name: string): Promise<{
25
+ role_id: string | number;
26
+ name: string;
27
+ }>;
28
+ deleteRole(roleId: string): Promise<boolean>;
29
+ };
30
+ pickGuildMember(guildId: string, userId: string): {
31
+ addToBlackList(remark?: string, delMsgDays?: number): Promise<boolean>;
32
+ removeFromBlackList(): Promise<boolean>;
33
+ grant(roleId: string): Promise<boolean>;
34
+ revoke(roleId: string): Promise<boolean>;
35
+ setNickname(nickname: string): Promise<boolean>;
36
+ };
37
+ /** GET /api/v3/guild/list(kook-client 内部分页聚合)。 */
38
+ getGuildList(): Promise<Array<{
39
+ id: string | number;
40
+ name?: string;
41
+ }>>;
42
+ /** GET /api/v3/channel/list(type: 1=文字 2=语音;is_category 为分组)。 */
43
+ getChannelList(guildId: string): Promise<Array<{
44
+ id: string | number;
45
+ name?: string;
46
+ type?: string | number;
47
+ is_category?: boolean;
48
+ }>>;
49
+ /** GET /api/v3/guild/user-list。 */
50
+ getGuildUserList(guildId: string, channelId?: string): Promise<unknown[]>;
51
+ self_id?: string | number;
52
+ }
53
+ export type CreateKookClient = (config: ResolvedKookConfig) => KookClientTransport;
54
+ export declare function normalizeKookMessage(raw: unknown): KookInboundMessage | null;
55
+ export declare function defaultCreateClient(config: ResolvedKookWebsocketConfig): KookClientTransport;
56
+ export declare function defaultCreateWebhookClient(config: ResolvedKookWebhookConfig): KookClientTransport;
package/lib/ws.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * KOOK WebSocket transport: kook-client wrapper and inbound message normalization.
3
+ */
4
+ import path from 'node:path';
5
+ import { Client } from 'kook-client';
6
+ export function normalizeKookMessage(raw) {
7
+ if (!raw || typeof raw !== 'object')
8
+ return null;
9
+ const msg = raw;
10
+ if (msg.message_id == null || msg.author_id == null)
11
+ return null;
12
+ const channelKind = msg.message_type === 'channel' ? 'channel' : 'private';
13
+ const channelId = channelKind === 'channel'
14
+ ? String(msg.channel_id ?? '')
15
+ : String(msg.author_id);
16
+ if (!channelId)
17
+ return null;
18
+ const textParts = [];
19
+ for (const seg of msg.message ?? []) {
20
+ if (seg.type === 'text' || seg.type === 'markdown') {
21
+ if (seg.text)
22
+ textParts.push(seg.text);
23
+ }
24
+ }
25
+ const content = textParts.join('') || msg.raw_message || '';
26
+ return {
27
+ id: String(msg.message_id),
28
+ content,
29
+ channelKind,
30
+ channelId,
31
+ authorId: String(msg.author_id),
32
+ authorName: msg.author?.info?.nickname
33
+ || msg.author?.info?.username
34
+ || String(msg.author_id),
35
+ authorBot: msg.author?.bot === true || msg.author?.info?.bot === true,
36
+ authorRoles: msg.author?.info?.roles ?? msg.author?.roles,
37
+ timestamp: msg.timestamp ?? Date.now(),
38
+ guildId: msg.channel?.info?.guild_id,
39
+ rawMessage: msg.raw_message,
40
+ };
41
+ }
42
+ export function defaultCreateClient(config) {
43
+ return new Client({
44
+ token: config.token,
45
+ mode: 'websocket',
46
+ data_dir: config.data_dir || path.join(process.cwd(), 'data', 'kook'),
47
+ timeout: config.timeout,
48
+ max_retry: config.max_retry,
49
+ ignore: config.ignore,
50
+ logLevel: config.logLevel,
51
+ });
52
+ }
53
+ export function defaultCreateWebhookClient(config) {
54
+ return new Client({
55
+ token: config.token,
56
+ mode: 'webhook',
57
+ data_dir: path.join(process.cwd(), 'data', 'kook'),
58
+ timeout: 10_000,
59
+ max_retry: 3,
60
+ ignore: config.ignore,
61
+ logLevel: config.logLevel,
62
+ });
63
+ }