@zhin.js/adapter-kook 5.0.1 → 5.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +58 -451
  3. package/adapters/kook.ts +46 -0
  4. package/agent/tools/blacklist.ts +33 -0
  5. package/agent/tools/create_role.ts +25 -0
  6. package/agent/tools/delete_role.ts +21 -0
  7. package/agent/tools/grant_role.ts +22 -0
  8. package/agent/tools/list_roles.ts +27 -0
  9. package/agent/tools/revoke_role.ts +22 -0
  10. package/lib/endpoint.d.ts +80 -151
  11. package/lib/endpoint.js +260 -736
  12. package/lib/index.d.ts +6 -18
  13. package/lib/index.js +6 -350
  14. package/lib/kook-agent-deps.d.ts +29 -0
  15. package/lib/kook-agent-deps.js +30 -0
  16. package/lib/platform-permit.d.ts +12 -3
  17. package/lib/platform-permit.js +5 -3
  18. package/lib/protocol.d.ts +147 -0
  19. package/lib/protocol.js +279 -0
  20. package/lib/webhook.d.ts +15 -0
  21. package/lib/webhook.js +76 -0
  22. package/lib/ws.d.ts +42 -0
  23. package/lib/ws.js +63 -0
  24. package/package.json +47 -33
  25. package/plugin.ts +13 -0
  26. package/schema.json +55 -0
  27. package/src/endpoint.ts +287 -866
  28. package/src/index.ts +55 -331
  29. package/src/kook-agent-deps.ts +55 -0
  30. package/src/platform-permit.ts +13 -4
  31. package/src/protocol.ts +429 -0
  32. package/src/webhook.ts +116 -0
  33. package/src/ws.ts +120 -0
  34. package/client/Dashboard.tsx +0 -255
  35. package/client/index.tsx +0 -11
  36. package/client/tsconfig.json +0 -7
  37. package/client/utils/api.ts +0 -30
  38. package/dist/index.js +0 -31
  39. package/lib/adapter.d.ts +0 -30
  40. package/lib/adapter.d.ts.map +0 -1
  41. package/lib/adapter.js +0 -67
  42. package/lib/adapter.js.map +0 -1
  43. package/lib/endpoint.d.ts.map +0 -1
  44. package/lib/endpoint.js.map +0 -1
  45. package/lib/index.d.ts.map +0 -1
  46. package/lib/index.js.map +0 -1
  47. package/lib/kook-asset-upload.d.ts +0 -12
  48. package/lib/kook-asset-upload.d.ts.map +0 -1
  49. package/lib/kook-asset-upload.js +0 -46
  50. package/lib/kook-asset-upload.js.map +0 -1
  51. package/lib/kook-inbound.d.ts +0 -6
  52. package/lib/kook-inbound.d.ts.map +0 -1
  53. package/lib/kook-inbound.js +0 -22
  54. package/lib/kook-inbound.js.map +0 -1
  55. package/lib/kook-msg-route.d.ts +0 -34
  56. package/lib/kook-msg-route.d.ts.map +0 -1
  57. package/lib/kook-msg-route.js +0 -106
  58. package/lib/kook-msg-route.js.map +0 -1
  59. package/lib/kook-side-events.d.ts +0 -46
  60. package/lib/kook-side-events.d.ts.map +0 -1
  61. package/lib/kook-side-events.js +0 -189
  62. package/lib/kook-side-events.js.map +0 -1
  63. package/lib/outbound-keyboard.d.ts +0 -13
  64. package/lib/outbound-keyboard.d.ts.map +0 -1
  65. package/lib/outbound-keyboard.js +0 -63
  66. package/lib/outbound-keyboard.js.map +0 -1
  67. package/lib/outbound-media.d.ts +0 -10
  68. package/lib/outbound-media.d.ts.map +0 -1
  69. package/lib/outbound-media.js +0 -42
  70. package/lib/outbound-media.js.map +0 -1
  71. package/lib/outbound-sendable.d.ts +0 -12
  72. package/lib/outbound-sendable.d.ts.map +0 -1
  73. package/lib/outbound-sendable.js +0 -64
  74. package/lib/outbound-sendable.js.map +0 -1
  75. package/lib/platform-permit.d.ts.map +0 -1
  76. package/lib/platform-permit.js.map +0 -1
  77. package/lib/segment-mapper.d.ts +0 -2
  78. package/lib/segment-mapper.d.ts.map +0 -1
  79. package/lib/segment-mapper.js +0 -2
  80. package/lib/segment-mapper.js.map +0 -1
  81. package/lib/types.d.ts +0 -54
  82. package/lib/types.d.ts.map +0 -1
  83. package/lib/types.js +0 -8
  84. package/lib/types.js.map +0 -1
  85. package/plugin.yml +0 -3
  86. package/src/adapter.ts +0 -76
  87. package/src/kook-asset-upload.ts +0 -55
  88. package/src/kook-inbound.ts +0 -22
  89. package/src/kook-msg-route.ts +0 -122
  90. package/src/kook-side-events.ts +0 -250
  91. package/src/outbound-keyboard.ts +0 -78
  92. package/src/outbound-media.ts +0 -67
  93. package/src/outbound-sendable.ts +0 -73
  94. package/src/segment-mapper.ts +0 -1
  95. package/src/types.ts +0 -59
  96. /package/{skills/kook → agent}/PERMITS.md +0 -0
  97. /package/{skills/kook/SKILL.md → agent/skills/kook.md} +0 -0
