@kookapp/clawdbot-plugin 1.0.0 → 1.0.1

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/channel.ts DELETED
@@ -1,746 +0,0 @@
1
- import type { ClawdbotConfig, ReplyPayload, RuntimeEnv } from "clawdbot/plugin-sdk";
2
- import {
3
- applyAccountNameToChannelSection,
4
- DEFAULT_ACCOUNT_ID,
5
- deleteAccountFromConfigSection,
6
- emptyPluginConfigSchema,
7
- formatPairingApproveHint,
8
- migrateBaseNameToDefaultAccount,
9
- normalizeAccountId,
10
- setAccountEnabledInConfigSection,
11
- type ChannelPlugin,
12
- } from "clawdbot/plugin-sdk";
13
-
14
- import { getKookRuntime } from "./runtime.js";
15
-
16
- // 动态加载 WebSocket
17
- let wsModule: typeof import("ws") | null = null;
18
- async function getWs(): Promise<typeof import("ws").default> {
19
- if (!wsModule) {
20
- wsModule = await import("ws");
21
- }
22
- return wsModule.default;
23
- }
24
-
25
- function normalizeAllowEntry(entry: string): string {
26
- return entry.trim().replace(/^(kook|user):/i, "").toLowerCase();
27
- }
28
-
29
- function getKookChatType(channelType: string): "direct" | "group" | "channel" {
30
- switch (channelType) {
31
- case "PERSON":
32
- return "direct";
33
- case "BROADCAST":
34
- return "group";
35
- default:
36
- return "channel";
37
- }
38
- }
39
-
40
- const meta = {
41
- id: "kook",
42
- label: "Kook",
43
- selectionLabel: "Kook",
44
- detailLabel: "Kook Bot",
45
- docsPath: "/channels/kook",
46
- blurb: "Kook chat platform",
47
- systemImage: "bubble.left.and.bubble.right",
48
- order: 70,
49
- } as const;
50
-
51
- type ResolvedKookAccount = {
52
- accountId: string;
53
- name?: string;
54
- enabled: boolean;
55
- token?: string;
56
- tokenSource?: string;
57
- config: Record<string, unknown>;
58
- };
59
-
60
- function resolveKookAccount(params: { cfg: ClawdbotConfig; accountId?: string }): ResolvedKookAccount {
61
- const { cfg, accountId } = params;
62
- const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
63
- const topLevel = (cfg.channels as Record<string, unknown>)?.kook;
64
- const accountData = (topLevel as Record<string, unknown>)?.accounts;
65
- const account = accountData ? (accountData as Record<string, unknown>)[resolvedAccountId] : null;
66
-
67
- const token = (account as Record<string, string>)?.token ?? (topLevel as Record<string, string>)?.token;
68
- const name = (account as Record<string, string>)?.name ?? (topLevel as Record<string, string>)?.name;
69
- const enabled = (account as Record<string, boolean>)?.enabled ?? (topLevel as Record<string, boolean>)?.enabled ?? true;
70
-
71
- return {
72
- accountId: resolvedAccountId,
73
- name,
74
- enabled,
75
- token,
76
- config: (account as Record<string, unknown>)?.config ?? {},
77
- };
78
- }
79
-
80
- function listKookAccountIds(cfg: ClawdbotConfig): string[] {
81
- const accounts = (cfg.channels as Record<string, unknown>)?.kook?.accounts;
82
- if (!accounts) {
83
- return Object.keys((cfg.channels as Record<string, unknown>)?.kook || {}).length > 0
84
- ? [DEFAULT_ACCOUNT_ID]
85
- : [];
86
- }
87
- return Object.keys(accounts as Record<string, unknown>);
88
- }
89
-
90
- function resolveDefaultKookAccountId(cfg: ClawdbotConfig): string {
91
- const accounts = (cfg.channels as Record<string, unknown>)?.kook?.accounts;
92
- if (accounts && Object.keys(accounts as Record<string, unknown>).length > 0) {
93
- return Object.keys(accounts as Record<string, unknown>)[0];
94
- }
95
- return DEFAULT_ACCOUNT_ID;
96
- }
97
-
98
- async function sendMessageKook(
99
- targetId: string,
100
- content: string,
101
- ): Promise<{ ok: boolean; messageId?: string }> {
102
- const runtime = getKookRuntime();
103
- const config = runtime.config.loadConfig();
104
- const account = resolveKookAccount({ cfg: config });
105
- const token = account.token?.trim();
106
-
107
- if (!token) {
108
- throw new Error("Kook token is required");
109
- }
110
-
111
- try {
112
- const response = await fetch("https://www.kookapp.cn/api/v3/message/create", {
113
- method: "POST",
114
- headers: {
115
- "Content-Type": "application/json",
116
- Authorization: `Bot ${token}`,
117
- },
118
- body: JSON.stringify({
119
- target_id: targetId,
120
- content,
121
- type: 9,
122
- }),
123
- });
124
-
125
- const data = await response.json();
126
-
127
- if (data.code === 0) {
128
- return { ok: true, messageId: data.data.msg_id };
129
- }
130
-
131
- return { ok: false };
132
- } catch (error) {
133
- throw new Error(`Failed to send Kook message: ${error}`);
134
- }
135
- }
136
-
137
- // Kook WebSocket 监控
138
- async function monitorKookWebSocket(opts: {
139
- botToken: string;
140
- accountId: string;
141
- config: ClawdbotConfig;
142
- abortSignal?: AbortSignal;
143
- onStatus: (status: Record<string, unknown>) => void;
144
- log: (...args: unknown[]) => void;
145
- }): Promise<void> {
146
- const { botToken, accountId, abortSignal, onStatus, log } = opts;
147
-
148
- // 状态常量
149
- const STATUS_INIT = 0;
150
- const STATUS_GATEWAY = 10;
151
- const STATUS_WS_CONNECTED = 20;
152
- const STATUS_CONNECTED = 30;
153
- const STATUS_RETRY = 40;
154
-
155
- let currentStatus = STATUS_INIT;
156
- let gatewayUrl = "";
157
- let sessionId = "";
158
- let selfUserId = ""; // 机器人自己的用户ID
159
- let maxSn = 0;
160
- const messageQueue = new Map<number, unknown>();
161
- let ws: InstanceType<typeof import("ws").default> | null = null;
162
- let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
163
- let reconnectAttempts = 0;
164
- const maxReconnectAttempts = 5;
165
-
166
- const updateStatus = (patch: Record<string, unknown>) => {
167
- onStatus(patch);
168
- };
169
-
170
- const setStatus = (status: number) => {
171
- currentStatus = status;
172
- updateStatus({ running: status === STATUS_CONNECTED });
173
- log(`Kook status: ${status}`);
174
- };
175
-
176
- // 获取 Gateway
177
- const getGateway = async (): Promise<string> => {
178
- const response = await fetch("https://www.kookapp.cn/api/v3/gateway/index?compress=0", {
179
- headers: { Authorization: `Bot ${botToken}` },
180
- });
181
- const data = await response.json();
182
- if (data.code !== 0) {
183
- throw new Error(`Failed to get gateway: ${data.message}`);
184
- }
185
- return data.data.url;
186
- };
187
-
188
- // 获取机器人自己的用户 ID
189
- const getSelfUserId = async (): Promise<string> => {
190
- const response = await fetch("https://www.kookapp.cn/api/v3/user/me", {
191
- headers: { Authorization: `Bot ${botToken}` },
192
- });
193
- const data = await response.json();
194
- if (data.code !== 0) {
195
- log(`Failed to get self user ID: ${data.message}`);
196
- return "";
197
- }
198
- return String(data.data.id ?? "");
199
- };
200
-
201
- // 解析消息
202
- const parseMessage = (data: Buffer): { s: number; d?: Record<string, unknown>; sn?: number } | null => {
203
- try {
204
- return JSON.parse(data.toString());
205
- } catch {
206
- return null;
207
- }
208
- };
209
-
210
- // 处理消息
211
- const handleMessage = async (msg: { s: number; d?: Record<string, unknown>; sn?: number }) => {
212
- const { s, d, sn } = msg;
213
-
214
- switch (s) {
215
- case 0: // EVENT - 消息事件
216
- if (sn !== undefined) {
217
- messageQueue.set(sn, msg);
218
- // 按顺序处理消息
219
- while (messageQueue.has(maxSn + 1)) {
220
- maxSn++;
221
- const queuedMsg = messageQueue.get(maxSn);
222
- messageQueue.delete(maxSn);
223
- await processEvent(queuedMsg as { d: Record<string, unknown> });
224
- }
225
- }
226
- break;
227
-
228
- case 1: // HELLO - 握手结果
229
- const code = d?.code as number ?? 40100;
230
- if (code === 0) {
231
- sessionId = (d?.session_id as string) ?? "";
232
- selfUserId = String(d?.user_id ?? "");
233
- log(`Kook connected, session: ${sessionId}, userId: ${selfUserId}`);
234
- setStatus(STATUS_CONNECTED);
235
- startHeartbeat();
236
- } else {
237
- log(`Kook hello failed: ${code}`);
238
- if ([40100, 40101, 40102, 40103].includes(code)) {
239
- setStatus(STATUS_INIT);
240
- }
241
- }
242
- break;
243
-
244
- case 3: // PONG - 心跳响应
245
- log("Kook pong received");
246
- break;
247
-
248
- case 5: // RECONNECT - 服务端要求重连
249
- log("Kook reconnect requested");
250
- handleReconnect();
251
- break;
252
-
253
- case 6: // RESUME ACK - Resume 成功
254
- log("Kook resume successful");
255
- setStatus(STATUS_CONNECTED);
256
- break;
257
- }
258
- };
259
-
260
- // 处理事件消息
261
- const processEvent = async (event: { d: Record<string, unknown> }) => {
262
- const data = event.d;
263
- const channelType = data.channel_type as string;
264
- const type = data.type as number;
265
- const targetId = data.target_id as string;
266
- const authorId = data.author_id as string;
267
- const content = data.content as string;
268
- const msgId = data.msg_id as string;
269
- const extra = data.extra as Record<string, unknown>;
270
-
271
- // 跳过系统消息
272
- if (type === 255) return;
273
- // 跳过自己发的消息
274
- if (authorId === selfUserId) return;
275
- // 只处理文本消息
276
- if (type !== 1 && type !== 9) return;
277
-
278
- const author = extra?.author as Record<string, unknown> | undefined;
279
- const senderName = (author?.username as string) ?? authorId;
280
-
281
- const chatType = getKookChatType(channelType);
282
- const bodyText = content.trim();
283
- if (!bodyText) return;
284
-
285
- const runtime = getKookRuntime();
286
- const cfg = runtime.config.loadConfig();
287
- const account = resolveKookAccount({ cfg });
288
-
289
- // 权限检查
290
- const groupPolicy = (account.config.groupPolicy as string) ?? "allowlist";
291
- if (chatType !== "direct" && groupPolicy === "allowlist") {
292
- // 简化:允许所有消息
293
- }
294
-
295
- const fromLabel = chatType === "direct"
296
- ? `Kook user ${senderName}`
297
- : `Kook user ${senderName} in channel ${targetId}`;
298
-
299
- const route = runtime.channel.routing.resolveAgentRoute({
300
- cfg,
301
- channel: "kook",
302
- accountId,
303
- teamId: undefined,
304
- peer: {
305
- kind: chatType,
306
- id: chatType === "direct" ? authorId : targetId,
307
- },
308
- });
309
-
310
- const sessionKey = route.sessionKey;
311
- const to = chatType === "direct" ? `user:${authorId}` : `channel:${targetId}`;
312
-
313
- // 构建消息上下文
314
- const ctxPayload = runtime.channel.reply.finalizeInboundContext({
315
- Body: `${bodyText}\n[kook message id: ${msgId} channel: ${targetId}]`,
316
- RawBody: bodyText,
317
- CommandBody: bodyText,
318
- From: `kook:${authorId}`,
319
- To: to,
320
- SessionKey: sessionKey,
321
- AccountId: route.accountId,
322
- ChatType: chatType,
323
- ConversationLabel: fromLabel,
324
- SenderName: senderName,
325
- SenderId: authorId,
326
- Provider: "kook" as const,
327
- Surface: "kook" as const,
328
- MessageSid: msgId,
329
- Timestamp: data.msg_timestamp as number,
330
- });
331
-
332
- // 创建回复分发器
333
- const { dispatcher, replyOptions, markDispatchIdle } =
334
- runtime.channel.reply.createReplyDispatcherWithTyping({
335
- responsePrefix: "",
336
- humanDelay: runtime.channel.reply.resolveHumanDelayConfig(cfg, route.agentId),
337
- deliver: async (payload: ReplyPayload) => {
338
- const text = payload.text ?? "";
339
- const chunks = text.match(/.{1,2000}/g) ?? [text];
340
- for (const chunk of chunks) {
341
- if (!chunk) continue;
342
- await sendMessageKook(to.replace(/^(user|channel):/, ""), chunk);
343
- }
344
- },
345
- onError: (err, info) => {
346
- runtime.error?.(`Kook ${info.kind} reply failed: ${err}`);
347
- },
348
- });
349
-
350
- // 派发回复
351
- await runtime.channel.reply.dispatchReplyFromConfig({
352
- ctx: ctxPayload,
353
- cfg,
354
- dispatcher,
355
- replyOptions,
356
- });
357
-
358
- markDispatchIdle();
359
- updateStatus({ lastInboundAt: Date.now() });
360
- };
361
-
362
- // 开始心跳
363
- const startHeartbeat = () => {
364
- if (heartbeatInterval) clearInterval(heartbeatInterval);
365
- heartbeatInterval = setInterval(() => {
366
- if (ws && ws.readyState === 1 && currentStatus === STATUS_CONNECTED) {
367
- ws.send(JSON.stringify({ s: 2, sn: maxSn }));
368
- }
369
- }, 30000);
370
- };
371
-
372
- // 停止心跳
373
- const stopHeartbeat = () => {
374
- if (heartbeatInterval) {
375
- clearInterval(heartbeatInterval);
376
- heartbeatInterval = null;
377
- }
378
- };
379
-
380
- // 处理重连
381
- const handleReconnect = () => {
382
- stopHeartbeat();
383
- if (ws) {
384
- ws.close();
385
- ws = null;
386
- }
387
- messageQueue.clear();
388
- maxSn = 0;
389
- sessionId = "";
390
- gatewayUrl = "";
391
- setStatus(STATUS_INIT);
392
- };
393
-
394
- // 连接 WebSocket
395
- const connect = async (resume = false): Promise<void> => {
396
- const WS = await getWs();
397
-
398
- // gatewayUrl 本身已包含 token,不要重复添加
399
- const url = new URL(gatewayUrl);
400
- url.searchParams.set("compress", "0");
401
- if (resume && sessionId) {
402
- url.searchParams.set("resume", "1");
403
- url.searchParams.set("sn", String(maxSn));
404
- url.searchParams.set("session_id", sessionId);
405
- }
406
-
407
- ws = new WS(url.toString());
408
-
409
- const abortHandler = () => ws?.close();
410
- abortSignal?.addEventListener("abort", abortHandler, { once: true });
411
-
412
- return await new Promise((resolve, reject) => {
413
- const timeout = setTimeout(() => {
414
- ws?.close();
415
- reject(new Error("WebSocket connection timeout"));
416
- }, 10000);
417
-
418
- ws.on("open", () => {
419
- clearTimeout(timeout);
420
- setStatus(STATUS_WS_CONNECTED);
421
- log(`Kook WebSocket ${resume ? "resumed" : "connected"}`);
422
- });
423
-
424
- ws.on("message", async (data) => {
425
- const msg = parseMessage(data as Buffer);
426
- if (msg) {
427
- await handleMessage(msg);
428
- }
429
- });
430
-
431
- ws.on("close", () => {
432
- abortSignal?.removeEventListener("abort", abortHandler);
433
- stopHeartbeat();
434
- resolve();
435
- });
436
-
437
- ws.on("error", (err) => {
438
- clearTimeout(timeout);
439
- reject(err);
440
- });
441
- });
442
- };
443
-
444
- // 主循环
445
- const mainLoop = async () => {
446
- // 先获取机器人自己的用户 ID
447
- selfUserId = await getSelfUserId();
448
- log(`Kook self user ID: ${selfUserId}`);
449
-
450
- while (!abortSignal?.aborted) {
451
- try {
452
- if (currentStatus === STATUS_INIT) {
453
- gatewayUrl = await getGateway();
454
- setStatus(STATUS_GATEWAY);
455
- }
456
-
457
- if (currentStatus === STATUS_GATEWAY) {
458
- try {
459
- await connect(false);
460
- } catch (err) {
461
- log(`Kook connect error: ${err}`);
462
- reconnectAttempts++;
463
- }
464
- }
465
-
466
- if (abortSignal?.aborted) break;
467
-
468
- // 等待断开
469
- await new Promise((resolve) => setTimeout(resolve, 2000));
470
-
471
- // 重连逻辑
472
- if (currentStatus !== STATUS_CONNECTED && reconnectAttempts < maxReconnectAttempts) {
473
- if (sessionId) {
474
- try {
475
- await connect(true);
476
- } catch (err) {
477
- log(`Kook resume error: ${err}`);
478
- }
479
- }
480
- }
481
-
482
- if (reconnectAttempts >= maxReconnectAttempts) {
483
- reconnectAttempts = 0;
484
- sessionId = "";
485
- gatewayUrl = "";
486
- setStatus(STATUS_INIT);
487
- }
488
- } catch (err) {
489
- log(`Kook main loop error: ${err}`);
490
- await new Promise((resolve) => setTimeout(resolve, 5000));
491
- }
492
- }
493
-
494
- // 清理
495
- stopHeartbeat();
496
- if (ws) ws.close();
497
- updateStatus({ running: false, lastStopAt: Date.now() });
498
- };
499
-
500
- // 启动
501
- setStatus(STATUS_INIT);
502
- updateStatus({ lastStartAt: Date.now(), running: true });
503
- log(`Kook provider started for account: ${accountId}`);
504
- mainLoop();
505
- }
506
-
507
- export const kookPlugin: ChannelPlugin<ResolvedKookAccount> = {
508
- id: "kook",
509
- meta,
510
- pairing: {
511
- idLabel: "kookUserId",
512
- normalizeAllowEntry: (entry) => normalizeAllowEntry(entry),
513
- notifyApproval: async ({ id }) => {
514
- console.log(`[kook] User ${id} approved for pairing`);
515
- },
516
- },
517
- capabilities: {
518
- chatTypes: ["direct", "channel"],
519
- media: true,
520
- },
521
- streaming: {
522
- blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
523
- },
524
- reload: { configPrefixes: ["channels.kook"] },
525
- configSchema: emptyPluginConfigSchema(),
526
- config: {
527
- listAccountIds: (cfg) => listKookAccountIds(cfg),
528
- resolveAccount: (cfg, accountId) => resolveKookAccount({ cfg, accountId }),
529
- defaultAccountId: (cfg) => resolveDefaultKookAccountId(cfg),
530
- setAccountEnabled: ({ cfg, accountId, enabled }) =>
531
- setAccountEnabledInConfigSection({
532
- cfg,
533
- sectionKey: "kook",
534
- accountId,
535
- enabled,
536
- allowTopLevel: true,
537
- }),
538
- deleteAccount: ({ cfg, accountId }) =>
539
- deleteAccountFromConfigSection({
540
- cfg,
541
- sectionKey: "kook",
542
- accountId,
543
- clearBaseFields: ["token", "name"],
544
- }),
545
- isConfigured: (account) => Boolean(account.token?.trim()),
546
- describeAccount: (account) => ({
547
- accountId: account.accountId,
548
- name: account.name,
549
- enabled: account.enabled,
550
- configured: Boolean(account.token?.trim()),
551
- tokenSource: account.tokenSource,
552
- }),
553
- resolveAllowFrom: () => [],
554
- formatAllowFrom: () => [],
555
- },
556
- security: {
557
- resolveDmPolicy: () => ({
558
- policy: "pairing" as const,
559
- allowFrom: [] as string[],
560
- policyPath: "channels.kook.dmPolicy",
561
- allowFromPath: "channels.kook.allowFrom",
562
- approveHint: formatPairingApproveHint("kook"),
563
- normalizeEntry: (raw) => normalizeAllowEntry(raw),
564
- }),
565
- collectWarnings: () => [],
566
- },
567
- groups: {
568
- resolveRequireMention: () => false,
569
- },
570
- messaging: {
571
- normalizeTarget: (target) => target.trim(),
572
- targetResolver: {
573
- looksLikeId: () => true,
574
- hint: "<channelId|user:ID>",
575
- },
576
- },
577
- outbound: {
578
- deliveryMode: "direct",
579
- chunker: (text, limit) => {
580
- const chunks = [];
581
- let remaining = text;
582
- while (remaining.length > limit) {
583
- chunks.push(remaining.slice(0, limit));
584
- remaining = remaining.slice(limit);
585
- }
586
- if (remaining) chunks.push(remaining);
587
- return chunks;
588
- },
589
- textChunkLimit: 2000,
590
- resolveTarget: ({ to }) => {
591
- const trimmed = to?.trim();
592
- if (!trimmed) {
593
- return {
594
- ok: false,
595
- error: new Error("Delivering to Kook requires --to <channelId|user:ID>"),
596
- };
597
- }
598
- return { ok: true, to: trimmed };
599
- },
600
- sendText: async ({ to, text }) => {
601
- const result = await sendMessageKook(to, text);
602
- return { channel: "kook", ...result };
603
- },
604
- sendMedia: async ({ to, text, mediaUrl }) => {
605
- const result = await sendMessageKook(to, text || mediaUrl);
606
- return { channel: "kook", ...result };
607
- },
608
- },
609
- status: {
610
- defaultRuntime: {
611
- accountId: DEFAULT_ACCOUNT_ID,
612
- running: false,
613
- connected: false,
614
- lastStartAt: null,
615
- lastStopAt: null,
616
- lastError: null,
617
- lastInboundAt: null,
618
- lastOutboundAt: null,
619
- },
620
- buildChannelSummary: ({ snapshot }) => ({
621
- configured: snapshot.configured ?? false,
622
- tokenSource: snapshot.tokenSource ?? "none",
623
- running: snapshot.running ?? false,
624
- connected: snapshot.connected ?? false,
625
- lastStartAt: snapshot.lastStartAt ?? null,
626
- lastStopAt: snapshot.lastStopAt ?? null,
627
- lastError: snapshot.lastError ?? null,
628
- }),
629
- probeAccount: async ({ account }) => {
630
- const token = account.token?.trim();
631
- if (!token) {
632
- return { ok: false, error: "bot token missing" };
633
- }
634
- try {
635
- const response = await fetch("https://www.kookapp.cn/api/v3/user/me", {
636
- headers: { Authorization: `Bot ${token}` },
637
- });
638
- const data = await response.json();
639
- return { ok: data.code === 0, user: data.data };
640
- } catch (err) {
641
- return { ok: false, error: String(err) };
642
- }
643
- },
644
- buildAccountSnapshot: ({ account, runtime, probe }) => ({
645
- accountId: account.accountId,
646
- name: account.name,
647
- enabled: account.enabled,
648
- configured: Boolean(account.token?.trim()),
649
- tokenSource: account.tokenSource,
650
- running: runtime?.running ?? false,
651
- connected: runtime?.connected ?? false,
652
- lastStartAt: runtime?.lastStartAt ?? null,
653
- lastStopAt: runtime?.lastStopAt ?? null,
654
- lastError: runtime?.lastError ?? null,
655
- probe,
656
- lastInboundAt: runtime?.lastInboundAt ?? null,
657
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
658
- }),
659
- },
660
- setup: {
661
- resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
662
- applyAccountName: ({ cfg, accountId, name }) =>
663
- applyAccountNameToChannelSection({
664
- cfg,
665
- channelKey: "kook",
666
- accountId,
667
- name,
668
- }),
669
- validateInput: () => null,
670
- applyAccountConfig: ({ cfg, accountId, input }) => {
671
- const namedConfig = applyAccountNameToChannelSection({
672
- cfg,
673
- channelKey: "kook",
674
- accountId,
675
- name: input.name,
676
- });
677
- const next =
678
- accountId !== DEFAULT_ACCOUNT_ID
679
- ? migrateBaseNameToDefaultAccount({
680
- cfg: namedConfig,
681
- channelKey: "kook",
682
- })
683
- : namedConfig;
684
-
685
- if (accountId === DEFAULT_ACCOUNT_ID) {
686
- return {
687
- ...next,
688
- channels: {
689
- ...next.channels,
690
- kook: {
691
- ...next.channels?.kook,
692
- enabled: true,
693
- ...(input.useEnv ? {} : input.token ? { token: input.token } : {}),
694
- },
695
- },
696
- };
697
- }
698
-
699
- return {
700
- ...next,
701
- channels: {
702
- ...next.channels,
703
- kook: {
704
- ...next.channels?.kook,
705
- enabled: true,
706
- accounts: {
707
- ...next.channels?.kook?.accounts,
708
- [accountId]: {
709
- ...next.channels?.kook?.accounts?.[accountId],
710
- enabled: true,
711
- ...(input.token ? { token: input.token } : {}),
712
- },
713
- },
714
- },
715
- },
716
- };
717
- },
718
- },
719
- gateway: {
720
- startAccount: async (ctx) => {
721
- const account = ctx.account;
722
- const token = account.token?.trim();
723
-
724
- if (!token) {
725
- throw new Error(`Kook token missing for account "${account.accountId}"`);
726
- }
727
-
728
- ctx.setStatus({
729
- accountId: account.accountId,
730
- running: true,
731
- });
732
-
733
- ctx.log?.info(`[${account.accountId}] Starting Kook provider`);
734
-
735
- // 启动 WebSocket 监控
736
- return monitorKookWebSocket({
737
- botToken: token,
738
- accountId: account.accountId,
739
- config: ctx.cfg,
740
- abortSignal: ctx.abortSignal,
741
- onStatus: (status) => ctx.setStatus({ accountId: account.accountId, ...status }),
742
- log: (...args) => ctx.log?.info(`[kook] ${args.join(" ")}`),
743
- });
744
- },
745
- },
746
- };