@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/CHANGELOG.md +39 -0
- package/README.md +11 -4
- package/adapters/kook/index.js +3 -23
- package/adapters/kook/index.ts +3 -27
- package/commands/kook/endpoint/definition.js +0 -2
- package/commands/kook/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +14 -0
- package/lib/adapter.js +44 -0
- package/lib/client.d.ts +7 -8
- package/lib/client.js +1 -2
- package/lib/console-spec.js +10 -0
- package/lib/console.d.ts +1 -7
- package/lib/console.js +2 -5
- package/lib/endpoint.d.ts +10 -24
- package/lib/endpoint.js +160 -159
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2 -2
- package/lib/outbound.d.ts +3 -4
- package/lib/outbound.js +15 -11
- package/lib/protocol.d.ts +4 -0
- package/lib/protocol.js +3 -1
- package/lib/webhook.d.ts +3 -2
- package/lib/webhook.js +24 -21
- package/lib/ws.d.ts +4 -66
- package/lib/ws.js +17 -8
- package/package.json +18 -18
- package/plugin.js +1 -4
- package/src/adapter.ts +35 -0
- package/src/client.ts +4 -11
- package/src/console-spec.ts +10 -0
- package/src/console.ts +2 -6
- package/src/endpoint.ts +162 -163
- package/src/index.ts +4 -4
- package/src/outbound.ts +18 -14
- package/src/protocol.ts +6 -2
- package/src/webhook.ts +25 -22
- package/src/ws.ts +13 -64
- package/lib/kook-runtime-state.d.ts +0 -1
- package/lib/kook-runtime-state.js +0 -6
- package/src/kook-runtime-state.ts +0 -7
package/lib/endpoint.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { EndpointDeliveryError } from '@zhin.js/im-contract';
|
|
2
|
-
|
|
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 {
|
|
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
|
|
38
|
+
export class KookWebsocketEndpoint extends RuntimeKookClient {
|
|
39
|
+
[endpointSpec];
|
|
34
40
|
#sentConversations = new Map();
|
|
35
41
|
#logger;
|
|
36
42
|
#options;
|
|
37
|
-
#
|
|
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:
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
66
|
+
return this.getTransportState?.();
|
|
59
67
|
}
|
|
60
|
-
async
|
|
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.#
|
|
66
|
-
this
|
|
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
|
|
81
|
+
self_id: this.self_id != null ? String(this.self_id) : undefined,
|
|
73
82
|
}));
|
|
74
83
|
}
|
|
75
84
|
catch (error) {
|
|
76
|
-
await this
|
|
85
|
+
await this.#stopFramework();
|
|
77
86
|
this.#logger.error('Failed to connect KOOK websocket:', error);
|
|
78
87
|
throw error;
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
|
-
|
|
90
|
+
#openFramework() {
|
|
82
91
|
this.#open = true;
|
|
83
92
|
}
|
|
84
|
-
|
|
93
|
+
#closeFramework() {
|
|
85
94
|
this.#open = false;
|
|
86
95
|
}
|
|
87
|
-
async
|
|
96
|
+
async #stopFramework() {
|
|
88
97
|
this.#open = false;
|
|
89
98
|
this.#sentConversations.clear();
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
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.#
|
|
181
|
+
if (!this.#started)
|
|
174
182
|
throw new Error('KOOK client not connected');
|
|
175
|
-
return this
|
|
183
|
+
return this;
|
|
176
184
|
}
|
|
177
185
|
}
|
|
178
|
-
export class KookWebhookEndpoint extends
|
|
186
|
+
export class KookWebhookEndpoint extends RuntimeKookClient {
|
|
187
|
+
[endpointSpec];
|
|
179
188
|
#sentConversations = new Map();
|
|
180
189
|
#logger;
|
|
181
190
|
#options;
|
|
182
|
-
#
|
|
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:
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
215
|
+
get endpointConfig() {
|
|
207
216
|
return this.#options.config;
|
|
208
217
|
}
|
|
209
218
|
get selfId() {
|
|
210
|
-
return this
|
|
219
|
+
return this.self_id != null ? String(this.self_id) : undefined;
|
|
211
220
|
}
|
|
212
|
-
async
|
|
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
|
-
|
|
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
|
|
235
|
+
self_id: this.self_id != null ? String(this.self_id) : undefined,
|
|
226
236
|
}));
|
|
227
237
|
}
|
|
228
238
|
catch (error) {
|
|
229
|
-
await this
|
|
239
|
+
await this.#stopFramework();
|
|
230
240
|
this.#logger.error('Failed to connect KOOK webhook:', error);
|
|
231
241
|
throw error;
|
|
232
242
|
}
|
|
233
243
|
}
|
|
234
|
-
|
|
244
|
+
#openFramework() {
|
|
235
245
|
this.#open = true;
|
|
236
246
|
}
|
|
237
|
-
|
|
247
|
+
#closeFramework() {
|
|
238
248
|
this.#open = false;
|
|
239
249
|
}
|
|
240
|
-
async
|
|
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
|
-
|
|
247
|
-
|
|
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
|
|
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
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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.#
|
|
336
|
+
if (!this.#started)
|
|
330
337
|
throw new Error('KOOK client not initialized');
|
|
331
|
-
return this
|
|
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
|
|
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:
|
|
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 {
|
|
5
|
-
export {
|
|
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 {
|
|
5
|
-
export {
|
|
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
|
|
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:
|
|
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
|
|
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;
|