flashclaw 1.0.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/config/plugins.json +23 -0
  4. package/dist/agent-runner.d.ts +103 -0
  5. package/dist/agent-runner.d.ts.map +1 -0
  6. package/dist/agent-runner.js +530 -0
  7. package/dist/agent-runner.js.map +1 -0
  8. package/dist/cli.d.ts +7 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +497 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands.d.ts +68 -0
  13. package/dist/commands.d.ts.map +1 -0
  14. package/dist/commands.js +252 -0
  15. package/dist/commands.js.map +1 -0
  16. package/dist/config-schema.d.ts +21 -0
  17. package/dist/config-schema.d.ts.map +1 -0
  18. package/dist/config-schema.js +26 -0
  19. package/dist/config-schema.js.map +1 -0
  20. package/dist/config.d.ts +11 -0
  21. package/dist/config.d.ts.map +1 -0
  22. package/dist/config.js +36 -0
  23. package/dist/config.js.map +1 -0
  24. package/dist/core/api-client.d.ts +236 -0
  25. package/dist/core/api-client.d.ts.map +1 -0
  26. package/dist/core/api-client.js +369 -0
  27. package/dist/core/api-client.js.map +1 -0
  28. package/dist/core/memory.d.ts +291 -0
  29. package/dist/core/memory.d.ts.map +1 -0
  30. package/dist/core/memory.js +754 -0
  31. package/dist/core/memory.js.map +1 -0
  32. package/dist/core/model-capabilities.d.ts +45 -0
  33. package/dist/core/model-capabilities.d.ts.map +1 -0
  34. package/dist/core/model-capabilities.js +85 -0
  35. package/dist/core/model-capabilities.js.map +1 -0
  36. package/dist/db.d.ts +103 -0
  37. package/dist/db.d.ts.map +1 -0
  38. package/dist/db.js +380 -0
  39. package/dist/db.js.map +1 -0
  40. package/dist/errors.d.ts +22 -0
  41. package/dist/errors.d.ts.map +1 -0
  42. package/dist/errors.js +44 -0
  43. package/dist/errors.js.map +1 -0
  44. package/dist/health.d.ts +27 -0
  45. package/dist/health.d.ts.map +1 -0
  46. package/dist/health.js +55 -0
  47. package/dist/health.js.map +1 -0
  48. package/dist/index.d.ts +11 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +1181 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/logger.d.ts +9 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +19 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/message-queue.d.ts +69 -0
  57. package/dist/message-queue.d.ts.map +1 -0
  58. package/dist/message-queue.js +198 -0
  59. package/dist/message-queue.js.map +1 -0
  60. package/dist/metrics.d.ts +46 -0
  61. package/dist/metrics.d.ts.map +1 -0
  62. package/dist/metrics.js +101 -0
  63. package/dist/metrics.js.map +1 -0
  64. package/dist/paths.d.ts +81 -0
  65. package/dist/paths.d.ts.map +1 -0
  66. package/dist/paths.js +127 -0
  67. package/dist/paths.js.map +1 -0
  68. package/dist/plugins/index.d.ts +9 -0
  69. package/dist/plugins/index.d.ts.map +1 -0
  70. package/dist/plugins/index.js +13 -0
  71. package/dist/plugins/index.js.map +1 -0
  72. package/dist/plugins/installer.d.ts +120 -0
  73. package/dist/plugins/installer.d.ts.map +1 -0
  74. package/dist/plugins/installer.js +1008 -0
  75. package/dist/plugins/installer.js.map +1 -0
  76. package/dist/plugins/loader.d.ts +37 -0
  77. package/dist/plugins/loader.d.ts.map +1 -0
  78. package/dist/plugins/loader.js +429 -0
  79. package/dist/plugins/loader.js.map +1 -0
  80. package/dist/plugins/manager.d.ts +72 -0
  81. package/dist/plugins/manager.d.ts.map +1 -0
  82. package/dist/plugins/manager.js +187 -0
  83. package/dist/plugins/manager.js.map +1 -0
  84. package/dist/plugins/types.d.ts +101 -0
  85. package/dist/plugins/types.d.ts.map +1 -0
  86. package/dist/plugins/types.js +12 -0
  87. package/dist/plugins/types.js.map +1 -0
  88. package/dist/session-tracker.d.ts +81 -0
  89. package/dist/session-tracker.d.ts.map +1 -0
  90. package/dist/session-tracker.js +228 -0
  91. package/dist/session-tracker.js.map +1 -0
  92. package/dist/task-scheduler.d.ts +47 -0
  93. package/dist/task-scheduler.d.ts.map +1 -0
  94. package/dist/task-scheduler.js +331 -0
  95. package/dist/task-scheduler.js.map +1 -0
  96. package/dist/types.d.ts +57 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +2 -0
  99. package/dist/types.js.map +1 -0
  100. package/dist/utils/env-substitute.d.ts +63 -0
  101. package/dist/utils/env-substitute.d.ts.map +1 -0
  102. package/dist/utils/env-substitute.js +133 -0
  103. package/dist/utils/env-substitute.js.map +1 -0
  104. package/dist/utils/log-rotate.d.ts +19 -0
  105. package/dist/utils/log-rotate.d.ts.map +1 -0
  106. package/dist/utils/log-rotate.js +85 -0
  107. package/dist/utils/log-rotate.js.map +1 -0
  108. package/dist/utils/rate-limiter.d.ts +38 -0
  109. package/dist/utils/rate-limiter.d.ts.map +1 -0
  110. package/dist/utils/rate-limiter.js +79 -0
  111. package/dist/utils/rate-limiter.js.map +1 -0
  112. package/dist/utils/retry.d.ts +10 -0
  113. package/dist/utils/retry.d.ts.map +1 -0
  114. package/dist/utils/retry.js +47 -0
  115. package/dist/utils/retry.js.map +1 -0
  116. package/dist/utils.d.ts +86 -0
  117. package/dist/utils.d.ts.map +1 -0
  118. package/dist/utils.js +218 -0
  119. package/dist/utils.js.map +1 -0
  120. package/package.json +78 -0
  121. package/plugins/cancel-task/index.ts +161 -0
  122. package/plugins/cancel-task/plugin.json +9 -0
  123. package/plugins/feishu/index.ts +944 -0
  124. package/plugins/feishu/plugin.json +29 -0
  125. package/plugins/list-tasks/index.ts +150 -0
  126. package/plugins/list-tasks/plugin.json +9 -0
  127. package/plugins/memory/index.ts +190 -0
  128. package/plugins/memory/plugin.json +7 -0
  129. package/plugins/pause-task/index.ts +95 -0
  130. package/plugins/pause-task/plugin.json +8 -0
  131. package/plugins/register-group/index.ts +147 -0
  132. package/plugins/register-group/plugin.json +7 -0
  133. package/plugins/resume-task/index.ts +92 -0
  134. package/plugins/resume-task/plugin.json +8 -0
  135. package/plugins/schedule-task/index.ts +248 -0
  136. package/plugins/schedule-task/plugin.json +9 -0
  137. package/plugins/send-message/index.ts +75 -0
  138. package/plugins/send-message/plugin.json +9 -0
