nextclaw 0.2.7 → 0.2.8

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/dist/cli/index.js CHANGED
@@ -1,3321 +1,47 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- APP_NAME,
4
- APP_REPLY_SUBJECT,
5
- APP_TAGLINE,
6
- APP_TITLE,
7
- AgentLoop,
8
- ConfigSchema,
9
- LiteLLMProvider,
10
- PROVIDERS,
11
- SessionManager,
12
- getApiBase,
13
- getConfigPath,
14
- getDataDir,
15
- getDataPath,
16
- getProvider,
17
- getProviderName,
18
- getWorkspacePath,
19
- loadConfig,
20
- saveConfig
21
- } from "../chunk-Z2YDZBBE.js";
22
-
23
- // src/cli/index.ts
24
- import { Command } from "commander";
25
- import {
26
- existsSync as existsSync6,
27
- mkdirSync as mkdirSync5,
28
- readFileSync as readFileSync5,
29
- writeFileSync as writeFileSync4,
30
- cpSync,
31
- rmSync,
32
- openSync,
33
- closeSync
34
- } from "fs";
35
- import { join as join6, resolve } from "path";
36
- import { spawn, spawnSync } from "child_process";
37
- import { createInterface } from "readline";
38
- import { fileURLToPath } from "url";
39
- import { createServer } from "net";
40
- import chokidar from "chokidar";
41
-
42
- // src/config/reload.ts
43
- var isPlainObject = (value) => {
44
- if (!value || typeof value !== "object") {
45
- return false;
46
- }
47
- if (Array.isArray(value)) {
48
- return false;
49
- }
50
- const proto = Object.getPrototypeOf(value);
51
- return proto === Object.prototype || proto === null;
52
- };
53
- var RELOAD_RULES = [
54
- { prefix: "channels", kind: "restart-channels" },
55
- { prefix: "providers", kind: "restart-required" },
56
- { prefix: "agents.defaults.model", kind: "restart-required" },
57
- { prefix: "agents.defaults.maxTokens", kind: "restart-required" },
58
- { prefix: "agents.defaults.temperature", kind: "restart-required" },
59
- { prefix: "agents.defaults.maxToolIterations", kind: "restart-required" },
60
- { prefix: "tools", kind: "restart-required" },
61
- { prefix: "gateway", kind: "none" },
62
- { prefix: "ui", kind: "none" }
63
- ];
64
- var matchRule = (path) => {
65
- for (const rule of RELOAD_RULES) {
66
- if (path === rule.prefix || path.startsWith(`${rule.prefix}.`)) {
67
- return rule;
68
- }
69
- }
70
- return null;
71
- };
72
- function diffConfigPaths(prev, next, prefix = "") {
73
- if (prev === next) {
74
- return [];
75
- }
76
- if (isPlainObject(prev) && isPlainObject(next)) {
77
- const keys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
78
- const paths = [];
79
- for (const key of keys) {
80
- const prevValue = prev[key];
81
- const nextValue = next[key];
82
- if (prevValue === void 0 && nextValue === void 0) {
83
- continue;
84
- }
85
- const childPrefix = prefix ? `${prefix}.${key}` : key;
86
- const childPaths = diffConfigPaths(prevValue, nextValue, childPrefix);
87
- if (childPaths.length > 0) {
88
- paths.push(...childPaths);
89
- }
90
- }
91
- return paths;
92
- }
93
- if (Array.isArray(prev) && Array.isArray(next)) {
94
- if (prev.length === next.length && prev.every((val, idx) => val === next[idx])) {
95
- return [];
96
- }
97
- }
98
- return [prefix || "<root>"];
99
- }
100
- function buildReloadPlan(changedPaths) {
101
- const plan = {
102
- changedPaths,
103
- restartChannels: false,
104
- restartRequired: [],
105
- noopPaths: []
106
- };
107
- for (const path of changedPaths) {
108
- const rule = matchRule(path);
109
- if (!rule) {
110
- plan.restartRequired.push(path);
111
- continue;
112
- }
113
- if (rule.kind === "restart-channels") {
114
- plan.restartChannels = true;
115
- continue;
116
- }
117
- if (rule.kind === "restart-required") {
118
- plan.restartRequired.push(path);
119
- continue;
120
- }
121
- plan.noopPaths.push(path);
122
- }
123
- return plan;
124
- }
125
-
126
- // src/bus/queue.ts
127
- var AsyncQueue = class {
128
- items = [];
129
- waiters = [];
130
- enqueue(item) {
131
- const waiter = this.waiters.shift();
132
- if (waiter) {
133
- waiter(item);
134
- } else {
135
- this.items.push(item);
136
- }
137
- }
138
- async dequeue() {
139
- if (this.items.length > 0) {
140
- return this.items.shift();
141
- }
142
- return new Promise((resolve2) => {
143
- this.waiters.push(resolve2);
144
- });
145
- }
146
- size() {
147
- return this.items.length;
148
- }
149
- };
150
- var MessageBus = class {
151
- inboundQueue = new AsyncQueue();
152
- outboundQueue = new AsyncQueue();
153
- outboundSubscribers = {};
154
- running = false;
155
- async publishInbound(msg) {
156
- this.inboundQueue.enqueue(msg);
157
- }
158
- async consumeInbound() {
159
- return this.inboundQueue.dequeue();
160
- }
161
- async publishOutbound(msg) {
162
- this.outboundQueue.enqueue(msg);
163
- }
164
- async consumeOutbound() {
165
- return this.outboundQueue.dequeue();
166
- }
167
- subscribeOutbound(channel, callback) {
168
- if (!this.outboundSubscribers[channel]) {
169
- this.outboundSubscribers[channel] = [];
170
- }
171
- this.outboundSubscribers[channel].push(callback);
172
- }
173
- async dispatchOutbound() {
174
- this.running = true;
175
- while (this.running) {
176
- const msg = await this.consumeOutbound();
177
- const subscribers = this.outboundSubscribers[msg.channel] ?? [];
178
- for (const callback of subscribers) {
179
- try {
180
- await callback(msg);
181
- } catch (err2) {
182
- console.error(`Error dispatching to ${msg.channel}: ${String(err2)}`);
183
- }
184
- }
185
- }
186
- }
187
- stop() {
188
- this.running = false;
189
- }
190
- get inboundSize() {
191
- return this.inboundQueue.size();
192
- }
193
- get outboundSize() {
194
- return this.outboundQueue.size();
195
- }
196
- };
197
-
198
- // src/channels/telegram.ts
199
- import TelegramBot from "node-telegram-bot-api";
200
-
201
- // src/channels/base.ts
202
- var BaseChannel = class {
203
- constructor(config, bus) {
204
- this.config = config;
205
- this.bus = bus;
206
- }
207
- running = false;
208
- isAllowed(senderId) {
209
- const allowList = this.config.allowFrom ?? [];
210
- if (!allowList.length) {
211
- return true;
212
- }
213
- if (allowList.includes(senderId)) {
214
- return true;
215
- }
216
- if (senderId.includes("|")) {
217
- return senderId.split("|").some((part) => allowList.includes(part));
218
- }
219
- return false;
220
- }
221
- async handleMessage(params) {
222
- if (!this.isAllowed(params.senderId)) {
223
- return;
224
- }
225
- const msg = {
226
- channel: this.name,
227
- senderId: params.senderId,
228
- chatId: params.chatId,
229
- content: params.content,
230
- timestamp: /* @__PURE__ */ new Date(),
231
- media: params.media ?? [],
232
- metadata: params.metadata ?? {}
233
- };
234
- await this.bus.publishInbound(msg);
235
- }
236
- get isRunning() {
237
- return this.running;
238
- }
239
- };
240
-
241
- // src/providers/transcription.ts
242
- import { createReadStream, existsSync } from "fs";
243
- import { basename } from "path";
244
- import { FormData, fetch } from "undici";
245
- var GroqTranscriptionProvider = class {
246
- apiKey;
247
- apiUrl = "https://api.groq.com/openai/v1/audio/transcriptions";
248
- constructor(apiKey) {
249
- this.apiKey = apiKey ?? process.env.GROQ_API_KEY ?? null;
250
- }
251
- async transcribe(filePath) {
252
- if (!this.apiKey) {
253
- return "";
254
- }
255
- if (!existsSync(filePath)) {
256
- return "";
257
- }
258
- const form = new FormData();
259
- form.append("file", createReadStream(filePath), basename(filePath));
260
- form.append("model", "whisper-large-v3");
261
- const response = await fetch(this.apiUrl, {
262
- method: "POST",
263
- headers: {
264
- Authorization: `Bearer ${this.apiKey}`
265
- },
266
- body: form
267
- });
268
- if (!response.ok) {
269
- return "";
270
- }
271
- const data = await response.json();
272
- return data.text ?? "";
273
- }
274
- };
275
-
276
- // src/channels/telegram.ts
277
- import { join } from "path";
278
- import { mkdirSync } from "fs";
279
- var BOT_COMMANDS = [
280
- { command: "start", description: "Start the bot" },
281
- { command: "reset", description: "Reset conversation history" },
282
- { command: "help", description: "Show available commands" }
283
- ];
284
- var TelegramChannel = class extends BaseChannel {
285
- constructor(config, bus, groqApiKey, sessionManager) {
286
- super(config, bus);
287
- this.sessionManager = sessionManager;
288
- this.transcriber = new GroqTranscriptionProvider(groqApiKey ?? null);
289
- }
290
- name = "telegram";
291
- bot = null;
292
- typingTasks = /* @__PURE__ */ new Map();
293
- transcriber;
294
- async start() {
295
- if (!this.config.token) {
296
- throw new Error("Telegram bot token not configured");
297
- }
298
- this.running = true;
299
- const options = { polling: true };
300
- if (this.config.proxy) {
301
- options.request = { proxy: this.config.proxy };
302
- }
303
- this.bot = new TelegramBot(this.config.token, options);
304
- this.bot.onText(/^\/start$/, async (msg) => {
305
- await this.bot?.sendMessage(
306
- msg.chat.id,
307
- `\u{1F44B} Hi ${msg.from?.first_name ?? ""}! I'm ${APP_NAME}.
308
-
309
- Send me a message and I'll respond!
310
- Type /help to see available commands.`
311
- );
312
- });
313
- this.bot.onText(/^\/help$/, async (msg) => {
314
- const helpText = `\u{1F916} <b>${APP_NAME} commands</b>
315
-
316
- /start \u2014 Start the bot
317
- /reset \u2014 Reset conversation history
318
- /help \u2014 Show this help message
319
-
320
- Just send me a text message to chat!`;
321
- await this.bot?.sendMessage(msg.chat.id, helpText, { parse_mode: "HTML" });
322
- });
323
- this.bot.onText(/^\/reset$/, async (msg) => {
324
- const chatId = String(msg.chat.id);
325
- if (!this.sessionManager) {
326
- await this.bot?.sendMessage(msg.chat.id, "\u26A0\uFE0F Session management is not available.");
327
- return;
328
- }
329
- const sessionKey = `${this.name}:${chatId}`;
330
- const session = this.sessionManager.getOrCreate(sessionKey);
331
- const count = session.messages.length;
332
- this.sessionManager.clear(session);
333
- this.sessionManager.save(session);
334
- await this.bot?.sendMessage(msg.chat.id, `\u{1F504} Conversation history cleared (${count} messages).`);
335
- });
336
- this.bot.on("message", async (msg) => {
337
- if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) {
338
- return;
339
- }
340
- if (msg.text?.startsWith("/")) {
341
- return;
342
- }
343
- await this.handleIncoming(msg);
344
- });
345
- await this.bot.setMyCommands(BOT_COMMANDS);
346
- }
347
- async stop() {
348
- this.running = false;
349
- for (const task of this.typingTasks.values()) {
350
- clearInterval(task);
351
- }
352
- this.typingTasks.clear();
353
- if (this.bot) {
354
- await this.bot.stopPolling();
355
- this.bot = null;
356
- }
357
- }
358
- async send(msg) {
359
- if (!this.bot) {
360
- return;
361
- }
362
- this.stopTyping(msg.chatId);
363
- const htmlContent = markdownToTelegramHtml(msg.content ?? "");
364
- try {
365
- await this.bot.sendMessage(Number(msg.chatId), htmlContent, { parse_mode: "HTML" });
366
- } catch {
367
- await this.bot.sendMessage(Number(msg.chatId), msg.content ?? "");
368
- }
369
- }
370
- async handleIncoming(message) {
371
- if (!this.bot || !message.from) {
372
- return;
373
- }
374
- const chatId = String(message.chat.id);
375
- let senderId = String(message.from.id);
376
- if (message.from.username) {
377
- senderId = `${senderId}|${message.from.username}`;
378
- }
379
- const contentParts = [];
380
- const mediaPaths = [];
381
- if (message.text) {
382
- contentParts.push(message.text);
383
- }
384
- if (message.caption) {
385
- contentParts.push(message.caption);
386
- }
387
- const { fileId, mediaType, mimeType } = resolveMedia(message);
388
- if (fileId && mediaType) {
389
- const mediaDir = join(getDataPath(), "media");
390
- mkdirSync(mediaDir, { recursive: true });
391
- const extension = getExtension(mediaType, mimeType);
392
- const downloaded = await this.bot.downloadFile(fileId, mediaDir);
393
- const finalPath = extension && !downloaded.endsWith(extension) ? `${downloaded}${extension}` : downloaded;
394
- mediaPaths.push(finalPath);
395
- if (mediaType === "voice" || mediaType === "audio") {
396
- const transcription = await this.transcriber.transcribe(finalPath);
397
- if (transcription) {
398
- contentParts.push(`[transcription: ${transcription}]`);
399
- } else {
400
- contentParts.push(`[${mediaType}: ${finalPath}]`);
401
- }
402
- } else {
403
- contentParts.push(`[${mediaType}: ${finalPath}]`);
404
- }
405
- }
406
- const content = contentParts.length ? contentParts.join("\n") : "[empty message]";
407
- this.startTyping(chatId);
408
- await this.dispatchToBus(senderId, chatId, content, mediaPaths, {
409
- message_id: message.message_id,
410
- user_id: message.from.id,
411
- username: message.from.username,
412
- first_name: message.from.first_name,
413
- is_group: message.chat.type !== "private"
414
- });
415
- }
416
- async dispatchToBus(senderId, chatId, content, media, metadata) {
417
- await this.handleMessage({ senderId, chatId, content, media, metadata });
418
- }
419
- startTyping(chatId) {
420
- this.stopTyping(chatId);
421
- if (!this.bot) {
422
- return;
423
- }
424
- const task = setInterval(() => {
425
- void this.bot?.sendChatAction(Number(chatId), "typing");
426
- }, 4e3);
427
- this.typingTasks.set(chatId, task);
428
- }
429
- stopTyping(chatId) {
430
- const task = this.typingTasks.get(chatId);
431
- if (task) {
432
- clearInterval(task);
433
- this.typingTasks.delete(chatId);
434
- }
435
- }
436
- };
437
- function resolveMedia(message) {
438
- if (message.photo?.length) {
439
- const photo = message.photo[message.photo.length - 1];
440
- return { fileId: photo.file_id, mediaType: "image", mimeType: "image/jpeg" };
441
- }
442
- if (message.voice) {
443
- return { fileId: message.voice.file_id, mediaType: "voice", mimeType: message.voice.mime_type };
444
- }
445
- if (message.audio) {
446
- return { fileId: message.audio.file_id, mediaType: "audio", mimeType: message.audio.mime_type };
447
- }
448
- if (message.document) {
449
- return { fileId: message.document.file_id, mediaType: "file", mimeType: message.document.mime_type };
450
- }
451
- return {};
452
- }
453
- function getExtension(mediaType, mimeType) {
454
- const map = {
455
- "image/jpeg": ".jpg",
456
- "image/png": ".png",
457
- "image/gif": ".gif",
458
- "audio/ogg": ".ogg",
459
- "audio/mpeg": ".mp3",
460
- "audio/mp4": ".m4a"
461
- };
462
- if (mimeType && map[mimeType]) {
463
- return map[mimeType];
464
- }
465
- const fallback = {
466
- image: ".jpg",
467
- voice: ".ogg",
468
- audio: ".mp3",
469
- file: ""
470
- };
471
- return fallback[mediaType] ?? "";
472
- }
473
- function markdownToTelegramHtml(text) {
474
- if (!text) {
475
- return "";
476
- }
477
- const codeBlocks = [];
478
- text = text.replace(/```[\w]*\n?([\s\S]*?)```/g, (_m, code) => {
479
- codeBlocks.push(code);
480
- return `\0CB${codeBlocks.length - 1}\0`;
481
- });
482
- const inlineCodes = [];
483
- text = text.replace(/`([^`]+)`/g, (_m, code) => {
484
- inlineCodes.push(code);
485
- return `\0IC${inlineCodes.length - 1}\0`;
486
- });
487
- text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
488
- text = text.replace(/^>\s*(.*)$/gm, "$1");
489
- text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
490
- text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
491
- text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
492
- text = text.replace(/__(.+?)__/g, "<b>$1</b>");
493
- text = text.replace(/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g, "<i>$1</i>");
494
- text = text.replace(/~~(.+?)~~/g, "<s>$1</s>");
495
- text = text.replace(/^[-*]\s+/gm, "\u2022 ");
496
- inlineCodes.forEach((code, i) => {
497
- const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
498
- text = text.replace(`\0IC${i}\0`, `<code>${escaped}</code>`);
499
- });
500
- codeBlocks.forEach((code, i) => {
501
- const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
502
- text = text.replace(`\0CB${i}\0`, `<pre><code>${escaped}</code></pre>`);
503
- });
504
- return text;
505
- }
506
-
507
- // src/channels/whatsapp.ts
508
- import WebSocket from "ws";
509
- var WhatsAppChannel = class extends BaseChannel {
510
- name = "whatsapp";
511
- ws = null;
512
- connected = false;
513
- constructor(config, bus) {
514
- super(config, bus);
515
- }
516
- async start() {
517
- this.running = true;
518
- const bridgeUrl = this.config.bridgeUrl;
519
- while (this.running) {
520
- try {
521
- await new Promise((resolve2, reject) => {
522
- const ws = new WebSocket(bridgeUrl);
523
- this.ws = ws;
524
- ws.on("open", () => {
525
- this.connected = true;
526
- });
527
- ws.on("message", (data) => {
528
- const payload = data.toString();
529
- void this.handleBridgeMessage(payload);
530
- });
531
- ws.on("close", () => {
532
- this.connected = false;
533
- this.ws = null;
534
- resolve2();
535
- });
536
- ws.on("error", (_err) => {
537
- this.connected = false;
538
- this.ws = null;
539
- reject(_err);
540
- });
541
- });
542
- } catch {
543
- if (!this.running) {
544
- break;
545
- }
546
- await sleep(5e3);
547
- }
548
- }
549
- }
550
- async stop() {
551
- this.running = false;
552
- this.connected = false;
553
- if (this.ws) {
554
- this.ws.close();
555
- this.ws = null;
556
- }
557
- }
558
- async send(msg) {
559
- if (!this.ws || !this.connected) {
560
- return;
561
- }
562
- const payload = {
563
- type: "send",
564
- to: msg.chatId,
565
- text: msg.content
566
- };
567
- this.ws.send(JSON.stringify(payload));
568
- }
569
- async handleBridgeMessage(raw) {
570
- let data;
571
- try {
572
- data = JSON.parse(raw);
573
- } catch {
574
- return;
575
- }
576
- const msgType = data.type;
577
- if (msgType === "message") {
578
- const pn = data.pn ?? "";
579
- const sender = data.sender ?? "";
580
- let content = data.content ?? "";
581
- const userId = pn || sender;
582
- const senderId = userId.includes("@") ? userId.split("@")[0] : userId;
583
- if (content === "[Voice Message]") {
584
- content = "[Voice Message: Transcription not available for WhatsApp yet]";
585
- }
586
- await this.handleMessage({
587
- senderId,
588
- chatId: sender || userId,
589
- content,
590
- media: [],
591
- metadata: {
592
- message_id: data.id,
593
- timestamp: data.timestamp,
594
- is_group: Boolean(data.isGroup)
595
- }
596
- });
597
- return;
598
- }
599
- if (msgType === "status") {
600
- const status = data.status;
601
- if (status === "connected") {
602
- this.connected = true;
603
- } else if (status === "disconnected") {
604
- this.connected = false;
605
- }
606
- return;
607
- }
608
- if (msgType === "qr") {
609
- return;
610
- }
611
- if (msgType === "error") {
612
- return;
613
- }
614
- }
615
- };
616
- function sleep(ms) {
617
- return new Promise((resolve2) => setTimeout(resolve2, ms));
618
- }
619
-
620
- // src/channels/discord.ts
621
- import {
622
- Client,
623
- GatewayIntentBits,
624
- Partials
625
- } from "discord.js";
626
- import { fetch as fetch2 } from "undici";
627
- import { join as join2 } from "path";
628
- import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
629
- var MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
630
- var DiscordChannel = class extends BaseChannel {
631
- name = "discord";
632
- client = null;
633
- typingTasks = /* @__PURE__ */ new Map();
634
- constructor(config, bus) {
635
- super(config, bus);
636
- }
637
- async start() {
638
- if (!this.config.token) {
639
- throw new Error("Discord token not configured");
640
- }
641
- this.running = true;
642
- this.client = new Client({
643
- intents: this.config.intents ?? GatewayIntentBits.Guilds | GatewayIntentBits.GuildMessages | GatewayIntentBits.DirectMessages,
644
- partials: [Partials.Channel]
645
- });
646
- this.client.on("ready", () => {
647
- console.log("Discord bot connected");
648
- });
649
- this.client.on("messageCreate", async (message) => {
650
- await this.handleIncoming(message);
651
- });
652
- await this.client.login(this.config.token);
653
- }
654
- async stop() {
655
- this.running = false;
656
- for (const task of this.typingTasks.values()) {
657
- clearInterval(task);
658
- }
659
- this.typingTasks.clear();
660
- if (this.client) {
661
- await this.client.destroy();
662
- this.client = null;
663
- }
664
- }
665
- async send(msg) {
666
- if (!this.client) {
667
- return;
668
- }
669
- const channel = await this.client.channels.fetch(msg.chatId);
670
- if (!channel || !channel.isTextBased()) {
671
- return;
672
- }
673
- this.stopTyping(msg.chatId);
674
- const textChannel = channel;
675
- const payload = {
676
- content: msg.content ?? ""
677
- };
678
- if (msg.replyTo) {
679
- payload.reply = { messageReference: msg.replyTo };
680
- }
681
- await textChannel.send(payload);
682
- }
683
- async handleIncoming(message) {
684
- if (message.author.bot) {
685
- return;
686
- }
687
- const senderId = message.author.id;
688
- const channelId = message.channelId;
689
- if (!this.isAllowed(senderId)) {
690
- return;
691
- }
692
- const contentParts = [];
693
- const mediaPaths = [];
694
- if (message.content) {
695
- contentParts.push(message.content);
696
- }
697
- if (message.attachments.size) {
698
- const mediaDir = join2(getDataPath(), "media");
699
- mkdirSync2(mediaDir, { recursive: true });
700
- for (const attachment of message.attachments.values()) {
701
- if (attachment.size && attachment.size > MAX_ATTACHMENT_BYTES) {
702
- contentParts.push(`[attachment: ${attachment.name ?? "file"} - too large]`);
703
- continue;
704
- }
705
- try {
706
- const res = await fetch2(attachment.url);
707
- if (!res.ok) {
708
- contentParts.push(`[attachment: ${attachment.name ?? "file"} - download failed]`);
709
- continue;
710
- }
711
- const buffer = Buffer.from(await res.arrayBuffer());
712
- const filename = `${attachment.id}_${(attachment.name ?? "file").replace(/\//g, "_")}`;
713
- const filePath = join2(mediaDir, filename);
714
- writeFileSync(filePath, buffer);
715
- mediaPaths.push(filePath);
716
- contentParts.push(`[attachment: ${filePath}]`);
717
- } catch {
718
- contentParts.push(`[attachment: ${attachment.name ?? "file"} - download failed]`);
719
- }
720
- }
721
- }
722
- const replyTo = message.reference?.messageId ?? null;
723
- this.startTyping(channelId);
724
- await this.handleMessage({
725
- senderId,
726
- chatId: channelId,
727
- content: contentParts.length ? contentParts.join("\n") : "[empty message]",
728
- media: mediaPaths,
729
- metadata: {
730
- message_id: message.id,
731
- guild_id: message.guildId,
732
- reply_to: replyTo
733
- }
734
- });
735
- }
736
- startTyping(channelId) {
737
- this.stopTyping(channelId);
738
- if (!this.client) {
739
- return;
740
- }
741
- const channel = this.client.channels.cache.get(channelId);
742
- if (!channel || !channel.isTextBased()) {
743
- return;
744
- }
745
- const textChannel = channel;
746
- const task = setInterval(() => {
747
- void textChannel.sendTyping();
748
- }, 8e3);
749
- this.typingTasks.set(channelId, task);
750
- }
751
- stopTyping(channelId) {
752
- const task = this.typingTasks.get(channelId);
753
- if (task) {
754
- clearInterval(task);
755
- this.typingTasks.delete(channelId);
756
- }
757
- }
758
- };
759
-
760
- // src/channels/feishu.ts
761
- import * as Lark from "@larksuiteoapi/node-sdk";
762
- var MSG_TYPE_MAP = {
763
- image: "[image]",
764
- audio: "[audio]",
765
- file: "[file]",
766
- sticker: "[sticker]"
767
- };
768
- var TABLE_RE = /((?:^[ \t]*\|.+\|[ \t]*\n)(?:^[ \t]*\|[-:\s|]+\|[ \t]*\n)(?:^[ \t]*\|.+\|[ \t]*\n?)+)/gm;
769
- var FeishuChannel = class extends BaseChannel {
770
- name = "feishu";
771
- client = null;
772
- wsClient = null;
773
- processedMessageIds = [];
774
- processedSet = /* @__PURE__ */ new Set();
775
- constructor(config, bus) {
776
- super(config, bus);
777
- }
778
- async start() {
779
- if (!this.config.appId || !this.config.appSecret) {
780
- throw new Error("Feishu appId/appSecret not configured");
781
- }
782
- this.running = true;
783
- this.client = new Lark.Client({ appId: this.config.appId, appSecret: this.config.appSecret });
784
- const dispatcher = new Lark.EventDispatcher({
785
- encryptKey: this.config.encryptKey || void 0,
786
- verificationToken: this.config.verificationToken || void 0
787
- }).register({
788
- "im.message.receive_v1": async (data) => {
789
- await this.handleIncoming(data);
790
- }
791
- });
792
- this.wsClient = new Lark.WSClient({
793
- appId: this.config.appId,
794
- appSecret: this.config.appSecret,
795
- loggerLevel: Lark.LoggerLevel.info
796
- });
797
- this.wsClient.start({ eventDispatcher: dispatcher });
798
- }
799
- async stop() {
800
- this.running = false;
801
- if (this.wsClient) {
802
- this.wsClient.close();
803
- this.wsClient = null;
804
- }
805
- }
806
- async send(msg) {
807
- if (!this.client) {
808
- return;
809
- }
810
- const receiveIdType = msg.chatId.startsWith("oc_") ? "chat_id" : "open_id";
811
- const elements = buildCardElements(msg.content ?? "");
812
- const card = {
813
- config: { wide_screen_mode: true },
814
- elements
815
- };
816
- const content = JSON.stringify(card);
817
- await this.client.im.message.create({
818
- params: { receive_id_type: receiveIdType },
819
- data: {
820
- receive_id: msg.chatId,
821
- msg_type: "interactive",
822
- content
823
- }
824
- });
825
- }
826
- async handleIncoming(data) {
827
- const message = data.message ?? {};
828
- const sender = message.sender ?? data.sender ?? {};
829
- const senderIdObj = sender.sender_id ?? {};
830
- const senderId = senderIdObj.open_id || senderIdObj.user_id || senderIdObj.union_id || sender.open_id || sender.user_id || "";
831
- const senderType = sender.sender_type ?? sender.senderType;
832
- if (senderType === "bot") {
833
- return;
834
- }
835
- const chatId = message.chat_id ?? "";
836
- const chatType = message.chat_type ?? "";
837
- const msgType = message.msg_type ?? message.message_type ?? "";
838
- const messageId = message.message_id ?? "";
839
- if (!senderId || !chatId) {
840
- return;
841
- }
842
- if (!this.isAllowed(String(senderId))) {
843
- return;
844
- }
845
- if (messageId && this.isDuplicate(messageId)) {
846
- return;
847
- }
848
- if (messageId) {
849
- await this.addReaction(messageId, "THUMBSUP");
850
- }
851
- let content = "";
852
- if (message.content) {
853
- try {
854
- const parsed = JSON.parse(String(message.content));
855
- content = String(parsed.text ?? parsed.content ?? "");
856
- } catch {
857
- content = String(message.content);
858
- }
859
- }
860
- if (!content && MSG_TYPE_MAP[msgType]) {
861
- content = MSG_TYPE_MAP[msgType];
862
- }
863
- if (!content) {
864
- return;
865
- }
866
- const replyTo = chatType === "group" ? chatId : String(senderId);
867
- await this.handleMessage({
868
- senderId: String(senderId),
869
- chatId: replyTo,
870
- content,
871
- media: [],
872
- metadata: {
873
- message_id: messageId,
874
- chat_type: chatType,
875
- msg_type: msgType
876
- }
877
- });
878
- }
879
- isDuplicate(messageId) {
880
- if (this.processedSet.has(messageId)) {
881
- return true;
882
- }
883
- this.processedSet.add(messageId);
884
- this.processedMessageIds.push(messageId);
885
- if (this.processedMessageIds.length > 1e3) {
886
- const removed = this.processedMessageIds.splice(0, 500);
887
- for (const id of removed) {
888
- this.processedSet.delete(id);
889
- }
890
- }
891
- return false;
892
- }
893
- async addReaction(messageId, emojiType) {
894
- if (!this.client) {
895
- return;
896
- }
897
- try {
898
- await this.client.im.messageReaction.create({
899
- path: { message_id: messageId },
900
- data: { reaction_type: { emoji_type: emojiType } }
901
- });
902
- } catch {
903
- }
904
- }
905
- };
906
- function buildCardElements(content) {
907
- const elements = [];
908
- let lastEnd = 0;
909
- for (const match of content.matchAll(TABLE_RE)) {
910
- const start = match.index ?? 0;
911
- const tableText = match[1] ?? "";
912
- const before = content.slice(lastEnd, start).trim();
913
- if (before) {
914
- elements.push({ tag: "markdown", content: before });
915
- }
916
- elements.push(parseMdTable(tableText) ?? { tag: "markdown", content: tableText });
917
- lastEnd = start + tableText.length;
918
- }
919
- const remaining = content.slice(lastEnd).trim();
920
- if (remaining) {
921
- elements.push({ tag: "markdown", content: remaining });
922
- }
923
- if (!elements.length) {
924
- elements.push({ tag: "markdown", content });
925
- }
926
- return elements;
927
- }
928
- function parseMdTable(tableText) {
929
- const lines = tableText.trim().split("\n").map((line) => line.trim()).filter(Boolean);
930
- if (lines.length < 3) {
931
- return null;
932
- }
933
- const split = (line) => line.replace(/^\|+|\|+$/g, "").split("|").map((item) => item.trim());
934
- const headers = split(lines[0]);
935
- const rows = lines.slice(2).map(split);
936
- const columns = headers.map((header, index) => ({
937
- tag: "column",
938
- name: `c${index}`,
939
- display_name: header,
940
- width: "auto"
941
- }));
942
- const tableRows = rows.map((row) => {
943
- const values = {};
944
- headers.forEach((_, index) => {
945
- values[`c${index}`] = row[index] ?? "";
946
- });
947
- return values;
948
- });
949
- return {
950
- tag: "table",
951
- page_size: rows.length + 1,
952
- columns,
953
- rows: tableRows
954
- };
955
- }
956
-
957
- // src/channels/mochat.ts
958
- import { io } from "socket.io-client";
959
- import { fetch as fetch3 } from "undici";
960
- import { join as join3 } from "path";
961
- import { mkdirSync as mkdirSync3, existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
962
- var MAX_SEEN_MESSAGE_IDS = 2e3;
963
- var CURSOR_SAVE_DEBOUNCE_MS = 500;
964
- var AsyncLock = class {
965
- queue = Promise.resolve();
966
- async run(task) {
967
- const run = this.queue.then(task, task);
968
- this.queue = run.then(
969
- () => void 0,
970
- () => void 0
971
- );
972
- return run;
973
- }
974
- };
975
- var MochatChannel = class extends BaseChannel {
976
- name = "mochat";
977
- socket = null;
978
- wsConnected = false;
979
- wsReady = false;
980
- stateDir = join3(getDataPath(), "mochat");
981
- cursorPath = join3(this.stateDir, "session_cursors.json");
982
- sessionCursor = {};
983
- cursorSaveTimer = null;
984
- sessionSet = /* @__PURE__ */ new Set();
985
- panelSet = /* @__PURE__ */ new Set();
986
- autoDiscoverSessions = false;
987
- autoDiscoverPanels = false;
988
- coldSessions = /* @__PURE__ */ new Set();
989
- sessionByConverse = /* @__PURE__ */ new Map();
990
- seenSet = /* @__PURE__ */ new Map();
991
- seenQueue = /* @__PURE__ */ new Map();
992
- delayStates = /* @__PURE__ */ new Map();
993
- fallbackMode = false;
994
- sessionFallbackTasks = /* @__PURE__ */ new Map();
995
- panelFallbackTasks = /* @__PURE__ */ new Map();
996
- refreshTimer = null;
997
- targetLocks = /* @__PURE__ */ new Map();
998
- refreshInFlight = false;
999
- constructor(config, bus) {
1000
- super(config, bus);
1001
- }
1002
- async start() {
1003
- this.running = true;
1004
- if (!this.config.clawToken) {
1005
- throw new Error("Mochat clawToken not configured");
1006
- }
1007
- mkdirSync3(this.stateDir, { recursive: true });
1008
- await this.loadSessionCursors();
1009
- this.seedTargetsFromConfig();
1010
- await this.refreshTargets(false);
1011
- const socketReady = await this.startSocketClient();
1012
- if (!socketReady) {
1013
- await this.ensureFallbackWorkers();
1014
- }
1015
- const intervalMs = Math.max(1e3, this.config.refreshIntervalMs);
1016
- this.refreshTimer = setInterval(() => {
1017
- void this.refreshLoopTick();
1018
- }, intervalMs);
1019
- }
1020
- async stop() {
1021
- this.running = false;
1022
- if (this.refreshTimer) {
1023
- clearInterval(this.refreshTimer);
1024
- this.refreshTimer = null;
1025
- }
1026
- await this.stopFallbackWorkers();
1027
- await this.cancelDelayTimers();
1028
- if (this.socket) {
1029
- this.socket.disconnect();
1030
- this.socket = null;
1031
- }
1032
- if (this.cursorSaveTimer) {
1033
- clearTimeout(this.cursorSaveTimer);
1034
- this.cursorSaveTimer = null;
1035
- }
1036
- await this.saveSessionCursors();
1037
- this.wsConnected = false;
1038
- this.wsReady = false;
1039
- }
1040
- async send(msg) {
1041
- if (!this.config.clawToken) {
1042
- return;
1043
- }
1044
- const parts = [];
1045
- if (msg.content && msg.content.trim()) {
1046
- parts.push(msg.content.trim());
1047
- }
1048
- if (msg.media?.length) {
1049
- for (const item of msg.media) {
1050
- if (typeof item === "string" && item.trim()) {
1051
- parts.push(item.trim());
1052
- }
1053
- }
1054
- }
1055
- const content = parts.join("\n").trim();
1056
- if (!content) {
1057
- return;
1058
- }
1059
- const target = resolveMochatTarget(msg.chatId);
1060
- if (!target.id) {
1061
- return;
1062
- }
1063
- const isPanel = (target.isPanel || this.panelSet.has(target.id)) && !target.id.startsWith("session_");
1064
- if (isPanel) {
1065
- await this.apiSend(
1066
- "/api/claw/groups/panels/send",
1067
- "panelId",
1068
- target.id,
1069
- content,
1070
- msg.replyTo,
1071
- readGroupId(msg.metadata ?? {})
1072
- );
1073
- return;
1074
- }
1075
- await this.apiSend("/api/claw/sessions/send", "sessionId", target.id, content, msg.replyTo);
1076
- }
1077
- seedTargetsFromConfig() {
1078
- const [sessions, autoSessions] = normalizeIdList(this.config.sessions);
1079
- const [panels, autoPanels] = normalizeIdList(this.config.panels);
1080
- this.autoDiscoverSessions = autoSessions;
1081
- this.autoDiscoverPanels = autoPanels;
1082
- sessions.forEach((sid) => {
1083
- this.sessionSet.add(sid);
1084
- if (!(sid in this.sessionCursor)) {
1085
- this.coldSessions.add(sid);
1086
- }
1087
- });
1088
- panels.forEach((pid) => {
1089
- this.panelSet.add(pid);
1090
- });
1091
- }
1092
- async startSocketClient() {
1093
- let parser = void 0;
1094
- if (!this.config.socketDisableMsgpack) {
1095
- try {
1096
- const mod = await import("socket.io-msgpack-parser");
1097
- parser = mod.default ?? mod;
1098
- } catch {
1099
- parser = void 0;
1100
- }
1101
- }
1102
- const socketUrl = (this.config.socketUrl || this.config.baseUrl).trim().replace(/\/$/, "");
1103
- const socketPath = (this.config.socketPath || "/socket.io").trim();
1104
- const reconnectionDelay = Math.max(100, this.config.socketReconnectDelayMs);
1105
- const reconnectionDelayMax = Math.max(100, this.config.socketMaxReconnectDelayMs);
1106
- const timeout = Math.max(1e3, this.config.socketConnectTimeoutMs);
1107
- const reconnectionAttempts = this.config.maxRetryAttempts > 0 ? this.config.maxRetryAttempts : Number.MAX_SAFE_INTEGER;
1108
- const socket = io(socketUrl, {
1109
- path: socketPath.startsWith("/") ? socketPath : `/${socketPath}`,
1110
- transports: ["websocket"],
1111
- auth: { token: this.config.clawToken },
1112
- reconnection: true,
1113
- reconnectionAttempts,
1114
- reconnectionDelay,
1115
- reconnectionDelayMax,
1116
- timeout,
1117
- parser
1118
- });
1119
- socket.on("connect", async () => {
1120
- this.wsConnected = true;
1121
- this.wsReady = false;
1122
- const subscribed = await this.subscribeAll();
1123
- this.wsReady = subscribed;
1124
- if (subscribed) {
1125
- await this.stopFallbackWorkers();
1126
- } else {
1127
- await this.ensureFallbackWorkers();
1128
- }
1129
- });
1130
- socket.on("disconnect", async () => {
1131
- if (!this.running) {
1132
- return;
1133
- }
1134
- this.wsConnected = false;
1135
- this.wsReady = false;
1136
- await this.ensureFallbackWorkers();
1137
- });
1138
- socket.on("connect_error", () => {
1139
- this.wsConnected = false;
1140
- this.wsReady = false;
1141
- });
1142
- socket.on("claw.session.events", async (payload) => {
1143
- await this.handleWatchPayload(payload, "session");
1144
- });
1145
- socket.on("claw.panel.events", async (payload) => {
1146
- await this.handleWatchPayload(payload, "panel");
1147
- });
1148
- const notifyHandler = (eventName) => async (payload) => {
1149
- if (eventName === "notify:chat.inbox.append") {
1150
- await this.handleNotifyInboxAppend(payload);
1151
- return;
1152
- }
1153
- if (eventName.startsWith("notify:chat.message.")) {
1154
- await this.handleNotifyChatMessage(payload);
1155
- }
1156
- };
1157
- [
1158
- "notify:chat.inbox.append",
1159
- "notify:chat.message.add",
1160
- "notify:chat.message.update",
1161
- "notify:chat.message.recall",
1162
- "notify:chat.message.delete"
1163
- ].forEach((eventName) => {
1164
- socket.on(eventName, notifyHandler(eventName));
1165
- });
1166
- this.socket = socket;
1167
- return new Promise((resolve2) => {
1168
- const timer = setTimeout(() => resolve2(false), timeout);
1169
- socket.once("connect", () => {
1170
- clearTimeout(timer);
1171
- resolve2(true);
1172
- });
1173
- socket.once("connect_error", () => {
1174
- clearTimeout(timer);
1175
- resolve2(false);
1176
- });
1177
- });
1178
- }
1179
- async subscribeAll() {
1180
- const sessions = Array.from(this.sessionSet).sort();
1181
- const panels = Array.from(this.panelSet).sort();
1182
- let ok2 = await this.subscribeSessions(sessions);
1183
- ok2 = await this.subscribePanels(panels) && ok2;
1184
- if (this.autoDiscoverSessions || this.autoDiscoverPanels) {
1185
- await this.refreshTargets(true);
1186
- }
1187
- return ok2;
1188
- }
1189
- async subscribeSessions(sessionIds) {
1190
- if (!sessionIds.length) {
1191
- return true;
1192
- }
1193
- for (const sid of sessionIds) {
1194
- if (!(sid in this.sessionCursor)) {
1195
- this.coldSessions.add(sid);
1196
- }
1197
- }
1198
- const ack = await this.socketCall("com.claw.im.subscribeSessions", {
1199
- sessionIds,
1200
- cursors: this.sessionCursor,
1201
- limit: this.config.watchLimit
1202
- });
1203
- if (!ack.result) {
1204
- return false;
1205
- }
1206
- const data = ack.data;
1207
- let items = [];
1208
- if (Array.isArray(data)) {
1209
- items = data.filter((item) => typeof item === "object" && item !== null);
1210
- } else if (data && typeof data === "object") {
1211
- const sessions = data.sessions;
1212
- if (Array.isArray(sessions)) {
1213
- items = sessions.filter((item) => typeof item === "object" && item !== null);
1214
- } else if (data.sessionId) {
1215
- items = [data];
1216
- }
1217
- }
1218
- for (const payload of items) {
1219
- await this.handleWatchPayload(payload, "session");
1220
- }
1221
- return true;
1222
- }
1223
- async subscribePanels(panelIds) {
1224
- if (!this.autoDiscoverPanels && !panelIds.length) {
1225
- return true;
1226
- }
1227
- const ack = await this.socketCall("com.claw.im.subscribePanels", { panelIds });
1228
- if (!ack.result) {
1229
- return false;
1230
- }
1231
- return true;
1232
- }
1233
- async socketCall(eventName, payload) {
1234
- if (!this.socket) {
1235
- return { result: false, message: "socket not connected" };
1236
- }
1237
- return new Promise((resolve2) => {
1238
- this.socket?.timeout(1e4).emit(eventName, payload, (err2, response) => {
1239
- if (err2) {
1240
- resolve2({ result: false, message: String(err2) });
1241
- return;
1242
- }
1243
- if (response && typeof response === "object") {
1244
- resolve2(response);
1245
- return;
1246
- }
1247
- resolve2({ result: true, data: response });
1248
- });
1249
- });
1250
- }
1251
- async refreshLoopTick() {
1252
- if (!this.running || this.refreshInFlight) {
1253
- return;
1254
- }
1255
- this.refreshInFlight = true;
1256
- try {
1257
- await this.refreshTargets(this.wsReady);
1258
- if (this.fallbackMode) {
1259
- await this.ensureFallbackWorkers();
1260
- }
1261
- } finally {
1262
- this.refreshInFlight = false;
1263
- }
1264
- }
1265
- async refreshTargets(subscribeNew) {
1266
- if (this.autoDiscoverSessions) {
1267
- await this.refreshSessionsDirectory(subscribeNew);
1268
- }
1269
- if (this.autoDiscoverPanels) {
1270
- await this.refreshPanels(subscribeNew);
1271
- }
1272
- }
1273
- async refreshSessionsDirectory(subscribeNew) {
1274
- let response;
1275
- try {
1276
- response = await this.postJson("/api/claw/sessions/list", {});
1277
- } catch {
1278
- return;
1279
- }
1280
- const sessions = response.sessions;
1281
- if (!Array.isArray(sessions)) {
1282
- return;
1283
- }
1284
- const newIds = [];
1285
- for (const session of sessions) {
1286
- const sid = strField(session, "sessionId");
1287
- if (!sid) {
1288
- continue;
1289
- }
1290
- if (!this.sessionSet.has(sid)) {
1291
- this.sessionSet.add(sid);
1292
- newIds.push(sid);
1293
- if (!(sid in this.sessionCursor)) {
1294
- this.coldSessions.add(sid);
1295
- }
1296
- }
1297
- const converseId = strField(session, "converseId");
1298
- if (converseId) {
1299
- this.sessionByConverse.set(converseId, sid);
1300
- }
1301
- }
1302
- if (!newIds.length) {
1303
- return;
1304
- }
1305
- if (this.wsReady && subscribeNew) {
1306
- await this.subscribeSessions(newIds);
1307
- }
1308
- if (this.fallbackMode) {
1309
- await this.ensureFallbackWorkers();
1310
- }
1311
- }
1312
- async refreshPanels(subscribeNew) {
1313
- let response;
1314
- try {
1315
- response = await this.postJson("/api/claw/groups/get", {});
1316
- } catch {
1317
- return;
1318
- }
1319
- const panels = response.panels;
1320
- if (!Array.isArray(panels)) {
1321
- return;
1322
- }
1323
- const newIds = [];
1324
- for (const panel of panels) {
1325
- const panelType = panel.type;
1326
- if (typeof panelType === "number" && panelType !== 0) {
1327
- continue;
1328
- }
1329
- const pid = strField(panel, "id", "_id");
1330
- if (pid && !this.panelSet.has(pid)) {
1331
- this.panelSet.add(pid);
1332
- newIds.push(pid);
1333
- }
1334
- }
1335
- if (!newIds.length) {
1336
- return;
1337
- }
1338
- if (this.wsReady && subscribeNew) {
1339
- await this.subscribePanels(newIds);
1340
- }
1341
- if (this.fallbackMode) {
1342
- await this.ensureFallbackWorkers();
1343
- }
1344
- }
1345
- async ensureFallbackWorkers() {
1346
- if (!this.running) {
1347
- return;
1348
- }
1349
- this.fallbackMode = true;
1350
- for (const sid of this.sessionSet) {
1351
- if (this.sessionFallbackTasks.has(sid)) {
1352
- continue;
1353
- }
1354
- const task = this.sessionWatchWorker(sid).finally(() => {
1355
- if (this.sessionFallbackTasks.get(sid) === task) {
1356
- this.sessionFallbackTasks.delete(sid);
1357
- }
1358
- });
1359
- this.sessionFallbackTasks.set(sid, task);
1360
- }
1361
- for (const pid of this.panelSet) {
1362
- if (this.panelFallbackTasks.has(pid)) {
1363
- continue;
1364
- }
1365
- const task = this.panelPollWorker(pid).finally(() => {
1366
- if (this.panelFallbackTasks.get(pid) === task) {
1367
- this.panelFallbackTasks.delete(pid);
1368
- }
1369
- });
1370
- this.panelFallbackTasks.set(pid, task);
1371
- }
1372
- }
1373
- async stopFallbackWorkers() {
1374
- this.fallbackMode = false;
1375
- const tasks = [...this.sessionFallbackTasks.values(), ...this.panelFallbackTasks.values()];
1376
- this.sessionFallbackTasks.clear();
1377
- this.panelFallbackTasks.clear();
1378
- await Promise.allSettled(tasks);
1379
- }
1380
- async sessionWatchWorker(sessionId) {
1381
- while (this.running && this.fallbackMode) {
1382
- try {
1383
- const payload = await this.postJson("/api/claw/sessions/watch", {
1384
- sessionId,
1385
- cursor: this.sessionCursor[sessionId] ?? 0,
1386
- timeoutMs: this.config.watchTimeoutMs,
1387
- limit: this.config.watchLimit
1388
- });
1389
- await this.handleWatchPayload(payload, "session");
1390
- } catch {
1391
- await sleep2(Math.max(100, this.config.retryDelayMs));
1392
- }
1393
- }
1394
- }
1395
- async panelPollWorker(panelId) {
1396
- const sleepMs = Math.max(1e3, this.config.refreshIntervalMs);
1397
- while (this.running && this.fallbackMode) {
1398
- try {
1399
- const payload = await this.postJson("/api/claw/groups/panels/messages", {
1400
- panelId,
1401
- limit: Math.min(100, Math.max(1, this.config.watchLimit))
1402
- });
1403
- const messages = payload.messages;
1404
- if (Array.isArray(messages)) {
1405
- for (const msg of [...messages].reverse()) {
1406
- const event = makeSyntheticEvent({
1407
- messageId: String(msg.messageId ?? ""),
1408
- author: String(msg.author ?? ""),
1409
- content: msg.content,
1410
- meta: msg.meta,
1411
- groupId: String(payload.groupId ?? ""),
1412
- converseId: panelId,
1413
- timestamp: msg.createdAt,
1414
- authorInfo: msg.authorInfo
1415
- });
1416
- await this.processInboundEvent(panelId, event, "panel");
1417
- }
1418
- }
1419
- } catch {
1420
- await sleep2(sleepMs);
1421
- }
1422
- await sleep2(sleepMs);
1423
- }
1424
- }
1425
- async handleWatchPayload(payload, targetKind) {
1426
- if (!payload || typeof payload !== "object") {
1427
- return;
1428
- }
1429
- const targetId = strField(payload, "sessionId");
1430
- if (!targetId) {
1431
- return;
1432
- }
1433
- const lockKey = `${targetKind}:${targetId}`;
1434
- const lock = this.targetLocks.get(lockKey) ?? new AsyncLock();
1435
- this.targetLocks.set(lockKey, lock);
1436
- await lock.run(async () => {
1437
- const previousCursor = this.sessionCursor[targetId] ?? 0;
1438
- const cursor = payload.cursor;
1439
- if (targetKind === "session" && typeof cursor === "number" && cursor >= 0) {
1440
- this.markSessionCursor(targetId, cursor);
1441
- }
1442
- const rawEvents = payload.events;
1443
- if (!Array.isArray(rawEvents)) {
1444
- return;
1445
- }
1446
- if (targetKind === "session" && this.coldSessions.has(targetId)) {
1447
- this.coldSessions.delete(targetId);
1448
- return;
1449
- }
1450
- for (const event of rawEvents) {
1451
- const seq = event.seq;
1452
- if (targetKind === "session" && typeof seq === "number" && seq > (this.sessionCursor[targetId] ?? previousCursor)) {
1453
- this.markSessionCursor(targetId, seq);
1454
- }
1455
- if (event.type === "message.add") {
1456
- await this.processInboundEvent(targetId, event, targetKind);
1457
- }
1458
- }
1459
- });
1460
- }
1461
- async processInboundEvent(targetId, event, targetKind) {
1462
- const payload = event.payload;
1463
- if (!payload) {
1464
- return;
1465
- }
1466
- const author = strField(payload, "author");
1467
- if (!author || this.config.agentUserId && author === this.config.agentUserId) {
1468
- return;
1469
- }
1470
- if (!this.isAllowed(author)) {
1471
- return;
1472
- }
1473
- const messageId = strField(payload, "messageId");
1474
- const seenKey = `${targetKind}:${targetId}`;
1475
- if (messageId && this.rememberMessageId(seenKey, messageId)) {
1476
- return;
1477
- }
1478
- const rawBody = normalizeMochatContent(payload.content) || "[empty message]";
1479
- const authorInfo = safeDict(payload.authorInfo);
1480
- const senderName = strField(authorInfo, "nickname", "email");
1481
- const senderUsername = strField(authorInfo, "agentId");
1482
- const groupId = strField(payload, "groupId");
1483
- const isGroup = Boolean(groupId);
1484
- const wasMentioned = resolveWasMentioned(payload, this.config.agentUserId);
1485
- const requireMention = targetKind === "panel" && isGroup && resolveRequireMention(this.config, targetId, groupId);
1486
- const useDelay = targetKind === "panel" && this.config.replyDelayMode === "non-mention";
1487
- if (requireMention && !wasMentioned && !useDelay) {
1488
- return;
1489
- }
1490
- const entry = {
1491
- rawBody,
1492
- author,
1493
- senderName,
1494
- senderUsername,
1495
- timestamp: parseTimestamp(event.timestamp),
1496
- messageId,
1497
- groupId
1498
- };
1499
- if (useDelay) {
1500
- const delayKey = seenKey;
1501
- if (wasMentioned) {
1502
- await this.flushDelayedEntries(delayKey, targetId, targetKind, true, entry);
1503
- } else {
1504
- await this.enqueueDelayedEntry(delayKey, targetId, targetKind, entry);
1505
- }
1506
- return;
1507
- }
1508
- await this.dispatchEntries(targetId, targetKind, [entry], wasMentioned);
1509
- }
1510
- rememberMessageId(key, messageId) {
1511
- const seenSet = this.seenSet.get(key) ?? /* @__PURE__ */ new Set();
1512
- const seenQueue = this.seenQueue.get(key) ?? [];
1513
- if (seenSet.has(messageId)) {
1514
- return true;
1515
- }
1516
- seenSet.add(messageId);
1517
- seenQueue.push(messageId);
1518
- while (seenQueue.length > MAX_SEEN_MESSAGE_IDS) {
1519
- const removed = seenQueue.shift();
1520
- if (removed) {
1521
- seenSet.delete(removed);
1522
- }
1523
- }
1524
- this.seenSet.set(key, seenSet);
1525
- this.seenQueue.set(key, seenQueue);
1526
- return false;
1527
- }
1528
- async enqueueDelayedEntry(key, targetId, targetKind, entry) {
1529
- const state = this.delayStates.get(key) ?? { entries: [], timer: null, lock: new AsyncLock() };
1530
- this.delayStates.set(key, state);
1531
- await state.lock.run(async () => {
1532
- state.entries.push(entry);
1533
- if (state.timer) {
1534
- clearTimeout(state.timer);
1535
- }
1536
- state.timer = setTimeout(() => {
1537
- void this.flushDelayedEntries(key, targetId, targetKind, false, null);
1538
- }, Math.max(0, this.config.replyDelayMs));
1539
- });
1540
- }
1541
- async flushDelayedEntries(key, targetId, targetKind, mentioned, entry) {
1542
- const state = this.delayStates.get(key) ?? { entries: [], timer: null, lock: new AsyncLock() };
1543
- this.delayStates.set(key, state);
1544
- let entries = [];
1545
- await state.lock.run(async () => {
1546
- if (entry) {
1547
- state.entries.push(entry);
1548
- }
1549
- if (state.timer) {
1550
- clearTimeout(state.timer);
1551
- state.timer = null;
1552
- }
1553
- entries = [...state.entries];
1554
- state.entries = [];
1555
- });
1556
- if (entries.length) {
1557
- await this.dispatchEntries(targetId, targetKind, entries, mentioned);
1558
- }
1559
- }
1560
- async dispatchEntries(targetId, targetKind, entries, wasMentioned) {
1561
- const last = entries[entries.length - 1];
1562
- const isGroup = Boolean(last.groupId);
1563
- const body = buildBufferedBody(entries, isGroup) || "[empty message]";
1564
- await this.handleMessage({
1565
- senderId: last.author,
1566
- chatId: targetId,
1567
- content: body,
1568
- media: [],
1569
- metadata: {
1570
- message_id: last.messageId,
1571
- timestamp: last.timestamp,
1572
- is_group: isGroup,
1573
- group_id: last.groupId,
1574
- sender_name: last.senderName,
1575
- sender_username: last.senderUsername,
1576
- target_kind: targetKind,
1577
- was_mentioned: wasMentioned,
1578
- buffered_count: entries.length
1579
- }
1580
- });
1581
- }
1582
- async cancelDelayTimers() {
1583
- for (const state of this.delayStates.values()) {
1584
- if (state.timer) {
1585
- clearTimeout(state.timer);
1586
- }
1587
- }
1588
- this.delayStates.clear();
1589
- }
1590
- async handleNotifyChatMessage(payload) {
1591
- if (!payload || typeof payload !== "object") {
1592
- return;
1593
- }
1594
- const data = payload;
1595
- const groupId = strField(data, "groupId");
1596
- const panelId = strField(data, "converseId", "panelId");
1597
- if (!groupId || !panelId) {
1598
- return;
1599
- }
1600
- if (this.panelSet.size && !this.panelSet.has(panelId)) {
1601
- return;
1602
- }
1603
- const event = makeSyntheticEvent({
1604
- messageId: String(data._id ?? data.messageId ?? ""),
1605
- author: String(data.author ?? ""),
1606
- content: data.content,
1607
- meta: data.meta,
1608
- groupId,
1609
- converseId: panelId,
1610
- timestamp: data.createdAt,
1611
- authorInfo: data.authorInfo
1612
- });
1613
- await this.processInboundEvent(panelId, event, "panel");
1614
- }
1615
- async handleNotifyInboxAppend(payload) {
1616
- if (!payload || typeof payload !== "object") {
1617
- return;
1618
- }
1619
- const data = payload;
1620
- if (data.type !== "message") {
1621
- return;
1622
- }
1623
- const detail = data.payload;
1624
- if (!detail || typeof detail !== "object") {
1625
- return;
1626
- }
1627
- if (strField(detail, "groupId")) {
1628
- return;
1629
- }
1630
- const converseId = strField(detail, "converseId");
1631
- if (!converseId) {
1632
- return;
1633
- }
1634
- let sessionId = this.sessionByConverse.get(converseId);
1635
- if (!sessionId) {
1636
- await this.refreshSessionsDirectory(this.wsReady);
1637
- sessionId = this.sessionByConverse.get(converseId);
1638
- }
1639
- if (!sessionId) {
1640
- return;
1641
- }
1642
- const event = makeSyntheticEvent({
1643
- messageId: String(detail.messageId ?? data._id ?? ""),
1644
- author: String(detail.messageAuthor ?? ""),
1645
- content: String(detail.messagePlainContent ?? detail.messageSnippet ?? ""),
1646
- meta: { source: "notify:chat.inbox.append", converseId },
1647
- groupId: "",
1648
- converseId,
1649
- timestamp: data.createdAt
1650
- });
1651
- await this.processInboundEvent(sessionId, event, "session");
1652
- }
1653
- markSessionCursor(sessionId, cursor) {
1654
- if (cursor < 0) {
1655
- return;
1656
- }
1657
- const current = this.sessionCursor[sessionId] ?? 0;
1658
- if (cursor < current) {
1659
- return;
1660
- }
1661
- this.sessionCursor[sessionId] = cursor;
1662
- if (!this.cursorSaveTimer) {
1663
- this.cursorSaveTimer = setTimeout(() => {
1664
- this.cursorSaveTimer = null;
1665
- void this.saveSessionCursors();
1666
- }, CURSOR_SAVE_DEBOUNCE_MS);
1667
- }
1668
- }
1669
- async loadSessionCursors() {
1670
- if (!existsSync2(this.cursorPath)) {
1671
- return;
1672
- }
1673
- try {
1674
- const raw = readFileSync(this.cursorPath, "utf-8");
1675
- const data = JSON.parse(raw);
1676
- const cursors = data.cursors;
1677
- if (cursors && typeof cursors === "object") {
1678
- for (const [sid, value] of Object.entries(cursors)) {
1679
- if (typeof value === "number" && value >= 0) {
1680
- this.sessionCursor[sid] = value;
1681
- }
1682
- }
1683
- }
1684
- } catch {
1685
- return;
1686
- }
1687
- }
1688
- async saveSessionCursors() {
1689
- try {
1690
- mkdirSync3(this.stateDir, { recursive: true });
1691
- const payload = {
1692
- schemaVersion: 1,
1693
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1694
- cursors: this.sessionCursor
1695
- };
1696
- writeFileSync2(this.cursorPath, JSON.stringify(payload, null, 2) + "\n");
1697
- } catch {
1698
- return;
1699
- }
1700
- }
1701
- async postJson(path, payload) {
1702
- const url = `${this.config.baseUrl.trim().replace(/\/$/, "")}${path}`;
1703
- const response = await fetch3(url, {
1704
- method: "POST",
1705
- headers: {
1706
- "content-type": "application/json",
1707
- "X-Claw-Token": this.config.clawToken
1708
- },
1709
- body: JSON.stringify(payload)
1710
- });
1711
- if (!response.ok) {
1712
- throw new Error(`Mochat HTTP ${response.status}`);
1713
- }
1714
- let parsed;
1715
- try {
1716
- parsed = await response.json();
1717
- } catch {
1718
- parsed = await response.text();
1719
- }
1720
- if (parsed && typeof parsed === "object" && parsed.code !== void 0) {
1721
- const data = parsed;
1722
- if (typeof data.code === "number" && data.code !== 200) {
1723
- throw new Error(String(data.message ?? data.name ?? "request failed"));
1724
- }
1725
- if (data.data && typeof data.data === "object") {
1726
- return data.data;
1727
- }
1728
- return {};
1729
- }
1730
- if (parsed && typeof parsed === "object") {
1731
- return parsed;
1732
- }
1733
- return {};
1734
- }
1735
- async apiSend(path, idKey, idValue, content, replyTo, groupId) {
1736
- const body = { [idKey]: idValue, content };
1737
- if (replyTo) {
1738
- body.replyTo = replyTo;
1739
- }
1740
- if (groupId) {
1741
- body.groupId = groupId;
1742
- }
1743
- await this.postJson(path, body);
1744
- }
1745
- };
1746
- function normalizeIdList(values) {
1747
- const cleaned = values.map((value) => String(value).trim()).filter(Boolean);
1748
- const unique = Array.from(new Set(cleaned.filter((value) => value !== "*"))).sort();
1749
- return [unique, cleaned.includes("*")];
1750
- }
1751
- function safeDict(value) {
1752
- return value && typeof value === "object" ? value : {};
1753
- }
1754
- function strField(src, ...keys) {
1755
- for (const key of keys) {
1756
- const value = src[key];
1757
- if (typeof value === "string" && value.trim()) {
1758
- return value.trim();
1759
- }
1760
- }
1761
- return "";
1762
- }
1763
- function makeSyntheticEvent(params) {
1764
- const payload = {
1765
- messageId: params.messageId,
1766
- author: params.author,
1767
- content: params.content,
1768
- meta: safeDict(params.meta),
1769
- groupId: params.groupId,
1770
- converseId: params.converseId
1771
- };
1772
- if (params.authorInfo) {
1773
- payload.authorInfo = safeDict(params.authorInfo);
1774
- }
1775
- return {
1776
- type: "message.add",
1777
- timestamp: params.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
1778
- payload
1779
- };
1780
- }
1781
- function normalizeMochatContent(content) {
1782
- if (typeof content === "string") {
1783
- return content.trim();
1784
- }
1785
- if (content === null || content === void 0) {
1786
- return "";
1787
- }
1788
- try {
1789
- return JSON.stringify(content);
1790
- } catch {
1791
- return String(content);
1792
- }
1793
- }
1794
- function resolveMochatTarget(raw) {
1795
- const trimmed = (raw || "").trim();
1796
- if (!trimmed) {
1797
- return { id: "", isPanel: false };
1798
- }
1799
- const lowered = trimmed.toLowerCase();
1800
- let cleaned = trimmed;
1801
- let forcedPanel = false;
1802
- for (const prefix of ["mochat:", "group:", "channel:", "panel:"]) {
1803
- if (lowered.startsWith(prefix)) {
1804
- cleaned = trimmed.slice(prefix.length).trim();
1805
- forcedPanel = prefix !== "mochat:";
1806
- break;
1807
- }
1808
- }
1809
- if (!cleaned) {
1810
- return { id: "", isPanel: false };
1811
- }
1812
- return { id: cleaned, isPanel: forcedPanel || !cleaned.startsWith("session_") };
1813
- }
1814
- function extractMentionIds(value) {
1815
- if (!Array.isArray(value)) {
1816
- return [];
1817
- }
1818
- const ids = [];
1819
- for (const item of value) {
1820
- if (typeof item === "string" && item.trim()) {
1821
- ids.push(item.trim());
1822
- } else if (item && typeof item === "object") {
1823
- const obj = item;
1824
- for (const key of ["id", "userId", "_id"]) {
1825
- const candidate = obj[key];
1826
- if (typeof candidate === "string" && candidate.trim()) {
1827
- ids.push(candidate.trim());
1828
- break;
1829
- }
1830
- }
1831
- }
1832
- }
1833
- return ids;
1834
- }
1835
- function resolveWasMentioned(payload, agentUserId) {
1836
- const meta = payload.meta;
1837
- if (meta) {
1838
- if (meta.mentioned === true || meta.wasMentioned === true) {
1839
- return true;
1840
- }
1841
- for (const field of ["mentions", "mentionIds", "mentionedUserIds", "mentionedUsers"]) {
1842
- if (agentUserId && extractMentionIds(meta[field]).includes(agentUserId)) {
1843
- return true;
1844
- }
1845
- }
1846
- }
1847
- if (!agentUserId) {
1848
- return false;
1849
- }
1850
- const content = payload.content;
1851
- if (typeof content !== "string" || !content) {
1852
- return false;
1853
- }
1854
- return content.includes(`<@${agentUserId}>`) || content.includes(`@${agentUserId}`);
1855
- }
1856
- function resolveRequireMention(config, sessionId, groupId) {
1857
- const groups = config.groups ?? {};
1858
- for (const key of [groupId, sessionId, "*"]) {
1859
- if (key && groups[key]) {
1860
- return Boolean(groups[key].requireMention);
1861
- }
1862
- }
1863
- return Boolean(config.mention.requireInGroups);
1864
- }
1865
- function buildBufferedBody(entries, isGroup) {
1866
- if (!entries.length) {
1867
- return "";
1868
- }
1869
- if (entries.length === 1) {
1870
- return entries[0].rawBody;
1871
- }
1872
- const lines = [];
1873
- for (const entry of entries) {
1874
- if (!entry.rawBody) {
1875
- continue;
1876
- }
1877
- if (isGroup) {
1878
- const label = entry.senderName.trim() || entry.senderUsername.trim() || entry.author;
1879
- if (label) {
1880
- lines.push(`${label}: ${entry.rawBody}`);
1881
- continue;
1882
- }
1883
- }
1884
- lines.push(entry.rawBody);
1885
- }
1886
- return lines.join("\n").trim();
1887
- }
1888
- function parseTimestamp(value) {
1889
- if (typeof value !== "string" || !value.trim()) {
1890
- return null;
1891
- }
1892
- const parsed = Date.parse(value);
1893
- return Number.isNaN(parsed) ? null : parsed;
1894
- }
1895
- function readGroupId(metadata) {
1896
- const value = metadata.group_id ?? metadata.groupId;
1897
- if (typeof value === "string" && value.trim()) {
1898
- return value.trim();
1899
- }
1900
- return null;
1901
- }
1902
- function sleep2(ms) {
1903
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1904
- }
1905
-
1906
- // src/channels/dingtalk.ts
1907
- import { DWClient, EventAck, TOPIC_ROBOT } from "dingtalk-stream";
1908
- import { fetch as fetch4 } from "undici";
1909
- var DingTalkChannel = class extends BaseChannel {
1910
- name = "dingtalk";
1911
- client = null;
1912
- accessToken = null;
1913
- tokenExpiry = 0;
1914
- constructor(config, bus) {
1915
- super(config, bus);
1916
- }
1917
- async start() {
1918
- this.running = true;
1919
- if (!this.config.clientId || !this.config.clientSecret) {
1920
- throw new Error("DingTalk clientId/clientSecret not configured");
1921
- }
1922
- this.client = new DWClient({
1923
- clientId: this.config.clientId,
1924
- clientSecret: this.config.clientSecret,
1925
- debug: false
1926
- });
1927
- this.client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
1928
- await this.handleRobotMessage(res);
1929
- });
1930
- this.client.registerAllEventListener(() => ({ status: EventAck.SUCCESS }));
1931
- await this.client.connect();
1932
- }
1933
- async stop() {
1934
- this.running = false;
1935
- if (this.client) {
1936
- this.client.disconnect();
1937
- this.client = null;
1938
- }
1939
- }
1940
- async send(msg) {
1941
- const token = await this.getAccessToken();
1942
- if (!token) {
1943
- return;
1944
- }
1945
- const url = "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend";
1946
- const payload = {
1947
- robotCode: this.config.clientId,
1948
- userIds: [msg.chatId],
1949
- msgKey: "sampleMarkdown",
1950
- msgParam: JSON.stringify({
1951
- text: msg.content,
1952
- title: `${APP_TITLE} Reply`
1953
- })
1954
- };
1955
- const response = await fetch4(url, {
1956
- method: "POST",
1957
- headers: {
1958
- "content-type": "application/json",
1959
- "x-acs-dingtalk-access-token": token
1960
- },
1961
- body: JSON.stringify(payload)
1962
- });
1963
- if (!response.ok) {
1964
- throw new Error(`DingTalk send failed: ${response.status}`);
1965
- }
1966
- }
1967
- async handleRobotMessage(res) {
1968
- if (!res?.data) {
1969
- return;
1970
- }
1971
- let parsed;
1972
- try {
1973
- parsed = JSON.parse(res.data);
1974
- } catch {
1975
- return;
1976
- }
1977
- const text = parsed.text?.content?.trim() ?? "";
1978
- if (!text) {
1979
- this.client?.socketCallBackResponse(res.headers.messageId, { ok: true });
1980
- return;
1981
- }
1982
- const senderId = parsed.senderStaffId || parsed.senderId || "";
1983
- const senderName = parsed.senderNick || "";
1984
- if (!senderId) {
1985
- this.client?.socketCallBackResponse(res.headers.messageId, { ok: true });
1986
- return;
1987
- }
1988
- await this.handleMessage({
1989
- senderId,
1990
- chatId: senderId,
1991
- content: text,
1992
- media: [],
1993
- metadata: {
1994
- sender_name: senderName,
1995
- platform: "dingtalk"
1996
- }
1997
- });
1998
- this.client?.socketCallBackResponse(res.headers.messageId, { ok: true });
1999
- }
2000
- async getAccessToken() {
2001
- if (this.accessToken && Date.now() < this.tokenExpiry) {
2002
- return this.accessToken;
2003
- }
2004
- const url = "https://api.dingtalk.com/v1.0/oauth2/accessToken";
2005
- const payload = {
2006
- appKey: this.config.clientId,
2007
- appSecret: this.config.clientSecret
2008
- };
2009
- const response = await fetch4(url, {
2010
- method: "POST",
2011
- headers: { "content-type": "application/json" },
2012
- body: JSON.stringify(payload)
2013
- });
2014
- if (!response.ok) {
2015
- return null;
2016
- }
2017
- const data = await response.json();
2018
- const token = data.accessToken;
2019
- const expiresIn = Number(data.expireIn ?? 7200);
2020
- if (!token) {
2021
- return null;
2022
- }
2023
- this.accessToken = token;
2024
- this.tokenExpiry = Date.now() + (expiresIn - 60) * 1e3;
2025
- return token;
2026
- }
2027
- };
2028
-
2029
- // src/channels/email.ts
2030
- import { ImapFlow } from "imapflow";
2031
- import { simpleParser } from "mailparser";
2032
- import nodemailer from "nodemailer";
2033
- var sleep3 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
2034
- var EmailChannel = class extends BaseChannel {
2035
- name = "email";
2036
- lastSubjectByChat = /* @__PURE__ */ new Map();
2037
- lastMessageIdByChat = /* @__PURE__ */ new Map();
2038
- processedUids = /* @__PURE__ */ new Set();
2039
- maxProcessedUids = 1e5;
2040
- constructor(config, bus) {
2041
- super(config, bus);
2042
- }
2043
- async start() {
2044
- if (!this.config.consentGranted) {
2045
- return;
2046
- }
2047
- if (!this.validateConfig()) {
2048
- return;
2049
- }
2050
- this.running = true;
2051
- const pollSeconds = Math.max(5, Number(this.config.pollIntervalSeconds ?? 30));
2052
- while (this.running) {
2053
- try {
2054
- const items = await this.fetchNewMessages();
2055
- for (const item of items) {
2056
- if (item.subject) {
2057
- this.lastSubjectByChat.set(item.sender, item.subject);
2058
- }
2059
- if (item.messageId) {
2060
- this.lastMessageIdByChat.set(item.sender, item.messageId);
2061
- }
2062
- await this.handleMessage({
2063
- senderId: item.sender,
2064
- chatId: item.sender,
2065
- content: item.content,
2066
- media: [],
2067
- metadata: item.metadata ?? {}
2068
- });
2069
- }
2070
- } catch {
2071
- }
2072
- await sleep3(pollSeconds * 1e3);
2073
- }
2074
- }
2075
- async stop() {
2076
- this.running = false;
2077
- }
2078
- async send(msg) {
2079
- if (!this.config.consentGranted) {
2080
- return;
2081
- }
2082
- const forceSend = Boolean((msg.metadata ?? {}).force_send);
2083
- if (!this.config.autoReplyEnabled && !forceSend) {
2084
- return;
2085
- }
2086
- if (!this.config.smtpHost) {
2087
- return;
2088
- }
2089
- const toAddr = msg.chatId.trim();
2090
- if (!toAddr) {
2091
- return;
2092
- }
2093
- const baseSubject = this.lastSubjectByChat.get(toAddr) ?? APP_REPLY_SUBJECT;
2094
- const subject = msg.metadata?.subject?.trim() || this.replySubject(baseSubject);
2095
- const transporter = nodemailer.createTransport({
2096
- host: this.config.smtpHost,
2097
- port: this.config.smtpPort,
2098
- secure: this.config.smtpUseSsl,
2099
- auth: {
2100
- user: this.config.smtpUsername,
2101
- pass: this.config.smtpPassword
2102
- },
2103
- tls: this.config.smtpUseTls ? { rejectUnauthorized: false } : void 0
2104
- });
2105
- await transporter.sendMail({
2106
- from: this.config.fromAddress || this.config.smtpUsername || this.config.imapUsername,
2107
- to: toAddr,
2108
- subject,
2109
- text: msg.content ?? "",
2110
- inReplyTo: this.lastMessageIdByChat.get(toAddr) ?? void 0,
2111
- references: this.lastMessageIdByChat.get(toAddr) ?? void 0
2112
- });
2113
- }
2114
- validateConfig() {
2115
- const missing = [];
2116
- if (!this.config.imapHost) missing.push("imapHost");
2117
- if (!this.config.imapUsername) missing.push("imapUsername");
2118
- if (!this.config.imapPassword) missing.push("imapPassword");
2119
- if (!this.config.smtpHost) missing.push("smtpHost");
2120
- if (!this.config.smtpUsername) missing.push("smtpUsername");
2121
- if (!this.config.smtpPassword) missing.push("smtpPassword");
2122
- return missing.length === 0;
2123
- }
2124
- replySubject(subject) {
2125
- const prefix = this.config.subjectPrefix || "Re: ";
2126
- return subject.startsWith(prefix) ? subject : `${prefix}${subject}`;
2127
- }
2128
- async fetchNewMessages() {
2129
- const client = new ImapFlow({
2130
- host: this.config.imapHost,
2131
- port: this.config.imapPort,
2132
- secure: this.config.imapUseSsl,
2133
- auth: {
2134
- user: this.config.imapUsername,
2135
- pass: this.config.imapPassword
2136
- }
2137
- });
2138
- await client.connect();
2139
- const lock = await client.getMailboxLock(this.config.imapMailbox || "INBOX");
2140
- const items = [];
2141
- try {
2142
- const uids = await client.search({ seen: false });
2143
- if (!Array.isArray(uids)) {
2144
- return items;
2145
- }
2146
- for (const uid of uids) {
2147
- const key = String(uid);
2148
- if (this.processedUids.has(key)) {
2149
- continue;
2150
- }
2151
- const message = await client.fetchOne(uid, { uid: true, source: true, envelope: true });
2152
- if (!message || !message.source) {
2153
- continue;
2154
- }
2155
- const parsed = await simpleParser(message.source);
2156
- const sender = parsed.from?.value?.[0]?.address ?? "";
2157
- if (!sender) {
2158
- continue;
2159
- }
2160
- if (!this.isAllowed(sender)) {
2161
- continue;
2162
- }
2163
- const rawContent = parsed.text ?? parsed.html ?? "";
2164
- const content = typeof rawContent === "string" ? rawContent : "";
2165
- const subject = parsed.subject ?? "";
2166
- const messageId = parsed.messageId ?? "";
2167
- items.push({
2168
- sender,
2169
- subject,
2170
- content: content.slice(0, this.config.maxBodyChars),
2171
- messageId,
2172
- metadata: { subject }
2173
- });
2174
- if (this.config.markSeen) {
2175
- await client.messageFlagsAdd(uid, ["\\Seen"]);
2176
- }
2177
- this.processedUids.add(key);
2178
- if (this.processedUids.size > this.maxProcessedUids) {
2179
- const iterator = this.processedUids.values();
2180
- const oldest = iterator.next().value;
2181
- if (oldest) {
2182
- this.processedUids.delete(oldest);
2183
- }
2184
- }
2185
- }
2186
- } finally {
2187
- lock.release();
2188
- await client.logout();
2189
- }
2190
- return items;
2191
- }
2192
- };
2193
-
2194
- // src/channels/slack.ts
2195
- import { WebClient } from "@slack/web-api";
2196
- import { SocketModeClient } from "@slack/socket-mode";
2197
- var SlackChannel = class extends BaseChannel {
2198
- name = "slack";
2199
- webClient = null;
2200
- socketClient = null;
2201
- botUserId = null;
2202
- constructor(config, bus) {
2203
- super(config, bus);
2204
- }
2205
- async start() {
2206
- if (!this.config.botToken || !this.config.appToken) {
2207
- throw new Error("Slack bot/app token not configured");
2208
- }
2209
- if (this.config.mode !== "socket") {
2210
- throw new Error(`Unsupported Slack mode: ${this.config.mode}`);
2211
- }
2212
- this.running = true;
2213
- this.webClient = new WebClient(this.config.botToken);
2214
- this.socketClient = new SocketModeClient({
2215
- appToken: this.config.appToken
2216
- });
2217
- this.socketClient.on("events_api", async ({ body, ack }) => {
2218
- await ack();
2219
- await this.handleEvent(body?.event);
2220
- });
2221
- try {
2222
- const auth = await this.webClient.auth.test();
2223
- this.botUserId = auth.user_id ?? null;
2224
- } catch {
2225
- this.botUserId = null;
2226
- }
2227
- await this.socketClient.start();
2228
- }
2229
- async stop() {
2230
- this.running = false;
2231
- if (this.socketClient) {
2232
- await this.socketClient.disconnect();
2233
- this.socketClient = null;
2234
- }
2235
- }
2236
- async send(msg) {
2237
- if (!this.webClient) {
2238
- return;
2239
- }
2240
- const slackMeta = msg.metadata?.slack ?? {};
2241
- const threadTs = slackMeta.thread_ts;
2242
- const channelType = slackMeta.channel_type;
2243
- const useThread = Boolean(threadTs && channelType !== "im");
2244
- await this.webClient.chat.postMessage({
2245
- channel: msg.chatId,
2246
- text: msg.content ?? "",
2247
- thread_ts: useThread ? threadTs : void 0
2248
- });
2249
- }
2250
- async handleEvent(event) {
2251
- if (!event) {
2252
- return;
2253
- }
2254
- const eventType = event.type;
2255
- if (eventType !== "message" && eventType !== "app_mention") {
2256
- return;
2257
- }
2258
- if (event.subtype) {
2259
- return;
2260
- }
2261
- const senderId = event.user;
2262
- const chatId = event.channel;
2263
- const channelType = event.channel_type ?? "";
2264
- const text = event.text ?? "";
2265
- if (!senderId || !chatId) {
2266
- return;
2267
- }
2268
- if (this.botUserId && senderId === this.botUserId) {
2269
- return;
2270
- }
2271
- if (eventType === "message" && this.botUserId && text.includes(`<@${this.botUserId}>`)) {
2272
- return;
2273
- }
2274
- if (!this.isAllowedInSlack(senderId, chatId, channelType)) {
2275
- return;
2276
- }
2277
- if (channelType !== "im" && !this.shouldRespondInChannel(eventType, text, chatId)) {
2278
- return;
2279
- }
2280
- const cleanText = this.stripBotMention(text);
2281
- const threadTs = event.thread_ts ?? event.ts;
2282
- try {
2283
- if (this.webClient && event.ts) {
2284
- await this.webClient.reactions.add({
2285
- channel: chatId,
2286
- name: "eyes",
2287
- timestamp: event.ts
2288
- });
2289
- }
2290
- } catch {
2291
- }
2292
- await this.handleMessage({
2293
- senderId,
2294
- chatId,
2295
- content: cleanText,
2296
- media: [],
2297
- metadata: {
2298
- slack: {
2299
- event,
2300
- thread_ts: threadTs,
2301
- channel_type: channelType
2302
- }
2303
- }
2304
- });
2305
- }
2306
- isAllowedInSlack(senderId, chatId, channelType) {
2307
- if (channelType === "im") {
2308
- if (!this.config.dm.enabled) {
2309
- return false;
2310
- }
2311
- if (this.config.dm.policy === "allowlist") {
2312
- return this.config.dm.allowFrom.includes(senderId);
2313
- }
2314
- return true;
2315
- }
2316
- if (this.config.groupPolicy === "allowlist") {
2317
- return this.config.groupAllowFrom.includes(chatId);
2318
- }
2319
- return true;
2320
- }
2321
- shouldRespondInChannel(eventType, text, chatId) {
2322
- if (this.config.groupPolicy === "open") {
2323
- return true;
2324
- }
2325
- if (this.config.groupPolicy === "mention") {
2326
- if (eventType === "app_mention") {
2327
- return true;
2328
- }
2329
- return this.botUserId ? text.includes(`<@${this.botUserId}>`) : false;
2330
- }
2331
- if (this.config.groupPolicy === "allowlist") {
2332
- return this.config.groupAllowFrom.includes(chatId);
2333
- }
2334
- return false;
2335
- }
2336
- stripBotMention(text) {
2337
- if (!text || !this.botUserId) {
2338
- return text;
2339
- }
2340
- const pattern = new RegExp(`<@${this.botUserId}>\\s*`, "g");
2341
- return text.replace(pattern, "").trim();
2342
- }
2343
- };
2344
-
2345
- // src/channels/qq.ts
2346
- import {
2347
- Bot,
2348
- ReceiverMode,
2349
- segment
2350
- } from "qq-official-bot";
2351
- var QQChannel = class extends BaseChannel {
2352
- name = "qq";
2353
- bot = null;
2354
- processedIds = [];
2355
- processedSet = /* @__PURE__ */ new Set();
2356
- constructor(config, bus) {
2357
- super(config, bus);
2358
- }
2359
- async start() {
2360
- this.running = true;
2361
- if (!this.config.appId || !this.config.secret) {
2362
- throw new Error("QQ appId/appSecret not configured");
2363
- }
2364
- this.bot = new Bot({
2365
- appid: this.config.appId,
2366
- secret: this.config.secret,
2367
- mode: ReceiverMode.WEBSOCKET,
2368
- intents: ["C2C_MESSAGE_CREATE", "GROUP_AT_MESSAGE_CREATE"],
2369
- removeAt: true,
2370
- logLevel: "info"
2371
- });
2372
- this.bot.on("message.private", async (event) => {
2373
- await this.handleIncoming(event);
2374
- });
2375
- this.bot.on("message.group", async (event) => {
2376
- await this.handleIncoming(event);
2377
- });
2378
- await this.bot.start();
2379
- console.log("QQ bot connected");
2380
- }
2381
- async stop() {
2382
- this.running = false;
2383
- if (this.bot) {
2384
- this.bot.removeAllListeners("message.private");
2385
- this.bot.removeAllListeners("message.group");
2386
- await this.bot.stop();
2387
- this.bot = null;
2388
- }
2389
- }
2390
- async send(msg) {
2391
- if (!this.bot) {
2392
- return;
2393
- }
2394
- const qqMeta = msg.metadata?.qq ?? {};
2395
- const messageType = qqMeta.messageType ?? "private";
2396
- const metadataMessageId = msg.metadata?.message_id ?? null;
2397
- const sourceId = msg.replyTo ?? metadataMessageId ?? void 0;
2398
- const source = sourceId ? { id: sourceId } : void 0;
2399
- const content = this.normalizeContent(msg.content ?? "");
2400
- const payload = this.config.markdownSupport ? segment.markdown(content) : content;
2401
- if (messageType === "group") {
2402
- const groupId = qqMeta.groupId ?? msg.chatId;
2403
- await this.sendWithTokenRetry(() => this.bot?.sendGroupMessage(groupId, payload, source));
2404
- return;
2405
- }
2406
- if (messageType === "direct") {
2407
- const guildId = qqMeta.guildId ?? msg.chatId;
2408
- await this.sendWithTokenRetry(() => this.bot?.sendDirectMessage(guildId, payload, source));
2409
- return;
2410
- }
2411
- if (messageType === "guild") {
2412
- const channelId = qqMeta.channelId ?? msg.chatId;
2413
- await this.sendWithTokenRetry(() => this.bot?.sendGuildMessage(channelId, payload, source));
2414
- return;
2415
- }
2416
- const userId = qqMeta.userId ?? msg.chatId;
2417
- await this.sendWithTokenRetry(() => this.bot?.sendPrivateMessage(userId, payload, source));
2418
- }
2419
- async handleIncoming(event) {
2420
- const messageId = event.message_id || event.id || "";
2421
- if (messageId && this.isDuplicate(messageId)) {
2422
- return;
2423
- }
2424
- if (event.user_id === event.self_id) {
2425
- return;
2426
- }
2427
- const rawEvent = event;
2428
- const senderId = event.user_id || rawEvent.sender?.member_openid || rawEvent.sender?.user_openid || rawEvent.sender?.user_id || "";
2429
- if (!senderId) {
2430
- return;
2431
- }
2432
- const content = event.raw_message?.trim() ?? "";
2433
- const safeContent = content || "[empty message]";
2434
- let chatId = senderId;
2435
- let messageType = "private";
2436
- const qqMeta = {};
2437
- if (event.message_type === "group") {
2438
- messageType = "group";
2439
- const groupId = event.group_id || rawEvent.group_openid || "";
2440
- chatId = groupId;
2441
- qqMeta.groupId = groupId;
2442
- qqMeta.userId = senderId;
2443
- } else if (event.message_type === "guild") {
2444
- messageType = "guild";
2445
- chatId = event.channel_id ?? "";
2446
- qqMeta.guildId = event.guild_id;
2447
- qqMeta.channelId = event.channel_id;
2448
- qqMeta.userId = senderId;
2449
- } else if (event.sub_type === "direct") {
2450
- messageType = "direct";
2451
- chatId = event.guild_id ?? "";
2452
- qqMeta.guildId = event.guild_id;
2453
- qqMeta.userId = senderId;
2454
- } else {
2455
- qqMeta.userId = senderId;
2456
- }
2457
- qqMeta.messageType = messageType;
2458
- if (!chatId) {
2459
- return;
2460
- }
2461
- if (!this.isAllowed(senderId)) {
2462
- return;
2463
- }
2464
- await this.handleMessage({
2465
- senderId,
2466
- chatId,
2467
- content: safeContent,
2468
- media: [],
2469
- metadata: {
2470
- message_id: messageId,
2471
- qq: qqMeta
2472
- }
2473
- });
2474
- }
2475
- isDuplicate(messageId) {
2476
- if (this.processedSet.has(messageId)) {
2477
- return true;
2478
- }
2479
- this.processedSet.add(messageId);
2480
- this.processedIds.push(messageId);
2481
- if (this.processedIds.length > 1e3) {
2482
- const removed = this.processedIds.splice(0, 500);
2483
- for (const id of removed) {
2484
- this.processedSet.delete(id);
2485
- }
2486
- }
2487
- return false;
2488
- }
2489
- normalizeContent(content) {
2490
- const withoutThink = content.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<\/?think>/gi, "");
2491
- const cleaned = withoutThink.trim();
2492
- return cleaned || "[empty message]";
2493
- }
2494
- async sendWithTokenRetry(send) {
2495
- try {
2496
- await send();
2497
- } catch (error) {
2498
- if (!this.isTokenExpiredError(error) || !this.bot) {
2499
- throw error;
2500
- }
2501
- try {
2502
- await this.bot.sessionManager.getAccessToken();
2503
- } catch (refreshError) {
2504
- throw refreshError;
2505
- }
2506
- await send();
2507
- }
2508
- }
2509
- isTokenExpiredError(error) {
2510
- const message = error instanceof Error ? error.message : String(error);
2511
- return message.includes("code(11244)") || message.toLowerCase().includes("token not exist or expire");
2512
- }
2513
- };
2514
-
2515
- // src/channels/manager.ts
2516
- var ChannelManager = class {
2517
- constructor(config, bus, sessionManager) {
2518
- this.config = config;
2519
- this.bus = bus;
2520
- this.sessionManager = sessionManager;
2521
- this.initChannels();
2522
- }
2523
- channels = {};
2524
- dispatchTask = null;
2525
- dispatching = false;
2526
- initChannels() {
2527
- if (this.config.channels.telegram.enabled) {
2528
- const channel = new TelegramChannel(
2529
- this.config.channels.telegram,
2530
- this.bus,
2531
- this.config.providers.groq.apiKey,
2532
- this.sessionManager
2533
- );
2534
- this.channels.telegram = channel;
2535
- }
2536
- if (this.config.channels.whatsapp.enabled) {
2537
- const channel = new WhatsAppChannel(this.config.channels.whatsapp, this.bus);
2538
- this.channels.whatsapp = channel;
2539
- }
2540
- if (this.config.channels.discord.enabled) {
2541
- const channel = new DiscordChannel(this.config.channels.discord, this.bus);
2542
- this.channels.discord = channel;
2543
- }
2544
- if (this.config.channels.feishu.enabled) {
2545
- const channel = new FeishuChannel(this.config.channels.feishu, this.bus);
2546
- this.channels.feishu = channel;
2547
- }
2548
- if (this.config.channels.mochat.enabled) {
2549
- const channel = new MochatChannel(this.config.channels.mochat, this.bus);
2550
- this.channels.mochat = channel;
2551
- }
2552
- if (this.config.channels.dingtalk.enabled) {
2553
- const channel = new DingTalkChannel(this.config.channels.dingtalk, this.bus);
2554
- this.channels.dingtalk = channel;
2555
- }
2556
- if (this.config.channels.email.enabled) {
2557
- const channel = new EmailChannel(this.config.channels.email, this.bus);
2558
- this.channels.email = channel;
2559
- }
2560
- if (this.config.channels.slack.enabled) {
2561
- const channel = new SlackChannel(this.config.channels.slack, this.bus);
2562
- this.channels.slack = channel;
2563
- }
2564
- if (this.config.channels.qq.enabled) {
2565
- const channel = new QQChannel(this.config.channels.qq, this.bus);
2566
- this.channels.qq = channel;
2567
- }
2568
- }
2569
- async startChannel(name, channel) {
2570
- try {
2571
- await channel.start();
2572
- } catch (err2) {
2573
- console.error(`Failed to start channel ${name}: ${String(err2)}`);
2574
- }
2575
- }
2576
- async startAll() {
2577
- if (!Object.keys(this.channels).length) {
2578
- return;
2579
- }
2580
- this.dispatching = true;
2581
- this.dispatchTask = this.dispatchOutbound();
2582
- const tasks = Object.entries(this.channels).map(([name, channel]) => this.startChannel(name, channel));
2583
- await Promise.allSettled(tasks);
2584
- }
2585
- async stopAll() {
2586
- this.dispatching = false;
2587
- await this.bus.publishOutbound({
2588
- channel: "__control__",
2589
- chatId: "",
2590
- content: "",
2591
- media: [],
2592
- metadata: { reason: "shutdown" }
2593
- });
2594
- if (this.dispatchTask) {
2595
- await this.dispatchTask;
2596
- }
2597
- const tasks = Object.entries(this.channels).map(async ([name, channel]) => {
2598
- try {
2599
- await channel.stop();
2600
- } catch (err2) {
2601
- console.error(`Error stopping ${name}: ${String(err2)}`);
2602
- }
2603
- });
2604
- await Promise.allSettled(tasks);
2605
- }
2606
- async dispatchOutbound() {
2607
- while (this.dispatching) {
2608
- const msg = await this.bus.consumeOutbound();
2609
- const channel = this.channels[msg.channel];
2610
- if (!channel) {
2611
- continue;
2612
- }
2613
- try {
2614
- await channel.send(msg);
2615
- } catch (err2) {
2616
- console.error(`Error sending to ${msg.channel}: ${String(err2)}`);
2617
- }
2618
- }
2619
- }
2620
- getChannel(name) {
2621
- return this.channels[name];
2622
- }
2623
- getStatus() {
2624
- return Object.fromEntries(
2625
- Object.entries(this.channels).map(([name, channel]) => [name, { enabled: true, running: channel.isRunning }])
2626
- );
2627
- }
2628
- get enabledChannels() {
2629
- return Object.keys(this.channels);
2630
- }
2631
- };
2632
-
2633
- // src/cron/service.ts
2634
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync4 } from "fs";
2635
- import { dirname } from "path";
2636
- import { randomUUID } from "crypto";
2637
- import cronParser from "cron-parser";
2638
- var nowMs = () => Date.now();
2639
- function computeNextRun(schedule, now) {
2640
- if (schedule.kind === "at") {
2641
- return schedule.atMs && schedule.atMs > now ? schedule.atMs : null;
2642
- }
2643
- if (schedule.kind === "every") {
2644
- if (!schedule.everyMs || schedule.everyMs <= 0) {
2645
- return null;
2646
- }
2647
- return now + schedule.everyMs;
2648
- }
2649
- if (schedule.kind === "cron" && schedule.expr) {
2650
- try {
2651
- const interval = cronParser.parseExpression(schedule.expr, { currentDate: new Date(now) });
2652
- return interval.next().getTime();
2653
- } catch {
2654
- return null;
2655
- }
2656
- }
2657
- return null;
2658
- }
2659
- var CronService = class {
2660
- constructor(storePath, onJob) {
2661
- this.storePath = storePath;
2662
- this.onJob = onJob;
2663
- }
2664
- store = null;
2665
- timer = null;
2666
- running = false;
2667
- onJob;
2668
- loadStore() {
2669
- if (this.store) {
2670
- return this.store;
2671
- }
2672
- if (existsSync3(this.storePath)) {
2673
- try {
2674
- const data = JSON.parse(readFileSync2(this.storePath, "utf-8"));
2675
- const jobs = (data.jobs ?? []).map((job) => ({
2676
- id: String(job.id),
2677
- name: String(job.name),
2678
- enabled: Boolean(job.enabled ?? true),
2679
- schedule: job.schedule ?? {},
2680
- payload: job.payload ?? {},
2681
- state: job.state ?? {},
2682
- createdAtMs: Number(job.createdAtMs ?? 0),
2683
- updatedAtMs: Number(job.updatedAtMs ?? 0),
2684
- deleteAfterRun: Boolean(job.deleteAfterRun ?? false)
2685
- }));
2686
- this.store = { version: data.version ?? 1, jobs };
2687
- } catch {
2688
- this.store = { version: 1, jobs: [] };
2689
- }
2690
- } else {
2691
- this.store = { version: 1, jobs: [] };
2692
- }
2693
- return this.store;
2694
- }
2695
- saveStore() {
2696
- if (!this.store) {
2697
- return;
2698
- }
2699
- mkdirSync4(dirname(this.storePath), { recursive: true });
2700
- writeFileSync3(this.storePath, JSON.stringify(this.store, null, 2));
2701
- }
2702
- async start() {
2703
- this.running = true;
2704
- this.loadStore();
2705
- this.recomputeNextRuns();
2706
- this.saveStore();
2707
- this.armTimer();
2708
- }
2709
- stop() {
2710
- this.running = false;
2711
- if (this.timer) {
2712
- clearTimeout(this.timer);
2713
- this.timer = null;
2714
- }
2715
- }
2716
- recomputeNextRuns() {
2717
- if (!this.store) {
2718
- return;
2719
- }
2720
- const now = nowMs();
2721
- for (const job of this.store.jobs) {
2722
- if (job.enabled) {
2723
- job.state.nextRunAtMs = computeNextRun(job.schedule, now);
2724
- }
2725
- }
2726
- }
2727
- getNextWakeMs() {
2728
- if (!this.store) {
2729
- return null;
2730
- }
2731
- const times = this.store.jobs.filter((job) => job.enabled && job.state.nextRunAtMs).map((job) => job.state.nextRunAtMs);
2732
- if (!times.length) {
2733
- return null;
2734
- }
2735
- return Math.min(...times);
2736
- }
2737
- armTimer() {
2738
- if (this.timer) {
2739
- clearTimeout(this.timer);
2740
- }
2741
- if (!this.running) {
2742
- return;
2743
- }
2744
- const nextWake = this.getNextWakeMs();
2745
- if (!nextWake) {
2746
- return;
2747
- }
2748
- const delayMs = Math.max(0, nextWake - nowMs());
2749
- this.timer = setTimeout(() => {
2750
- void this.onTimer();
2751
- }, delayMs);
2752
- }
2753
- async onTimer() {
2754
- if (!this.store) {
2755
- return;
2756
- }
2757
- const now = nowMs();
2758
- const dueJobs = this.store.jobs.filter(
2759
- (job) => job.enabled && job.state.nextRunAtMs && now >= (job.state.nextRunAtMs ?? 0)
2760
- );
2761
- for (const job of dueJobs) {
2762
- await this.executeJob(job);
2763
- }
2764
- this.saveStore();
2765
- this.armTimer();
2766
- }
2767
- async executeJob(job) {
2768
- const start = nowMs();
2769
- try {
2770
- if (this.onJob) {
2771
- await this.onJob(job);
2772
- }
2773
- job.state.lastStatus = "ok";
2774
- job.state.lastError = null;
2775
- } catch (err2) {
2776
- job.state.lastStatus = "error";
2777
- job.state.lastError = String(err2);
2778
- }
2779
- job.state.lastRunAtMs = start;
2780
- job.updatedAtMs = nowMs();
2781
- if (job.schedule.kind === "at") {
2782
- if (job.deleteAfterRun) {
2783
- if (this.store) {
2784
- this.store.jobs = this.store.jobs.filter((existing) => existing.id !== job.id);
2785
- }
2786
- } else {
2787
- job.enabled = false;
2788
- job.state.nextRunAtMs = null;
2789
- }
2790
- } else {
2791
- job.state.nextRunAtMs = computeNextRun(job.schedule, nowMs());
2792
- }
2793
- }
2794
- listJobs(includeDisabled = false) {
2795
- const store = this.loadStore();
2796
- const jobs = includeDisabled ? store.jobs : store.jobs.filter((job) => job.enabled);
2797
- return jobs.sort((a, b) => (a.state.nextRunAtMs ?? Infinity) - (b.state.nextRunAtMs ?? Infinity));
2798
- }
2799
- addJob(params) {
2800
- const store = this.loadStore();
2801
- const now = nowMs();
2802
- const job = {
2803
- id: randomUUID().slice(0, 8),
2804
- name: params.name,
2805
- enabled: true,
2806
- schedule: params.schedule,
2807
- payload: {
2808
- kind: "agent_turn",
2809
- message: params.message,
2810
- deliver: params.deliver ?? false,
2811
- channel: params.channel,
2812
- to: params.to
2813
- },
2814
- state: {
2815
- nextRunAtMs: computeNextRun(params.schedule, now)
2816
- },
2817
- createdAtMs: now,
2818
- updatedAtMs: now,
2819
- deleteAfterRun: params.deleteAfterRun ?? false
2820
- };
2821
- store.jobs.push(job);
2822
- this.saveStore();
2823
- this.armTimer();
2824
- return job;
2825
- }
2826
- removeJob(jobId) {
2827
- const store = this.loadStore();
2828
- const before = store.jobs.length;
2829
- store.jobs = store.jobs.filter((job) => job.id !== jobId);
2830
- const removed = store.jobs.length < before;
2831
- if (removed) {
2832
- this.saveStore();
2833
- this.armTimer();
2834
- }
2835
- return removed;
2836
- }
2837
- enableJob(jobId, enabled = true) {
2838
- const store = this.loadStore();
2839
- for (const job of store.jobs) {
2840
- if (job.id === jobId) {
2841
- job.enabled = enabled;
2842
- job.updatedAtMs = nowMs();
2843
- job.state.nextRunAtMs = enabled ? computeNextRun(job.schedule, nowMs()) : null;
2844
- this.saveStore();
2845
- this.armTimer();
2846
- return job;
2847
- }
2848
- }
2849
- return null;
2850
- }
2851
- async runJob(jobId, force = false) {
2852
- const store = this.loadStore();
2853
- for (const job of store.jobs) {
2854
- if (job.id === jobId) {
2855
- if (!force && !job.enabled) {
2856
- return false;
2857
- }
2858
- await this.executeJob(job);
2859
- this.saveStore();
2860
- this.armTimer();
2861
- return true;
2862
- }
2863
- }
2864
- return false;
2865
- }
2866
- status() {
2867
- const store = this.loadStore();
2868
- return {
2869
- enabled: this.running,
2870
- jobs: store.jobs.length,
2871
- nextWakeAtMs: this.getNextWakeMs()
2872
- };
2873
- }
2874
- };
2875
-
2876
- // src/heartbeat/service.ts
2877
- import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
2878
- import { join as join4 } from "path";
2879
- var DEFAULT_HEARTBEAT_INTERVAL_S = 30 * 60;
2880
- var HEARTBEAT_PROMPT = "Read HEARTBEAT.md in your workspace (if it exists).\nFollow any instructions or tasks listed there.\nIf nothing needs attention, reply with just: HEARTBEAT_OK";
2881
- var HEARTBEAT_OK_TOKEN = "HEARTBEAT_OK";
2882
- function isHeartbeatEmpty(content) {
2883
- if (!content) {
2884
- return true;
2885
- }
2886
- const skipPatterns = /* @__PURE__ */ new Set(["- [ ]", "* [ ]", "- [x]", "* [x]"]);
2887
- for (const line of content.split("\n")) {
2888
- const trimmed = line.trim();
2889
- if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("<!--") || skipPatterns.has(trimmed)) {
2890
- continue;
2891
- }
2892
- return false;
2893
- }
2894
- return true;
2895
- }
2896
- var HeartbeatService = class {
2897
- constructor(workspace, onHeartbeat, intervalS = DEFAULT_HEARTBEAT_INTERVAL_S, enabled = true) {
2898
- this.workspace = workspace;
2899
- this.onHeartbeat = onHeartbeat;
2900
- this.intervalS = intervalS;
2901
- this.enabled = enabled;
2902
- }
2903
- running = false;
2904
- timer = null;
2905
- get heartbeatFile() {
2906
- return join4(this.workspace, "HEARTBEAT.md");
2907
- }
2908
- readHeartbeatFile() {
2909
- if (existsSync4(this.heartbeatFile)) {
2910
- try {
2911
- return readFileSync3(this.heartbeatFile, "utf-8");
2912
- } catch {
2913
- return null;
2914
- }
2915
- }
2916
- return null;
2917
- }
2918
- async start() {
2919
- if (!this.enabled) {
2920
- return;
2921
- }
2922
- this.running = true;
2923
- this.timer = setInterval(() => {
2924
- void this.tick();
2925
- }, this.intervalS * 1e3);
2926
- }
2927
- stop() {
2928
- this.running = false;
2929
- if (this.timer) {
2930
- clearInterval(this.timer);
2931
- this.timer = null;
2932
- }
2933
- }
2934
- async tick() {
2935
- if (!this.running) {
2936
- return;
2937
- }
2938
- const content = this.readHeartbeatFile();
2939
- if (isHeartbeatEmpty(content)) {
2940
- return;
2941
- }
2942
- if (this.onHeartbeat) {
2943
- const response = await this.onHeartbeat(HEARTBEAT_PROMPT);
2944
- if (response.toUpperCase().replace(/_/g, "").includes(HEARTBEAT_OK_TOKEN.replace(/_/g, ""))) {
2945
- return;
2946
- }
2947
- }
2948
- }
2949
- async triggerNow() {
2950
- if (!this.onHeartbeat) {
2951
- return null;
2952
- }
2953
- return this.onHeartbeat(HEARTBEAT_PROMPT);
2954
- }
2955
- };
2956
-
2957
- // src/ui/server.ts
2958
- import { Hono as Hono2 } from "hono";
2959
- import { cors } from "hono/cors";
2960
- import { serve } from "@hono/node-server";
2961
- import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
2962
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
2963
- import { readFile, stat } from "fs/promises";
2964
- import { join as join5 } from "path";
2965
-
2966
- // src/ui/router.ts
2967
- import { Hono } from "hono";
2968
-
2969
- // src/ui/config.ts
2970
- var MASK_MIN_LENGTH = 8;
2971
- function maskApiKey(value) {
2972
- if (!value) {
2973
- return { apiKeySet: false };
2974
- }
2975
- if (value.length < MASK_MIN_LENGTH) {
2976
- return { apiKeySet: true, apiKeyMasked: "****" };
2977
- }
2978
- return {
2979
- apiKeySet: true,
2980
- apiKeyMasked: `${value.slice(0, 2)}****${value.slice(-4)}`
2981
- };
2982
- }
2983
- function toProviderView(provider) {
2984
- const masked = maskApiKey(provider.apiKey);
2985
- return {
2986
- apiKeySet: masked.apiKeySet,
2987
- apiKeyMasked: masked.apiKeyMasked,
2988
- apiBase: provider.apiBase ?? null,
2989
- extraHeaders: provider.extraHeaders ?? null
2990
- };
2991
- }
2992
- function buildConfigView(config) {
2993
- const providers = {};
2994
- for (const [name, provider] of Object.entries(config.providers)) {
2995
- providers[name] = toProviderView(provider);
2996
- }
2997
- return {
2998
- agents: config.agents,
2999
- providers,
3000
- channels: config.channels,
3001
- tools: config.tools,
3002
- gateway: config.gateway,
3003
- ui: config.ui
3004
- };
3005
- }
3006
- function buildConfigMeta(config) {
3007
- const providers = PROVIDERS.map((spec) => ({
3008
- name: spec.name,
3009
- displayName: spec.displayName,
3010
- keywords: spec.keywords,
3011
- envKey: spec.envKey,
3012
- isGateway: spec.isGateway,
3013
- isLocal: spec.isLocal,
3014
- defaultApiBase: spec.defaultApiBase
3015
- }));
3016
- const channels2 = Object.keys(config.channels).map((name) => ({
3017
- name,
3018
- displayName: name,
3019
- enabled: Boolean(config.channels[name]?.enabled)
3020
- }));
3021
- return { providers, channels: channels2 };
3022
- }
3023
- function loadConfigOrDefault(configPath) {
3024
- return loadConfig(configPath);
3025
- }
3026
- function updateModel(configPath, model) {
3027
- const config = loadConfigOrDefault(configPath);
3028
- config.agents.defaults.model = model;
3029
- const next = ConfigSchema.parse(config);
3030
- saveConfig(next, configPath);
3031
- return buildConfigView(next);
3032
- }
3033
- function updateProvider(configPath, providerName, patch) {
3034
- const config = loadConfigOrDefault(configPath);
3035
- const provider = config.providers[providerName];
3036
- if (!provider) {
3037
- return null;
3038
- }
3039
- if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
3040
- provider.apiKey = patch.apiKey ?? "";
3041
- }
3042
- if (Object.prototype.hasOwnProperty.call(patch, "apiBase")) {
3043
- provider.apiBase = patch.apiBase ?? null;
3044
- }
3045
- if (Object.prototype.hasOwnProperty.call(patch, "extraHeaders")) {
3046
- provider.extraHeaders = patch.extraHeaders ?? null;
3047
- }
3048
- const next = ConfigSchema.parse(config);
3049
- saveConfig(next, configPath);
3050
- const updated = next.providers[providerName];
3051
- return toProviderView(updated);
3052
- }
3053
- function updateChannel(configPath, channelName, patch) {
3054
- const config = loadConfigOrDefault(configPath);
3055
- const channel = config.channels[channelName];
3056
- if (!channel) {
3057
- return null;
3058
- }
3059
- config.channels[channelName] = { ...channel, ...patch };
3060
- const next = ConfigSchema.parse(config);
3061
- saveConfig(next, configPath);
3062
- return next.channels[channelName];
3063
- }
3064
- function updateUi(configPath, patch) {
3065
- const config = loadConfigOrDefault(configPath);
3066
- config.ui = { ...config.ui, ...patch };
3067
- const next = ConfigSchema.parse(config);
3068
- saveConfig(next, configPath);
3069
- return next.ui;
3070
- }
3071
-
3072
- // src/channels/feishu-probe.ts
3073
- import * as Lark2 from "@larksuiteoapi/node-sdk";
3074
- var isRecord = (value) => typeof value === "object" && value !== null;
3075
- async function probeFeishu(appId, appSecret) {
3076
- if (!appId || !appSecret) {
3077
- return { ok: false, error: "missing credentials (appId, appSecret)" };
3078
- }
3079
- try {
3080
- const client = new Lark2.Client({ appId, appSecret });
3081
- const response = await client.request({
3082
- method: "GET",
3083
- url: "/open-apis/bot/v3/info",
3084
- data: {}
3085
- });
3086
- if (!isRecord(response)) {
3087
- return {
3088
- ok: false,
3089
- appId,
3090
- error: "API error: invalid response"
3091
- };
3092
- }
3093
- const code = typeof response.code === "number" ? response.code : null;
3094
- if (code !== 0) {
3095
- const msg = typeof response.msg === "string" ? response.msg : void 0;
3096
- return {
3097
- ok: false,
3098
- appId,
3099
- error: `API error: ${msg || `code ${code ?? "unknown"}`}`
3100
- };
3101
- }
3102
- const botFromResponse = isRecord(response.bot) ? response.bot : void 0;
3103
- const data = isRecord(response.data) ? response.data : void 0;
3104
- const botFromData = data && isRecord(data.bot) ? data.bot : void 0;
3105
- const bot = botFromResponse ?? botFromData;
3106
- const botName = bot && typeof bot.bot_name === "string" ? bot.bot_name : void 0;
3107
- const botOpenId = bot && typeof bot.open_id === "string" ? bot.open_id : void 0;
3108
- return {
3109
- ok: true,
3110
- appId,
3111
- botName,
3112
- botOpenId
3113
- };
3114
- } catch (error) {
3115
- return {
3116
- ok: false,
3117
- appId,
3118
- error: error instanceof Error ? error.message : String(error)
3119
- };
3120
- }
3121
- }
3122
-
3123
- // src/ui/router.ts
3124
- function ok(data) {
3125
- return { ok: true, data };
3126
- }
3127
- function err(code, message, details) {
3128
- return { ok: false, error: { code, message, details } };
3129
- }
3130
- async function readJson(req) {
3131
- try {
3132
- const data = await req.json();
3133
- return { ok: true, data };
3134
- } catch {
3135
- return { ok: false };
3136
- }
3137
- }
3138
- function createUiRouter(options) {
3139
- const app = new Hono();
3140
- app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
3141
- app.get("/api/health", (c) => c.json(ok({ status: "ok" })));
3142
- app.get("/api/config", (c) => {
3143
- const config = loadConfigOrDefault(options.configPath);
3144
- return c.json(ok(buildConfigView(config)));
3145
- });
3146
- app.get("/api/config/meta", (c) => {
3147
- const config = loadConfigOrDefault(options.configPath);
3148
- return c.json(ok(buildConfigMeta(config)));
3149
- });
3150
- app.put("/api/config/model", async (c) => {
3151
- const body = await readJson(c.req.raw);
3152
- if (!body.ok || !body.data.model) {
3153
- return c.json(err("INVALID_BODY", "model is required"), 400);
3154
- }
3155
- const view = updateModel(options.configPath, body.data.model);
3156
- options.publish({ type: "config.updated", payload: { path: "agents.defaults.model" } });
3157
- return c.json(ok({ model: view.agents.defaults.model }));
3158
- });
3159
- app.put("/api/config/providers/:provider", async (c) => {
3160
- const provider = c.req.param("provider");
3161
- const body = await readJson(c.req.raw);
3162
- if (!body.ok) {
3163
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3164
- }
3165
- const result = updateProvider(options.configPath, provider, body.data);
3166
- if (!result) {
3167
- return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
3168
- }
3169
- options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
3170
- return c.json(ok(result));
3171
- });
3172
- app.put("/api/config/channels/:channel", async (c) => {
3173
- const channel = c.req.param("channel");
3174
- const body = await readJson(c.req.raw);
3175
- if (!body.ok) {
3176
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3177
- }
3178
- const result = updateChannel(options.configPath, channel, body.data);
3179
- if (!result) {
3180
- return c.json(err("NOT_FOUND", `unknown channel: ${channel}`), 404);
3181
- }
3182
- options.publish({ type: "config.updated", payload: { path: `channels.${channel}` } });
3183
- return c.json(ok(result));
3184
- });
3185
- app.post("/api/channels/feishu/probe", async (c) => {
3186
- const config = loadConfigOrDefault(options.configPath);
3187
- const feishu = config.channels.feishu;
3188
- if (!feishu?.appId || !feishu?.appSecret) {
3189
- return c.json(err("MISSING_CREDENTIALS", "Feishu appId/appSecret not configured"), 400);
3190
- }
3191
- const result = await probeFeishu(String(feishu.appId), String(feishu.appSecret));
3192
- if (!result.ok) {
3193
- return c.json(err("PROBE_FAILED", result.error), 400);
3194
- }
3195
- return c.json(
3196
- ok({
3197
- appId: result.appId,
3198
- botName: result.botName ?? null,
3199
- botOpenId: result.botOpenId ?? null
3200
- })
3201
- );
3202
- });
3203
- app.put("/api/config/ui", async (c) => {
3204
- const body = await readJson(c.req.raw);
3205
- if (!body.ok) {
3206
- return c.json(err("INVALID_BODY", "invalid json body"), 400);
3207
- }
3208
- const result = updateUi(options.configPath, body.data);
3209
- options.publish({ type: "config.updated", payload: { path: "ui" } });
3210
- return c.json(ok(result));
3211
- });
3212
- app.post("/api/config/reload", async (c) => {
3213
- options.publish({ type: "config.reload.started" });
3214
- try {
3215
- await options.onReload?.();
3216
- } catch (error) {
3217
- options.publish({
3218
- type: "error",
3219
- payload: { message: "reload failed", code: "RELOAD_FAILED" }
3220
- });
3221
- return c.json(err("RELOAD_FAILED", "reload failed"), 500);
3222
- }
3223
- options.publish({ type: "config.reload.finished" });
3224
- return c.json(ok({ status: "ok" }));
3225
- });
3226
- return app;
3227
- }
3228
-
3229
- // src/ui/server.ts
3230
- import { serveStatic } from "hono/serve-static";
3231
- var DEFAULT_CORS_ORIGINS = (origin) => {
3232
- if (!origin) {
3233
- return void 0;
3234
- }
3235
- if (origin.startsWith("http://localhost:") || origin.startsWith("http://127.0.0.1:")) {
3236
- return origin;
3237
- }
3238
- return void 0;
3239
- };
3240
- function startUiServer(options) {
3241
- const app = new Hono2();
3242
- const origin = options.corsOrigins ?? DEFAULT_CORS_ORIGINS;
3243
- app.use("/api/*", cors({ origin }));
3244
- const clients = /* @__PURE__ */ new Set();
3245
- const publish = (event) => {
3246
- const payload = JSON.stringify(event);
3247
- for (const client of clients) {
3248
- if (client.readyState === WebSocket2.OPEN) {
3249
- client.send(payload);
3250
- }
3251
- }
3252
- };
3253
- app.route(
3254
- "/",
3255
- createUiRouter({
3256
- configPath: options.configPath,
3257
- publish,
3258
- onReload: options.onReload
3259
- })
3260
- );
3261
- const staticDir = options.staticDir;
3262
- if (staticDir && existsSync5(join5(staticDir, "index.html"))) {
3263
- const indexHtml = readFileSync4(join5(staticDir, "index.html"), "utf-8");
3264
- app.use(
3265
- "/*",
3266
- serveStatic({
3267
- root: staticDir,
3268
- join: join5,
3269
- getContent: async (path) => {
3270
- try {
3271
- return await readFile(path);
3272
- } catch {
3273
- return null;
3274
- }
3275
- },
3276
- isDir: async (path) => {
3277
- try {
3278
- return (await stat(path)).isDirectory();
3279
- } catch {
3280
- return false;
3281
- }
3282
- }
3283
- })
3284
- );
3285
- app.get("*", (c) => {
3286
- const path = c.req.path;
3287
- if (path.startsWith("/api") || path.startsWith("/ws")) {
3288
- return c.notFound();
3289
- }
3290
- return c.html(indexHtml);
3291
- });
3292
- }
3293
- const server = serve({
3294
- fetch: app.fetch,
3295
- port: options.port,
3296
- hostname: options.host
3297
- });
3298
- const wss = new WebSocketServer({
3299
- server,
3300
- path: "/ws"
3301
- });
3302
- wss.on("connection", (socket) => {
3303
- clients.add(socket);
3304
- socket.on("close", () => clients.delete(socket));
3305
- });
3306
- return {
3307
- host: options.host,
3308
- port: options.port,
3309
- publish,
3310
- close: () => new Promise((resolve2) => {
3311
- wss.close(() => {
3312
- server.close(() => resolve2());
3313
- });
3314
- })
3315
- };
3316
- }
3317
2
 
