flashclaw 1.0.0 → 1.1.0
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/README.md +2 -1
- package/dist/agent-runner.d.ts +2 -0
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +27 -4
- package/dist/agent-runner.js.map +1 -1
- package/dist/cli.js +48 -48
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config.d.ts +24 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -18
- package/dist/config.js.map +1 -1
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/api-client.js +109 -0
- package/dist/core/api-client.js.map +1 -1
- package/dist/core/memory.d.ts +1 -1
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +16 -3
- package/dist/core/memory.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +4 -1
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +264 -150
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +49 -8
- package/dist/logger.js.map +1 -1
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +20 -0
- package/dist/plugins/installer.js.map +1 -1
- package/dist/plugins/types.d.ts +2 -0
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/plugins/types.js.map +1 -1
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +5 -12
- package/dist/task-scheduler.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/plugins/feishu/index.ts +77 -20
- package/plugins/send-message/index.ts +83 -17
- package/scripts/postinstall.js +21 -0
package/dist/index.js
CHANGED
|
@@ -7,14 +7,16 @@ import fs from 'fs';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { isIP } from 'net';
|
|
9
9
|
import pino from 'pino';
|
|
10
|
+
import { z } from 'zod';
|
|
10
11
|
import { paths, ensureDirectories, getBuiltinPluginsDir } from './paths.js';
|
|
11
12
|
import { pluginManager } from './plugins/manager.js';
|
|
12
13
|
import { loadFromDir, watchPlugins, stopWatching } from './plugins/loader.js';
|
|
13
14
|
import { getApiClient } from './core/api-client.js';
|
|
14
15
|
import { getMemoryManager } from './core/memory.js';
|
|
15
|
-
import { BOT_NAME,
|
|
16
|
+
import { BOT_NAME, MAIN_GROUP_FOLDER, IPC_POLL_INTERVAL, TIMEZONE, HISTORY_CONTEXT_LIMIT, THINKING_THRESHOLD_MS, MAX_DIRECT_FETCH_CHARS, MAX_IPC_FILE_BYTES, MAX_IPC_MESSAGE_CHARS, MAX_IPC_CHAT_ID_CHARS, MAX_IMAGE_BYTES, MESSAGE_QUEUE_MAX_SIZE, MESSAGE_QUEUE_MAX_CONCURRENT, MESSAGE_QUEUE_PROCESSING_TIMEOUT_MS, MESSAGE_QUEUE_MAX_RETRIES } from './config.js';
|
|
16
17
|
import { initDatabase, storeMessage, storeChatMetadata, getMessagesSince, getChatHistory, messageExists, getAllTasks, getAllChats } from './db.js';
|
|
17
|
-
import { startSchedulerLoop, stopScheduler } from './task-scheduler.js';
|
|
18
|
+
import { startSchedulerLoop, stopScheduler, wake } from './task-scheduler.js';
|
|
19
|
+
import { startHealthServer, stopHealthServer } from './health.js';
|
|
18
20
|
import { runAgent, writeTasksSnapshot, writeGroupsSnapshot } from './agent-runner.js';
|
|
19
21
|
import { loadJson, saveJson } from './utils.js';
|
|
20
22
|
import { MessageQueue } from './message-queue.js';
|
|
@@ -59,7 +61,8 @@ class ChannelManager {
|
|
|
59
61
|
try {
|
|
60
62
|
return await channel.sendMessage(chatId, content);
|
|
61
63
|
}
|
|
62
|
-
catch {
|
|
64
|
+
catch (err) {
|
|
65
|
+
logger.debug({ channel: channel.name, chatId, err }, '渠道发送消息失败,尝试下一个');
|
|
63
66
|
continue;
|
|
64
67
|
}
|
|
65
68
|
}
|
|
@@ -81,7 +84,8 @@ class ChannelManager {
|
|
|
81
84
|
await channel.updateMessage(messageId, content);
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
84
|
-
catch {
|
|
87
|
+
catch (err) {
|
|
88
|
+
logger.debug({ channel: channel.name, messageId, err }, '渠道更新消息失败,尝试下一个');
|
|
85
89
|
continue;
|
|
86
90
|
}
|
|
87
91
|
}
|
|
@@ -101,11 +105,39 @@ class ChannelManager {
|
|
|
101
105
|
await channel.deleteMessage(messageId);
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
104
|
-
catch {
|
|
108
|
+
catch (err) {
|
|
109
|
+
logger.debug({ channel: channel.name, messageId, err }, '渠道删除消息失败,尝试下一个');
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async sendImage(chatId, imageData, caption, platform) {
|
|
116
|
+
// 如果指定了平台,使用指定的渠道
|
|
117
|
+
if (platform) {
|
|
118
|
+
const channel = this.channels.find(c => c.name === platform);
|
|
119
|
+
if (channel?.sendImage) {
|
|
120
|
+
return await channel.sendImage(chatId, imageData, caption);
|
|
121
|
+
}
|
|
122
|
+
// 渠道不支持发送图片,降级为发送文本提示
|
|
123
|
+
logger.warn({ platform, chatId }, '渠道不支持发送图片,已降级');
|
|
124
|
+
return await this.sendMessage(chatId, caption || '[图片无法显示]', platform);
|
|
125
|
+
}
|
|
126
|
+
// 尝试所有支持图片的渠道
|
|
127
|
+
for (const channel of this.channels) {
|
|
128
|
+
if (channel.sendImage) {
|
|
129
|
+
try {
|
|
130
|
+
return await channel.sendImage(chatId, imageData, caption);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
logger.debug({ channel: channel.name, chatId, err }, '渠道发送图片失败,尝试下一个');
|
|
105
134
|
continue;
|
|
106
135
|
}
|
|
107
136
|
}
|
|
108
137
|
}
|
|
138
|
+
// 所有渠道都不支持,降级为文本
|
|
139
|
+
logger.warn({ chatId }, '没有渠道支持发送图片,已降级');
|
|
140
|
+
return await this.sendMessage(chatId, caption || '[图片无法显示]', platform);
|
|
109
141
|
}
|
|
110
142
|
getEnabledPlatforms() {
|
|
111
143
|
return this.enabledPlatforms;
|
|
@@ -132,24 +164,12 @@ let registeredGroups = {};
|
|
|
132
164
|
let lastAgentTimestamp = {};
|
|
133
165
|
let messageQueue;
|
|
134
166
|
let isShuttingDown = false;
|
|
135
|
-
// 消息历史上下文配置
|
|
136
|
-
// 现在由 MemoryManager 基于 token 自动管理,这里只是一个备用限制
|
|
137
|
-
const HISTORY_CONTEXT_LIMIT = 500;
|
|
138
|
-
// "正在思考..." 提示配置
|
|
139
|
-
// 注意:飞书不支持更新普通文本消息,所以默认禁用此功能
|
|
140
|
-
// 如需启用,设置环境变量 THINKING_THRESHOLD_MS=2500(毫秒)
|
|
141
|
-
const THINKING_THRESHOLD_MS = Number(process.env.THINKING_THRESHOLD_MS ?? 0);
|
|
142
167
|
// 直接网页抓取触发(避免模型不触发工具)
|
|
143
168
|
const WEB_FETCH_TOOL_NAME = 'web_fetch';
|
|
144
169
|
const WEB_FETCH_INTENT_RE = /(抓取|获取|读取|访问|打开|爬取|网页|网站|链接|fetch|web)/i;
|
|
145
170
|
const WEB_FETCH_URL_RE = /https?:\/\/[^\s<>()]+/i;
|
|
146
171
|
const WEB_FETCH_DOMAIN_RE = /(?:^|[^A-Za-z0-9.-])((?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,})(:\d{2,5})?(\/[^\s<>()]*)?/i;
|
|
147
172
|
const TRAILING_PUNCT_RE = /[)\],.。,;;!!??]+$/;
|
|
148
|
-
const MAX_DIRECT_FETCH_CHARS = 4000;
|
|
149
|
-
const MAX_IPC_FILE_BYTES = 1024 * 1024;
|
|
150
|
-
const MAX_IPC_MESSAGE_CHARS = 10000;
|
|
151
|
-
const MAX_IPC_CHAT_ID_CHARS = 256;
|
|
152
|
-
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
153
173
|
// ==================== 状态管理 ====================
|
|
154
174
|
// 默认的 main 群组配置模板(用于自动注册新会话)
|
|
155
175
|
const DEFAULT_MAIN_GROUP = {
|
|
@@ -159,11 +179,12 @@ const DEFAULT_MAIN_GROUP = {
|
|
|
159
179
|
added_at: new Date().toISOString()
|
|
160
180
|
};
|
|
161
181
|
function loadState() {
|
|
162
|
-
const
|
|
182
|
+
const dataDir = paths.data();
|
|
183
|
+
const statePath = path.join(dataDir, 'router_state.json');
|
|
163
184
|
const state = loadJson(statePath, {});
|
|
164
185
|
lastAgentTimestamp = state.last_agent_timestamp || {};
|
|
165
|
-
sessions = loadJson(path.join(
|
|
166
|
-
registeredGroups = loadJson(path.join(
|
|
186
|
+
sessions = loadJson(path.join(dataDir, 'sessions.json'), {});
|
|
187
|
+
registeredGroups = loadJson(path.join(dataDir, 'registered_groups.json'), {});
|
|
167
188
|
// 确保有 main 群组配置模板(用于自动注册)
|
|
168
189
|
const hasMainGroup = Object.values(registeredGroups).some(g => g.folder === MAIN_GROUP_FOLDER);
|
|
169
190
|
if (!hasMainGroup) {
|
|
@@ -174,12 +195,17 @@ function loadState() {
|
|
|
174
195
|
logger.info({ groupCount: Object.keys(registeredGroups).length }, '⚡ 状态已加载');
|
|
175
196
|
}
|
|
176
197
|
function saveState() {
|
|
177
|
-
|
|
178
|
-
saveJson(path.join(
|
|
198
|
+
const dataDir = paths.data();
|
|
199
|
+
saveJson(path.join(dataDir, 'router_state.json'), { last_agent_timestamp: lastAgentTimestamp });
|
|
200
|
+
saveJson(path.join(dataDir, 'sessions.json'), sessions);
|
|
179
201
|
}
|
|
180
202
|
function registerGroup(chatId, group) {
|
|
181
203
|
registeredGroups[chatId] = group;
|
|
182
|
-
saveJson(path.join(
|
|
204
|
+
saveJson(path.join(paths.data(), 'registered_groups.json'), registeredGroups);
|
|
205
|
+
// 同步更新全局 Map
|
|
206
|
+
if (global.__flashclaw_registered_groups) {
|
|
207
|
+
global.__flashclaw_registered_groups.set(chatId, group);
|
|
208
|
+
}
|
|
183
209
|
// 创建群组文件夹
|
|
184
210
|
const groupDir = path.join(paths.groups(), group.folder);
|
|
185
211
|
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
|
|
@@ -224,7 +250,7 @@ function shouldTriggerAgent(msg, group) {
|
|
|
224
250
|
}
|
|
225
251
|
return false;
|
|
226
252
|
}
|
|
227
|
-
function extractFirstUrl(text) {
|
|
253
|
+
export function extractFirstUrl(text) {
|
|
228
254
|
const match = text.match(WEB_FETCH_URL_RE);
|
|
229
255
|
if (match) {
|
|
230
256
|
return match[0].replace(TRAILING_PUNCT_RE, '');
|
|
@@ -238,7 +264,7 @@ function extractFirstUrl(text) {
|
|
|
238
264
|
const candidate = `https://${host}${port}${path}`;
|
|
239
265
|
return candidate.replace(TRAILING_PUNCT_RE, '');
|
|
240
266
|
}
|
|
241
|
-
function isPrivateIpv4(ip) {
|
|
267
|
+
export function isPrivateIpv4(ip) {
|
|
242
268
|
const parts = ip.split('.').map((part) => Number(part));
|
|
243
269
|
if (parts.length !== 4 || parts.some((part) => Number.isNaN(part)))
|
|
244
270
|
return false;
|
|
@@ -259,7 +285,7 @@ function isPrivateIpv4(ip) {
|
|
|
259
285
|
return true;
|
|
260
286
|
return false;
|
|
261
287
|
}
|
|
262
|
-
function isPrivateIpv6(ip) {
|
|
288
|
+
export function isPrivateIpv6(ip) {
|
|
263
289
|
const normalized = ip.toLowerCase();
|
|
264
290
|
if (normalized === '::' || normalized === '::1')
|
|
265
291
|
return true;
|
|
@@ -276,7 +302,7 @@ function isPrivateIpv6(ip) {
|
|
|
276
302
|
}
|
|
277
303
|
return false;
|
|
278
304
|
}
|
|
279
|
-
function isPrivateIp(ip) {
|
|
305
|
+
export function isPrivateIp(ip) {
|
|
280
306
|
const family = isIP(ip);
|
|
281
307
|
if (family === 4)
|
|
282
308
|
return isPrivateIpv4(ip);
|
|
@@ -284,7 +310,7 @@ function isPrivateIp(ip) {
|
|
|
284
310
|
return isPrivateIpv6(ip);
|
|
285
311
|
return false;
|
|
286
312
|
}
|
|
287
|
-
function isBlockedHostname(hostname) {
|
|
313
|
+
export function isBlockedHostname(hostname) {
|
|
288
314
|
const normalized = hostname.trim().toLowerCase();
|
|
289
315
|
if (normalized === 'localhost')
|
|
290
316
|
return true;
|
|
@@ -292,7 +318,7 @@ function isBlockedHostname(hostname) {
|
|
|
292
318
|
normalized.endsWith('.local') ||
|
|
293
319
|
normalized.endsWith('.internal'));
|
|
294
320
|
}
|
|
295
|
-
function estimateBase64Bytes(content) {
|
|
321
|
+
export function estimateBase64Bytes(content) {
|
|
296
322
|
if (!content)
|
|
297
323
|
return null;
|
|
298
324
|
const raw = content.startsWith('data:') ? content.split(',')[1] ?? '' : content;
|
|
@@ -302,13 +328,13 @@ function estimateBase64Bytes(content) {
|
|
|
302
328
|
const padding = normalized.endsWith('==') ? 2 : normalized.endsWith('=') ? 1 : 0;
|
|
303
329
|
return Math.max(0, Math.floor((normalized.length * 3) / 4) - padding);
|
|
304
330
|
}
|
|
305
|
-
function truncateText(text, maxLength) {
|
|
331
|
+
export function truncateText(text, maxLength) {
|
|
306
332
|
if (text.length <= maxLength) {
|
|
307
333
|
return { text, truncated: false };
|
|
308
334
|
}
|
|
309
335
|
return { text: `${text.slice(0, maxLength)}\n\n...(内容已截断)`, truncated: true };
|
|
310
336
|
}
|
|
311
|
-
function formatDirectWebFetchResponse(url, result) {
|
|
337
|
+
export function formatDirectWebFetchResponse(url, result) {
|
|
312
338
|
if (!result.success) {
|
|
313
339
|
return `❌ 抓取失败: ${result.error || '未知错误'}`;
|
|
314
340
|
}
|
|
@@ -375,6 +401,9 @@ async function tryHandleDirectWebFetch(msg, group) {
|
|
|
375
401
|
userId: msg.senderId,
|
|
376
402
|
sendMessage: async (text) => {
|
|
377
403
|
await sendMessage(msg.chatId, `${BOT_NAME}: ${text}`, msg.platform);
|
|
404
|
+
},
|
|
405
|
+
sendImage: async (imageData, caption) => {
|
|
406
|
+
await channelManager.sendImage(msg.chatId, imageData, caption, msg.platform);
|
|
378
407
|
}
|
|
379
408
|
};
|
|
380
409
|
const normalizedUrl = urlObj.toString();
|
|
@@ -469,8 +498,8 @@ async function processQueuedMessage(queuedMsg) {
|
|
|
469
498
|
logger.debug({ chatId, messageId: placeholderMessageId }, '已发送思考提示');
|
|
470
499
|
}
|
|
471
500
|
}
|
|
472
|
-
catch {
|
|
473
|
-
|
|
501
|
+
catch (err) {
|
|
502
|
+
logger.debug({ chatId, err }, '发送思考提示失败');
|
|
474
503
|
}
|
|
475
504
|
}, THINKING_THRESHOLD_MS) : null;
|
|
476
505
|
try {
|
|
@@ -492,12 +521,15 @@ async function processQueuedMessage(queuedMsg) {
|
|
|
492
521
|
await channelManager.updateMessage(placeholderMessageId, finalText, msg.platform);
|
|
493
522
|
logger.info({ chatId, messageId: placeholderMessageId }, '⚡ 消息已更新');
|
|
494
523
|
}
|
|
495
|
-
catch {
|
|
524
|
+
catch (updateErr) {
|
|
496
525
|
// 更新失败,尝试删除并发送新消息
|
|
526
|
+
logger.debug({ chatId, messageId: placeholderMessageId, err: updateErr }, '更新占位消息失败,尝试删除并重发');
|
|
497
527
|
try {
|
|
498
528
|
await channelManager.deleteMessage(placeholderMessageId, msg.platform);
|
|
499
529
|
}
|
|
500
|
-
catch {
|
|
530
|
+
catch (deleteErr) {
|
|
531
|
+
logger.debug({ chatId, messageId: placeholderMessageId, err: deleteErr }, '删除占位消息失败');
|
|
532
|
+
}
|
|
501
533
|
await sendMessage(chatId, finalText, msg.platform);
|
|
502
534
|
}
|
|
503
535
|
}
|
|
@@ -520,7 +552,9 @@ async function processQueuedMessage(queuedMsg) {
|
|
|
520
552
|
try {
|
|
521
553
|
await channelManager.deleteMessage(placeholderMessageId, msg.platform);
|
|
522
554
|
}
|
|
523
|
-
catch {
|
|
555
|
+
catch (deleteErr) {
|
|
556
|
+
logger.debug({ chatId, messageId: placeholderMessageId, err: deleteErr }, '删除占位消息失败(无响应)');
|
|
557
|
+
}
|
|
524
558
|
}
|
|
525
559
|
}
|
|
526
560
|
catch (err) {
|
|
@@ -533,7 +567,9 @@ async function processQueuedMessage(queuedMsg) {
|
|
|
533
567
|
try {
|
|
534
568
|
await channelManager.deleteMessage(placeholderMessageId, msg.platform);
|
|
535
569
|
}
|
|
536
|
-
catch {
|
|
570
|
+
catch (deleteErr) {
|
|
571
|
+
logger.debug({ chatId, messageId: placeholderMessageId, err: deleteErr }, '删除占位消息失败(错误恢复)');
|
|
572
|
+
}
|
|
537
573
|
}
|
|
538
574
|
throw err;
|
|
539
575
|
}
|
|
@@ -726,7 +762,7 @@ async function executeAgent(group, prompt, chatId, options) {
|
|
|
726
762
|
});
|
|
727
763
|
if (output.newSessionId) {
|
|
728
764
|
sessions[group.folder] = output.newSessionId;
|
|
729
|
-
saveJson(path.join(
|
|
765
|
+
saveJson(path.join(paths.data(), 'sessions.json'), sessions);
|
|
730
766
|
}
|
|
731
767
|
if (output.status === 'error') {
|
|
732
768
|
logger.error({ group: group.name, error: output.error }, 'Agent 错误');
|
|
@@ -764,7 +800,7 @@ function quarantineIpcFile(ipcBaseDir, sourceGroup, filePath, reason, err) {
|
|
|
764
800
|
logger.warn({ file: fileName, sourceGroup, reason, err }, 'IPC 文件已隔离');
|
|
765
801
|
}
|
|
766
802
|
function startIpcWatcher() {
|
|
767
|
-
const ipcBaseDir = path.join(
|
|
803
|
+
const ipcBaseDir = path.join(paths.data(), 'ipc');
|
|
768
804
|
fs.mkdirSync(ipcBaseDir, { recursive: true });
|
|
769
805
|
const processIpcFiles = async () => {
|
|
770
806
|
let groupFolders;
|
|
@@ -816,6 +852,28 @@ function startIpcWatcher() {
|
|
|
816
852
|
logger.warn({ chatId: data.chatJid, sourceGroup }, '未授权的 IPC 消息被阻止');
|
|
817
853
|
}
|
|
818
854
|
}
|
|
855
|
+
else if (data.type === 'image' && data.chatJid && data.imageData) {
|
|
856
|
+
// 处理图片消息
|
|
857
|
+
if (typeof data.chatJid !== 'string' || data.chatJid.length > MAX_IPC_CHAT_ID_CHARS) {
|
|
858
|
+
logger.warn({ sourceGroup }, 'IPC 图片消息 chatJid 格式不合法');
|
|
859
|
+
fs.unlinkSync(filePath);
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (typeof data.imageData !== 'string') {
|
|
863
|
+
logger.warn({ sourceGroup }, 'IPC 图片消息 imageData 格式不合法');
|
|
864
|
+
fs.unlinkSync(filePath);
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
const targetGroup = registeredGroups[data.chatJid];
|
|
868
|
+
if (isMain || (targetGroup && targetGroup.folder === sourceGroup)) {
|
|
869
|
+
const caption = data.caption ? `${BOT_NAME}: ${data.caption}` : undefined;
|
|
870
|
+
await channelManager.sendImage(data.chatJid, data.imageData, caption);
|
|
871
|
+
logger.info({ chatId: data.chatJid, sourceGroup }, 'IPC 图片已发送');
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
logger.warn({ chatId: data.chatJid, sourceGroup }, '未授权的 IPC 图片被阻止');
|
|
875
|
+
}
|
|
876
|
+
}
|
|
819
877
|
fs.unlinkSync(filePath);
|
|
820
878
|
}
|
|
821
879
|
catch (err) {
|
|
@@ -860,128 +918,171 @@ function startIpcWatcher() {
|
|
|
860
918
|
processIpcFiles();
|
|
861
919
|
logger.info('⚡ IPC 监听已启动');
|
|
862
920
|
}
|
|
863
|
-
|
|
921
|
+
// ==================== IPC Schema 验证 ====================
|
|
922
|
+
/** 基础 IPC 消息 schema */
|
|
923
|
+
const IpcBaseSchema = z.object({
|
|
924
|
+
type: z.string().min(1).max(50),
|
|
925
|
+
});
|
|
926
|
+
/** schedule_task IPC schema */
|
|
927
|
+
const IpcScheduleTaskSchema = IpcBaseSchema.extend({
|
|
928
|
+
type: z.literal('schedule_task'),
|
|
929
|
+
prompt: z.string().min(1).max(10000),
|
|
930
|
+
schedule_type: z.enum(['cron', 'interval', 'once']),
|
|
931
|
+
schedule_value: z.string().min(1).max(200),
|
|
932
|
+
groupFolder: z.string().min(1).max(100),
|
|
933
|
+
context_mode: z.enum(['group', 'isolated']).optional(),
|
|
934
|
+
max_retries: z.number().int().min(0).max(10).optional(),
|
|
935
|
+
timeout_ms: z.number().int().min(1000).max(3600000).optional(),
|
|
936
|
+
});
|
|
937
|
+
/** pause/resume/cancel task IPC schema */
|
|
938
|
+
const IpcTaskActionSchema = IpcBaseSchema.extend({
|
|
939
|
+
type: z.enum(['pause_task', 'resume_task', 'cancel_task']),
|
|
940
|
+
taskId: z.string().min(1).max(100),
|
|
941
|
+
});
|
|
942
|
+
/** register_group IPC schema */
|
|
943
|
+
const IpcRegisterGroupSchema = IpcBaseSchema.extend({
|
|
944
|
+
type: z.literal('register_group'),
|
|
945
|
+
jid: z.string().min(1).max(256),
|
|
946
|
+
name: z.string().min(1).max(200),
|
|
947
|
+
folder: z.string().min(1).max(100).regex(/^[a-zA-Z0-9_-]+$/),
|
|
948
|
+
trigger: z.string().min(1).max(50),
|
|
949
|
+
agentConfig: z.object({
|
|
950
|
+
timeout: z.number().int().min(1000).max(3600000).optional(),
|
|
951
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
952
|
+
}).optional(),
|
|
953
|
+
});
|
|
954
|
+
/** 联合 IPC schema */
|
|
955
|
+
const IpcMessageSchema = z.discriminatedUnion('type', [
|
|
956
|
+
IpcScheduleTaskSchema,
|
|
957
|
+
IpcTaskActionSchema.extend({ type: z.literal('pause_task') }),
|
|
958
|
+
IpcTaskActionSchema.extend({ type: z.literal('resume_task') }),
|
|
959
|
+
IpcTaskActionSchema.extend({ type: z.literal('cancel_task') }),
|
|
960
|
+
IpcRegisterGroupSchema,
|
|
961
|
+
]);
|
|
962
|
+
async function processTaskIpc(rawData, sourceGroup, isMain) {
|
|
963
|
+
// Zod schema 验证
|
|
964
|
+
const parseResult = IpcMessageSchema.safeParse(rawData);
|
|
965
|
+
if (!parseResult.success) {
|
|
966
|
+
logger.warn({
|
|
967
|
+
sourceGroup,
|
|
968
|
+
errors: parseResult.error.flatten().fieldErrors
|
|
969
|
+
}, 'IPC 消息验证失败');
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const data = parseResult.data;
|
|
864
973
|
const { createTask, updateTask, deleteTask, getTaskById: getTask } = await import('./db.js');
|
|
865
974
|
const { CronExpressionParser } = await import('cron-parser');
|
|
866
975
|
switch (data.type) {
|
|
867
|
-
case 'schedule_task':
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
976
|
+
case 'schedule_task': {
|
|
977
|
+
// Zod 已验证必填字段,直接使用
|
|
978
|
+
const targetGroup = data.groupFolder;
|
|
979
|
+
if (!isMain && targetGroup !== sourceGroup) {
|
|
980
|
+
logger.warn({ sourceGroup, targetGroup }, '未授权的 schedule_task 被阻止');
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
const targetChatId = Object.entries(registeredGroups).find(([, group]) => group.folder === targetGroup)?.[0];
|
|
984
|
+
if (!targetChatId) {
|
|
985
|
+
logger.warn({ targetGroup }, '无法创建任务:目标群组未注册');
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
const scheduleType = data.schedule_type;
|
|
989
|
+
let nextRun = null;
|
|
990
|
+
if (scheduleType === 'cron') {
|
|
991
|
+
try {
|
|
992
|
+
const interval = CronExpressionParser.parse(data.schedule_value, { tz: TIMEZONE });
|
|
993
|
+
nextRun = interval.next().toISOString();
|
|
873
994
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
logger.warn({ targetGroup }, '无法创建任务:目标群组未注册');
|
|
995
|
+
catch {
|
|
996
|
+
logger.warn({ scheduleValue: data.schedule_value }, '无效的 cron 表达式');
|
|
877
997
|
break;
|
|
878
998
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
catch {
|
|
887
|
-
logger.warn({ scheduleValue: data.schedule_value }, '无效的 cron 表达式');
|
|
888
|
-
break;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
else if (scheduleType === 'interval') {
|
|
892
|
-
const ms = parseInt(data.schedule_value, 10);
|
|
893
|
-
if (isNaN(ms) || ms <= 0) {
|
|
894
|
-
logger.warn({ scheduleValue: data.schedule_value }, '无效的间隔值');
|
|
895
|
-
break;
|
|
896
|
-
}
|
|
897
|
-
nextRun = new Date(Date.now() + ms).toISOString();
|
|
999
|
+
}
|
|
1000
|
+
else if (scheduleType === 'interval') {
|
|
1001
|
+
const ms = parseInt(data.schedule_value, 10);
|
|
1002
|
+
if (isNaN(ms) || ms <= 0) {
|
|
1003
|
+
logger.warn({ scheduleValue: data.schedule_value }, '无效的间隔值');
|
|
1004
|
+
break;
|
|
898
1005
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
}
|
|
905
|
-
|
|
1006
|
+
nextRun = new Date(Date.now() + ms).toISOString();
|
|
1007
|
+
}
|
|
1008
|
+
else if (scheduleType === 'once') {
|
|
1009
|
+
const scheduled = new Date(data.schedule_value);
|
|
1010
|
+
if (isNaN(scheduled.getTime())) {
|
|
1011
|
+
logger.warn({ scheduleValue: data.schedule_value }, '无效的时间戳');
|
|
1012
|
+
break;
|
|
906
1013
|
}
|
|
907
|
-
|
|
908
|
-
const contextMode = (data.context_mode === 'group' || data.context_mode === 'isolated')
|
|
909
|
-
? data.context_mode
|
|
910
|
-
: 'isolated';
|
|
911
|
-
createTask({
|
|
912
|
-
id: taskId,
|
|
913
|
-
group_folder: targetGroup,
|
|
914
|
-
chat_jid: targetChatId,
|
|
915
|
-
prompt: data.prompt,
|
|
916
|
-
schedule_type: scheduleType,
|
|
917
|
-
schedule_value: data.schedule_value,
|
|
918
|
-
context_mode: contextMode,
|
|
919
|
-
next_run: nextRun,
|
|
920
|
-
status: 'active',
|
|
921
|
-
created_at: new Date().toISOString(),
|
|
922
|
-
retry_count: 0,
|
|
923
|
-
max_retries: data.max_retries ?? 3,
|
|
924
|
-
timeout_ms: data.timeout_ms ?? 300000
|
|
925
|
-
});
|
|
926
|
-
logger.info({ taskId, sourceGroup, targetGroup, contextMode }, '⚡ 任务已创建');
|
|
1014
|
+
nextRun = scheduled.toISOString();
|
|
927
1015
|
}
|
|
1016
|
+
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1017
|
+
const contextMode = data.context_mode ?? 'isolated';
|
|
1018
|
+
createTask({
|
|
1019
|
+
id: taskId,
|
|
1020
|
+
group_folder: targetGroup,
|
|
1021
|
+
chat_jid: targetChatId,
|
|
1022
|
+
prompt: data.prompt,
|
|
1023
|
+
schedule_type: scheduleType,
|
|
1024
|
+
schedule_value: data.schedule_value,
|
|
1025
|
+
context_mode: contextMode,
|
|
1026
|
+
next_run: nextRun,
|
|
1027
|
+
status: 'active',
|
|
1028
|
+
created_at: new Date().toISOString(),
|
|
1029
|
+
retry_count: 0,
|
|
1030
|
+
max_retries: data.max_retries ?? 3,
|
|
1031
|
+
timeout_ms: data.timeout_ms ?? 300000
|
|
1032
|
+
});
|
|
1033
|
+
// 唤醒调度器,确保新任务立即生效
|
|
1034
|
+
wake();
|
|
1035
|
+
logger.info({ taskId, sourceGroup, targetGroup, contextMode }, '⚡ 任务已创建');
|
|
928
1036
|
break;
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
1037
|
+
}
|
|
1038
|
+
case 'pause_task': {
|
|
1039
|
+
const task = getTask(data.taskId);
|
|
1040
|
+
if (task && (isMain || task.group_folder === sourceGroup)) {
|
|
1041
|
+
updateTask(data.taskId, { status: 'paused' });
|
|
1042
|
+
logger.info({ taskId: data.taskId, sourceGroup }, '任务已暂停');
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
logger.warn({ taskId: data.taskId, sourceGroup }, '未授权的任务暂停操作');
|
|
939
1046
|
}
|
|
940
1047
|
break;
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
1048
|
+
}
|
|
1049
|
+
case 'resume_task': {
|
|
1050
|
+
const task = getTask(data.taskId);
|
|
1051
|
+
if (task && (isMain || task.group_folder === sourceGroup)) {
|
|
1052
|
+
updateTask(data.taskId, { status: 'active' });
|
|
1053
|
+
logger.info({ taskId: data.taskId, sourceGroup }, '任务已恢复');
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
logger.warn({ taskId: data.taskId, sourceGroup }, '未授权的任务恢复操作');
|
|
951
1057
|
}
|
|
952
1058
|
break;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
1059
|
+
}
|
|
1060
|
+
case 'cancel_task': {
|
|
1061
|
+
const task = getTask(data.taskId);
|
|
1062
|
+
if (task && (isMain || task.group_folder === sourceGroup)) {
|
|
1063
|
+
deleteTask(data.taskId);
|
|
1064
|
+
logger.info({ taskId: data.taskId, sourceGroup }, '任务已取消');
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
logger.warn({ taskId: data.taskId, sourceGroup }, '未授权的任务取消操作');
|
|
963
1068
|
}
|
|
964
1069
|
break;
|
|
965
|
-
|
|
1070
|
+
}
|
|
1071
|
+
case 'register_group': {
|
|
966
1072
|
if (!isMain) {
|
|
967
1073
|
logger.warn({ sourceGroup }, '未授权的 register_group 被阻止');
|
|
968
1074
|
break;
|
|
969
1075
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
}
|
|
979
|
-
else {
|
|
980
|
-
logger.warn({ data }, '无效的 register_group 请求');
|
|
981
|
-
}
|
|
1076
|
+
// Zod 已验证必填字段,直接使用
|
|
1077
|
+
registerGroup(data.jid, {
|
|
1078
|
+
name: data.name,
|
|
1079
|
+
folder: data.folder,
|
|
1080
|
+
trigger: data.trigger,
|
|
1081
|
+
added_at: new Date().toISOString(),
|
|
1082
|
+
agentConfig: data.agentConfig
|
|
1083
|
+
});
|
|
982
1084
|
break;
|
|
983
|
-
|
|
984
|
-
logger.warn({ type: data.type }, '未知的 IPC 任务类型');
|
|
1085
|
+
}
|
|
985
1086
|
}
|
|
986
1087
|
}
|
|
987
1088
|
// ==================== 启动横幅 ====================
|
|
@@ -1074,12 +1175,15 @@ export async function main() {
|
|
|
1074
1175
|
logger.info({ platforms: enabledPlatforms }, '⚡ 渠道管理器已初始化');
|
|
1075
1176
|
// 加载状态
|
|
1076
1177
|
loadState();
|
|
1178
|
+
// 注入全局变量,供 Web UI 等插件使用
|
|
1179
|
+
global.__flashclaw_run_agent = runAgent;
|
|
1180
|
+
global.__flashclaw_registered_groups = new Map(Object.entries(registeredGroups));
|
|
1077
1181
|
// 初始化消息队列
|
|
1078
1182
|
messageQueue = new MessageQueue(processQueuedMessage, {
|
|
1079
|
-
maxQueueSize:
|
|
1080
|
-
maxConcurrent:
|
|
1081
|
-
processingTimeout:
|
|
1082
|
-
maxRetries:
|
|
1183
|
+
maxQueueSize: MESSAGE_QUEUE_MAX_SIZE,
|
|
1184
|
+
maxConcurrent: MESSAGE_QUEUE_MAX_CONCURRENT,
|
|
1185
|
+
processingTimeout: MESSAGE_QUEUE_PROCESSING_TIMEOUT_MS,
|
|
1186
|
+
maxRetries: MESSAGE_QUEUE_MAX_RETRIES
|
|
1083
1187
|
});
|
|
1084
1188
|
messageQueue.start();
|
|
1085
1189
|
logger.info('⚡ 消息队列已初始化');
|
|
@@ -1101,6 +1205,11 @@ export async function main() {
|
|
|
1101
1205
|
platforms: enabledPlatforms,
|
|
1102
1206
|
groups: groupCount
|
|
1103
1207
|
}, '⚡ FlashClaw 已启动');
|
|
1208
|
+
// 启动健康检查服务(可通过 HEALTH_PORT 环境变量配置端口,默认 9090)
|
|
1209
|
+
const healthPort = parseInt(process.env.HEALTH_PORT || '9090', 10);
|
|
1210
|
+
if (healthPort > 0) {
|
|
1211
|
+
startHealthServer(healthPort);
|
|
1212
|
+
}
|
|
1104
1213
|
// 注册优雅关闭处理
|
|
1105
1214
|
setupGracefulShutdown();
|
|
1106
1215
|
}
|
|
@@ -1144,7 +1253,10 @@ async function gracefulShutdown(signal) {
|
|
|
1144
1253
|
// 7. 卸载插件
|
|
1145
1254
|
logger.info('⚡ 卸载插件...');
|
|
1146
1255
|
await pluginManager.clear();
|
|
1147
|
-
// 8.
|
|
1256
|
+
// 8. 停止健康检查服务
|
|
1257
|
+
logger.info('⚡ 停止健康检查服务...');
|
|
1258
|
+
stopHealthServer();
|
|
1259
|
+
// 9. 保存状态
|
|
1148
1260
|
logger.info('⚡ 保存状态...');
|
|
1149
1261
|
saveState();
|
|
1150
1262
|
logger.info('⚡ FlashClaw 已安全关闭');
|
|
@@ -1173,9 +1285,11 @@ function setupGracefulShutdown() {
|
|
|
1173
1285
|
logger.error({ reason }, '未处理的 Promise 拒绝');
|
|
1174
1286
|
});
|
|
1175
1287
|
}
|
|
1176
|
-
//
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1288
|
+
// 直接运行时启动(测试环境可通过 FLASHCLAW_SKIP_MAIN=1 禁用)
|
|
1289
|
+
if (process.env.FLASHCLAW_SKIP_MAIN !== '1') {
|
|
1290
|
+
main().catch(err => {
|
|
1291
|
+
logger.error({ err }, '⚡ FlashClaw 启动失败');
|
|
1292
|
+
process.exit(1);
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1181
1295
|
//# sourceMappingURL=index.js.map
|