@@ -0,0 +1,279 @@
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
+ export var KookPermission;
9
+ (function (KookPermission) {
10
+ KookPermission[KookPermission["Normal"] = 1] = "Normal";
11
+ KookPermission[KookPermission["Admin"] = 2] = "Admin";
12
+ KookPermission[KookPermission["Owner"] = 4] = "Owner";
13
+ KookPermission[KookPermission["ChannelAdmin"] = 5] = "ChannelAdmin";
14
+ })(KookPermission || (KookPermission = {}));
15
+ export function resolveKookConfig(config = {}) {
16
+ const entry = config.endpoints?.find((item) => item.context === 'kook' || !item.context);
17
+ const token = pickCredential(config.token, entry?.token, process.env.KOOK_TOKEN, process.env.KOOK_BOT_TOKEN);
18
+ if (!token) {
19
+ throw new TypeError('KOOK adapter requires token (plugins.<key>.token or endpoints with context: kook)');
20
+ }
21
+ const name = (typeof config.name === 'string' && config.name)
22
+ || (typeof entry?.name === 'string' && entry.name)
23
+ || process.env.KOOK_BOT_NAME
24
+ || 'kook-bot';
25
+ const connection = config.connection
26
+ ?? entry?.connection
27
+ ?? 'websocket';
28
+ if (connection === 'webhook') {
29
+ const verifyToken = (typeof config.verify_token === 'string' && config.verify_token)
30
+ || (typeof entry?.verify_token === 'string' && entry.verify_token)
31
+ || process.env.KOOK_VERIFY_TOKEN
32
+ || '';
33
+ if (!verifyToken) {
34
+ throw new TypeError('KOOK webhook mode requires verify_token (plugins.<key>.verify_token or KOOK_VERIFY_TOKEN)');
35
+ }
36
+ const encryptKey = config.encrypt_key ?? entry?.encrypt_key ?? process.env.KOOK_ENCRYPT_KEY;
37
+ return {
38
+ context: 'kook',
39
+ connection: 'webhook',
40
+ name,
41
+ token,
42
+ webhookPath: normalizeWebhookPath(config.webhookPath ?? entry?.webhookPath ?? process.env.KOOK_WEBHOOK_PATH ?? '/kook/webhook'),
43
+ verifyToken,
44
+ encryptKey: typeof encryptKey === 'string' && encryptKey ? encryptKey : undefined,
45
+ ignore: config.ignore ?? entry?.ignore ?? 'bot',
46
+ logLevel: config.logLevel ?? entry?.logLevel ?? 'info',
47
+ };
48
+ }
49
+ return {
50
+ context: 'kook',
51
+ connection: 'websocket',
52
+ name,
53
+ token,
54
+ data_dir: config.data_dir ?? entry?.data_dir,
55
+ timeout: config.timeout ?? entry?.timeout ?? 10_000,
56
+ max_retry: config.max_retry ?? entry?.max_retry ?? 3,
57
+ ignore: config.ignore ?? entry?.ignore ?? 'bot',
58
+ logLevel: config.logLevel ?? entry?.logLevel ?? 'info',
59
+ };
60
+ }
61
+ /**
62
+ * Gateway reply target:`private:uid` / `channel:cid`,便于 send() 还原 API。
63
+ */
64
+ export function formatInboundTarget(msg) {
65
+ return `${msg.channelKind}:${msg.channelId}`;
66
+ }
67
+ export function parseSendTarget(target) {
68
+ const sep = target.indexOf(':');
69
+ if (sep <= 0) {
70
+ return { kind: 'channel', id: target };
71
+ }
72
+ const head = target.slice(0, sep);
73
+ const rest = target.slice(sep + 1);
74
+ if (head === 'private')
75
+ return { kind: 'private', id: rest };
76
+ if (head === 'channel' || head === 'group')
77
+ return { kind: 'channel', id: rest };
78
+ return { kind: 'channel', id: target };
79
+ }
80
+ export function senderDisplayName(msg) {
81
+ return msg.authorName || msg.authorId;
82
+ }
83
+ /** Build inbound text for MessageGateway.receive */
84
+ export function formatInboundContent(msg) {
85
+ const text = (msg.content || msg.rawMessage || '').trim();
86
+ return text || '(Empty message)';
87
+ }
88
+ /**
89
+ * KOOK @ 用户标记为 `(met)<userId>(met)`;content 含 `(met)<botId>(met)` 时视为 @ 机器人。
90
+ * botId 取 client 缓存的 self_id(/me 类接口),拿不到则不标注。
91
+ */
92
+ export function isKookBotMentioned(msg, selfId) {
93
+ if (!selfId)
94
+ return false;
95
+ const marker = `(met)${selfId}(met)`;
96
+ return (msg.content || '').includes(marker) || (msg.rawMessage || '').includes(marker);
97
+ }
98
+ /**
99
+ * Wire-encode an already-rendered outbound payload into KOOK KMarkdown.
100
+ * Segment canonicalization is intentionally not done here.
101
+ */
102
+ export function formatOutboundKmarkdown(payload) {
103
+ if (typeof payload === 'string')
104
+ return payload;
105
+ const segments = Array.isArray(payload)
106
+ ? payload
107
+ : payload && typeof payload === 'object' && 'type' in payload
108
+ ? [payload]
109
+ : [];
110
+ if (segments.length === 0) {
111
+ return payload == null
112
+ ? ''
113
+ : typeof payload === 'object'
114
+ ? JSON.stringify(payload)
115
+ : String(payload);
116
+ }
117
+ return segments
118
+ .map((item) => {
119
+ if (typeof item === 'string')
120
+ return item;
121
+ const data = item.data ?? {};
122
+ switch (item.type) {
123
+ case 'text':
124
+ return String(data.text ?? data.content ?? '');
125
+ case 'at': {
126
+ const id = String(data.user_id ?? data.qq ?? data.id ?? '');
127
+ return id === 'all' ? '(met)all(met)' : `(met)${id}(met)`;
128
+ }
129
+ case 'image':
130
+ return `![${String(data.alt || '图片')}](${String(data.url || data.file || '')})`;
131
+ case 'face':
132
+ return `(emj)${String(data.name || 'emoji')}(emj)[${String(data.id ?? '')}]`;
133
+ case 'link':
134
+ return `[${String(data.text || data.url || '')}](${String(data.url || '')})`;
135
+ case 'video':
136
+ return `[视频](${String(data.url || data.file || '')})`;
137
+ case 'audio':
138
+ return `[音频](${String(data.url || data.file || '')})`;
139
+ case 'file':
140
+ return `[文件: ${String(data.name || '未命名')}](${String(data.url || data.file || '')})`;
141
+ case 'bold':
142
+ return `**${String(data.text ?? '')}**`;
143
+ case 'italic':
144
+ return `*${String(data.text ?? '')}*`;
145
+ case 'code':
146
+ return `\`${String(data.text ?? '')}\``;
147
+ case 'code_block':
148
+ return `\`\`\`${String(data.language || '')}\n${String(data.text ?? '')}\n\`\`\``;
149
+ case 'reply':
150
+ return '';
151
+ default:
152
+ return data.text != null ? String(data.text) : '';
153
+ }
154
+ })
155
+ .filter(Boolean)
156
+ .join('');
157
+ }
158
+ export function normalizeWebhookPath(path) {
159
+ const trimmed = path.trim() || '/kook/webhook';
160
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
161
+ }
162
+ export async function readTextBody(request, options = {}) {
163
+ const limit = options.limit ?? 1_048_576;
164
+ const chunks = [];
165
+ let size = 0;
166
+ for await (const chunk of request) {
167
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
168
+ size += buffer.length;
169
+ if (size > limit) {
170
+ request.destroy();
171
+ throw new Error(`Request body exceeds ${limit} bytes`);
172
+ }
173
+ chunks.push(buffer);
174
+ }
175
+ return Buffer.concat(chunks).toString('utf8');
176
+ }
177
+ export async function readRequestBody(request, options = {}) {
178
+ const limit = options.limit ?? 1_048_576;
179
+ const chunks = [];
180
+ let size = 0;
181
+ for await (const chunk of request) {
182
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
183
+ size += buffer.length;
184
+ if (size > limit) {
185
+ request.destroy();
186
+ throw new Error(`Request body exceeds ${limit} bytes`);
187
+ }
188
+ chunks.push(buffer);
189
+ }
190
+ return Buffer.concat(chunks);
191
+ }
192
+ export function padKookEncryptKey(key) {
193
+ const buf = Buffer.alloc(32, 0);
194
+ Buffer.from(key, 'utf8').copy(buf, 0, 0, Math.min(32, Buffer.byteLength(key, 'utf8')));
195
+ return buf;
196
+ }
197
+ export function decryptKookWebhookPayload(encrypted, encryptKey) {
198
+ const decoded = Buffer.from(encrypted, 'base64');
199
+ const iv = decoded.subarray(0, 16);
200
+ const ciphertext = decoded.subarray(16);
201
+ const decipher = createDecipheriv('aes-256-cbc', padKookEncryptKey(encryptKey), iv);
202
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
203
+ }
204
+ export function parseKookWebhookBody(rawBody, encryptKey) {
205
+ let text = rawBody.toString('utf8').trim();
206
+ if (!text.startsWith('{')) {
207
+ text = inflateSync(rawBody).toString('utf8');
208
+ }
209
+ let parsed;
210
+ try {
211
+ parsed = JSON.parse(text);
212
+ }
213
+ catch {
214
+ throw new TypeError('Invalid KOOK webhook JSON body');
215
+ }
216
+ if (parsed.encrypt) {
217
+ if (!encryptKey) {
218
+ throw new TypeError('KOOK webhook payload is encrypted but encrypt_key is not configured');
219
+ }
220
+ const decrypted = decryptKookWebhookPayload(parsed.encrypt, encryptKey);
221
+ // 在字节层面判断:明文 JSON 以 '{' 开头,否则按 zlib 压缩数据处理。
222
+ // 不能先 toString('utf8') 再转回 Buffer —— 压缩二进制会被替换字符损坏。
223
+ let offset = 0;
224
+ while (offset < decrypted.length
225
+ && (decrypted[offset] === 0x20 || decrypted[offset] === 0x09
226
+ || decrypted[offset] === 0x0a || decrypted[offset] === 0x0d)) {
227
+ offset += 1;
228
+ }
229
+ const decryptedText = decrypted[offset] === 0x7b // '{'
230
+ ? decrypted.toString('utf8')
231
+ : inflateSync(decrypted).toString('utf8');
232
+ parsed = JSON.parse(decryptedText);
233
+ }
234
+ return parsed;
235
+ }
236
+ export function isKookWebhookChallenge(event) {
237
+ return event.type === 255 && event.channel_type === 'WEBHOOK_CHALLENGE';
238
+ }
239
+ export function verifyKookWebhookToken(expected, actual) {
240
+ if (!actual)
241
+ return false;
242
+ const left = Buffer.from(expected, 'utf8');
243
+ const right = Buffer.from(actual, 'utf8');
244
+ if (left.length !== right.length)
245
+ return false;
246
+ return timingSafeEqual(left, right);
247
+ }
248
+ export function normalizeKookWebhookEvent(event, options = {}) {
249
+ if (isKookWebhookChallenge(event))
250
+ return null;
251
+ if (event.msg_id == null || event.author_id == null)
252
+ return null;
253
+ const authorBot = event.extra?.author?.bot === true;
254
+ if (authorBot && options.ignore === 'bot')
255
+ return null;
256
+ if (options.selfId && String(event.author_id) === options.selfId && options.ignore === 'self') {
257
+ return null;
258
+ }
259
+ const channelKind = event.channel_type === 'GROUP' ? 'channel' : 'private';
260
+ const channelId = channelKind === 'channel'
261
+ ? String(event.target_id ?? '')
262
+ : String(event.author_id);
263
+ if (!channelId)
264
+ return null;
265
+ return {
266
+ id: String(event.msg_id),
267
+ content: event.content ?? '',
268
+ channelKind,
269
+ channelId,
270
+ authorId: String(event.author_id),
271
+ authorName: event.extra?.author?.username
272
+ || event.extra?.author?.id
273
+ || String(event.author_id),
274
+ authorBot,
275
+ authorRoles: event.extra?.author?.roles,
276
+ timestamp: event.msg_timestamp ?? Date.now(),
277
+ guildId: event.extra?.guild_id,
278
+ };
279
+ }
@@ -0,0 +1,15 @@
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 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
+ checkAndRememberSn(sn: number): boolean;
13
+ }
14
+ export declare function registerKookWebhookRoutes(http: HttpHost, handler: KookWebhookHandler): HttpRouteRegistration[];
15
+ export declare function handleKookWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: KookWebhookHandler): Promise<void>;
package/lib/webhook.js ADDED
@@ -0,0 +1,76 @@
1
+ import { formatCompact, getLogger } from '@zhin.js/logger';
2
+ import { isKookWebhookChallenge, normalizeKookWebhookEvent, 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
+ const msg = normalizeKookWebhookEvent(event, {
71
+ ignore: handler.config.ignore,
72
+ selfId: handler.selfId,
73
+ });
74
+ if (msg)
75
+ handler.admit(msg);
76
+ }
package/lib/ws.d.ts ADDED
@@ -0,0 +1,42 @@
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
+ pickGuild(guildId: string): {
15
+ kick(userId: string): Promise<boolean>;
16
+ getRoleList(): Promise<Array<{
17
+ role_id: string | number;
18
+ name: string;
19
+ color?: number;
20
+ position?: number;
21
+ permissions?: unknown;
22
+ }>>;
23
+ createRole(name: string): Promise<{
24
+ role_id: string | number;
25
+ name: string;
26
+ }>;
27
+ deleteRole(roleId: string): Promise<boolean>;
28
+ };
29
+ pickGuildMember(guildId: string, userId: string): {
30
+ addToBlackList(remark?: string, delMsgDays?: number): Promise<boolean>;
31
+ removeFromBlackList(): Promise<boolean>;
32
+ grant(roleId: string): Promise<boolean>;
33
+ revoke(roleId: string): Promise<boolean>;
34
+ setNickname(nickname: string): Promise<boolean>;
35
+ };
36
+ getGuildUserList?(guildId: string, channelId?: string): Promise<unknown[]>;
37
+ self_id?: string | number;
38
+ }
39
+ export type CreateKookClient = (config: ResolvedKookConfig) => KookClientTransport;
40
+ export declare function normalizeKookMessage(raw: unknown): KookInboundMessage | null;
41
+ export declare function defaultCreateClient(config: ResolvedKookWebsocketConfig): KookClientTransport;
42
+ 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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-kook",
3
- "version": "5.0.1",
4
- "description": "Zhin.js adapter for KOOK (开黑啦)",
3
+ "version": "5.0.3",
4
+ "description": "Zhin.js KOOK adapter for Plugin Runtime (WebSocket gateway or Webhook)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
@@ -33,49 +33,49 @@
33
33
  "directory": "plugins/adapters/kook"