3318
3
  // src/cli/index.ts
4
+ import { Command } from "commander";
5
+ import {
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ writeFileSync,
10
+ cpSync,
11
+ rmSync,
12
+ openSync,
13
+ closeSync
14
+ } from "fs";
15
+ import { join, resolve } from "path";
16
+ import { spawn, spawnSync } from "child_process";
17
+ import { createInterface } from "readline";
18
+ import { fileURLToPath } from "url";
19
+ import { createServer } from "net";
20
+ import chokidar from "chokidar";
21
+ import {
22
+ loadConfig,
23
+ saveConfig,
24
+ getConfigPath,
25
+ getDataDir,
26
+ ConfigSchema,
27
+ getApiBase,
28
+ getProvider,
29
+ getProviderName,
30
+ buildReloadPlan,
31
+ diffConfigPaths,
32
+ getWorkspacePath,
33
+ MessageBus,
34
+ AgentLoop,
35
+ LiteLLMProvider,
36
+ ChannelManager,
37
+ SessionManager,
38
+ CronService,
39
+ HeartbeatService,
40
+ PROVIDERS,
41
+ APP_NAME,
42
+ APP_TAGLINE
43
+ } from "nextclaw-core";
44
+ import { startUiServer } from "nextclaw-server";
3319
45
  var LOGO = "\u{1F916}";
