@zhin.js/adapter-kook 1.1.17 → 1.1.19

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/src/outbound.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { EndpointDeliveryError, isMediaRef, type MessageRef } from '@zhin.js/im-contract';
3
- import type { EndpointSendRequest } from 'zhin.js/adapter';
2
+ import type { Sendable } from 'kook-client';
3
+ import { EndpointDeliveryError, isMediaRef } from '@zhin.js/im-contract';
4
+ import { createEndpointOperation, type EndpointRecallRequest, type EndpointSendRequest } from 'zhin.js/adapter';
4
5
  import { formatOutboundKmarkdown } from './protocol.js';
5
6
  import type { KookClientTransport } from './ws.js';
6
7
  import { outboundMessages as t } from './i18n.js';
7
8
 
8
9
  /** Native cards preserve mixed text/images; SDK's ordinary segments stop at first media. */
9
- export async function sendKookOutbound(client: KookClientTransport, { conversation, payload, locale }: EndpointSendRequest): Promise<string> {
10
+ export async function sendKookOutbound(client: KookClientTransport, { conversation, payload, locale, signal }: EndpointSendRequest): Promise<string> {
11
+ const operation = createEndpointOperation({ signal });
10
12
  try {
11
13
  const items = Array.isArray(payload) ? payload : payload && typeof payload === 'object' ? [payload] : [payload];
12
14
  let quote: { message_id: string } | undefined;
@@ -52,13 +54,13 @@ export async function sendKookOutbound(client: KookClientTransport, { conversati
52
54
  if (!client.uploadMedia) throw new EndpointDeliveryError('unsupported_media', 'KOOK transport cannot upload images', 'rejected');
53
55
  let bytes: Buffer;
54
56
  if (media.kind === 'base64') bytes = Buffer.from(media.value.replace(/^data:[^,]*,/, ''), 'base64');
55
- else if (media.kind === 'path') bytes = await readFile(media.value);
57
+ else if (media.kind === 'path') bytes = await operation.prepare(() => readFile(media.value));
56
58
  else {
57
- const response = await fetch(media.value, { signal: AbortSignal.timeout(30_000) });
59
+ const response = await operation.prepare(() => fetch(media.value, { signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000) }));
58
60
  if (!response.ok) throw new EndpointDeliveryError('media_download_failed', 'KOOK media download failed', 'rejected');
59
- bytes = Buffer.from(await response.arrayBuffer());
61
+ bytes = Buffer.from(await operation.prepare(() => response.arrayBuffer()));
60
62
  }
61
- url = await client.uploadMedia(bytes);
63
+ url = await operation.prepare(() => client.uploadMedia!(bytes));
62
64
  }
63
65
  if (typeof url !== 'string' || !/^https:\/\//.test(url)) throw new EndpointDeliveryError('delivery_unconfirmed', 'KOOK upload returned no media URL', 'unknown');
64
66
  modules.push({ type: 'container', elements: [{ type: 'image', src: url, alt: String(item.data?.alt ?? 'image') }] });
@@ -72,10 +74,11 @@ export async function sendKookOutbound(client: KookClientTransport, { conversati
72
74
  }
73
75
  }
74
76
  const body = hasImage || hasCard ? { __isCard: true, type: 'card', theme: 'secondary', size: 'lg', modules } : formatOutboundKmarkdown(payload, locale);
75
- const result = conversation.kind === 'private'
76
- ? await client.sendPrivateMsg(conversation.id, body, quote)
77
- : await client.sendChannelMsg(conversation.id, body, quote);
78
- const id = result?.msg_id != null ? String(result.msg_id) : '';
77
+ // SDK 1.0.5 implements __isCard and returns the API's msg_id; its declaration still describes Message.Ret.
78
+ const result: unknown = conversation.kind === 'private'
79
+ ? await operation.run(() => client.sendPrivateMsg(conversation.id, body as unknown as Sendable, quote))
80
+ : await operation.run(() => client.sendChannelMsg(conversation.id, body as unknown as Sendable, quote));
81
+ const id = result && typeof result === 'object' && 'msg_id' in result && result.msg_id != null ? String(result.msg_id) : '';
79
82
  if (!id.trim()) throw new EndpointDeliveryError('delivery_unconfirmed', 'KOOK did not return a real message ID', 'unknown');
80
83
  return id;
81
84
  } catch (error) {
@@ -89,16 +92,17 @@ export async function sendKookOutbound(client: KookClientTransport, { conversati
89
92
 
90
93
 
91
94
  /** Deletion requires the canonical conversation: KOOK has separate channel/private APIs. */
92
- export async function recallKookMessage(client: KookClientTransport, message: MessageRef): Promise<void> {
95
+ export async function recallKookMessage(client: KookClientTransport, { message, signal }: EndpointRecallRequest): Promise<void> {
93
96
  if (!message.id.trim()) throw new EndpointDeliveryError('invalid_message_id', 'KOOK recall requires a message ID', 'rejected');
97
+ const operation = createEndpointOperation({ signal });
94
98
  try {
95
99
  let deleted: boolean;
96
100
  if (message.conversation.kind === 'private') {
97
101
  if (!client.recallPrivateMsg) throw new EndpointDeliveryError('unsupported_operation', 'KOOK transport cannot delete private messages', 'rejected');
98
- deleted = await client.recallPrivateMsg(message.conversation.id, message.id);
102
+ deleted = await operation.run(() => client.recallPrivateMsg!(message.conversation.id, message.id));
99
103
  } else {
100
104
  if (!client.recallChannelMsg) throw new EndpointDeliveryError('unsupported_operation', 'KOOK transport cannot delete channel messages', 'rejected');
101
- deleted = await client.recallChannelMsg(message.conversation.id, message.id);
105
+ deleted = await operation.run(() => client.recallChannelMsg!(message.conversation.id, message.id));
102
106
  }
103
107
  if (deleted !== true) throw new EndpointDeliveryError('platform_rejected', 'KOOK did not confirm message deletion', 'rejected');
104
108
  } catch (error) {
package/src/protocol.ts CHANGED
@@ -114,6 +114,8 @@ export interface KookWebhookFrame {
114
114
  export type ResolvedKookConfig = ResolvedKookWebsocketConfig | ResolvedKookWebhookConfig;
115
115
 
116
116
  export interface KookInboundMessage {
117
+ readonly event?: unknown;
118
+ readonly platformSentAt?: number;
117
119
  readonly id: string;
118
120
  readonly content: string;
119
121
  readonly channelKind: 'private' | 'channel';
@@ -445,6 +447,8 @@ export function normalizeKookWebhookEvent(
445
447
 
446
448
  return {
447
449
  id: String(event.msg_id),
450
+ event,
451
+ ...(typeof event.msg_timestamp === 'number' ? { platformSentAt: event.msg_timestamp } : {}),
448
452
  content: event.content ?? '',
449
453
  channelKind,
450
454
  channelId,
@@ -460,7 +464,7 @@ export function normalizeKookWebhookEvent(
460
464
  }
461
465
 
462
466
  /** Native Card return-val events; routing and actor must come from the click event. */
463
- export function normalizeKookButtonEvent(raw: unknown): { id: string; sourceMessageId: string; payload: string; userId: string; channelKind: 'private' | 'channel'; channelId: string; timestamp: number } | null {
467
+ export function normalizeKookButtonEvent(raw: unknown): { event: unknown; platformSentAt?: number; id: string; sourceMessageId: string; payload: string; userId: string; channelKind: 'private' | 'channel'; channelId: string; timestamp: number } | null {
464
468
  if (!raw || typeof raw !== 'object') return null;
465
469
  const event = raw as KookWebhookEventData;
466
470
  if (event.extra?.type !== 'message_btn_click') return null;
@@ -473,5 +477,5 @@ export function normalizeKookButtonEvent(raw: unknown): { id: string; sourceMess
473
477
  const channelId = channelKind === 'private' ? String(body.user_id) : String(bodyTarget || event.target_id || '');
474
478
  if (!channelId) return null;
475
479
  const timestamp = event.msg_timestamp ?? Date.now();
476
- return { id: String(event.msg_id || `button:${body.msg_id}:${timestamp}`), sourceMessageId: String(body.msg_id), payload: body.value, userId: String(body.user_id), channelKind, channelId, timestamp };
480
+ return { event: raw, ...(typeof event.msg_timestamp === 'number' ? { platformSentAt: event.msg_timestamp } : {}), id: String(event.msg_id || `button:${body.msg_id}:${timestamp}`), sourceMessageId: String(body.msg_id), payload: body.value, userId: String(body.user_id), channelKind, channelId, timestamp };
477
481
  }
package/src/webhook.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { endpointSpec, type Endpoint } from 'zhin.js/adapter';
1
2
  /**
2
3
  * KOOK webhook HTTP: verify → parse → admit.
3
4
  */
@@ -17,8 +18,8 @@ import {
17
18
 
18
19
  const logger = getLogger('kook');
19
20
 
20
- export interface KookWebhookHandler {
21
- readonly config: ResolvedKookWebhookConfig;
21
+ export interface KookWebhookHandler extends Endpoint {
22
+ readonly endpointConfig: ResolvedKookWebhookConfig;
22
23
  readonly isOpen: boolean;
23
24
  readonly selfId?: string;
24
25
  admit(msg: KookInboundMessage): void;
@@ -31,7 +32,7 @@ export function registerKookWebhookRoutes(
31
32
  http: HttpHost,
32
33
  handler: KookWebhookHandler,
33
34
  ): HttpRouteRegistration[] {
34
- const path = handler.config.webhookPath;
35
+ const path = handler.endpointConfig.webhookPath;
35
36
  return [
36
37
  http.route('POST', path, async (request, response) => {
37
38
  await handleKookWebhookRequest(request, response, handler);
@@ -48,7 +49,7 @@ export async function handleKookWebhookRequest(
48
49
  const rawBody = await readRequestBody(request);
49
50
  let frame: KookWebhookFrame;
50
51
  try {
51
- frame = parseKookWebhookBody(rawBody, handler.config.encryptKey);
52
+ frame = parseKookWebhookBody(rawBody, handler.endpointConfig.encryptKey);
52
53
  } catch (error) {
53
54
  logger.warn(formatCompact({
54
55
  op: 'kook_webhook',
@@ -68,7 +69,7 @@ export async function handleKookWebhookRequest(
68
69
  }
69
70
 
70
71
  if (isKookWebhookChallenge(event)) {
71
- if (!verifyKookWebhookToken(handler.config.verifyToken, event.verify_token)) {
72
+ if (!verifyKookWebhookToken(handler.endpointConfig.verifyToken, event.verify_token)) {
72
73
  response.writeHead(403, { 'Content-Type': 'application/json' });
73
74
  response.end(JSON.stringify({ message: 'Invalid verify_token' }));
74
75
  return;
@@ -78,7 +79,7 @@ export async function handleKookWebhookRequest(
78
79
  return;
79
80
  }
80
81
 
81
- if (!verifyKookWebhookToken(handler.config.verifyToken, event.verify_token)) {
82
+ if (!verifyKookWebhookToken(handler.endpointConfig.verifyToken, event.verify_token)) {
82
83
  response.writeHead(403, { 'Content-Type': 'application/json' });
83
84
  response.end(JSON.stringify({ message: 'Invalid verify_token' }));
84
85
  return;
@@ -89,25 +90,27 @@ export async function handleKookWebhookRequest(
89
90
  response.end(JSON.stringify({ message: 'Endpoint unavailable' }));
90
91
  return;
91
92
  }
92
- if (frame.sn != null) {
93
- if (!handler.checkAndRememberSn(frame.sn)) {
94
- response.writeHead(200, { 'Content-Type': 'application/json' });
95
- response.end(JSON.stringify({ message: 'OK' }));
96
- return;
93
+ await handler[endpointSpec].events.run(async () => {
94
+ if (frame.sn != null) {
95
+ if (!handler.checkAndRememberSn(frame.sn)) {
96
+ response.writeHead(200, { 'Content-Type': 'application/json' });
97
+ response.end(JSON.stringify({ message: 'OK' }));
98
+ return;
99
+ }
97
100
  }
98
- }
99
101
 
100
- try {
101
- await handler.admitEvent(event);
102
- } catch {
103
- if (frame.sn != null) handler.forgetSn?.(frame.sn);
104
- response.writeHead(503, { 'Content-Type': 'application/json' });
105
- response.end(JSON.stringify({ message: 'Admission unavailable' }));
106
- return;
107
- }
102
+ try {
103
+ await handler.admitEvent(event);
104
+ } catch {
105
+ if (frame.sn != null) handler.forgetSn?.(frame.sn);
106
+ response.writeHead(503, { 'Content-Type': 'application/json' });
107
+ response.end(JSON.stringify({ message: 'Admission unavailable' }));
108
+ return;
109
+ }
108
110
 
109
- response.writeHead(200, { 'Content-Type': 'application/json' });
110
- response.end(JSON.stringify({ message: 'OK' }));
111
+ response.writeHead(200, { 'Content-Type': 'application/json' });
112
+ response.end(JSON.stringify({ message: 'OK' }));
113
+ });
111
114
  } catch (error) {
112
115
  logger.error('KOOK webhook error:', error);
113
116
  response.writeHead(200, { 'Content-Type': 'application/json' });
package/src/ws.ts CHANGED
@@ -15,62 +15,7 @@ import {
15
15
  type ResolvedKookWebsocketConfig,
16
16
  } from './protocol.js';
17
17
 
18
- /** Minimal client surface used by the endpoint (real kook-client or test mock). */
19
- export interface KookClientTransport {
20
- getTransportState?(): EndpointTransportState | undefined;
21
- connect(): Promise<void>;
22
- disconnect(): Promise<void>;
23
- on(event: string, listener: (...args: unknown[]) => void): void;
24
- removeAllListeners(): void;
25
- sendChannelMsg(
26
- channelId: string,
27
- message: unknown,
28
- quote?: { message_id: string },
29
- ): Promise<{ msg_id?: string | number }>;
30
- sendPrivateMsg(
31
- userId: string,
32
- message: unknown,
33
- quote?: { message_id: string },
34
- ): Promise<{ msg_id?: string | number }>;
35
- uploadMedia?(data: Buffer): Promise<string>;
36
- recallChannelMsg?(channelId: string, messageId: string): Promise<boolean>;
37
- recallPrivateMsg?(userId: string, messageId: string): Promise<boolean>;
38
- /** GET /api/v3/guild/view(群主 id)。 */
39
- getGuildInfo?(guildId: string): Promise<{ user_id?: string | number }>;
40
- pickGuild(guildId: string): {
41
- kick(userId: string): Promise<boolean>;
42
- getRoleList(): Promise<Array<{
43
- role_id: string | number;
44
- name: string;
45
- color?: number;
46
- position?: number;
47
- permissions?: unknown;
48
- }>>;
49
- createRole(name: string): Promise<{ role_id: string | number; name: string }>;
50
- deleteRole(roleId: string): Promise<boolean>;
51
- };
52
- pickGuildMember(guildId: string, userId: string): {
53
- addToBlackList(remark?: string, delMsgDays?: number): Promise<boolean>;
54
- removeFromBlackList(): Promise<boolean>;
55
- grant(roleId: string): Promise<boolean>;
56
- revoke(roleId: string): Promise<boolean>;
57
- setNickname(nickname: string): Promise<boolean>;
58
- };
59
- /** GET /api/v3/guild/list(kook-client 内部分页聚合)。 */
60
- getGuildList(): Promise<Array<{ id: string | number; name?: string }>>;
61
- /** GET /api/v3/channel/list(type: 1=文字 2=语音;is_category 为分组)。 */
62
- getChannelList(guildId: string): Promise<Array<{
63
- id: string | number;
64
- name?: string;
65
- type?: string | number;
66
- is_category?: boolean;
67
- }>>;
68
- /** GET /api/v3/guild/user-list。 */
69
- getGuildUserList(guildId: string, channelId?: string): Promise<unknown[]>;
70
- self_id?: string | number;
71
- }
72
-
73
- export type CreateKookClient = (config: ResolvedKookConfig) => KookClientTransport;
18
+ export type KookClientTransport = RuntimeKookClient;
74
19
 
75
20
  export function normalizeKookMessage(raw: unknown): KookInboundMessage | null {
76
21
  if (!raw || typeof raw !== 'object') return null;
@@ -109,6 +54,8 @@ export function normalizeKookMessage(raw: unknown): KookInboundMessage | null {
109
54
 
110
55
  return {
111
56
  id: String(msg.message_id),
57
+ event: raw,
58
+ ...(typeof msg.timestamp === 'number' ? { platformSentAt: msg.timestamp } : {}),
112
59
  content,
113
60
  channelKind,
114
61
  channelId,
@@ -125,6 +72,7 @@ export function normalizeKookMessage(raw: unknown): KookInboundMessage | null {
125
72
  }
126
73
 
127
74
  export class RuntimeKookClient extends Client {
75
+ readonly #apiAgent?: ReturnType<typeof createKookApiAgent>;
128
76
  readonly #lifecycle: ReturnType<typeof createEndpointLifecycle>;
129
77
  #initialized = false;
130
78
  #connectionAttempt = 0;
@@ -132,7 +80,8 @@ export class RuntimeKookClient extends Client {
132
80
  super({ ...config, autoReconnect: false, handleProcessErrors: false } as ConstructorParameters<typeof Client>[0]);
133
81
  this.#lifecycle = createEndpointLifecycle(lifecycleOptions);
134
82
  if (apiProxy) {
135
- this.request.defaults.httpsAgent = createKookApiAgent(apiProxy);
83
+ this.#apiAgent = createKookApiAgent(apiProxy);
84
+ this.request.defaults.httpsAgent = this.#apiAgent;
136
85
  this.request.defaults.proxy = false;
137
86
  this.request.defaults.maxRedirects = 0;
138
87
  this.request.interceptors.request.use(request => {
@@ -182,7 +131,7 @@ export class RuntimeKookClient extends Client {
182
131
  catch (error) { active = false; receiver.off('disconnected', disconnected); if (attempt === this.#connectionAttempt) await receiver.disconnect(); throw error; }
183
132
  });
184
133
  }
185
- async disconnect(): Promise<void> { await this.#lifecycle.stop(); }
134
+ async disconnect(): Promise<void> { try { await this.#lifecycle.stop(); } finally { this.#apiAgent?.destroy(); } }
186
135
  /** Native FormData avoids kook-client's Buffer-to-text multipart conversion. */
187
136
  async uploadMedia(data: Buffer | string): Promise<string> {
188
137
  let bytes: Buffer;
@@ -241,8 +190,8 @@ export class RuntimeKookClient extends Client {
241
190
  }
242
191
  }
243
192
 
244
- export function defaultCreateClient(config: ResolvedKookWebsocketConfig): KookClientTransport {
245
- return new RuntimeKookClient({
193
+ export function kookWebsocketClientOptions(config: ResolvedKookWebsocketConfig): ConstructorParameters<typeof Client>[0] {
194
+ return {
246
195
  token: config.token,
247
196
  mode: 'websocket',
248
197
  socketFactory: (url: string) => createKookStreamSocket(url, config.streamProxy),
@@ -251,11 +200,11 @@ export function defaultCreateClient(config: ResolvedKookWebsocketConfig): KookCl
251
200
  max_retry: config.max_retry,
252
201
  ignore: config.ignore,
253
202
  logLevel: config.logLevel,
254
- } as ConstructorParameters<typeof RuntimeKookClient>[0], undefined, config.apiProxy) as unknown as KookClientTransport;
203
+ } as ConstructorParameters<typeof Client>[0];
255
204
  }
256
205
 
257
- export function defaultCreateWebhookClient(config: ResolvedKookWebhookConfig): KookClientTransport {
258
- return new RuntimeKookClient({
206
+ export function kookWebhookClientOptions(config: ResolvedKookWebhookConfig): ConstructorParameters<typeof Client>[0] {
207
+ return {
259
208
  token: config.token,
260
209
  mode: 'webhook',
261
210
  data_dir: path.join(process.cwd(), 'data', 'kook'),
@@ -263,5 +212,5 @@ export function defaultCreateWebhookClient(config: ResolvedKookWebhookConfig): K
263
212
  max_retry: 3,
264
213
  ignore: config.ignore,
265
214
  logLevel: config.logLevel,
266
- } as ConstructorParameters<typeof RuntimeKookClient>[0], undefined, config.apiProxy) as unknown as KookClientTransport;
215
+ } as ConstructorParameters<typeof Client>[0];
267
216
  }
@@ -1 +0,0 @@
1
- export declare const kookRuntimeStateToken: import("zhin.js").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
@@ -1,6 +0,0 @@
1
- /**
2
- * KOOK 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `kook endpoint` 命令共享(同一 owner generation)。
4
- */
5
- import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
- export const kookRuntimeStateToken = defineEndpointRuntimeStateToken('kook');
@@ -1,7 +0,0 @@
1
- /**
2
- * KOOK 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `kook endpoint` 命令共享(同一 owner generation)。
4
- */
5
- import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
6
-
7
- export const kookRuntimeStateToken = defineEndpointRuntimeStateToken('kook');