@zhin.js/adapter-kook 1.1.18 → 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/lib/endpoint.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { EndpointDeliveryError } from '@zhin.js/im-contract';
2
- import { Endpoint, } from 'zhin.js/adapter';
2
+ /**
3
+ * KookEndpoint — lifecycle, outbound, admit, OpenAPI helpers for agent tools.
4
+ */
5
+ import { createEndpointSpecification, endpointSpec, createEndpointOperation, } from 'zhin.js/adapter';
3
6
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
4
7
  import { formatInboundContent, isKookBotMentioned, kookInboundConversation, normalizeKookWebhookEvent, normalizeKookButtonEvent, senderDisplayName, } from './protocol.js';
5
8
  import { registerKookWebhookRoutes } from './webhook.js';
6
- import { defaultCreateClient, defaultCreateWebhookClient, normalizeKookMessage, } from './ws.js';
9
+ import { RuntimeKookClient, kookWebsocketClientOptions, kookWebhookClientOptions, normalizeKookMessage, } from './ws.js';
7
10
  import { recallKookMessage, sendKookOutbound } from './outbound.js';
8
11
  import { KookSenderRoleResolver } from './sender-roles.js';
9
12
  import { receiveKookSideEvent } from './side-event-dispatch.js';
@@ -11,6 +14,8 @@ import { receiveKookSideEvent } from './side-event-dispatch.js';
11
14
  function kookInboundPayload(endpointId, conversation, msg, projection, selfId) {
12
15
  return {
13
16
  conversation,
17
+ event: msg.event,
18
+ platformSentAt: msg.platformSentAt,
14
19
  message: { conversation, id: msg.id },
15
20
  content: formatInboundContent(msg),
16
21
  sender: {
@@ -30,77 +35,73 @@ function kookInboundPayload(endpointId, conversation, msg, projection, selfId) {
30
35
  }),
31
36
  };
32
37
  }