3320
46
  var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
3321
47
  var VERSION = getPackageVersion();
@@ -3323,7 +49,7 @@ var program = new Command();
3323
49
  program.name(APP_NAME).description(`${LOGO} ${APP_NAME} - ${APP_TAGLINE}`).version(VERSION, "-v, --version", "show version");
3324
50
  program.command("onboard").description(`Initialize ${APP_NAME} configuration and workspace`).action(() => {
3325
51
  const configPath = getConfigPath();
3326
- if (existsSync6(configPath)) {
52
+ if (existsSync(configPath)) {
3327
53
  console.log(`Config already exists at ${configPath}`);
3328
54
  }
3329
55
  const config = ConfigSchema.parse({});
@@ -3474,14 +200,14 @@ program.command("agent").description("Interact with the agent directly").option(
3474
200
  }
3475
201
  console.log(`${LOGO} Interactive mode (type exit or Ctrl+C to quit)
3476
202
  `);
3477
- const historyFile = join6(getDataDir(), "history", "cli_history");
203
+ const historyFile = join(getDataDir(), "history", "cli_history");
3478
204
  const historyDir = resolve(historyFile, "..");
3479
- mkdirSync5(historyDir, { recursive: true });
3480
- const history = existsSync6(historyFile) ? readFileSync5(historyFile, "utf-8").split("\n").filter(Boolean) : [];
205
+ mkdirSync(historyDir, { recursive: true });
206
+ const history = existsSync(historyFile) ? readFileSync(historyFile, "utf-8").split("\n").filter(Boolean) : [];
3481
207
  const rl = createInterface({ input: process.stdin, output: process.stdout });
3482
208
  rl.on("close", () => {
3483
209
  const merged = history.concat(rl.history ?? []);
3484
- writeFileSync4(historyFile, merged.join("\n"));
210
+ writeFileSync(historyFile, merged.join("\n"));
3485
211
  process.exit(0);
3486
212
  });
3487
213
  let running = true;
@@ -3523,7 +249,7 @@ channels.command("login").description("Link device via QR code").action(() => {
3523
249
  });
3524
250
  var cron = program.command("cron").description("Manage scheduled tasks");
3525
251
  cron.command("list").option("-a, --all", "Include disabled jobs").action((opts) => {
3526
- const storePath = join6(getDataDir(), "cron", "jobs.json");
252
+ const storePath = join(getDataDir(), "cron", "jobs.json");
3527
253
  const service = new CronService(storePath);
3528
254
  const jobs = service.listJobs(Boolean(opts.all));
3529
255
  if (!jobs.length) {
@@ -3543,7 +269,7 @@ cron.command("list").option("-a, --all", "Include disabled jobs").action((opts)
3543
269
  }
3544
270
  });
3545
271
  cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOption("-m, --message <message>", "Message for agent").option("-e, --every <seconds>", "Run every N seconds").option("-c, --cron <expr>", "Cron expression").option("--at <iso>", "Run once at time (ISO format)").option("-d, --deliver", "Deliver response to channel").option("--to <recipient>", "Recipient for delivery").option("--channel <channel>", "Channel for delivery").action((opts) => {
3546
- const storePath = join6(getDataDir(), "cron", "jobs.json");
272
+ const storePath = join(getDataDir(), "cron", "jobs.json");
3547
273
  const service = new CronService(storePath);
3548
274
  let schedule = null;
3549
275
  if (opts.every) {
@@ -3568,7 +294,7 @@ cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOpti
3568
294
  console.log(`\u2713 Added job '${job.name}' (${job.id})`);
3569
295
  });
3570
296
  cron.command("remove <jobId>").action((jobId) => {
3571
- const storePath = join6(getDataDir(), "cron", "jobs.json");
297
+ const storePath = join(getDataDir(), "cron", "jobs.json");
3572
298
  const service = new CronService(storePath);
3573
299
  if (service.removeJob(jobId)) {
3574
300
  console.log(`\u2713 Removed job ${jobId}`);
@@ -3577,7 +303,7 @@ cron.command("remove <jobId>").action((jobId) => {
3577
303
  }
3578
304
  });
3579
305
  cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => {
3580
- const storePath = join6(getDataDir(), "cron", "jobs.json");
306
+ const storePath = join(getDataDir(), "cron", "jobs.json");
3581
307
  const service = new CronService(storePath);
3582
308
  const job = service.enableJob(jobId, !opts.disable);
3583
309
  if (job) {
@@ -3587,10 +313,10 @@ cron.command("enable <jobId>").option("--disable", "Disable instead of enable").
3587
313
  }
3588
314
  });
3589
315
  cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => {
3590
- const storePath = join6(getDataDir(), "cron", "jobs.json");
316
+ const storePath = join(getDataDir(), "cron", "jobs.json");
3591
317
  const service = new CronService(storePath);
3592
- const ok2 = await service.runJob(jobId, Boolean(opts.force));
3593
- console.log(ok2 ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
318
+ const ok = await service.runJob(jobId, Boolean(opts.force));
319
+ console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
3594
320
  });
3595
321
  program.command("status").description(`Show ${APP_NAME} status`).action(() => {
3596
322
  const configPath = getConfigPath();
@@ -3598,8 +324,8 @@ program.command("status").description(`Show ${APP_NAME} status`).action(() => {
3598
324
  const workspace = getWorkspacePath(config.agents.defaults.workspace);
3599
325
  console.log(`${LOGO} ${APP_NAME} Status
3600
326
  `);
3601
- console.log(`Config: ${configPath} ${existsSync6(configPath) ? "\u2713" : "\u2717"}`);
3602
- console.log(`Workspace: ${workspace} ${existsSync6(workspace) ? "\u2713" : "\u2717"}`);
327
+ console.log(`Config: ${configPath} ${existsSync(configPath) ? "\u2713" : "\u2717"}`);
328
+ console.log(`Workspace: ${workspace} ${existsSync(workspace) ? "\u2713" : "\u2717"}`);
3603
329
  console.log(`Model: ${config.agents.defaults.model}`);
3604
330
  for (const spec of PROVIDERS) {
3605
331
  const provider = config.providers[spec.name];
@@ -3619,7 +345,7 @@ async function startGateway(options = {}) {
3619
345
  const bus = new MessageBus();
3620
346
  const provider = options.allowMissingProvider === true ? makeProvider(config, { allowMissing: true }) : makeProvider(config);
3621
347
  const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
3622
- const cronStorePath = join6(getDataDir(), "cron", "jobs.json");
348
+ const cronStorePath = join(getDataDir(), "cron", "jobs.json");
3623
349
  const cron2 = new CronService(cronStorePath);
3624
350
  const uiConfig = resolveUiConfig(config, options.uiOverrides);
3625
351
  const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
@@ -3810,8 +536,8 @@ async function findAvailablePort(port, host, attempts = 20) {
3810
536
  const basePort = Number.isFinite(port) ? port : 0;
3811
537
  let candidate = basePort;
3812
538
  for (let i = 0; i < attempts; i += 1) {
3813
- const ok2 = await isPortAvailable(candidate, host);
3814
- if (ok2) {
539
+ const ok = await isPortAvailable(candidate, host);
540
+ if (ok) {
3815
541
  return candidate;
3816
542
  }
3817
543
  candidate += 1;
@@ -3827,8 +553,8 @@ async function isPortAvailable(port, host) {
3827
553
  hostsToCheck.push("127.0.0.1");
3828
554
  }
3829
555
  for (const hostToCheck of hostsToCheck) {
3830
- const ok2 = await canBindPort(port, hostToCheck);
3831
- if (!ok2) {
556
+ const ok = await canBindPort(port, hostToCheck);
557
+ if (!ok) {
3832
558
  return false;
3833
559
  }
3834
560
  }
@@ -3900,7 +626,7 @@ async function startService(options) {
3900
626
  }
3901
627
  const logPath = resolveServiceLogPath();
3902
628
  const logDir = resolve(logPath, "..");
3903
- mkdirSync5(logDir, { recursive: true });
629
+ mkdirSync(logDir, { recursive: true });
3904
630
  const logFd = openSync(logPath, "a");
3905
631
  const serveArgs = buildServeArgs({
3906
632
  uiHost: uiConfig.host,
@@ -3980,11 +706,11 @@ function buildServeArgs(options) {
3980
706
  }
3981
707
  function readServiceState() {
3982
708
  const path = resolveServiceStatePath();
3983
- if (!existsSync6(path)) {
709
+ if (!existsSync(path)) {
3984
710
  return null;
3985
711
  }
3986
712
  try {
3987
- const raw = readFileSync5(path, "utf-8");
713
+ const raw = readFileSync(path, "utf-8");
3988
714
  return JSON.parse(raw);
3989
715
  } catch {
3990
716
  return null;
@@ -3992,12 +718,12 @@ function readServiceState() {
3992
718
  }
3993
719
  function writeServiceState(state) {
3994
720
  const path = resolveServiceStatePath();
3995
- mkdirSync5(resolve(path, ".."), { recursive: true });
3996
- writeFileSync4(path, JSON.stringify(state, null, 2));
721
+ mkdirSync(resolve(path, ".."), { recursive: true });
722
+ writeFileSync(path, JSON.stringify(state, null, 2));
3997
723
  }
3998
724
  function clearServiceState() {
3999
725
  const path = resolveServiceStatePath();
4000
- if (existsSync6(path)) {
726
+ if (existsSync(path)) {
4001
727
  rmSync(path, { force: true });
4002
728
  }
4003
729
  }
@@ -4033,18 +759,18 @@ function resolveUiStaticDir() {
4033
759
  }
4034
760
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
4035
761
  const pkgRoot = resolve(cliDir, "..", "..");
4036
- candidates.push(join6(pkgRoot, "ui-dist"));
4037
- candidates.push(join6(pkgRoot, "ui"));
4038
- candidates.push(join6(pkgRoot, "..", "ui-dist"));
4039
- candidates.push(join6(pkgRoot, "..", "ui"));
762
+ candidates.push(join(pkgRoot, "ui-dist"));
763
+ candidates.push(join(pkgRoot, "ui"));
764
+ candidates.push(join(pkgRoot, "..", "ui-dist"));
765
+ candidates.push(join(pkgRoot, "..", "ui"));
4040
766
  const cwd = process.cwd();
4041
- candidates.push(join6(cwd, "packages", "nextclaw-ui", "dist"));
4042
- candidates.push(join6(cwd, "nextclaw-ui", "dist"));
4043
- candidates.push(join6(pkgRoot, "..", "nextclaw-ui", "dist"));
4044
- candidates.push(join6(pkgRoot, "..", "..", "packages", "nextclaw-ui", "dist"));
4045
- candidates.push(join6(pkgRoot, "..", "..", "nextclaw-ui", "dist"));
767
+ candidates.push(join(cwd, "packages", "nextclaw-ui", "dist"));
768
+ candidates.push(join(cwd, "nextclaw-ui", "dist"));
769
+ candidates.push(join(pkgRoot, "..", "nextclaw-ui", "dist"));
770
+ candidates.push(join(pkgRoot, "..", "..", "packages", "nextclaw-ui", "dist"));
771
+ candidates.push(join(pkgRoot, "..", "..", "nextclaw-ui", "dist"));
4046
772
  for (const dir of candidates) {
4047
- if (existsSync6(join6(dir, "index.html"))) {
773
+ if (existsSync(join(dir, "index.html"))) {
4048
774
  return dir;
4049
775
  }
4050
776
  }
@@ -4108,22 +834,22 @@ I am ${APP_NAME}, a lightweight AI assistant.
4108
834
  "USER.md": "# User\n\nInformation about the user goes here.\n\n## Preferences\n\n- Communication style: (casual/formal)\n- Timezone: (your timezone)\n- Language: (your preferred language)\n"
4109
835
  };
4110
836
  for (const [filename, content] of Object.entries(templates)) {
4111
- const filePath = join6(workspace, filename);
4112
- if (!existsSync6(filePath)) {
4113
- writeFileSync4(filePath, content);
837
+ const filePath = join(workspace, filename);
838
+ if (!existsSync(filePath)) {
839
+ writeFileSync(filePath, content);
4114
840
  }
4115
841
  }
4116
- const memoryDir = join6(workspace, "memory");
4117
- mkdirSync5(memoryDir, { recursive: true });
4118
- const memoryFile = join6(memoryDir, "MEMORY.md");
4119
- if (!existsSync6(memoryFile)) {
4120
- writeFileSync4(
842
+ const memoryDir = join(workspace, "memory");
843
+ mkdirSync(memoryDir, { recursive: true });
844
+ const memoryFile = join(memoryDir, "MEMORY.md");
845
+ if (!existsSync(memoryFile)) {
846
+ writeFileSync(
4121
847
  memoryFile,
4122
848
  "# Long-term Memory\n\nThis file stores important information that should persist across sessions.\n\n## User Information\n\n(Important facts about the user)\n\n## Preferences\n\n(User preferences learned over time)\n\n## Important Notes\n\n(Things to remember)\n"
4123
849
  );
4124
850
  }
4125
- const skillsDir = join6(workspace, "skills");
4126
- mkdirSync5(skillsDir, { recursive: true });
851
+ const skillsDir = join(workspace, "skills");
852
+ mkdirSync(skillsDir, { recursive: true });
4127
853
  }
4128
854
  function printAgentResponse(response) {
4129
855
  console.log("\n" + response + "\n");
@@ -4136,8 +862,8 @@ async function prompt(rl, question) {
4136
862
  });
4137
863
  }
4138
864
  function getBridgeDir() {
4139
- const userBridge = join6(getDataDir(), "bridge");
4140
- if (existsSync6(join6(userBridge, "dist", "index.js"))) {
865
+ const userBridge = join(getDataDir(), "bridge");
866
+ if (existsSync(join(userBridge, "dist", "index.js"))) {
4141
867
  return userBridge;
4142
868
  }
4143
869
  if (!which("npm")) {
@@ -4146,12 +872,12 @@ function getBridgeDir() {
4146
872
  }
4147
873
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
4148
874
  const pkgRoot = resolve(cliDir, "..", "..");
4149
- const pkgBridge = join6(pkgRoot, "bridge");
4150
- const srcBridge = join6(pkgRoot, "..", "..", "bridge");
875
+ const pkgBridge = join(pkgRoot, "bridge");
876
+ const srcBridge = join(pkgRoot, "..", "..", "bridge");
4151
877
  let source = null;
4152
- if (existsSync6(join6(pkgBridge, "package.json"))) {
878
+ if (existsSync(join(pkgBridge, "package.json"))) {
4153
879
  source = pkgBridge;
4154
- } else if (existsSync6(join6(srcBridge, "package.json"))) {
880
+ } else if (existsSync(join(srcBridge, "package.json"))) {
4155
881
  source = srcBridge;
4156
882
  }
4157
883
  if (!source) {
@@ -4159,8 +885,8 @@ function getBridgeDir() {
4159
885
  process.exit(1);
4160
886
  }
4161
887
  console.log(`${LOGO} Setting up bridge...`);
4162
- mkdirSync5(resolve(userBridge, ".."), { recursive: true });
4163
- if (existsSync6(userBridge)) {
888
+ mkdirSync(resolve(userBridge, ".."), { recursive: true });
889
+ if (existsSync(userBridge)) {
4164
890
  rmSync(userBridge, { recursive: true, force: true });
4165
891
  }
4166
892
  cpSync(source, userBridge, {
@@ -4190,7 +916,7 @@ function getPackageVersion() {
4190
916
  try {
4191
917
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
4192
918
  const pkgPath = resolve(cliDir, "..", "..", "package.json");
4193
- const raw = readFileSync5(pkgPath, "utf-8");
919
+ const raw = readFileSync(pkgPath, "utf-8");
4194
920
  const parsed = JSON.parse(raw);
4195
921
  return typeof parsed.version === "string" ? parsed.version : "0.0.0";
4196
922
  } catch {
@@ -4200,8 +926,8 @@ function getPackageVersion() {
4200
926
  function which(binary) {
4201
927
  const paths = (process.env.PATH ?? "").split(":");
4202
928
  for (const dir of paths) {
4203
- const full = join6(dir, binary);
4204
- if (existsSync6(full)) {
929
+ const full = join(dir, binary);
930
+ if (existsSync(full)) {
4205
931
  return true;
4206
932
  }
4207
933
  }
@@ -4251,15 +977,15 @@ function resolveUiFrontendDir() {
4251
977
  candidates.push(envDir);
4252
978
  }
4253
979
  const cwd = process.cwd();
4254
- candidates.push(join6(cwd, "packages", "nextclaw-ui"));
4255
- candidates.push(join6(cwd, "nextclaw-ui"));
980
+ candidates.push(join(cwd, "packages", "nextclaw-ui"));
981
+ candidates.push(join(cwd, "nextclaw-ui"));
4256
982
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
4257
983
  const pkgRoot = resolve(cliDir, "..", "..");
4258
- candidates.push(join6(pkgRoot, "..", "nextclaw-ui"));
4259
- candidates.push(join6(pkgRoot, "..", "..", "packages", "nextclaw-ui"));
4260
- candidates.push(join6(pkgRoot, "..", "..", "nextclaw-ui"));
984
+ candidates.push(join(pkgRoot, "..", "nextclaw-ui"));
985
+ candidates.push(join(pkgRoot, "..", "..", "packages", "nextclaw-ui"));
986
+ candidates.push(join(pkgRoot, "..", "..", "nextclaw-ui"));
4261
987
  for (const dir of candidates) {
4262
- if (existsSync6(join6(dir, "package.json"))) {
988
+ if (existsSync(join(dir, "package.json"))) {
4263
989
  return dir;
4264
990
  }
4265
991
  }