@cuebot/skill 1.0.5

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.
@@ -0,0 +1,136 @@
1
+ /**
2
+ * 文件操作工具
3
+ * 封装 fs-extra 提供便捷的文件操作
4
+ */
5
+
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { homedir } from 'os';
9
+
10
+ const CUECUE_DIR = path.join(homedir(), '.cuecue');
11
+
12
+ /**
13
+ * 确保目录存在
14
+ * @param {string} dir - 目录路径
15
+ */
16
+ export async function ensureDir(dir) {
17
+ await fs.ensureDir(dir);
18
+ }
19
+
20
+ /**
21
+ * 读取 JSON 文件
22
+ * @param {string} filePath - 文件路径
23
+ * @returns {Promise<object>} JSON 对象
24
+ */
25
+ export async function readJson(filePath) {
26
+ try {
27
+ return await fs.readJson(filePath);
28
+ } catch (error) {
29
+ if (error.code === 'ENOENT') {
30
+ return null;
31
+ }
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * 写入 JSON 文件
38
+ * @param {string} filePath - 文件路径
39
+ * @param {object} data - 数据对象
40
+ */
41
+ export async function writeJson(filePath, data) {
42
+ await fs.ensureDir(path.dirname(filePath));
43
+ await fs.writeJson(filePath, data, { spaces: 2 });
44
+ }
45
+
46
+ /**
47
+ * 获取用户数据目录
48
+ * @param {string} chatId - 用户 ID
49
+ * @returns {string} 用户目录路径
50
+ */
51
+ export function getUserDir(chatId) {
52
+ return path.join(CUECUE_DIR, 'users', chatId);
53
+ }
54
+
55
+ /**
56
+ * 获取任务文件路径
57
+ * @param {string} chatId - 用户 ID
58
+ * @param {string} taskId - 任务 ID
59
+ * @returns {string} 任务文件路径
60
+ */
61
+ export function getTaskFilePath(chatId, taskId) {
62
+ return path.join(getUserDir(chatId), 'tasks', `${taskId}.json`);
63
+ }
64
+
65
+ /**
66
+ * 获取监控文件路径
67
+ * @param {string} chatId - 用户 ID
68
+ * @param {string} monitorId - 监控 ID
69
+ * @returns {string} 监控文件路径
70
+ */
71
+ export function getMonitorFilePath(chatId, monitorId) {
72
+ return path.join(getUserDir(chatId), 'monitors', `${monitorId}.json`);
73
+ }
74
+
75
+ /**
76
+ * 获取通知文件路径
77
+ * @param {string} chatId - 用户 ID
78
+ * @param {string} notificationId - 通知 ID
79
+ * @returns {string} 通知文件路径
80
+ */
81
+ export function getNotificationFilePath(chatId, notificationId) {
82
+ return path.join(getUserDir(chatId), 'notifications', `${notificationId}.json`);
83
+ }
84
+
85
+ /**
86
+ * 获取日志目录
87
+ * @returns {string} 日志目录路径
88
+ */
89
+ export function getLogDir() {
90
+ return path.join(CUECUE_DIR, 'logs');
91
+ }
92
+
93
+ /**
94
+ * 列出目录中的 JSON 文件
95
+ * @param {string} dir - 目录路径
96
+ * @returns {Promise<string[]>} 文件名列表
97
+ */
98
+ export async function listJsonFiles(dir) {
99
+ try {
100
+ const files = await fs.readdir(dir);
101
+ return files.filter(f => f.endsWith('.json')).sort().reverse();
102
+ } catch (error) {
103
+ if (error.code === 'ENOENT') {
104
+ return [];
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 检查文件是否存在
112
+ * @param {string} filePath - 文件路径
113
+ * @returns {Promise<boolean>}
114
+ */
115
+ export async function fileExists(filePath) {
116
+ try {
117
+ await fs.access(filePath);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 获取文件修改时间
126
+ * @param {string} filePath - 文件路径
127
+ * @returns {Promise<Date|null>}
128
+ */
129
+ export async function getFileMtime(filePath) {
130
+ try {
131
+ const stats = await fs.stat(filePath);
132
+ return stats.mtime;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * 可靠通知队列
3
+ * 本地持久化 + 失败重试机制
4
+ */
5
+
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { getUserDir } from './fileUtils.js';
9
+ import { createLogger } from '../core/logger.js';
10
+
11
+ const logger = createLogger('NotificationQueue');
12
+
13
+ /**
14
+ * 获取通知队列目录
15
+ * @param {string} chatId
16
+ * @returns {string}
17
+ */
18
+ function getQueueDir(chatId) {
19
+ return path.join(getUserDir(chatId), 'notification-queue');
20
+ }
21
+
22
+ /**
23
+ * 初始化队列目录
24
+ * @param {string} chatId
25
+ */
26
+ async function initQueue(chatId) {
27
+ const queueDir = getQueueDir(chatId);
28
+ await fs.ensureDir(queueDir);
29
+
30
+ // 创建子目录
31
+ await fs.ensureDir(path.join(queueDir, 'pending'));
32
+ await fs.ensureDir(path.join(queueDir, 'failed'));
33
+ await fs.ensureDir(path.join(queueDir, 'sent'));
34
+ }
35
+
36
+ /**
37
+ * 生成通知 ID
38
+ * @returns {string}
39
+ */
40
+ function generateNotificationId() {
41
+ return `notif_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
42
+ }
43
+
44
+ /**
45
+ * 添加通知到队列
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
+ */
52
+ export async function enqueueNotification({ chatId, type, data }) {
53
+ await initQueue(chatId);
54
+
55
+ const notificationId = generateNotificationId();
56
+ const notification = {
57
+ id: notificationId,
58
+ chat_id: chatId,
59
+ type,
60
+ data,
61
+ status: 'pending',
62
+ created_at: new Date().toISOString(),
63
+ retry_count: 0,
64
+ max_retries: 3
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})`);
72
+
73
+ return notificationId;
74
+ }
75
+
76
+ /**
77
+ * 获取待发送通知
78
+ * @param {string} chatId
79
+ * @returns {Promise<Array>}
80
+ */
81
+ export async function getPendingNotifications(chatId) {
82
+ try {
83
+ const queueDir = getQueueDir(chatId);
84
+ const pendingDir = path.join(queueDir, 'pending');
85
+
86
+ const files = await fs.readdir(pendingDir);
87
+ const notifications = [];
88
+
89
+ for (const file of files.filter(f => f.endsWith('.json'))) {
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 [];
106
+ }
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
+
178
+ return pending.filter(notif => {
179
+ if (notif.retry_count === 0) return true;
180
+
181
+ // 指数退避:1分钟, 5分钟, 25分钟
182
+ const backoffMinutes = Math.pow(5, notif.retry_count - 1);
183
+ const lastAttempt = new Date(notif.last_attempt || notif.created_at).getTime();
184
+ const nextRetry = lastAttempt + (backoffMinutes * 60 * 1000);
185
+
186
+ return now >= nextRetry;
187
+ });
188
+ }
189
+
190
+ /**
191
+ * 获取失败通知列表
192
+ * @param {string} chatId
193
+ * @param {number} limit
194
+ * @returns {Promise<Array>}
195
+ */
196
+ export async function getFailedNotifications(chatId, limit = 10) {
197
+ try {
198
+ const queueDir = getQueueDir(chatId);
199
+ const failedDir = path.join(queueDir, 'failed');
200
+
201
+ const files = await fs.readdir(failedDir);
202
+ const notifications = [];
203
+
204
+ for (const file of files.filter(f => f.endsWith('.json')).slice(0, limit)) {
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 [];
218
+ }
219
+ }
220
+
221
+ /**
222
+ * 清理已发送通知(保留最近7天)
223
+ * @param {string} chatId
224
+ */
225
+ export async function cleanupSentNotifications(chatId) {
226
+ try {
227
+ const queueDir = getQueueDir(chatId);
228
+ const sentDir = path.join(queueDir, 'sent');
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);
253
+ }
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
+
270
+ // 每小时清理一次已发送通知
271
+ const cleanupInterval = setInterval(() => {
272
+ cleanupSentNotifications(chatId);
273
+ }, 60 * 60 * 1000);
274
+
275
+ return {
276
+ stop: () => {
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);
291
+
292
+ for (const notif of notifications) {
293
+ try {
294
+ await logger.info(`Processing notification: ${notif.id}`);
295
+
296
+ // 调用实际发送函数
297
+ const success = await sendFn(notif);
298
+
299
+ if (success) {
300
+ await markNotificationSent(chatId, notif.id);
301
+ } else {
302
+ await markNotificationFailed(chatId, notif.id, 'Send returned false');
303
+ }
304
+ } catch (error) {
305
+ await markNotificationFailed(chatId, notif.id, error.message);
306
+ }
307
+ }
308
+ } catch (error) {
309
+ await logger.error('Failed to process notification queue', error);
310
+ }
311
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * OpenClaw 工具复用模块
3
+ * 检测并复用 OpenClaw 核心能力
4
+ */
5
+
6
+ import { execSync } from 'child_process';
7
+ import { createLogger } from '../core/logger.js';
8
+
9
+ const logger = createLogger('OpenClawUtils');
10
+
11
+ /**
12
+ * 检测是否运行在 OpenClaw Gateway 环境中
13
+ * @returns {boolean}
14
+ */
15
+ export function isOpenClawGateway() {
16
+ try {
17
+ execSync('openclaw cron status', { stdio: 'ignore' });
18
+ return true;
19
+ } catch (e) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * 注册监控任务到 OpenClaw Cron
26
+ * @param {Object} options - 选项
27
+ * @returns {boolean} 是否成功
28
+ */
29
+ export async function registerCronTask(options) {
30
+ const { name, message, cron, chatId } = options;
31
+
32
+ if (!isOpenClawGateway()) {
33
+ logger.info('Not in Gateway mode, skipping OpenClaw cron registration');
34
+ return false;
35
+ }
36
+
37
+ try {
38
+ const cmd = [
39
+ 'openclaw cron add',
40
+ `--name "${name}"`,
41
+ `--message "${message}"`,
42
+ `--cron "${cron}"`,
43
+ '--channel feishu',
44
+ `--to "${chatId}"`,
45
+ '--announce'
46
+ ].join(' ');
47
+
48
+ execSync(cmd, { stdio: 'inherit' });
49
+ logger.info(`Registered cron task: ${name}`);
50
+ return true;
51
+ } catch (error) {
52
+ logger.error('Failed to register cron task:', error.message);
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 注销监控任务
59
+ * @param {string} name - 任务名称
60
+ * @returns {boolean}
61
+ */
62
+ export async function unregisterCronTask(name) {
63
+ if (!isOpenClawGateway()) return false;
64
+
65
+ try {
66
+ execSync(`openclaw cron rm "${name}"`, { stdio: 'ignore' });
67
+ logger.info(`Unregistered cron task: ${name}`);
68
+ return true;
69
+ } catch (error) {
70
+ logger.warn('Failed to unregister cron task:', error.message);
71
+ return false;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * 列出 OpenClaw Cron 任务
77
+ * @returns {Array}
78
+ */
79
+ export function listCronTasks() {
80
+ if (!isOpenClawGateway()) return [];
81
+
82
+ try {
83
+ const output = execSync('openclaw cron list --json', { encoding: 'utf-8' });
84
+ return JSON.parse(output || '[]');
85
+ } catch (e) {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 使用 OpenClaw Subagent 并行执行研究
92
+ * 通过 OpenClaw agent 命令实现
93
+ * @param {Array} topics - 研究主题列表
94
+ * @param {Object} options - 选项
95
+ * @returns {Promise<Array>}
96
+ */
97
+ export async function runParallelResearch(topics, options = {}) {
98
+ if (!isOpenClawGateway()) {
99
+ return null; // 降级到内部处理
100
+ }
101
+
102
+ try {
103
+ const results = [];
104
+ for (const topic of topics) {
105
+ const cmd = `openclaw agent --message "深度研究: ${topic}" --deliver --json`;
106
+ const output = execSync(cmd, { encoding: 'utf-8' });
107
+ results.push(JSON.parse(output));
108
+ }
109
+ return results;
110
+ } catch (error) {
111
+ logger.error('Parallel research failed:', error.message);
112
+ return null;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 发送消息到子会话
118
+ * @param {string} sessionKey - 会话 key
119
+ * @param {string} message - 消息
120
+ * @returns {Promise}
121
+ */
122
+ export async function sendToSubagent(sessionKey, message) {
123
+ if (!isOpenClawGateway()) {
124
+ return null;
125
+ }
126
+
127
+ try {
128
+ const cmd = `openclaw sessions send --session "${sessionKey}" --message "${message}"`;
129
+ execSync(cmd, { stdio: 'ignore' });
130
+ return true;
131
+ } catch (error) {
132
+ logger.error('Send to subagent failed:', error.message);
133
+ return null;
134
+ }
135
+ }