34
34
  },
35
35
  "dependencies": {
36
- "kook-client": "^1.0.4"
37
- },
38
- "devDependencies": {
39
- "@types/react": "^19.2.17",
40
- "@types/react-dom": "^19.2.3",
41
- "lucide-react": "^1.22.0",
42
- "typescript": "^6.0.3",
43
- "@zhin.js/cli": "1.0.92",
44
- "@zhin.js/client": "2.0.4",
45
- "@zhin.js/contract": "1.0.2",
46
- "@zhin.js/host-api": "2.0.4",
47
- "@zhin.js/host-router": "2.0.2",
48
- "zhin.js": "4.1.1",
49
- "@zhin.js/logger": "1.0.73"
36
+ "kook-client": "^1.0.4",
37
+ "@zhin.js/adapter": "1.0.1",
38
+ "@zhin.js/core": "1.3.5",
39
+ "@zhin.js/host-http": "1.0.1",
40
+ "@zhin.js/logger": "1.0.75",
41
+ "@zhin.js/plugin-runtime": "1.0.1"
50
42
  },
51
43
  "peerDependencies": {
52
- "@zhin.js/client": "2.0.4",
53
- "@zhin.js/host-api": "2.0.4",
54
- "@zhin.js/contract": "1.0.2",
55
- "@zhin.js/logger": "1.0.73",
56
- "@zhin.js/host-router": "2.0.2",
57
- "zhin.js": "4.1.1"
44
+ "zod": "^4.0.0",
45
+ "@zhin.js/adapter": "1.0.1",
46
+ "@zhin.js/agent": "1.0.4",
47
+ "@zhin.js/core": "1.3.5",
48
+ "@zhin.js/plugin-runtime": "1.0.1",
49
+ "zhin.js": "4.1.3"
58
50
  },