package/dist/index.js ADDED
@@ -0,0 +1,1181 @@
1
+ /**
2
+ * FlashClaw 主入口
3
+ * ⚡ 闪电龙虾 - 快如闪电的 AI 助手
4
+ */
5
+ import 'dotenv/config';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { isIP } from 'net';
9
+ import pino from 'pino';
10
+ import { paths, ensureDirectories, getBuiltinPluginsDir } from './paths.js';
11
+ import { pluginManager } from './plugins/manager.js';
12
+ import { loadFromDir, watchPlugins, stopWatching } from './plugins/loader.js';
13
+ import { getApiClient } from './core/api-client.js';
14
+ import { getMemoryManager } from './core/memory.js';
15
+ import { BOT_NAME, DATA_DIR, MAIN_GROUP_FOLDER, IPC_POLL_INTERVAL, TIMEZONE } from './config.js';
16
+ import { initDatabase, storeMessage, storeChatMetadata, getMessagesSince, getChatHistory, messageExists, getAllTasks, getAllChats } from './db.js';
17
+ import { startSchedulerLoop, stopScheduler } from './task-scheduler.js';
18
+ import { runAgent, writeTasksSnapshot, writeGroupsSnapshot } from './agent-runner.js';
19
+ import { loadJson, saveJson } from './utils.js';
20
+ import { MessageQueue } from './message-queue.js';
21
+ import { isCommand, handleCommand, getCompactSuggestion } from './commands.js';
22
+ import { getSessionStats as getTrackerStats, resetSession as resetTrackerSession, checkCompactThreshold, getContextWindowSize } from './session-tracker.js';
23
+ // ⚡ FlashClaw Logger
24
+ const logger = pino({
25
+ level: process.env.LOG_LEVEL || 'info',
26
+ transport: { target: 'pino-pretty', options: { colorize: true } }
27
+ });
28
+ // ==================== 渠道管理 ====================
29
+ /**
30
+ * 渠道管理器 - 管理所有已启用的通讯渠道插件
31
+ */
32
+ class ChannelManager {
33
+ channels = [];
34
+ enabledPlatforms = [];
35
+ async initialize() {
36
+ this.channels = pluginManager.getActiveChannels();
37
+ this.enabledPlatforms = this.channels.map(c => c.name);
38
+ if (this.channels.length === 0) {
39
+ throw new Error('没有启用任何通讯渠道');
40
+ }
41
+ }
42
+ async start(onMessage) {
43
+ for (const channel of this.channels) {
44
+ channel.onMessage(onMessage);
45
+ await channel.start();
46
+ logger.info({ channel: channel.name }, '⚡ 渠道已启动');
47
+ }
48
+ }
49
+ async sendMessage(chatId, content, platform) {
50
+ // 如果指定了平台,使用指定的渠道
51
+ if (platform) {
52
+ const channel = this.channels.find(c => c.name === platform);
53
+ if (channel) {
54
+ return await channel.sendMessage(chatId, content);
55
+ }
56
+ }
57
+ // 否则尝试所有渠道
58
+ for (const channel of this.channels) {
59
+ try {
60
+ return await channel.sendMessage(chatId, content);
61
+ }
62
+ catch {
63
+ continue;
64
+ }
65
+ }
66
+ return { success: false, error: `无法发送消息到 ${chatId}` };
67
+ }
68
+ async updateMessage(messageId, content, platform) {
69
+ // 如果指定了平台,使用指定的渠道
70
+ if (platform) {
71
+ const channel = this.channels.find(c => c.name === platform);
72
+ if (channel?.updateMessage) {
73
+ await channel.updateMessage(messageId, content);
74
+ return;
75
+ }
76
+ }
77
+ // 尝试所有支持更新的渠道
78
+ for (const channel of this.channels) {
79
+ if (channel.updateMessage) {
80
+ try {
81
+ await channel.updateMessage(messageId, content);
82
+ return;
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ async deleteMessage(messageId, platform) {
91
+ if (platform) {
92
+ const channel = this.channels.find(c => c.name === platform);
93
+ if (channel?.deleteMessage) {
94
+ await channel.deleteMessage(messageId);
95
+ return;
96
+ }
97
+ }
98
+ for (const channel of this.channels) {
99
+ if (channel.deleteMessage) {
100
+ try {
101
+ await channel.deleteMessage(messageId);
102
+ return;
103
+ }
104
+ catch {
105
+ continue;
106
+ }
107
+ }
108
+ }
109
+ }
110
+ getEnabledPlatforms() {
111
+ return this.enabledPlatforms;
112
+ }
113
+ getPlatformDisplayName(platform) {
114
+ const names = {
115
+ 'feishu': '飞书',
116
+ };
117
+ return names[platform] || platform;
118
+ }
119
+ shouldRespondInGroup(msg) {
120
+ // 检查是否被 @ 或提到机器人名称
121
+ const botName = process.env.BOT_NAME || 'FlashClaw';
122
+ return msg.content.includes(`@${botName}`) ||
123
+ msg.content.toLowerCase().includes(botName.toLowerCase());
124
+ }
125
+ }
126
+ // ==================== 全局状态 ====================
127
+ let channelManager;
128
+ let apiClient;
129
+ let memoryManager;
130
+ let sessions = {};
131
+ let registeredGroups = {};
132
+ let lastAgentTimestamp = {};
133
+ let messageQueue;
134
+ 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
+ // 直接网页抓取触发(避免模型不触发工具)
143
+ const WEB_FETCH_TOOL_NAME = 'web_fetch';
144
+ const WEB_FETCH_INTENT_RE = /(抓取|获取|读取|访问|打开|爬取|网页|网站|链接|fetch|web)/i;
145
+ const WEB_FETCH_URL_RE = /https?:\/\/[^\s<>()]+/i;
146
+ const WEB_FETCH_DOMAIN_RE = /(?:^|[^A-Za-z0-9.-])((?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,})(:\d{2,5})?(\/[^\s<>()]*)?/i;
147
+ 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
+ // ==================== 状态管理 ====================
154
+ // 默认的 main 群组配置模板(用于自动注册新会话)
155
+ const DEFAULT_MAIN_GROUP = {
156
+ name: 'main',
157
+ folder: MAIN_GROUP_FOLDER,
158
+ trigger: '@', // 默认 @ 触发
159
+ added_at: new Date().toISOString()
160
+ };
161
+ function loadState() {
162
+ const statePath = path.join(DATA_DIR, 'router_state.json');
163
+ const state = loadJson(statePath, {});
164
+ lastAgentTimestamp = state.last_agent_timestamp || {};
165
+ sessions = loadJson(path.join(DATA_DIR, 'sessions.json'), {});
166
+ registeredGroups = loadJson(path.join(DATA_DIR, 'registered_groups.json'), {});
167
+ // 确保有 main 群组配置模板(用于自动注册)
168
+ const hasMainGroup = Object.values(registeredGroups).some(g => g.folder === MAIN_GROUP_FOLDER);
169
+ if (!hasMainGroup) {
170
+ // 用占位符 ID 注册 main 模板,实际会话会在收到消息时动态注册
171
+ registeredGroups['__main_template__'] = DEFAULT_MAIN_GROUP;
172
+ logger.info('⚡ 已初始化 main 群组模板');
173
+ }
174
+ logger.info({ groupCount: Object.keys(registeredGroups).length }, '⚡ 状态已加载');
175
+ }
176
+ function saveState() {
177
+ saveJson(path.join(DATA_DIR, 'router_state.json'), { last_agent_timestamp: lastAgentTimestamp });
178
+ saveJson(path.join(DATA_DIR, 'sessions.json'), sessions);
179
+ }
180
+ function registerGroup(chatId, group) {
181
+ registeredGroups[chatId] = group;
182
+ saveJson(path.join(DATA_DIR, 'registered_groups.json'), registeredGroups);
183
+ // 创建群组文件夹
184
+ const groupDir = path.join(paths.groups(), group.folder);
185
+ fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
186
+ logger.info({ chatId, name: group.name, folder: group.folder }, '⚡ 群组已注册');
187
+ }
188
+ /**
189
+ * 获取可用群组列表
190
+ */
191
+ function getAvailableGroups() {
192
+ const chats = getAllChats();
193
+ const registeredIds = new Set(Object.keys(registeredGroups));
194
+ return chats
195
+ .filter(c => c.jid !== '__group_sync__')
196
+ .map(c => ({
197
+ jid: c.jid,
198
+ name: c.name,
199
+ lastActivity: c.last_message_time,
200
+ isRegistered: registeredIds.has(c.jid)
201
+ }));
202
+ }
203
+ // ==================== 消息处理 ====================
204
+ /**
205
+ * 判断是否应该触发 Agent
206
+ */
207
+ function shouldTriggerAgent(msg, group) {
208
+ const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
209
+ // 主群组响应所有消息
210
+ if (isMainGroup) {
211
+ return true;
212
+ }
213
+ // 私聊始终响应
214
+ if (msg.chatType === 'p2p') {
215
+ return true;
216
+ }
217
+ // 群聊:如果有 mentions(被 @),说明渠道插件已经验证过了
218
+ if (msg.mentions && msg.mentions.length > 0) {
219
+ return true;
220
+ }
221
+ // 群聊使用智能检测(检查消息内容)
222
+ if (channelManager.shouldRespondInGroup(msg)) {
223
+ return true;
224
+ }
225
+ return false;
226
+ }
227
+ function extractFirstUrl(text) {
228
+ const match = text.match(WEB_FETCH_URL_RE);
229
+ if (match) {
230
+ return match[0].replace(TRAILING_PUNCT_RE, '');
231
+ }
232
+ const domainMatch = text.match(WEB_FETCH_DOMAIN_RE);
233
+ if (!domainMatch)
234
+ return null;
235
+ const host = domainMatch[1];
236
+ const port = domainMatch[2] ?? '';
237
+ const path = domainMatch[3] ?? '';
238
+ const candidate = `https://${host}${port}${path}`;
239
+ return candidate.replace(TRAILING_PUNCT_RE, '');
240
+ }
241
+ function isPrivateIpv4(ip) {
242
+ const parts = ip.split('.').map((part) => Number(part));
243
+ if (parts.length !== 4 || parts.some((part) => Number.isNaN(part)))
244
+ return false;
245
+ const [a, b] = parts;
246
+ if (a === 0)
247
+ return true;
248
+ if (a === 10)
249
+ return true;
250
+ if (a === 127)
251
+ return true;
252
+ if (a === 169 && b === 254)
253
+ return true;
254
+ if (a === 172 && b >= 16 && b <= 31)
255
+ return true;
256
+ if (a === 192 && b === 168)
257
+ return true;
258
+ if (a === 100 && b >= 64 && b <= 127)
259
+ return true;
260
+ return false;
261
+ }
262
+ function isPrivateIpv6(ip) {
263
+ const normalized = ip.toLowerCase();
264
+ if (normalized === '::' || normalized === '::1')
265
+ return true;
266
+ if (normalized.startsWith('fe80:'))
267
+ return true;
268
+ if (normalized.startsWith('fec0:'))
269
+ return true;
270
+ if (normalized.startsWith('fc') || normalized.startsWith('fd'))
271
+ return true;
272
+ if (normalized.includes('::ffff:')) {
273
+ const ipv4Part = normalized.split('::ffff:')[1];
274
+ if (ipv4Part && isPrivateIpv4(ipv4Part))
275
+ return true;
276
+ }
277
+ return false;
278
+ }
279
+ function isPrivateIp(ip) {
280
+ const family = isIP(ip);
281
+ if (family === 4)
282
+ return isPrivateIpv4(ip);
283
+ if (family === 6)
284
+ return isPrivateIpv6(ip);
285
+ return false;
286
+ }
287
+ function isBlockedHostname(hostname) {
288
+ const normalized = hostname.trim().toLowerCase();
289
+ if (normalized === 'localhost')
290
+ return true;
291
+ return (normalized.endsWith('.localhost') ||
292
+ normalized.endsWith('.local') ||
293
+ normalized.endsWith('.internal'));
294
+ }
295
+ function estimateBase64Bytes(content) {
296
+ if (!content)
297
+ return null;
298
+ const raw = content.startsWith('data:') ? content.split(',')[1] ?? '' : content;
299
+ const normalized = raw.replace(/\s+/g, '');
300
+ if (!normalized)
301
+ return 0;
302
+ const padding = normalized.endsWith('==') ? 2 : normalized.endsWith('=') ? 1 : 0;
303
+ return Math.max(0, Math.floor((normalized.length * 3) / 4) - padding);
304
+ }
305
+ function truncateText(text, maxLength) {
306
+ if (text.length <= maxLength) {
307
+ return { text, truncated: false };
308
+ }
309
+ return { text: `${text.slice(0, maxLength)}\n\n...(内容已截断)`, truncated: true };
310
+ }
311
+ function formatDirectWebFetchResponse(url, result) {
312
+ if (!result.success) {
313
+ return `❌ 抓取失败: ${result.error || '未知错误'}`;
314
+ }
315
+ const data = result.data;
316
+ const content = typeof data?.content === 'string'
317
+ ? data.content
318
+ : typeof result.data === 'string'
319
+ ? result.data
320
+ : JSON.stringify(result.data ?? {}, null, 2);
321
+ const { text } = truncateText(content, MAX_DIRECT_FETCH_CHARS);
322
+ const lines = [];
323
+ lines.push(`✅ 已抓取: ${typeof data?.finalUrl === 'string' ? data.finalUrl : url}`);
324
+ if (typeof data?.title === 'string' && data.title.trim()) {
325
+ lines.push(`📝 标题: ${data.title.trim()}`);
326
+ }
327
+ if (typeof data?.status === 'number') {
328
+ lines.push(`📡 状态: ${data.status}`);
329
+ }
330
+ if (typeof data?.contentType === 'string') {
331
+ lines.push(`📄 类型: ${data.contentType}`);
332
+ }
333
+ if (typeof data?.bytes === 'number') {
334
+ lines.push(`📦 大小: ${data.bytes} bytes`);
335
+ }
336
+ lines.push('');
337
+ lines.push(text);
338
+ return lines.join('\n');
339
+ }
340
+ async function tryHandleDirectWebFetch(msg, group) {
341
+ const content = msg.content?.trim();
342
+ if (!content)
343
+ return false;
344
+ if (!WEB_FETCH_INTENT_RE.test(content))
345
+ return false;
346
+ const url = extractFirstUrl(content);
347
+ if (!url)
348
+ return false;
349
+ let urlObj;
350
+ try {
351
+ urlObj = new URL(url);
352
+ }
353
+ catch {
354
+ await sendMessage(msg.chatId, `${BOT_NAME}: URL 格式不合法`, msg.platform);
355
+ return true;
356
+ }
357
+ if (!['http:', 'https:'].includes(urlObj.protocol)) {
358
+ await sendMessage(msg.chatId, `${BOT_NAME}: 只支持 HTTP/HTTPS 协议`, msg.platform);
359
+ return true;
360
+ }
361
+ const allowPrivate = process.env.WEB_FETCH_ALLOW_PRIVATE === '1';
362
+ const hostname = urlObj.hostname;
363
+ if (!allowPrivate && (isBlockedHostname(hostname) || (isIP(hostname) && isPrivateIp(hostname)))) {
364
+ await sendMessage(msg.chatId, `${BOT_NAME}: 目标地址禁止访问内网`, msg.platform);
365
+ return true;
366
+ }
367
+ const toolInfo = pluginManager.getTool(WEB_FETCH_TOOL_NAME);
368
+ if (!toolInfo) {
369
+ await sendMessage(msg.chatId, `${BOT_NAME}: 未检测到 web_fetch 插件,请先安装后再使用。`, msg.platform);
370
+ return true;
371
+ }
372
+ const toolContext = {
373
+ chatId: msg.chatId,
374
+ groupId: group.folder,
375
+ userId: msg.senderId,
376
+ sendMessage: async (text) => {
377
+ await sendMessage(msg.chatId, `${BOT_NAME}: ${text}`, msg.platform);
378
+ }
379
+ };
380
+ const normalizedUrl = urlObj.toString();
381
+ logger.info({ chatId: msg.chatId, url: normalizedUrl }, '⚡ 触发直接网页抓取');
382
+ let result;
383
+ try {
384
+ const { plugin, isMultiTool } = toolInfo;
385
+ result = isMultiTool
386
+ ? await plugin.execute(WEB_FETCH_TOOL_NAME, { url: normalizedUrl, allowPrivate }, toolContext)
387
+ : await plugin.execute({ url: normalizedUrl, allowPrivate }, toolContext);
388
+ }
389
+ catch (error) {
390
+ result = { success: false, error: error instanceof Error ? error.message : String(error) };
391
+ }
392
+ const response = formatDirectWebFetchResponse(normalizedUrl, result);
393
+ await sendMessage(msg.chatId, `${BOT_NAME}: ${response}`, msg.platform);
394
+ return true;
395
+ }
396
+ /**
397
+ * 处理队列中的消息
398
+ */
399
+ async function processQueuedMessage(queuedMsg) {
400
+ const msg = queuedMsg.data;
401
+ const chatId = msg.chatId;
402
+ const group = registeredGroups[chatId];
403
+ logger.info({ chatId, msgId: msg.id }, '>>> 开始处理队列消息');
404
+ if (!group) {
405
+ logger.info({ chatId }, '群组未注册,跳过');
406
+ return;
407
+ }
408
+ // 获取自上次交互以来的消息
409
+ const sinceTimestamp = lastAgentTimestamp[chatId] || '';
410
+ logger.info({ chatId, sinceTimestamp }, '>>> 查询新消息');
411
+ const missedMessages = getMessagesSince(chatId, sinceTimestamp, BOT_NAME);
412
+ logger.info({ chatId, count: missedMessages.length }, '>>> 获取到消息数量');
413
+ if (missedMessages.length === 0) {
414
+ logger.info({ chatId, sinceTimestamp }, '无新消息,可能时间戳问题');
415
+ return;
416
+ }
417
+ // 获取历史上下文
418
+ const historyMessages = getChatHistory(chatId, HISTORY_CONTEXT_LIMIT, sinceTimestamp);
419
+ const escapeXml = (s) => s
420
+ .replace(/&/g, '&amp;')
421
+ .replace(/</g, '&lt;')
422
+ .replace(/>/g, '&gt;')
423
+ .replace(/"/g, '&quot;')
424
+ .replace(/'/g, '&apos;');
425
+ // 构建带历史上下文的 prompt
426
+ let prompt = '';
427
+ if (historyMessages.length > 0) {
428
+ const historyLines = historyMessages.map(m => `<message sender="${escapeXml(m.sender_name)}" time="${m.timestamp}">${escapeXml(m.content)}</message>`);
429
+ prompt += `<history_context>\n${historyLines.join('\n')}\n</history_context>\n\n`;
430
+ }
431
+ const newLines = missedMessages.map(m => `<message sender="${escapeXml(m.sender_name)}" time="${m.timestamp}">${escapeXml(m.content)}</message>`);
432
+ prompt += `<new_messages>\n${newLines.join('\n')}\n</new_messages>`;
433
+ // 提取图片附件(只处理当前消息的附件)
434
+ const imageAttachments = msg.attachments
435
+ ?.filter(a => a.type === 'image' && a.content)
436
+ .filter(a => {
437
+ const size = estimateBase64Bytes(a.content || '');
438
+ if (size === null)
439
+ return false;
440
+ if (size > MAX_IMAGE_BYTES) {
441
+ logger.warn({ chatId, size }, '附件过大,已忽略');
442
+ return false;
443
+ }
444
+ return true;
445
+ })
446
+ .map(a => ({
447
+ type: 'image',
448
+ content: a.content,
449
+ mimeType: a.mimeType
450
+ })) || [];
451
+ logger.info({
452
+ group: group.name,
453
+ newMessages: missedMessages.length,
454
+ historyContext: historyMessages.length,
455
+ platform: msg.platform,
456
+ imageCount: imageAttachments.length
457
+ }, '⚡ 处理消息');
458
+ // "正在思考..." 提示功能
459
+ let placeholderMessageId;
460
+ let thinkingDone = false;
461
+ // 设置定时器,超过阈值时发送"正在思考..."
462
+ const thinkingTimer = THINKING_THRESHOLD_MS > 0 ? setTimeout(async () => {
463
+ if (thinkingDone)
464
+ return;
465
+ try {
466
+ const result = await channelManager.sendMessage(chatId, `${BOT_NAME}: 正在思考...`, msg.platform);
467
+ if (result.success && result.messageId) {
468
+ placeholderMessageId = result.messageId;
469
+ logger.debug({ chatId, messageId: placeholderMessageId }, '已发送思考提示');
470
+ }
471
+ }
472
+ catch {
473
+ // 忽略错误
474
+ }
475
+ }, THINKING_THRESHOLD_MS) : null;
476
+ try {
477
+ const response = await executeAgent(group, prompt, chatId, {
478
+ attachments: imageAttachments.length > 0 ? imageAttachments : undefined,
479
+ userId: msg.senderId // 传递用户 ID 用于用户级别记忆
480
+ });
481
+ thinkingDone = true;
482
+ if (thinkingTimer) {
483
+ clearTimeout(thinkingTimer);
484
+ }
485
+ if (response) {
486
+ lastAgentTimestamp[chatId] = msg.timestamp;
487
+ saveState();
488
+ const finalText = `${BOT_NAME}: ${response}`;
489
+ // 如果有占位消息,更新它;否则发送新消息
490
+ if (placeholderMessageId) {
491
+ try {
492
+ await channelManager.updateMessage(placeholderMessageId, finalText, msg.platform);
493
+ logger.info({ chatId, messageId: placeholderMessageId }, '⚡ 消息已更新');
494
+ }
495
+ catch {
496
+ // 更新失败,尝试删除并发送新消息
497
+ try {
498
+ await channelManager.deleteMessage(placeholderMessageId, msg.platform);
499
+ }
500
+ catch { }
501
+ await sendMessage(chatId, finalText, msg.platform);
502
+ }
503
+ }
504
+ else {
505
+ await sendMessage(chatId, finalText, msg.platform);
506
+ }
507
+ // 检查是否需要提示用户压缩会话(70% 阈值)
508
+ const usagePercent = checkCompactThreshold(chatId);
509
+ if (usagePercent !== null) {
510
+ const stats = getTrackerStats(chatId);
511
+ if (stats) {
512
+ const suggestion = getCompactSuggestion(stats.tokenCount, stats.maxTokens);
513
+ await sendMessage(chatId, suggestion, msg.platform);
514
+ logger.info({ chatId, usagePercent }, '⚠️ 上下文使用率提示已发送');
515
+ }
516
+ }
517
+ }
518
+ else if (placeholderMessageId) {
519
+ // 没有响应,删除占位消息
520
+ try {
521
+ await channelManager.deleteMessage(placeholderMessageId, msg.platform);
522
+ }
523
+ catch { }
524
+ }
525
+ }
526
+ catch (err) {
527
+ thinkingDone = true;
528
+ if (thinkingTimer) {
529
+ clearTimeout(thinkingTimer);
530
+ }
531
+ // 删除占位消息
532
+ if (placeholderMessageId) {
533
+ try {
534
+ await channelManager.deleteMessage(placeholderMessageId, msg.platform);
535
+ }
536
+ catch { }
537
+ }
538
+ throw err;
539
+ }
540
+ }
541
+ /**
542
+ * 处理传入消息
543
+ */
544
+ async function handleIncomingMessage(msg) {
545
+ const chatId = msg.chatId;
546
+ // 存储聊天元数据
547
+ storeChatMetadata(chatId, msg.timestamp);
548
+ // 获取群组配置
549
+ let group = registeredGroups[chatId];
550
+ // 自动注册新会话(参考 openclaw 的动态 session key 设计)
551
+ if (!group) {
552
+ // 查找 main 群组配置作为模板
553
+ const mainGroup = Object.values(registeredGroups).find(g => g.folder === MAIN_GROUP_FOLDER);
554
+ if (mainGroup) {
555
+ // 根据聊天类型生成名称和文件夹
556
+ const chatName = msg.chatType === 'p2p'
557
+ ? `私聊-${msg.senderName || chatId.slice(-8)}`
558
+ : `群聊-${chatId.slice(-8)}`;
559
+ // 为新会话创建独立的文件夹名称(使用 chatId 后8位确保唯一性)
560
+ const folderName = msg.chatType === 'p2p'
561
+ ? `private-${chatId.slice(-8)}`
562
+ : `group-${chatId.slice(-8)}`;
563
+ // 创建新的群组配置(使用独立的 folder)
564
+ const newGroup = {
565
+ ...mainGroup,
566
+ name: chatName,
567
+ folder: folderName,
568
+ added_at: new Date().toISOString()
569
+ };
570
+ // 动态注册此会话
571
+ registerGroup(chatId, newGroup);
572
+ // 使用新创建的群组(而不是 mainGroup)
573
+ group = newGroup;
574
+ logger.info({
575
+ chatId,
576
+ chatType: msg.chatType,
577
+ name: chatName,
578
+ folder: folderName
579
+ }, '⚡ 会话已自动注册');
580
+ }
581
+ }
582
+ if (!group) {
583
+ logger.debug({ chatId, platform: msg.platform, chatType: msg.chatType }, '未注册的聊天,忽略');
584
+ return;
585
+ }
586
+ // 去重检查
587
+ if (messageExists(msg.id, chatId)) {
588
+ logger.debug({ chatId, messageId: msg.id }, '重复消息,忽略');
589
+ return;
590
+ }
591
+ // 存储消息
592
+ storeMessage({
593
+ id: msg.id,
594
+ chatId: chatId,
595
+ senderId: msg.senderId,
596
+ senderName: msg.senderName,
597
+ content: msg.content,
598
+ timestamp: msg.timestamp,
599
+ isFromMe: false
600
+ });
601
+ // 检查触发条件
602
+ const shouldTrigger = shouldTriggerAgent(msg, group);
603
+ logger.info({ chatId, shouldTrigger, chatType: msg.chatType }, '>>> 触发检查');
604
+ if (!shouldTrigger) {
605
+ return;
606
+ }
607
+ // 检查是否是斜杠命令
608
+ if (isCommand(msg.content)) {
609
+ const context = {
610
+ chatId,
611
+ userId: msg.senderId,
612
+ userName: msg.senderName || '用户',
613
+ platform: msg.platform,
614
+ getSessionStats: () => {
615
+ // 获取真实的 token 统计数据
616
+ const trackerStats = getTrackerStats(chatId);
617
+ if (trackerStats) {
618
+ return {
619
+ messageCount: trackerStats.messageCount,
620
+ tokenCount: trackerStats.tokenCount,
621
+ maxTokens: trackerStats.maxTokens,
622
+ model: trackerStats.model,
623
+ startedAt: trackerStats.startedAt
624
+ };
625
+ }
626
+ // 回退到历史记录(服务重启后 tracker 数据会丢失)
627
+ const history = getChatHistory(chatId, 1000);
628
+ const model = process.env.AI_MODEL || 'claude-4-5-sonnet-20250929';
629
+ return {
630
+ messageCount: history.length,
631
+ tokenCount: 0, // 服务重启后需要重新统计
632
+ maxTokens: getContextWindowSize(model),
633
+ model,
634
+ startedAt: history.length > 0 ? history[0].timestamp : undefined
635
+ };
636
+ },
637
+ resetSession: () => {
638
+ // 重置会话(清除内存中的 session ID 和 tracker)
639
+ if (sessions[group.folder]) {
640
+ delete sessions[group.folder];
641
+ }
642
+ resetTrackerSession(chatId);
643
+ logger.info({ chatId, folder: group.folder }, '⚡ 会话已重置');
644
+ },
645
+ getTasks: () => {
646
+ // 获取该会话的任务
647
+ const tasks = getAllTasks();
648
+ return tasks
649
+ .filter(t => t.chat_jid === chatId || group.folder === MAIN_GROUP_FOLDER)
650
+ .map(t => ({
651
+ id: t.id,
652
+ prompt: t.prompt,
653
+ scheduleType: t.schedule_type,
654
+ nextRun: t.next_run || undefined,
655
+ status: t.status
656
+ }));
657
+ },
658
+ compactSession: async () => {
659
+ // 压缩会话:让 AI 总结当前对话,然后重置会话
660
+ try {
661
+ const summary = await executeAgent(group, '请用 2-3 句话总结我们之前的对话要点,以便我们继续对话时能快速回顾。只输出总结,不要其他内容。', chatId, { userId: msg.senderId });
662
+ // 重置会话和 tracker
663
+ if (sessions[group.folder]) {
664
+ delete sessions[group.folder];
665
+ }
666
+ resetTrackerSession(chatId);
667
+ // 发送压缩完成消息
668
+ if (summary) {
669
+ await channelManager.sendMessage(chatId, `✅ **会话已压缩**\n\n📝 **对话摘要:**\n${summary}\n\n_上下文已清理,新对话已基于此摘要继续。_`, msg.platform);
670
+ }
671
+ return summary;
672
+ }
673
+ catch (error) {
674
+ logger.error({ error, chatId }, '会话压缩失败');
675
+ return null;
676
+ }
677
+ }
678
+ };
679
+ const result = handleCommand(msg.content, context);
680
+ if (result.isCommand && result.shouldRespond && result.response) {
681
+ // 发送命令响应
682
+ await channelManager.sendMessage(chatId, result.response, msg.platform);
683
+ // 如果是 /compact 命令,执行实际压缩
684
+ if (msg.content.trim().toLowerCase().startsWith('/compact') ||
685
+ msg.content.trim() === '/压缩') {
686
+ context.compactSession?.();
687
+ }
688
+ logger.info({ chatId, command: msg.content }, '⚡ 命令已处理');
689
+ return;
690
+ }
691
+ }
692
+ // 直接抓取网页(避免模型不触发工具)
693
+ if (await tryHandleDirectWebFetch(msg, group)) {
694
+ return;
695
+ }
696
+ // 添加到消息队列
697
+ logger.info({ chatId, msgId: msg.id }, '>>> 加入消息队列');
698
+ await messageQueue.enqueue(chatId, msg.id, msg);
699
+ }
700
+ async function executeAgent(group, prompt, chatId, options) {
701
+ const isMain = group.folder === MAIN_GROUP_FOLDER;
702
+ const sessionId = sessions[group.folder];
703
+ // 更新任务快照
704
+ const tasks = getAllTasks();
705
+ writeTasksSnapshot(group.folder, isMain, tasks.map(t => ({
706
+ id: t.id,
707
+ groupFolder: t.group_folder,
708
+ prompt: t.prompt,
709
+ schedule_type: t.schedule_type,
710
+ schedule_value: t.schedule_value,
711
+ status: t.status,
712
+ next_run: t.next_run
713
+ })));
714
+ // 更新可用群组快照
715
+ const availableGroups = getAvailableGroups();
716
+ writeGroupsSnapshot(group.folder, isMain, availableGroups, new Set(Object.keys(registeredGroups)));
717
+ try {
718
+ const output = await runAgent(group, {
719
+ prompt,
720
+ sessionId,
721
+ groupFolder: group.folder,
722
+ chatJid: chatId,
723
+ isMain,
724
+ userId: options?.userId || chatId, // 用户级别记忆
725
+ attachments: options?.attachments
726
+ });
727
+ if (output.newSessionId) {
728
+ sessions[group.folder] = output.newSessionId;
729
+ saveJson(path.join(DATA_DIR, 'sessions.json'), sessions);
730
+ }
731
+ if (output.status === 'error') {
732
+ logger.error({ group: group.name, error: output.error }, 'Agent 错误');
733
+ return null;
734
+ }
735
+ return output.result;
736
+ }
737
+ catch (err) {
738
+ logger.error({ group: group.name, err }, 'Agent 执行失败');
739
+ return null;
740
+ }
741
+ }
742
+ // ==================== 消息发送 ====================
743
+ async function sendMessage(chatId, text, platform) {
744
+ try {
745
+ await channelManager.sendMessage(chatId, text, platform);
746
+ logger.info({ chatId, length: text.length, platform }, '⚡ 消息已发送');
747
+ }
748
+ catch (err) {
749
+ logger.error({ chatId, err, platform }, '发送消息失败');
750
+ }
751
+ }
752
+ // ==================== IPC 处理 ====================
753
+ function quarantineIpcFile(ipcBaseDir, sourceGroup, filePath, reason, err) {
754
+ const errorDir = path.join(ipcBaseDir, 'errors');
755
+ const fileName = path.basename(filePath);
756
+ try {
757
+ fs.mkdirSync(errorDir, { recursive: true });
758
+ fs.renameSync(filePath, path.join(errorDir, `${sourceGroup}-${fileName}`));
759
+ }
760
+ catch (moveError) {
761
+ logger.warn({ file: fileName, sourceGroup, moveError }, '隔离 IPC 文件失败');
762
+ return;
763
+ }
764
+ logger.warn({ file: fileName, sourceGroup, reason, err }, 'IPC 文件已隔离');
765
+ }
766
+ function startIpcWatcher() {
767
+ const ipcBaseDir = path.join(DATA_DIR, 'ipc');
768
+ fs.mkdirSync(ipcBaseDir, { recursive: true });
769
+ const processIpcFiles = async () => {
770
+ let groupFolders;
771
+ try {
772
+ groupFolders = fs.readdirSync(ipcBaseDir).filter(f => {
773
+ const stat = fs.statSync(path.join(ipcBaseDir, f));
774
+ return stat.isDirectory() && f !== 'errors';
775
+ });
776
+ }
777
+ catch (err) {
778
+ logger.error({ err }, '读取 IPC 目录失败');
779
+ setTimeout(processIpcFiles, IPC_POLL_INTERVAL);
780
+ return;
781
+ }
782
+ for (const sourceGroup of groupFolders) {
783
+ const isMain = sourceGroup === MAIN_GROUP_FOLDER;
784
+ const messagesDir = path.join(ipcBaseDir, sourceGroup, 'messages');
785
+ const tasksDir = path.join(ipcBaseDir, sourceGroup, 'tasks');
786
+ // 处理消息
787
+ try {
788
+ if (fs.existsSync(messagesDir)) {
789
+ const messageFiles = fs.readdirSync(messagesDir).filter(f => f.endsWith('.json'));
790
+ for (const file of messageFiles) {
791
+ const filePath = path.join(messagesDir, file);
792
+ try {
793
+ const stat = fs.statSync(filePath);
794
+ if (stat.size > MAX_IPC_FILE_BYTES) {
795
+ quarantineIpcFile(ipcBaseDir, sourceGroup, filePath, `IPC 消息文件过大 (${stat.size} bytes)`);
796
+ continue;
797
+ }
798
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
799
+ if (data.type === 'message' && data.chatJid && data.text) {
800
+ if (typeof data.chatJid !== 'string' || data.chatJid.length > MAX_IPC_CHAT_ID_CHARS) {
801
+ logger.warn({ sourceGroup }, 'IPC 消息 chatJid 格式不合法');
802
+ fs.unlinkSync(filePath);
803
+ continue;
804
+ }
805
+ if (typeof data.text !== 'string' || data.text.length > MAX_IPC_MESSAGE_CHARS) {
806
+ logger.warn({ sourceGroup }, 'IPC 消息 text 过长或格式不合法');
807
+ fs.unlinkSync(filePath);
808
+ continue;
809
+ }
810
+ const targetGroup = registeredGroups[data.chatJid];
811
+ if (isMain || (targetGroup && targetGroup.folder === sourceGroup)) {
812
+ await sendMessage(data.chatJid, `${BOT_NAME}: ${data.text}`);
813
+ logger.info({ chatId: data.chatJid, sourceGroup }, 'IPC 消息已发送');
814
+ }
815
+ else {
816
+ logger.warn({ chatId: data.chatJid, sourceGroup }, '未授权的 IPC 消息被阻止');
817
+ }
818
+ }
819
+ fs.unlinkSync(filePath);
820
+ }
821
+ catch (err) {
822
+ logger.error({ file, sourceGroup, err }, '处理 IPC 消息失败');
823
+ quarantineIpcFile(ipcBaseDir, sourceGroup, filePath, '处理 IPC 消息失败', err);
824
+ }
825
+ }
826
+ }
827
+ }
828
+ catch (err) {
829
+ logger.error({ err, sourceGroup }, '读取 IPC 消息目录失败');
830
+ }
831
+ // 处理任务
832
+ try {
833
+ if (fs.existsSync(tasksDir)) {
834
+ const taskFiles = fs.readdirSync(tasksDir).filter(f => f.endsWith('.json'));
835
+ for (const file of taskFiles) {
836
+ const filePath = path.join(tasksDir, file);
837
+ try {
838
+ const stat = fs.statSync(filePath);
839
+ if (stat.size > MAX_IPC_FILE_BYTES) {
840
+ quarantineIpcFile(ipcBaseDir, sourceGroup, filePath, `IPC 任务文件过大 (${stat.size} bytes)`);
841
+ continue;
842
+ }
843
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
844
+ await processTaskIpc(data, sourceGroup, isMain);
845
+ fs.unlinkSync(filePath);
846
+ }
847
+ catch (err) {
848
+ logger.error({ file, sourceGroup, err }, '处理 IPC 任务失败');
849
+ quarantineIpcFile(ipcBaseDir, sourceGroup, filePath, '处理 IPC 任务失败', err);
850
+ }
851
+ }
852
+ }
853
+ }
854
+ catch (err) {
855
+ logger.error({ err, sourceGroup }, '读取 IPC 任务目录失败');
856
+ }
857
+ }
858
+ setTimeout(processIpcFiles, IPC_POLL_INTERVAL);
859
+ };
860
+ processIpcFiles();
861
+ logger.info('⚡ IPC 监听已启动');
862
+ }
863
+ async function processTaskIpc(data, sourceGroup, isMain) {
864
+ const { createTask, updateTask, deleteTask, getTaskById: getTask } = await import('./db.js');
865
+ const { CronExpressionParser } = await import('cron-parser');
866
+ switch (data.type) {
867
+ case 'schedule_task':
868
+ if (data.prompt && data.schedule_type && data.schedule_value && data.groupFolder) {
869
+ const targetGroup = data.groupFolder;
870
+ if (!isMain && targetGroup !== sourceGroup) {
871
+ logger.warn({ sourceGroup, targetGroup }, '未授权的 schedule_task 被阻止');
872
+ break;
873
+ }
874
+ const targetChatId = Object.entries(registeredGroups).find(([, group]) => group.folder === targetGroup)?.[0];
875
+ if (!targetChatId) {
876
+ logger.warn({ targetGroup }, '无法创建任务:目标群组未注册');
877
+ break;
878
+ }
879
+ const scheduleType = data.schedule_type;
880
+ let nextRun = null;
881
+ if (scheduleType === 'cron') {
882
+ try {
883
+ const interval = CronExpressionParser.parse(data.schedule_value, { tz: TIMEZONE });
884
+ nextRun = interval.next().toISOString();
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();
898
+ }
899
+ else if (scheduleType === 'once') {
900
+ const scheduled = new Date(data.schedule_value);
901
+ if (isNaN(scheduled.getTime())) {
902
+ logger.warn({ scheduleValue: data.schedule_value }, '无效的时间戳');
903
+ break;
904
+ }
905
+ nextRun = scheduled.toISOString();
906
+ }
907
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
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 }, '⚡ 任务已创建');
927
+ }
928
+ break;
929
+ case 'pause_task':
930
+ if (data.taskId) {
931
+ const task = getTask(data.taskId);
932
+ if (task && (isMain || task.group_folder === sourceGroup)) {
933
+ updateTask(data.taskId, { status: 'paused' });
934
+ logger.info({ taskId: data.taskId, sourceGroup }, '任务已暂停');
935
+ }
936
+ else {
937
+ logger.warn({ taskId: data.taskId, sourceGroup }, '未授权的任务暂停操作');
938
+ }
939
+ }
940
+ break;
941
+ case 'resume_task':
942
+ if (data.taskId) {
943
+ const task = getTask(data.taskId);
944
+ if (task && (isMain || task.group_folder === sourceGroup)) {
945
+ updateTask(data.taskId, { status: 'active' });
946
+ logger.info({ taskId: data.taskId, sourceGroup }, '任务已恢复');
947
+ }
948
+ else {
949
+ logger.warn({ taskId: data.taskId, sourceGroup }, '未授权的任务恢复操作');
950
+ }
951
+ }
952
+ break;
953
+ case 'cancel_task':
954
+ if (data.taskId) {
955
+ const task = getTask(data.taskId);
956
+ if (task && (isMain || task.group_folder === sourceGroup)) {
957
+ deleteTask(data.taskId);
958
+ logger.info({ taskId: data.taskId, sourceGroup }, '任务已取消');
959
+ }
960
+ else {
961
+ logger.warn({ taskId: data.taskId, sourceGroup }, '未授权的任务取消操作');
962
+ }
963
+ }
964
+ break;
965
+ case 'register_group':
966
+ if (!isMain) {
967
+ logger.warn({ sourceGroup }, '未授权的 register_group 被阻止');
968
+ break;
969
+ }
970
+ if (data.jid && data.name && data.folder && data.trigger) {
971
+ registerGroup(data.jid, {
972
+ name: data.name,
973
+ folder: data.folder,
974
+ trigger: data.trigger,
975
+ added_at: new Date().toISOString(),
976
+ agentConfig: data.agentConfig
977
+ });
978
+ }
979
+ else {
980
+ logger.warn({ data }, '无效的 register_group 请求');
981
+ }
982
+ break;
983
+ default:
984
+ logger.warn({ type: data.type }, '未知的 IPC 任务类型');
985
+ }
986
+ }
987
+ // ==================== 启动横幅 ====================
988
+ function displayBanner(enabledPlatforms, groupCount) {
989
+ const platformsDisplay = enabledPlatforms.map(p => channelManager.getPlatformDisplayName(p)).join(' | ');
990
+ const banner = `
991
+ \x1b[33m
992
+ ███████╗██╗ █████╗ ███████╗██╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗
993
+ ██╔════╝██║ ██╔══██╗██╔════╝██║ ██║██╔════╝██║ ██╔══██╗██║ ██║
994
+ █████╗ ██║ ███████║███████╗███████║██║ ██║ ███████║██║ █╗ ██║
995
+ ██╔══╝ ██║ ██╔══██║╚════██║██╔══██║██║ ██║ ██╔══██║██║███╗██║
996
+ ██║ ███████╗██║ ██║███████║██║ ██║╚██████╗███████╗██║ ██║╚███╔███╔╝
997
+ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
998
+ \x1b[0m
999
+ \x1b[36m ⚡ 闪电龙虾 - 快如闪电的 AI 助手\x1b[0m
1000
+
1001
+ ┌─────────────────────────────────────────────────────────────────────────┐
1002
+ │ │
1003
+ │ \x1b[32m✓\x1b[0m 状态: \x1b[32m运行中\x1b[0m │
1004
+ │ \x1b[32m✓\x1b[0m 模式: \x1b[33mDirect (Claude API)\x1b[0m │
1005
+ │ \x1b[32m✓\x1b[0m 平台: \x1b[36m${platformsDisplay.padEnd(55)}\x1b[0m│
1006
+ │ \x1b[32m✓\x1b[0m 群组: \x1b[33m${String(groupCount).padEnd(55)}\x1b[0m│
1007
+ │ │
1008
+ │ \x1b[90m所有平台使用 WebSocket 长连接,无需公网服务器\x1b[0m │
1009
+ │ │
1010
+ └─────────────────────────────────────────────────────────────────────────┘
1011
+
1012
+ \x1b[90m按 Ctrl+C 停止服务\x1b[0m
1013
+ `;
1014
+ console.log(banner);
1015
+ }
1016
+ // ==================== 主函数 ====================
1017
+ export async function main() {
1018
+ // 确保所有必要目录存在
1019
+ ensureDirectories();
1020
+ // 初始化 API 客户端(全局单例)
1021
+ apiClient = getApiClient();
1022
+ // 初始化记忆管理器(使用全局单例)
1023
+ memoryManager = getMemoryManager();
1024
+ // 初始化数据库(必须在加载插件之前,因为插件可能依赖数据库)
1025
+ initDatabase();
1026
+ logger.info('⚡ 数据库已初始化');
1027
+ // 加载插件(在数据库初始化之后)
1028
+ // 先加载内置插件
1029
+ const builtinPluginsDir = getBuiltinPluginsDir();
1030
+ if (fs.existsSync(builtinPluginsDir)) {
1031
+ logger.info({ dir: builtinPluginsDir }, '⚡ 加载内置插件');
1032
+ await loadFromDir(builtinPluginsDir);
1033
+ }
1034
+ // 再加载用户插件(可覆盖内置插件)
1035
+ const userPluginsDir = paths.userPlugins();
1036
+ if (fs.existsSync(userPluginsDir)) {
1037
+ logger.info({ dir: userPluginsDir }, '⚡ 加载用户插件');
1038
+ await loadFromDir(userPluginsDir);
1039
+ }
1040
+ // 启用热重载 - 只监听用户插件目录
1041
+ if (fs.existsSync(userPluginsDir)) {
1042
+ watchPlugins(userPluginsDir, (event, name) => {
1043
+ logger.info({ event, plugin: name }, '⚡ 插件变化');
1044
+ });
1045
+ }
1046
+ // 初始化渠道管理器
1047
+ channelManager = new ChannelManager();
1048
+ try {
1049
+ await channelManager.initialize();
1050
+ }
1051
+ catch (err) {
1052
+ console.error(`
1053
+ \x1b[31m
1054
+ ███████╗██████╗ ██████╗ ██████╗ ██████╗
1055
+ ██╔════╝██╔══██╗██╔══██╗██╔═══██╗██╔══██╗
1056
+ █████╗ ██████╔╝██████╔╝██║ ██║██████╔╝
1057
+ ██╔══╝ ██╔══██╗██╔══██╗██║ ██║██╔══██╗
1058
+ ███████╗██║ ██║██║ ██║╚██████╔╝██║ ██║
1059
+ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
1060
+ \x1b[0m
1061
+ \x1b[31m✗ 缺少消息平台配置\x1b[0m
1062
+
1063
+ 请在 \x1b[33m.env\x1b[0m 中配置飞书:
1064
+
1065
+ \x1b[36m飞书:\x1b[0m
1066
+ FEISHU_APP_ID=cli_xxxxx
1067
+ FEISHU_APP_SECRET=xxxxx
1068
+
1069
+ 详见 \x1b[33m.env.example\x1b[0m
1070
+ `);
1071
+ process.exit(1);
1072
+ }
1073
+ const enabledPlatforms = channelManager.getEnabledPlatforms();
1074
+ logger.info({ platforms: enabledPlatforms }, '⚡ 渠道管理器已初始化');
1075
+ // 加载状态
1076
+ loadState();
1077
+ // 初始化消息队列
1078
+ messageQueue = new MessageQueue(processQueuedMessage, {
1079
+ maxQueueSize: 100,
1080
+ maxConcurrent: 3,
1081
+ processingTimeout: 300000,
1082
+ maxRetries: 2
1083
+ });
1084
+ messageQueue.start();
1085
+ logger.info('⚡ 消息队列已初始化');
1086
+ // 启动任务调度器
1087
+ startSchedulerLoop({
1088
+ sendMessage: (chatId, text) => sendMessage(chatId, text),
1089
+ registeredGroups: () => registeredGroups,
1090
+ getSessions: () => sessions
1091
+ });
1092
+ // 启动 IPC 监听
1093
+ startIpcWatcher();
1094
+ // 启动所有渠道插件
1095
+ await channelManager.start(handleIncomingMessage);
1096
+ // 显示启动横幅
1097
+ const groupCount = Object.keys(registeredGroups).length;
1098
+ displayBanner(enabledPlatforms, groupCount);
1099
+ logger.info({
1100
+ mode: 'direct',
1101
+ platforms: enabledPlatforms,
1102
+ groups: groupCount
1103
+ }, '⚡ FlashClaw 已启动');
1104
+ // 注册优雅关闭处理
1105
+ setupGracefulShutdown();
1106
+ }
1107
+ // ==================== 优雅关闭 ====================
1108
+ /**
1109
+ * 优雅关闭函数
1110
+ */
1111
+ async function gracefulShutdown(signal) {
1112
+ if (isShuttingDown)
1113
+ return;
1114
+ isShuttingDown = true;
1115
+ logger.info({ signal }, '⚡ 收到关闭信号,正在优雅关闭...');
1116
+ try {
1117
+ // 1. 停止接收新消息
1118
+ logger.info('⚡ 停止接收新消息...');
1119
+ await pluginManager.stopAll();
1120
+ // 2. 等待当前任务完成(最多等待 30 秒)
1121
+ logger.info('⚡ 等待当前任务完成...');
1122
+ await new Promise(resolve => setTimeout(resolve, 2000));
1123
+ // 3. 停止消息队列
1124
+ logger.info('⚡ 停止消息队列...');
1125
+ messageQueue?.stop();
1126
+ // 4. 停止任务调度器
1127
+ logger.info('⚡ 停止任务调度器...');
1128
+ stopScheduler();
1129
+ // 5. 停止插件目录监听
1130
+ logger.info('⚡ 停止插件监听...');
1131
+ stopWatching();
1132
+ // 6. 关闭数据库连接
1133
+ logger.info('⚡ 关闭数据库连接...');
1134
+ try {
1135
+ // 访问全局数据库实例
1136
+ if (global.__flashclaw_db) {
1137
+ global.__flashclaw_db.close();
1138
+ global.__flashclaw_db = undefined;
1139
+ }
1140
+ }
1141
+ catch (err) {
1142
+ logger.warn({ err }, '关闭数据库连接时出错');
1143
+ }
1144
+ // 7. 卸载插件
1145
+ logger.info('⚡ 卸载插件...');
1146
+ await pluginManager.clear();
1147
+ // 8. 保存状态
1148
+ logger.info('⚡ 保存状态...');
1149
+ saveState();
1150
+ logger.info('⚡ FlashClaw 已安全关闭');
1151
+ process.exit(0);
1152
+ }
1153
+ catch (error) {
1154
+ logger.error({ error }, '关闭时发生错误');
1155
+ process.exit(1);
1156
+ }
1157
+ }
1158
+ /**
1159
+ * 设置优雅关闭处理
1160
+ */
1161
+ function setupGracefulShutdown() {
1162
+ // 监听关闭信号
1163
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
1164
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
1165
+ // 未捕获异常处理
1166
+ process.on('uncaughtException', (err) => {
1167
+ logger.error({ err }, '未捕获异常');
1168
+ gracefulShutdown('uncaughtException').catch(() => {
1169
+ process.exit(1);
1170
+ });
1171
+ });
1172
+ process.on('unhandledRejection', (reason) => {
1173
+ logger.error({ reason }, '未处理的 Promise 拒绝');
1174
+ });
1175
+ }
1176
+ // 直接运行时启动
1177
+ main().catch(err => {
1178
+ logger.error({ err }, '⚡ FlashClaw 启动失败');
1179
+ process.exit(1);
1180
+ });
1181
+ //# sourceMappingURL=index.js.map