@cuebot/skill 1.0.5 → 1.0.6
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 +1 -1
- package/config/modes.json +32 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/api/cuecueClient.js +54 -13
- package/src/core/backgroundExecutor.js +8 -8
- package/src/core/monitorManager.js +14 -0
- package/src/cron/monitor-daemon.js +36 -24
- package/src/index.js +4 -4
- package/src/notifier/index.js +43 -3
- package/src/utils/dataSource.js +145 -20
- package/src/utils/envAdapter.js +233 -0
- package/src/utils/envUtils.js +57 -11
- package/src/utils/fileUtils.js +10 -3
- package/src/utils/notificationQueue.js +134 -246
- package/src/utils/openclawUtils.js +28 -1
- package/src/utils/subagentScheduler.js +147 -0
- package/src/utils/validators.js +0 -122
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 可靠通知队列
|
|
3
|
-
*
|
|
3
|
+
* 优先使用 OpenClaw Queue,本地队列作为 fallback
|
|
4
|
+
* 支持自动重试和统计
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import fs from 'fs-extra';
|
|
@@ -10,302 +11,189 @@ import { createLogger } from '../core/logger.js';
|
|
|
10
11
|
|
|
11
12
|
const logger = createLogger('NotificationQueue');
|
|
12
13
|
|
|
14
|
+
const MAX_RETRY = 3;
|
|
15
|
+
const RETRY_DELAYS = [5000, 15000, 60000]; // 重试延迟: 5s, 15s, 60s
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 检测是否在 OpenClaw 环境中
|
|
19
|
+
*/
|
|
20
|
+
function isOpenClawEnv() {
|
|
21
|
+
return process.env.OPENCLAW_MODE === 'gateway' ||
|
|
22
|
+
process.env.OPENCLAW_GATEWAY_PORT ||
|
|
23
|
+
process.env.OPENCLAW_HOME;
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
/**
|
|
14
|
-
* 获取通知队列目录
|
|
15
|
-
* @param {string} chatId
|
|
16
|
-
* @returns {string}
|
|
27
|
+
* 获取通知队列目录 (本地 fallback)
|
|
17
28
|
*/
|
|
18
|
-
function
|
|
29
|
+
function getLocalQueueDir(chatId) {
|
|
19
30
|
return path.join(getUserDir(chatId), 'notification-queue');
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
/**
|
|
23
|
-
* 初始化队列目录
|
|
24
|
-
* @param {string} chatId
|
|
34
|
+
* 初始化队列目录 (本地 fallback)
|
|
25
35
|
*/
|
|
26
36
|
async function initQueue(chatId) {
|
|
27
|
-
|
|
28
|
-
await fs.ensureDir(queueDir);
|
|
37
|
+
if (isOpenClawEnv()) return;
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
const queueDir = getLocalQueueDir(chatId);
|
|
40
|
+
await fs.ensureDir(queueDir);
|
|
31
41
|
await fs.ensureDir(path.join(queueDir, 'pending'));
|
|
32
42
|
await fs.ensureDir(path.join(queueDir, 'failed'));
|
|
33
43
|
await fs.ensureDir(path.join(queueDir, 'sent'));
|
|
44
|
+
await fs.ensureDir(path.join(queueDir, 'processing'));
|
|
34
45
|
}
|
|
35
46
|
|
|
36
|
-
/**
|
|
37
|
-
* 生成通知 ID
|
|
38
|
-
* @returns {string}
|
|
39
|
-
*/
|
|
40
47
|
function generateNotificationId() {
|
|
41
48
|
return `notif_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/**
|
|
45
52
|
* 添加通知到队列
|
|
46
|
-
* @param {Object} notification
|
|
47
|
-
* @param {string} notification.chatId
|
|
48
|
-
* @param {string} notification.type - 类型: 'research_complete', 'monitor_trigger', 'progress'
|
|
49
|
-
* @param {Object} notification.data - 通知数据
|
|
50
|
-
* @returns {Promise<string>} 通知 ID
|
|
51
53
|
*/
|
|
52
|
-
export async function enqueueNotification(
|
|
54
|
+
export async function enqueueNotification(notification) {
|
|
55
|
+
const { chatId, type, data } = notification;
|
|
56
|
+
|
|
57
|
+
if (isOpenClawEnv()) {
|
|
58
|
+
try {
|
|
59
|
+
const { execSync } = await import('child_process');
|
|
60
|
+
const queueName = `cue-notifications-${chatId}`;
|
|
61
|
+
execSync(`openclaw queue add ${queueName} "${JSON.stringify(notification).replace(/"/g, '\\"')}"`, {
|
|
62
|
+
stdio: 'ignore'
|
|
63
|
+
});
|
|
64
|
+
logger.info(`Notification enqueued to OpenClaw Queue: ${queueName}`);
|
|
65
|
+
return generateNotificationId();
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.warn('OpenClaw Queue failed, using local fallback');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const queueDir = getLocalQueueDir(chatId);
|
|
53
72
|
await initQueue(chatId);
|
|
54
73
|
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
const notifId = generateNotificationId();
|
|
75
|
+
const filePath = path.join(queueDir, 'pending', `${notifId}.json`);
|
|
76
|
+
|
|
77
|
+
await fs.writeJson(filePath, {
|
|
78
|
+
id: notifId,
|
|
79
|
+
chatId,
|
|
59
80
|
type,
|
|
60
81
|
data,
|
|
61
|
-
status: 'pending',
|
|
62
82
|
created_at: new Date().toISOString(),
|
|
63
83
|
retry_count: 0,
|
|
64
|
-
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const queueDir = getQueueDir(chatId);
|
|
68
|
-
const filePath = path.join(queueDir, 'pending', `${notificationId}.json`);
|
|
69
|
-
|
|
70
|
-
await fs.writeJson(filePath, notification, { spaces: 2 });
|
|
71
|
-
await logger.info(`Notification enqueued: ${notificationId} (${type})`);
|
|
84
|
+
max_retry: MAX_RETRY
|
|
85
|
+
}, { spaces: 2 });
|
|
72
86
|
|
|
73
|
-
|
|
87
|
+
logger.info(`Notification enqueued locally: ${notifId}`);
|
|
88
|
+
return notifId;
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
/**
|
|
77
|
-
*
|
|
78
|
-
* @param {string} chatId
|
|
79
|
-
* @returns {Promise<Array>}
|
|
92
|
+
* 获取队列统计
|
|
80
93
|
*/
|
|
81
|
-
export async function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const notif = await fs.readJson(path.join(pendingDir, file));
|
|
92
|
-
if (notif.status === 'pending') {
|
|
93
|
-
notifications.push(notif);
|
|
94
|
-
}
|
|
95
|
-
} catch (e) {
|
|
96
|
-
// 忽略读取错误
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 按创建时间排序
|
|
101
|
-
return notifications.sort((a, b) =>
|
|
102
|
-
new Date(a.created_at) - new Date(b.created_at)
|
|
103
|
-
);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
return [];
|
|
94
|
+
export async function getQueueStats(chatId) {
|
|
95
|
+
if (isOpenClawEnv()) {
|
|
96
|
+
try {
|
|
97
|
+
const { execSync } = await import('child_process');
|
|
98
|
+
const result = execSync(`openclaw queue status cue-notifications-${chatId}`, {
|
|
99
|
+
encoding: 'utf-8'
|
|
100
|
+
});
|
|
101
|
+
return { source: 'openclaw', stats: result };
|
|
102
|
+
} catch {}
|
|
106
103
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 标记通知为已发送
|
|
111
|
-
* @param {string} chatId
|
|
112
|
-
* @param {string} notificationId
|
|
113
|
-
*/
|
|
114
|
-
export async function markNotificationSent(chatId, notificationId) {
|
|
115
|
-
try {
|
|
116
|
-
const queueDir = getQueueDir(chatId);
|
|
117
|
-
const pendingPath = path.join(queueDir, 'pending', `${notificationId}.json`);
|
|
118
|
-
const sentPath = path.join(queueDir, 'sent', `${notificationId}.json`);
|
|
119
|
-
|
|
120
|
-
// 读取并更新状态
|
|
121
|
-
const notification = await fs.readJson(pendingPath);
|
|
122
|
-
notification.status = 'sent';
|
|
123
|
-
notification.sent_at = new Date().toISOString();
|
|
124
|
-
|
|
125
|
-
// 移动到 sent 目录
|
|
126
|
-
await fs.writeJson(sentPath, notification, { spaces: 2 });
|
|
127
|
-
await fs.remove(pendingPath);
|
|
128
|
-
|
|
129
|
-
await logger.info(`Notification marked as sent: ${notificationId}`);
|
|
130
|
-
} catch (error) {
|
|
131
|
-
await logger.error(`Failed to mark notification as sent: ${notificationId}`, error);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* 标记通知为失败
|
|
137
|
-
* @param {string} chatId
|
|
138
|
-
* @param {string} notificationId
|
|
139
|
-
* @param {string} error
|
|
140
|
-
*/
|
|
141
|
-
export async function markNotificationFailed(chatId, notificationId, error) {
|
|
142
|
-
try {
|
|
143
|
-
const queueDir = getQueueDir(chatId);
|
|
144
|
-
const pendingPath = path.join(queueDir, 'pending', `${notificationId}.json`);
|
|
145
|
-
const failedPath = path.join(queueDir, 'failed', `${notificationId}.json`);
|
|
146
|
-
|
|
147
|
-
// 读取通知
|
|
148
|
-
const notification = await fs.readJson(pendingPath);
|
|
149
|
-
notification.retry_count++;
|
|
150
|
-
notification.last_error = error;
|
|
151
|
-
notification.last_attempt = new Date().toISOString();
|
|
152
|
-
|
|
153
|
-
if (notification.retry_count >= notification.max_retries) {
|
|
154
|
-
// 超过最大重试次数,移动到失败目录
|
|
155
|
-
notification.status = 'failed';
|
|
156
|
-
await fs.writeJson(failedPath, notification, { spaces: 2 });
|
|
157
|
-
await fs.remove(pendingPath);
|
|
158
|
-
await logger.warn(`Notification failed permanently: ${notificationId} (${notification.retry_count} retries)`);
|
|
159
|
-
} else {
|
|
160
|
-
// 更新重试计数,保留在 pending 目录
|
|
161
|
-
await fs.writeJson(pendingPath, notification, { spaces: 2 });
|
|
162
|
-
await logger.info(`Notification retry scheduled: ${notificationId} (attempt ${notification.retry_count})`);
|
|
163
|
-
}
|
|
164
|
-
} catch (err) {
|
|
165
|
-
await logger.error(`Failed to mark notification failed: ${notificationId}`, err);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 获取需要重试的通知
|
|
171
|
-
* @param {string} chatId
|
|
172
|
-
* @returns {Promise<Array>}
|
|
173
|
-
*/
|
|
174
|
-
export async function getNotificationsForRetry(chatId) {
|
|
175
|
-
const pending = await getPendingNotifications(chatId);
|
|
176
|
-
const now = Date.now();
|
|
177
104
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
105
|
+
const queueDir = getLocalQueueDir(chatId);
|
|
106
|
+
const pending = await fs.readdir(path.join(queueDir, 'pending')).catch(() => []);
|
|
107
|
+
const failed = await fs.readdir(path.join(queueDir, 'failed')).catch(() => []);
|
|
108
|
+
const sent = await fs.readdir(path.join(queueDir, 'sent')).catch(() => []);
|
|
109
|
+
const processing = await fs.readdir(path.join(queueDir, 'processing')).catch(() => []);
|
|
110
|
+
|
|
111
|
+
// 计算总处理数
|
|
112
|
+
const totalProcessed = sent.length + failed.length;
|
|
113
|
+
const successRate = totalProcessed > 0 ? ((sent.length / totalProcessed) * 100).toFixed(1) : 0;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
source: 'local',
|
|
117
|
+
pending: pending.length,
|
|
118
|
+
processing: processing.length,
|
|
119
|
+
failed: failed.length,
|
|
120
|
+
sent: sent.length,
|
|
121
|
+
totalProcessed,
|
|
122
|
+
successRate: `${successRate}%`,
|
|
123
|
+
lastUpdated: new Date().toISOString()
|
|
124
|
+
};
|
|
188
125
|
}
|
|
189
126
|
|
|
190
127
|
/**
|
|
191
|
-
*
|
|
192
|
-
* @param {string} chatId
|
|
193
|
-
* @param {number} limit
|
|
194
|
-
* @returns {Promise<Array>}
|
|
128
|
+
* 处理失败重试
|
|
195
129
|
*/
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
const notif = await fs.readJson(path.join(failedDir, file));
|
|
207
|
-
notifications.push(notif);
|
|
208
|
-
} catch (e) {
|
|
209
|
-
// 忽略
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return notifications.sort((a, b) =>
|
|
214
|
-
new Date(b.created_at) - new Date(a.created_at)
|
|
215
|
-
);
|
|
216
|
-
} catch (error) {
|
|
217
|
-
return [];
|
|
130
|
+
async function handleFailed(notif, queueDir) {
|
|
131
|
+
const filePath = path.join(queueDir, 'failed', `${notif.id}.json`);
|
|
132
|
+
const failedPath = path.join(queueDir, 'failed', `${notif.id}.json`);
|
|
133
|
+
|
|
134
|
+
// 检查重试次数
|
|
135
|
+
if (notif.retry_count >= MAX_RETRY) {
|
|
136
|
+
logger.warn(`Notification ${notif.id} exceeded max retries, moving to dead letter`);
|
|
137
|
+
await fs.move(failedPath, path.join(queueDir, 'dead', `${notif.id}.json`));
|
|
138
|
+
return;
|
|
218
139
|
}
|
|
140
|
+
|
|
141
|
+
// 延迟重试
|
|
142
|
+
const delay = RETRY_DELAYS[notif.retry_count] || RETRY_DELAYS[RETRY_DELAYS.length - 1];
|
|
143
|
+
notif.retry_count++;
|
|
144
|
+
notif.next_retry_at = new Date(Date.now() + delay).toISOString();
|
|
145
|
+
|
|
146
|
+
await fs.writeJson(failedPath, notif, { spaces: 2 });
|
|
147
|
+
logger.info(`Notification ${notif.id} scheduled for retry ${notif.retry_count}/${MAX_RETRY}`);
|
|
219
148
|
}
|
|
220
149
|
|
|
221
150
|
/**
|
|
222
|
-
*
|
|
223
|
-
* @param {string} chatId
|
|
151
|
+
* 启动通知处理器 - 支持自动重试
|
|
224
152
|
*/
|
|
225
|
-
export async function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const files = await fs.readdir(sentDir);
|
|
231
|
-
const cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7天前
|
|
232
|
-
|
|
233
|
-
let cleaned = 0;
|
|
234
|
-
for (const file of files.filter(f => f.endsWith('.json'))) {
|
|
235
|
-
try {
|
|
236
|
-
const notif = await fs.readJson(path.join(sentDir, file));
|
|
237
|
-
const sentAt = new Date(notif.sent_at || notif.created_at).getTime();
|
|
238
|
-
|
|
239
|
-
if (sentAt < cutoff) {
|
|
240
|
-
await fs.remove(path.join(sentDir, file));
|
|
241
|
-
cleaned++;
|
|
242
|
-
}
|
|
243
|
-
} catch (e) {
|
|
244
|
-
// 忽略
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (cleaned > 0) {
|
|
249
|
-
await logger.info(`Cleaned up ${cleaned} old sent notifications`);
|
|
250
|
-
}
|
|
251
|
-
} catch (error) {
|
|
252
|
-
await logger.error('Failed to cleanup sent notifications', error);
|
|
153
|
+
export async function startNotificationProcessor(chatId, handler) {
|
|
154
|
+
if (isOpenClawEnv()) {
|
|
155
|
+
logger.info('Using OpenClaw Queue processor');
|
|
156
|
+
return;
|
|
253
157
|
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* 启动后台通知处理器
|
|
258
|
-
* @param {string} chatId
|
|
259
|
-
* @param {Function} sendFn - 实际发送函数
|
|
260
|
-
*/
|
|
261
|
-
export function startNotificationProcessor(chatId, sendFn) {
|
|
262
|
-
// 立即处理一次
|
|
263
|
-
processNotificationQueue(chatId, sendFn);
|
|
264
|
-
|
|
265
|
-
// 每30秒检查一次队列
|
|
266
|
-
const intervalId = setInterval(() => {
|
|
267
|
-
processNotificationQueue(chatId, sendFn);
|
|
268
|
-
}, 30000);
|
|
269
158
|
|
|
270
|
-
|
|
271
|
-
const cleanupInterval = setInterval(() => {
|
|
272
|
-
cleanupSentNotifications(chatId);
|
|
273
|
-
}, 60 * 60 * 1000);
|
|
159
|
+
const queueDir = getLocalQueueDir(chatId);
|
|
274
160
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
clearInterval(intervalId);
|
|
278
|
-
clearInterval(cleanupInterval);
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* 处理通知队列
|
|
285
|
-
* @param {string} chatId
|
|
286
|
-
* @param {Function} sendFn
|
|
287
|
-
*/
|
|
288
|
-
async function processNotificationQueue(chatId, sendFn) {
|
|
289
|
-
try {
|
|
290
|
-
const notifications = await getNotificationsForRetry(chatId);
|
|
161
|
+
const processQueue = async () => {
|
|
162
|
+
const pending = await fs.readdir(path.join(queueDir, 'pending')).catch(() => []);
|
|
291
163
|
|
|
292
|
-
for (const
|
|
164
|
+
for (const file of pending) {
|
|
293
165
|
try {
|
|
294
|
-
|
|
166
|
+
const filePath = path.join(queueDir, 'pending', file);
|
|
167
|
+
const notif = await fs.readJson(filePath);
|
|
168
|
+
|
|
169
|
+
// 标记为处理中
|
|
170
|
+
await fs.move(filePath, path.join(queueDir, 'processing', file));
|
|
295
171
|
|
|
296
|
-
|
|
297
|
-
const success = await sendFn(notif);
|
|
172
|
+
await handler(notif);
|
|
298
173
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
174
|
+
// 成功移动到已发送
|
|
175
|
+
await fs.move(path.join(queueDir, 'processing', file),
|
|
176
|
+
path.join(queueDir, 'sent', file));
|
|
177
|
+
} catch (err) {
|
|
178
|
+
logger.error(`Failed to process ${file}:`, err.message);
|
|
179
|
+
|
|
180
|
+
// 处理失败,移至失败队列
|
|
181
|
+
const procPath = path.join(queueDir, 'processing', file);
|
|
182
|
+
if (await fs.pathExists(procPath)) {
|
|
183
|
+
const notif = await fs.readJson(procPath);
|
|
184
|
+
await handleFailed(notif, queueDir);
|
|
303
185
|
}
|
|
304
|
-
} catch (error) {
|
|
305
|
-
await markNotificationFailed(chatId, notif.id, error.message);
|
|
306
186
|
}
|
|
307
187
|
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
setInterval(processQueue, 60000);
|
|
191
|
+
processQueue();
|
|
311
192
|
}
|
|
193
|
+
|
|
194
|
+
export default {
|
|
195
|
+
enqueueNotification,
|
|
196
|
+
getQueueStats,
|
|
197
|
+
startNotificationProcessor,
|
|
198
|
+
initQueue
|
|
199
|
+
};
|
|
@@ -26,6 +26,27 @@ export function isOpenClawGateway() {
|
|
|
26
26
|
* @param {Object} options - 选项
|
|
27
27
|
* @returns {boolean} 是否成功
|
|
28
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* 检查 cron 任务是否已存在
|
|
31
|
+
* @param {string} name - 任务名称
|
|
32
|
+
* @returns {Promise<boolean>}
|
|
33
|
+
*/
|
|
34
|
+
export async function hasCronTask(name) {
|
|
35
|
+
if (!isOpenClawGateway()) return false;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const tasks = listCronTasks();
|
|
39
|
+
return tasks.some(t => t.name === name);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 注册 cron 任务(幂等)
|
|
47
|
+
* @param {Object} options - 选项
|
|
48
|
+
* @returns {Promise<boolean>} 是否成功(新增或已存在)
|
|
49
|
+
*/
|
|
29
50
|
export async function registerCronTask(options) {
|
|
30
51
|
const { name, message, cron, chatId } = options;
|
|
31
52
|
|
|
@@ -34,13 +55,19 @@ export async function registerCronTask(options) {
|
|
|
34
55
|
return false;
|
|
35
56
|
}
|
|
36
57
|
|
|
58
|
+
// ✅ 幂等检查:先检查是否已存在
|
|
59
|
+
if (await hasCronTask(name)) {
|
|
60
|
+
logger.info(`Cron task already exists: ${name}`);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
37
64
|
try {
|
|
38
65
|
const cmd = [
|
|
39
66
|
'openclaw cron add',
|
|
40
67
|
`--name "${name}"`,
|
|
41
68
|
`--message "${message}"`,
|
|
42
69
|
`--cron "${cron}"`,
|
|
43
|
-
'--channel feishu',
|
|
70
|
+
'--channel ${process.env.OPENCLAW_CHANNEL || \'feishu\'}',
|
|
44
71
|
`--to "${chatId}"`,
|
|
45
72
|
'--announce'
|
|
46
73
|
].join(' ');
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent 调度模块
|
|
3
|
+
* 支持并行研究和监控后台检查
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { createLogger } from '../core/logger.js';
|
|
8
|
+
import { isOpenClaw } from './envAdapter.js';
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('SubagentScheduler');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 启动并行研究任务
|
|
14
|
+
* @param {Array} topics - 研究主题列表
|
|
15
|
+
* @returns {Promise<Array>} 任务 ID 列表
|
|
16
|
+
*/
|
|
17
|
+
export async function startParallelResearch(topics) {
|
|
18
|
+
if (!isOpenClaw()) {
|
|
19
|
+
logger.warn('Not in OpenClaw environment, cannot start subagent research');
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const taskIds = [];
|
|
24
|
+
|
|
25
|
+
for (const topic of topics) {
|
|
26
|
+
try {
|
|
27
|
+
const taskId = await startResearchTask(topic);
|
|
28
|
+
taskIds.push(taskId);
|
|
29
|
+
logger.info(`Started research task: ${topic} -> ${taskId}`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
logger.error(`Failed to start research for ${topic}:`, error.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return taskIds;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 启动单个研究任务
|
|
40
|
+
* @param {string} topic - 研究主题
|
|
41
|
+
* @returns {Promise<string>} 任务 ID
|
|
42
|
+
*/
|
|
43
|
+
async function startResearchTask(topic) {
|
|
44
|
+
const cmd = [
|
|
45
|
+
'openclaw agent',
|
|
46
|
+
'--message', `/cue ${topic}`,
|
|
47
|
+
'--deliver',
|
|
48
|
+
'--json'
|
|
49
|
+
].join(' ');
|
|
50
|
+
|
|
51
|
+
const output = execSync(cmd, { encoding: 'utf-8' });
|
|
52
|
+
const result = JSON.parse(output);
|
|
53
|
+
|
|
54
|
+
return result.taskId || result.runId || `research_${Date.now()}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 启动监控检查任务
|
|
59
|
+
* @param {string} chatId - 用户 ID
|
|
60
|
+
* @returns {Promise<string>} 任务 ID
|
|
61
|
+
*/
|
|
62
|
+
export async function startMonitorCheck(chatId) {
|
|
63
|
+
if (!isOpenClaw()) {
|
|
64
|
+
logger.warn('Not in OpenClaw environment, cannot start monitor check');
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// 使用 sessions_spawn 启动独立检查任务
|
|
70
|
+
const cmd = [
|
|
71
|
+
'openclaw sessions spawn',
|
|
72
|
+
'--label', `monitor_${chatId}`,
|
|
73
|
+
'--message', `/cm check`,
|
|
74
|
+
'--timeout', '300'
|
|
75
|
+
].join(' ');
|
|
76
|
+
|
|
77
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
78
|
+
logger.info(`Started monitor check for ${chatId}`);
|
|
79
|
+
|
|
80
|
+
return `monitor_${chatId}_${Date.now()}`;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error('Failed to start monitor check:', error.message);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 发送消息到 Subagent 会话
|
|
89
|
+
* @param {string} sessionKey - 会话 key
|
|
90
|
+
* @param {string} message - 消息
|
|
91
|
+
* @returns {Promise<boolean>}
|
|
92
|
+
*/
|
|
93
|
+
export async function sendToSession(sessionKey, message) {
|
|
94
|
+
if (!isOpenClaw()) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const cmd = [
|
|
100
|
+
'openclaw sessions send',
|
|
101
|
+
'--session', sessionKey,
|
|
102
|
+
'--message', message
|
|
103
|
+
].join(' ');
|
|
104
|
+
|
|
105
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
logger.error(`Failed to send to session ${sessionKey}:`, error.message);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 列出所有 Subagent 会话
|
|
115
|
+
* @returns {Promise<Array>}
|
|
116
|
+
*/
|
|
117
|
+
export async function listSessions() {
|
|
118
|
+
if (!isOpenClaw()) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const output = execSync('openclaw sessions list --json', { encoding: 'utf-8' });
|
|
124
|
+
return JSON.parse(output || '[]');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 终止 Subagent 会话
|
|
132
|
+
* @param {string} sessionKey - 会话 key
|
|
133
|
+
* @returns {Promise<boolean>}
|
|
134
|
+
*/
|
|
135
|
+
export async function terminateSession(sessionKey) {
|
|
136
|
+
if (!isOpenClaw()) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
execSync(`openclaw sessions kill ${sessionKey}`, { stdio: 'ignore' });
|
|
142
|
+
return true;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger.error(`Failed to terminate session ${sessionKey}:`, error.message);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|