59
51
  "peerDependenciesMeta": {
60
- "@zhin.js/client": {
52
+ "zhin.js": {
61
53
  "optional": true
62
54
  },
63
- "@zhin.js/host-router": {
55
+ "@zhin.js/agent": {
64
56
  "optional": true
65
57
  },
66
- "@zhin.js/host-api": {
58
+ "zod": {
67
59
  "optional": true
68
60
  }
69
61
  },
62
+ "devDependencies": {
63
+ "@types/node": "^26.1.0",
64
+ "typescript": "^6.0.3",
65
+ "vitest": "^4.1.10",
66
+ "zod": "^4.4.3",
67
+ "@zhin.js/agent": "1.0.4",
68
+ "@zhin.js/host-http": "1.0.1",
69
+ "zhin.js": "4.1.3"
70
+ },
70
71
  "files": [
72
+ "adapters",
73
+ "plugin.ts",
74
+ "schema.json",
71
75
  "src",
72
76
  "lib",
73
- "client",
74
- "dist",
75
- "skills",
76
- "plugin.yml",
77
+ "agent",
77
78
  "README.md",
78
- "node",
79
79
  "CHANGELOG.md"
80
80
  ],
81
81
  "publishConfig": {
@@ -85,9 +85,23 @@
85
85
  "engines": {
86
86
  "node": "^20.19.0 || >=22.12.0"
87
87
  },
88
+ "zhin": {
89
+ "protocol": 1,
90
+ "type": "plugin",
91
+ "entry": "./plugin.ts",
92
+ "engine": "^1.0.0",
93
+ "runtime": "trusted",
94
+ "features": [
95
+ {
96
+ "package": "@zhin.js/adapter",
97
+ "api": "^1.0.0"
98
+ }
99
+ ],
100
+ "plugins": []
101
+ },
88
102
  "scripts": {
89
- "build": "zhin build",
90
- "clean": "rimraf lib dist",
91
- "build:node": "tsc"
103
+ "build": "tsc",
104
+ "clean": "rimraf lib",
105
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/kook/tests"
92
106
  }
93
107
  }
package/plugin.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { definePlugin } from '@zhin.js/plugin-runtime';
2
+ import { registerKookPlatformPermitChecker } from './src/platform-permit.js';
3
+
4
+ export default definePlugin({
5
+ name: 'kook',
6
+ metadata: {
7
+ displayName: 'KOOK WebSocket Adapter',
8
+ },
9
+ setup() {
10
+ // 平台权限门禁:guild_owner / guild_admin / channel_admin 等(agent 工具 platformPermit)
11
+ return registerKookPlatformPermitChecker();
12
+ },
13
+ });
package/schema.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "name": {
7
+ "type": "string",
8
+ "default": "kook-bot"
9
+ },
10
+ "token": {
11
+ "type": "string"
12
+ },
13
+ "connection": {
14
+ "type": "string",
15
+ "enum": ["websocket", "webhook"],
16
+ "default": "websocket",
17
+ "description": "WebSocket gateway (default). webhook requires httpHostToken, verify_token, and a public HTTPS callback URL."
18
+ },
19
+ "webhookPath": {
20
+ "type": "string",
21
+ "default": "/kook/webhook",
22
+ "description": "POST path registered via httpHostToken when connection is webhook."
23
+ },
24
+ "verify_token": {
25
+ "type": "string",
26
+ "description": "KOOK developer console verify token (required for webhook mode)."
27
+ },
28
+ "encrypt_key": {
29
+ "type": "string",
30
+ "description": "Optional Encrypt Key when message encryption is enabled in KOOK console."
31
+ },
32
+ "data_dir": {
33
+ "type": "string"
34
+ },
35
+ "timeout": {
36
+ "type": "number",
37
+ "default": 10000
38
+ },
39
+ "max_retry": {
40
+ "type": "number",
41
+ "default": 3
42
+ },
43
+ "ignore": {
44
+ "type": "string",
45
+ "enum": ["bot", "self"],
46
+ "default": "bot"
47
+ },
48
+ "logLevel": {
49
+ "type": "string",
50
+ "enum": ["trace", "debug", "info", "warn", "error", "fatal", "mark", "off"],
51
+ "default": "info"
52
+ }
53
+ },
54
+ "required": ["token"]
55
+ }