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 +103 -3377
- package/dist/index.d.ts +1 -2928
- package/dist/index.js +2 -88
- package/package.json +5 -28
- package/ui-dist/assets/{index-C8nOCIVG.js → index-CW60bJie.js} +43 -43
- package/ui-dist/index.html +1 -1
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
498
|
-
text = text.replace(`\0IC${i}\0`, `<code>${escaped}</code>`);
|
|
499
|
-
});
|
|
500
|
-
codeBlocks.forEach((code, i) => {
|
|
501
|
-
const escaped = code.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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 (
|
|
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 =
|
|
203
|
+
const historyFile = join(getDataDir(), "history", "cli_history");
|
|
3478
204
|
const historyDir = resolve(historyFile, "..");
|
|
3479
|
-
|
|
3480
|
-
const history =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
316
|
+
const storePath = join(getDataDir(), "cron", "jobs.json");
|
|
3591
317
|
const service = new CronService(storePath);
|
|
3592
|
-
const
|
|
3593
|
-
console.log(
|
|
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} ${
|
|
3602
|
-
console.log(`Workspace: ${workspace} ${
|
|
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 =
|
|
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
|
|
3814
|
-
if (
|
|
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
|
|
3831
|
-
if (!
|
|
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
|
-
|
|
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 (!
|
|
709
|
+
if (!existsSync(path)) {
|
|
3984
710
|
return null;
|
|
3985
711
|
}
|
|
3986
712
|
try {
|
|
3987
|
-
const raw =
|
|
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
|
-
|
|
3996
|
-
|
|
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 (
|
|
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(
|
|
4037
|
-
candidates.push(
|
|
4038
|
-
candidates.push(
|
|
4039
|
-
candidates.push(
|
|
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(
|
|
4042
|
-
candidates.push(
|
|
4043
|
-
candidates.push(
|
|
4044
|
-
candidates.push(
|
|
4045
|
-
candidates.push(
|
|
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 (
|
|
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 =
|
|
4112
|
-
if (!
|
|
4113
|
-
|
|
837
|
+
const filePath = join(workspace, filename);
|
|
838
|
+
if (!existsSync(filePath)) {
|
|
839
|
+
writeFileSync(filePath, content);
|
|
4114
840
|
}
|
|
4115
841
|
}
|
|
4116
|
-
const memoryDir =
|
|
4117
|
-
|
|
4118
|
-
const memoryFile =
|
|
4119
|
-
if (!
|
|
4120
|
-
|
|
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 =
|
|
4126
|
-
|
|
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 =
|
|
4140
|
-
if (
|
|
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 =
|
|
4150
|
-
const srcBridge =
|
|
875
|
+
const pkgBridge = join(pkgRoot, "bridge");
|
|
876
|
+
const srcBridge = join(pkgRoot, "..", "..", "bridge");
|
|
4151
877
|
let source = null;
|
|
4152
|
-
if (
|
|
878
|
+
if (existsSync(join(pkgBridge, "package.json"))) {
|
|
4153
879
|
source = pkgBridge;
|
|
4154
|
-
} else if (
|
|
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
|
-
|
|
4163
|
-
if (
|
|
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 =
|
|
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 =
|
|
4204
|
-
if (
|
|
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(
|
|
4255
|
-
candidates.push(
|
|
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(
|
|
4259
|
-
candidates.push(
|
|
4260
|
-
candidates.push(
|
|
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 (
|
|
988
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
4263
989
|
return dir;
|
|
4264
990
|
}
|
|
4265
991
|
}
|