33
- export class KookWebsocketEndpoint extends Endpoint {
38
+ export class KookWebsocketEndpoint extends RuntimeKookClient {
39
+ [endpointSpec];
34
40
  #sentConversations = new Map();
35
41
  #logger;
36
42
  #options;
37
- #createClient;
38
- #client = null;
39
- #roles = new KookSenderRoleResolver(() => this.#client);
43
+ #roles = new KookSenderRoleResolver(() => this);
40
44
  #open = false;
45
+ #executionSignal;
41
46
  #started = false;
42
- management = createKookEndpointManagement(() => this.#requireClient());
43
- control = Object.freeze({
44
- recall: (message) => recallKookMessage(this.#requireClient(), message),
47
+ #management = createKookEndpointManagement(() => this.#requireClient());
48
+ #control = Object.freeze({
49
+ recall: request => recallKookMessage(this.#requireClient(), request),
45
50
  });
46
51
  constructor(options) {
47
- super();
52
+ super(kookWebsocketClientOptions(options.config), undefined, options.config.apiProxy);
48
53
  this.#logger = getAdapterLogger('kook', options.config.id);
49
54
  this.#options = options;
50
- this.#createClient = options.createClient ?? defaultCreateClient;
51
- }
52
- get client() {
53
- return this.#requireClient();
55
+ const endpoint = this;
56
+ this[endpointSpec] = createEndpointSpecification(this, {
57
+ start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
58
+ stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
59
+ name: options.config.id, get transportState() { return endpoint.#transportState; },
60
+ management: this.#management, control: this.#control,
61
+ });
54
62
  }
55
- get transportState() {
63
+ get #transportState() {
56
64
  if (!this.#started)
57
65
  return 'stopped';
58
- return this.#client?.getTransportState?.();
66
+ return this.getTransportState?.();
59
67
  }
60
- async start() {
68
+ async #startFramework(signal) {
69
+ this.#executionSignal = signal;
70
+ this.request.interceptors.request.use(request => { request.signal = signal; return request; });
61
71
  if (this.#started)
62
72
  return;
63
73
  this.#started = true;
64
74
  try {
65
- this.#client = this.#createClient(this.#options.config);
66
- this.#bindClient(this.#client);
67
- await this.#client.connect();
75
+ this.#bindClient(this);
76
+ await this.connect();
68
77
  this.#logger.info(formatCompact({
69
78
  op: 'connect',
70
79
  endpoint: this.#options.config.id,
71
80
  mode: 'websocket',
72
- self_id: this.#client.self_id != null ? String(this.#client.self_id) : undefined,
81
+ self_id: this.self_id != null ? String(this.self_id) : undefined,
73
82
  }));
74
83
  }
75
84
  catch (error) {
76
- await this.stop();
85
+ await this.#stopFramework();
77
86
  this.#logger.error('Failed to connect KOOK websocket:', error);
78
87
  throw error;
79
88
  }
80
89
  }
81
- open() {
90
+ #openFramework() {
82
91
  this.#open = true;
83
92
  }
84
- close() {
93
+ #closeFramework() {
85
94
  this.#open = false;
86
95
  }
87
- async stop() {
96
+ async #stopFramework() {
88
97
  this.#open = false;
89
98
  this.#sentConversations.clear();
90
- if (this.#client) {
91
- try {
92
- this.#client.removeAllListeners();
93
- await this.#client.disconnect();
94
- }
95
- catch {
96
- /* ignore */
97
- }
98
- this.#client = null;
99
- }
99
+ this.removeAllListeners();
100
+ await this.disconnect();
100
101
  this.#started = false;
101
102
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
102
103
  }
103
- async send(request) {
104
+ async #sendFramework(request) {
104
105
  const { conversation } = request;
105
106
  const messageId = await sendKookOutbound(this.#requireClient(), request);
106
107
  rememberKookConversation(this.#sentConversations, messageId, conversation);
@@ -114,54 +115,61 @@ export class KookWebsocketEndpoint extends Endpoint {
114
115
  }
115
116
  /** Test / internal: admit a normalized message when open. */
116
117
  admit(msg) {
117
- if (!this.#open)
118
- return;
119
- if (msg.authorBot)
120
- return;
121
- const conversation = kookInboundConversation(String(this.#options.id), msg);
122
- const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
123
- void this.#roles.resolve(msg).then((projection) => this.emit('message.receive', kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId))).catch((err) => {
124
- this.#logger.warn(formatCompact({
125
- op: 'kook_gateway_receive_failed',
126
- target: `${conversation.kind}:${conversation.id}`,
127
- error: err instanceof Error ? err.message : String(err),
128
- }));
129
- });
118
+ void this[endpointSpec].events.run(() => {
119
+ if (msg.authorBot)
120
+ return;
121
+ const conversation = kookInboundConversation(String(this.#options.id), msg);
122
+ const selfId = this.self_id != null ? String(this.self_id) : undefined;
123
+ return this.#roles.resolve(msg).then((projection) => {
124
+ this.#executionSignal?.throwIfAborted();
125
+ return this[endpointSpec].events.emit('message.receive', kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId));
126
+ }).catch((err) => {
127
+ this.#logger.warn(formatCompact({
128
+ op: 'kook_gateway_receive_failed',
129
+ target: `${conversation.kind}:${conversation.id}`,
130
+ error: err instanceof Error ? err.message : String(err),
131
+ }));
132
+ });
133
+ }).catch(() => this.#logger.debug('Inbound callback was not admitted'));
130
134
  }
131
135
  async recallMessage(messageId, conversation) {
132
136
  if (!conversation)
133
137
  throw new EndpointDeliveryError('unsupported_operation', 'KOOK recall requires conversation routing', 'rejected');
134
- await recallKookMessage(this.#requireClient(), { id: messageId, conversation });
138
+ await recallKookMessage(this.#requireClient(), { message: { id: messageId, conversation } });
135
139
  }
136
140
  #bindClient(client) {
137
141
  client.on('message', (raw) => {
138
- this.#emitPlatformEvent('message', raw);
139
- const msg = normalizeKookMessage(raw);
140
- if (msg)
141
- this.admit(msg);
142
+ void this[endpointSpec].events.run(() => {
143
+ this.#emitPlatformEvent('message', raw);
144
+ const msg = normalizeKookMessage(raw);
145
+ if (msg)
146
+ this.admit(msg);
147
+ }).catch(() => this.#logger.debug('Native callback was not admitted'));
142
148
  });
143
149
  const receiver = client.receiver;
144
150
  receiver?.on('event', (raw) => {
145
- if (this.#client !== client || !this.#open)
146
- return;
147
- this.#emitPlatformEvent('event', raw);
148
- if (normalizeKookButtonEvent(raw)) {
149
- void this.admitButton(raw).catch(error => this.#logger.warn(formatCompact({ op: 'kook_button_failed', error: error instanceof Error ? error.message : 'dispatch failed' })));
150
- return;
151
- }
152
- receiveKookSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, raw, this.#logger);
151
+ void this[endpointSpec].events.run(() => {
152
+ if (this !== client || !this.#open)
153
+ return;
154
+ this.#emitPlatformEvent('event', raw);
155
+ if (normalizeKookButtonEvent(raw)) {
156
+ void this.admitButton(raw).catch(error => this.#logger.warn(formatCompact({ op: 'kook_button_failed', error: error instanceof Error ? error.message : 'dispatch failed' })));
157
+ return;
158
+ }
159
+ receiveKookSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, raw, this.#logger);
160
+ }).catch(() => this.#logger.debug('Native callback was not admitted'));
153
161
  });
154
162
  }
155
163
  async admitButton(raw) {
156
- if (!this.#open)
157
- throw new Error('KOOK endpoint is closed');
158
- const event = normalizeKookButtonEvent(raw);
159
- if (!event)
160
- throw new Error('Invalid KOOK button event');
161
- await this.emitAccepted('message.receive', kookButtonIngress(String(this.#options.id), this.#options.config.id, event, this.#sentConversations.get(event.sourceMessageId)));
164
+ return this[endpointSpec].events.run(async () => {
165
+ const event = normalizeKookButtonEvent(raw);
166
+ if (!event)
167
+ throw new Error('Invalid KOOK button event');
168
+ await this[endpointSpec].events.emitAccepted('message.receive', kookButtonIngress(String(this.#options.id), this.#options.config.id, event, this.#sentConversations.get(event.sourceMessageId)));
169
+ });
162
170
  }
163
171
  #emitPlatformEvent(name, event) {
164
- void this.emitPlatform(name, event).catch((error) => {
172
+ void this[endpointSpec].events.platform(name, event).catch((error) => {
165
173
  this.#logger.warn(formatCompact({
166
174
  op: 'kook_platform_event_failed',
167
175
  event: name,
@@ -170,93 +178,87 @@ export class KookWebsocketEndpoint extends Endpoint {
170
178
  });
171
179
  }
172
180
  #requireClient() {
173
- if (!this.#client)
181
+ if (!this.#started)
174
182
  throw new Error('KOOK client not connected');
175
- return this.#client;
183
+ return this;
176
184
  }
177
185
  }
178
- export class KookWebhookEndpoint extends Endpoint {
186
+ export class KookWebhookEndpoint extends RuntimeKookClient {
187
+ [endpointSpec];
179
188
  #sentConversations = new Map();
180
189
  #logger;
181
190
  #options;
182
- #createClient;
183
- #client = null;
184
- #roles = new KookSenderRoleResolver(() => this.#client);
191
+ #roles = new KookSenderRoleResolver(() => this);
185
192
  #routeReleases = [];
186
193
  #processedSn = new Set();
187
194
  #open = false;
195
+ #executionSignal;
188
196
  #started = false;
189
- management = createKookEndpointManagement(() => this.#requireClient());
190
- control = Object.freeze({
191
- recall: (message) => recallKookMessage(this.#requireClient(), message),
197
+ #management = createKookEndpointManagement(() => this.#requireClient());
198
+ #control = Object.freeze({
199
+ recall: request => recallKookMessage(this.#requireClient(), request),
192
200
  });
193
201
  constructor(options) {
194
- super();
202
+ super(kookWebhookClientOptions(options.config), undefined, options.config.apiProxy);
195
203
  this.#logger = getAdapterLogger('kook', options.config.id);
196
204
  this.#options = options;
197
- this.#createClient = options.createClient ?? defaultCreateWebhookClient;
198
- }
199
- get client() {
200
- return this.#requireClient();
205
+ this[endpointSpec] = createEndpointSpecification(this, {
206
+ start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
207
+ stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
208
+ management: this.#management, control: this.#control,
209
+ });
201
210
  }
202
211
  /** Used by webhook handler. */
203
212
  get isOpen() {
204
213
  return this.#open;
205
214
  }
206
- get config() {
215
+ get endpointConfig() {
207
216
  return this.#options.config;
208
217
  }
209
218
  get selfId() {
210
- return this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
219
+ return this.self_id != null ? String(this.self_id) : undefined;
211
220
  }
212
- async start() {
221
+ async #startFramework(signal) {
222
+ this.#executionSignal = signal;
223
+ this.request.interceptors.request.use(request => { request.signal = signal; return request; });
213
224
  if (this.#started)
214
225
  return;
215
226
  this.#started = true;
216
227
  try {
217
- this.#client = this.#createClient(this.#options.config);
218
- await this.#client.init();
228
+ await this.init();
219
229
  this.#routeReleases.push(...registerKookWebhookRoutes(this.#options.http, this));
220
230
  this.#logger.info(formatCompact({
221
231
  op: 'connect',
222
232
  endpoint: this.#options.config.id,
223
233
  mode: 'webhook',
224
234
  path: this.#options.config.webhookPath,
225
- self_id: this.#client.self_id != null ? String(this.#client.self_id) : undefined,
235
+ self_id: this.self_id != null ? String(this.self_id) : undefined,
226
236
  }));
227
237
  }
228
238
  catch (error) {
229
- await this.stop();
239
+ await this.#stopFramework();
230
240
  this.#logger.error('Failed to connect KOOK webhook:', error);
231
241
  throw error;
232
242
  }
233
243
  }
234
- open() {
244
+ #openFramework() {
235
245
  this.#open = true;
236
246
  }
237
- close() {
247
+ #closeFramework() {
238
248
  this.#open = false;
239
249
  }
240
- async stop() {
250
+ async #stopFramework() {
241
251
  this.#open = false;
242
252
  this.#sentConversations.clear();
243
253
  for (const release of this.#routeReleases.splice(0))
244
254
  release();
245
255
  this.#processedSn.clear();
246
- if (this.#client) {
247
- try {
248
- this.#client.removeAllListeners();
249
- await this.#client.disconnect();
250
- }
251
- catch {
252
- /* ignore */
253
- }
254
- this.#client = null;
255
- }
256
+ this.removeAllListeners();
257
+ await this.disconnect();
256
258
  this.#started = false;
257
259
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
258
260
  }
259
- async send(request) {
261
+ async #sendFramework(request) {
260
262
  const { conversation } = request;
261
263
  const messageId = await sendKookOutbound(this.#requireClient(), request);
262
264
  rememberKookConversation(this.#sentConversations, messageId, conversation);
@@ -269,49 +271,54 @@ export class KookWebhookEndpoint extends Endpoint {
269
271
  return messageId;
270
272
  }
271
273
  async admitEvent(event) {
272
- if (normalizeKookButtonEvent(event)) {
273
- if (!this.#open)
274
- throw new Error('KOOK endpoint is closed');
275
- const button = normalizeKookButtonEvent(event);
276
- await this.emitAccepted('message.receive', kookButtonIngress(String(this.#options.id), this.#options.config.id, button, this.#sentConversations.get(button.sourceMessageId)));
277
- return;
278
- }
279
- void this.emitPlatform(String(event.type ?? 'event'), event).catch((error) => {
280
- this.#logger.warn(formatCompact({
281
- op: 'kook_platform_event_failed',
282
- event: String(event.type ?? 'event'),
283
- error: error instanceof Error ? error.message : String(error),
284
- }));
285
- });
286
- if (receiveKookSideEvent((name, payload) => this.emit(name, payload), this.#options.config.id, event, this.#logger))
287
- return;
288
- const message = normalizeKookWebhookEvent(event, {
289
- ignore: this.#options.config.ignore,
290
- selfId: this.selfId,
274
+ return this[endpointSpec].events.run(async () => {
275
+ if (normalizeKookButtonEvent(event)) {
276
+ if (!this.#open)
277
+ throw new Error('KOOK endpoint is closed');
278
+ const button = normalizeKookButtonEvent(event);
279
+ await this[endpointSpec].events.emitAccepted('message.receive', kookButtonIngress(String(this.#options.id), this.#options.config.id, button, this.#sentConversations.get(button.sourceMessageId)));
280
+ return;
281
+ }
282
+ void this[endpointSpec].events.platform(String(event.type ?? 'event'), event).catch((error) => {
283
+ this.#logger.warn(formatCompact({
284
+ op: 'kook_platform_event_failed',
285
+ event: String(event.type ?? 'event'),
286
+ error: error instanceof Error ? error.message : String(error),
287
+ }));
288
+ });
289
+ if (receiveKookSideEvent((name, payload) => this[endpointSpec].events.emit(name, payload), this.#options.config.id, event, this.#logger))
290
+ return;
291
+ const message = normalizeKookWebhookEvent(event, {
292
+ ignore: this.#options.config.ignore,
293
+ selfId: this.selfId,
294
+ });
295
+ if (message)
296
+ this.admit(message);
291
297
  });
292
- if (message)
293
- this.admit(message);
294
298
  }
295
299
  /** Test / internal: admit a message when open. */
296
300
  admit(msg) {
297
- if (!this.#open)
298
- return;
299
- if (msg.authorBot)
300
- return;
301
- const conversation = kookInboundConversation(String(this.#options.id), msg);
302
- const selfId = this.#client?.self_id != null ? String(this.#client.self_id) : undefined;
303
- void this.#roles.resolve(msg).then((projection) => this.emit('message.receive', kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId))).catch((err) => {
304
- this.#logger.warn(formatCompact({
305
- op: 'kook_gateway_receive_failed',
306
- target: `${conversation.kind}:${conversation.id}`,
307
- error: err instanceof Error ? err.message : String(err),
308
- }));
309
- });
301
+ void this[endpointSpec].events.run(() => {
302
+ if (msg.authorBot)
303
+ return;
304
+ const conversation = kookInboundConversation(String(this.#options.id), msg);
305
+ const selfId = this.self_id != null ? String(this.self_id) : undefined;
306
+ return this.#roles.resolve(msg).then((projection) => {
307
+ this.#executionSignal?.throwIfAborted();
308
+ return this[endpointSpec].events.emit('message.receive', kookInboundPayload(this.#options.config.id, conversation, msg, projection, selfId));
309
+ }).catch((err) => {
310
+ this.#logger.warn(formatCompact({
311
+ op: 'kook_gateway_receive_failed',
312
+ target: `${conversation.kind}:${conversation.id}`,
313
+ error: err instanceof Error ? err.message : String(err),
314
+ }));
315
+ });
316
+ }).catch(() => this.#logger.debug('Inbound callback was not admitted'));
310
317
  }
311
318
  async recallMessage(messageId, conversation) {
312
319
  if (!conversation)
313
320
  throw new EndpointDeliveryError('unsupported_operation', 'KOOK recall requires conversation routing', 'rejected');
314
- await recallKookMessage(this.#requireClient(), { id: messageId, conversation });
321
+ await recallKookMessage(this.#requireClient(), { message: { id: messageId, conversation } });
315
322
  }
316
323
  checkAndRememberSn(sn) {
317
324
  if (this.#processedSn.has(sn))
@@ -326,20 +333,11 @@ export class KookWebhookEndpoint extends Endpoint {
326
333
  }
327
334
  forgetSn(sn) { this.#processedSn.delete(sn); }
328
335
  #requireClient() {
329
- if (!this.#client)
336
+ if (!this.#started)
330
337
  throw new Error('KOOK client not initialized');
331
- return this.#client;
338
+ return this;
332
339
  }
333
340
  }
334
- /**
335
- * KOOK guild/channel/user id 是字符串形式的雪花号,超出
336
- * Number.MAX_SAFE_INTEGER,Number() 会丢精度。Console 社交面只把 group_id
337
- * 当 JSON 值透传、并以字符串回传给 listGroupMembers,因此保留原始字符串
338
- * (仅按契约类型声明强转)是全链路最不丢信息的方案。
339
- */
340
- function toGroupId(id) {
341
- return id;
342
- }
343
341
  /** KOOK 文字频道标记:HTTP API type=1(2=语音),kook-client 消息侧用 'GROUP'。 */
344
342
  function rememberKookConversation(cache, messageId, conversation) {
345
343
  cache.set(messageId, conversation);
@@ -350,7 +348,7 @@ function kookButtonIngress(endpointKey, endpointId, event, sentConversation) {
350
348
  const conversation = { endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
351
349
  kind: sentConversation?.kind ?? event.channelKind, id: sentConversation?.id ?? event.channelId,
352
350
  ...(sentConversation?.parent ? { parent: sentConversation.parent } : {}) };
353
- return { conversation, message: { conversation, id: event.id }, content: `[action: ${event.payload}]`,
351
+ return { event: event.event, platformSentAt: event.platformSentAt, conversation, message: { conversation, id: event.id }, content: `[action: ${event.payload}]`,
354
352
  segments: [{ type: 'action', data: { id: event.id, payload: event.payload, sourceMessageId: event.sourceMessageId } }],
355
353
  sender: { id: event.userId }, endpointId,
356
354
  metadata: Object.freeze({ eventType: 'message_btn_click', payload: event.payload, sourceMessageId: event.sourceMessageId, timestamp: event.timestamp }) };
@@ -365,28 +363,30 @@ function isKookTextChannelType(type) {
365
363
  */
366
364
  export function createKookEndpointManagement(requireClient) {
367
365
  return Object.freeze({
368
- async listGroups() {
369
- const guilds = await requireClient().getGuildList();
366
+ async listGroups(request = {}) {
367
+ const operation = createEndpointOperation(request);
368
+ const guilds = await operation.run(() => requireClient().getGuildList());
370
369
  const groups = [];
371
370
  for (const guild of guilds) {
372
371
  if (guild?.id == null)
373
372
  continue;
374
373
  groups.push({
375
- group_id: toGroupId(String(guild.id)),
374
+ group_id: String(guild.id),
376
375
  name: String(guild.name ?? guild.id),
377
376
  });
378
377
  }
379
378
  return groups;
380
379
  },
381
- async listChannels() {
380
+ async listChannels(request = {}) {
381
+ const operation = createEndpointOperation(request);
382
382
  const client = requireClient();
383
383
  const channels = [];
384
- for (const guild of await client.getGuildList()) {
384
+ for (const guild of await operation.run(() => client.getGuildList())) {
385
385
  if (guild?.id == null)
386
386
  continue;
387
387
  const guildId = String(guild.id);
388
388
  const guildName = String(guild.name ?? guildId);
389
- for (const channel of await client.getChannelList(guildId)) {
389
+ for (const channel of await operation.run(() => client.getChannelList(guildId))) {
390
390
  if (channel?.id == null)
391
391
  continue;
392
392
  if (channel.is_category)
@@ -402,9 +402,10 @@ export function createKookEndpointManagement(requireClient) {
402
402
  }
403
403
  return channels;
404
404
  },
405
- async listGroupMembers(groupId) {
405
+ async listGroupMembers({ groupId, signal }) {
406
+ const operation = createEndpointOperation({ signal });
406
407
  // 平台形状(User.Info[])原样返回
407
- return requireClient().getGuildUserList(groupId);
408
+ return operation.run(() => requireClient().getGuildUserList(groupId));
408
409
  },
409
410
  });
410
411
  }
package/lib/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { formatInboundContent, formatOutboundKmarkdown, resolveKookConfig, senderDisplayName, KookPermission, type KookEndpointConfig, type KookInboundMessage, type KookWireSegment, type LogLevel, type ResolvedKookConfig, type ResolvedKookWebhookConfig, type ResolvedKookWebsocketConfig, } from './protocol.js';
2
2
  export { KookWebhookEndpoint, KookWebsocketEndpoint, type KookEndpointOptions, type KookWebhookEndpointOptions, } from './endpoint.js';
3
3
  export { registerKookWebhookRoutes, handleKookWebhookRequest, type KookWebhookHandler, } from './webhook.js';
4
- export { defaultCreateClient, defaultCreateWebhookClient, normalizeKookMessage, type CreateKookClient, type KookClientTransport, } from './ws.js';
5
- export { kookClient, type KookClientEventMap } from './client.js';
4
+ export { kookWebsocketClientOptions, kookWebhookClientOptions, normalizeKookMessage, type KookClientTransport, } from './ws.js';
5
+ export { type KookClientEventMap } from './client.js';
6
+ export { KookAdapter } from './adapter.js';
6
7
  export { checkKookPlatformPermit, kookGroupPermitResolver, normalizeKookSenderForPermit, platformPermit, type KookSenderInfo, } from './platform-permit.js';
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { formatInboundContent, formatOutboundKmarkdown, resolveKookConfig, senderDisplayName, KookPermission, } from './protocol.js';
2
2
  export { KookWebhookEndpoint, KookWebsocketEndpoint, } from './endpoint.js';
3
3
  export { registerKookWebhookRoutes, handleKookWebhookRequest, } from './webhook.js';
4
- export { defaultCreateClient, defaultCreateWebhookClient, normalizeKookMessage, } from './ws.js';
5
- export { kookClient } from './client.js';
4
+ export { kookWebsocketClientOptions, kookWebhookClientOptions, normalizeKookMessage, } from './ws.js';
5
+ export { KookAdapter } from './adapter.js';
6
6
  export { checkKookPlatformPermit, kookGroupPermitResolver, normalizeKookSenderForPermit, platformPermit, } from './platform-permit.js';
package/lib/outbound.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { type MessageRef } from '@zhin.js/im-contract';
2
- import type { EndpointSendRequest } from 'zhin.js/adapter';
1
+ import { type EndpointRecallRequest, type EndpointSendRequest } from 'zhin.js/adapter';
3
2
  import type { KookClientTransport } from './ws.js';
4
3
  /** Native cards preserve mixed text/images; SDK's ordinary segments stop at first media. */
5
- export declare function sendKookOutbound(client: KookClientTransport, { conversation, payload, locale }: EndpointSendRequest): Promise<string>;
4
+ export declare function sendKookOutbound(client: KookClientTransport, { conversation, payload, locale, signal }: EndpointSendRequest): Promise<string>;
6
5
  /** Deletion requires the canonical conversation: KOOK has separate channel/private APIs. */
7
- export declare function recallKookMessage(client: KookClientTransport, message: MessageRef): Promise<void>;
6
+ export declare function recallKookMessage(client: KookClientTransport, { message, signal }: EndpointRecallRequest): Promise<void>;
package/lib/outbound.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { EndpointDeliveryError, isMediaRef } from '@zhin.js/im-contract';
3
+ import { createEndpointOperation } from 'zhin.js/adapter';
3
4
  import { formatOutboundKmarkdown } from './protocol.js';
4
5
  import { outboundMessages as t } from './i18n.js';
5
6
  /** Native cards preserve mixed text/images; SDK's ordinary segments stop at first media. */
6
- export async function sendKookOutbound(client, { conversation, payload, locale }) {
7
+ export async function sendKookOutbound(client, { conversation, payload, locale, signal }) {
8
+ const operation = createEndpointOperation({ signal });
7
9
  try {
8
10
  const items = Array.isArray(payload) ? payload : payload && typeof payload === 'object' ? [payload] : [payload];
9
11
  let quote;
@@ -70,14 +72,14 @@ export async function sendKookOutbound(client, { conversation, payload, locale }
70
72
  if (media.kind === 'base64')
71
73
  bytes = Buffer.from(media.value.replace(/^data:[^,]*,/, ''), 'base64');
72
74
  else if (media.kind === 'path')
73
- bytes = await readFile(media.value);
75
+ bytes = await operation.prepare(() => readFile(media.value));
74
76
  else {
75
- const response = await fetch(media.value, { signal: AbortSignal.timeout(30_000) });
77
+ const response = await operation.prepare(() => fetch(media.value, { signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000) }));
76
78
  if (!response.ok)
77
79
  throw new EndpointDeliveryError('media_download_failed', 'KOOK media download failed', 'rejected');
78
- bytes = Buffer.from(await response.arrayBuffer());
80
+ bytes = Buffer.from(await operation.prepare(() => response.arrayBuffer()));
79
81
  }
80
- url = await client.uploadMedia(bytes);
82
+ url = await operation.prepare(() => client.uploadMedia(bytes));
81
83
  }
82
84
  if (typeof url !== 'string' || !/^https:\/\//.test(url))
83
85
  throw new EndpointDeliveryError('delivery_unconfirmed', 'KOOK upload returned no media URL', 'unknown');
@@ -94,10 +96,11 @@ export async function sendKookOutbound(client, { conversation, payload, locale }
94
96
  }
95
97
  }
96
98
  const body = hasImage || hasCard ? { __isCard: true, type: 'card', theme: 'secondary', size: 'lg', modules } : formatOutboundKmarkdown(payload, locale);
99
+ // SDK 1.0.5 implements __isCard and returns the API's msg_id; its declaration still describes Message.Ret.
97
100
  const result = conversation.kind === 'private'
98
- ? await client.sendPrivateMsg(conversation.id, body, quote)
99
- : await client.sendChannelMsg(conversation.id, body, quote);
100
- const id = result?.msg_id != null ? String(result.msg_id) : '';
101
+ ? await operation.run(() => client.sendPrivateMsg(conversation.id, body, quote))
102
+ : await operation.run(() => client.sendChannelMsg(conversation.id, body, quote));
103
+ const id = result && typeof result === 'object' && 'msg_id' in result && result.msg_id != null ? String(result.msg_id) : '';
101
104
  if (!id.trim())
102
105
  throw new EndpointDeliveryError('delivery_unconfirmed', 'KOOK did not return a real message ID', 'unknown');
103
106
  return id;
@@ -113,20 +116,21 @@ export async function sendKookOutbound(client, { conversation, payload, locale }
113
116
  }
114
117
  }
115
118
  /** Deletion requires the canonical conversation: KOOK has separate channel/private APIs. */
116
- export async function recallKookMessage(client, message) {
119
+ export async function recallKookMessage(client, { message, signal }) {
117
120
  if (!message.id.trim())
118
121
  throw new EndpointDeliveryError('invalid_message_id', 'KOOK recall requires a message ID', 'rejected');
122
+ const operation = createEndpointOperation({ signal });
119
123
  try {
120
124
  let deleted;
121
125
  if (message.conversation.kind === 'private') {
122
126
  if (!client.recallPrivateMsg)
123
127
  throw new EndpointDeliveryError('unsupported_operation', 'KOOK transport cannot delete private messages', 'rejected');
124
- deleted = await client.recallPrivateMsg(message.conversation.id, message.id);
128
+ deleted = await operation.run(() => client.recallPrivateMsg(message.conversation.id, message.id));
125
129
  }
126
130
  else {
127
131
  if (!client.recallChannelMsg)
128
132
  throw new EndpointDeliveryError('unsupported_operation', 'KOOK transport cannot delete channel messages', 'rejected');
129
- deleted = await client.recallChannelMsg(message.conversation.id, message.id);
133
+ deleted = await operation.run(() => client.recallChannelMsg(message.conversation.id, message.id));
130
134
  }
131
135
  if (deleted !== true)
132
136
  throw new EndpointDeliveryError('platform_rejected', 'KOOK did not confirm message deletion', 'rejected');
package/lib/protocol.d.ts CHANGED
@@ -93,6 +93,8 @@ export interface KookWebhookFrame {
93
93
  }
94
94
  export type ResolvedKookConfig = ResolvedKookWebsocketConfig | ResolvedKookWebhookConfig;
95
95
  export interface KookInboundMessage {
96
+ readonly event?: unknown;
97
+ readonly platformSentAt?: number;
96
98
  readonly id: string;
97
99
  readonly content: string;
98
100
  readonly channelKind: 'private' | 'channel';
@@ -146,6 +148,8 @@ export declare function normalizeKookWebhookEvent(event: KookWebhookEventData, o
146
148
  }): KookInboundMessage | null;
147
149
  /** Native Card return-val events; routing and actor must come from the click event. */
148
150
  export declare function normalizeKookButtonEvent(raw: unknown): {
151
+ event: unknown;
152
+ platformSentAt?: number;
149
153
  id: string;
150
154
  sourceMessageId: string;
151
155
  payload: string;