@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,212 @@
1
+ /**
2
+ * 任务管理
3
+ * 管理研究任务的创建、更新和查询
4
+ */
5
+
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { getTaskFilePath, listJsonFiles, ensureDir, getUserDir } from '../utils/fileUtils.js';
9
+ import { createLogger } from './logger.js';
10
+
11
+ const logger = createLogger('TaskManager');
12
+
13
+ /**
14
+ * 任务状态枚举
15
+ */
16
+ export const TaskStatus = {
17
+ RUNNING: 'running',
18
+ COMPLETED: 'completed',
19
+ FAILED: 'failed',
20
+ TIMEOUT: 'timeout'
21
+ };
22
+
23
+ /**
24
+ * 任务管理类
25
+ */
26
+ export class TaskManager {
27
+ constructor(chatId) {
28
+ this.chatId = chatId;
29
+ this.tasksDir = path.join(getUserDir(chatId), 'tasks');
30
+ }
31
+
32
+ /**
33
+ * 创建任务
34
+ * @param {Object} taskData - 任务数据
35
+ * @returns {Promise<Object>}
36
+ */
37
+ async createTask(taskData) {
38
+ const { taskId, topic, mode = 'default' } = taskData;
39
+
40
+ await ensureDir(this.tasksDir);
41
+
42
+ const task = {
43
+ task_id: taskId,
44
+ topic,
45
+ mode,
46
+ chat_id: this.chatId,
47
+ status: TaskStatus.RUNNING,
48
+ created_at: new Date().toISOString(),
49
+ progress: '初始化',
50
+ ...taskData
51
+ };
52
+
53
+ const filePath = getTaskFilePath(this.chatId, taskId);
54
+ await fs.writeJson(filePath, task, { spaces: 2 });
55
+
56
+ await logger.info(`Task created: ${taskId}`);
57
+ return task;
58
+ }
59
+
60
+ /**
61
+ * 更新任务状态
62
+ * @param {string} taskId - 任务 ID
63
+ * @param {Object} updates - 更新数据
64
+ * @returns {Promise<Object|null>}
65
+ */
66
+ async updateTask(taskId, updates) {
67
+ const filePath = getTaskFilePath(this.chatId, taskId);
68
+
69
+ try {
70
+ const task = await fs.readJson(filePath);
71
+ const updatedTask = { ...task, ...updates };
72
+
73
+ if (updates.status === TaskStatus.COMPLETED) {
74
+ updatedTask.completed_at = new Date().toISOString();
75
+ }
76
+
77
+ await fs.writeJson(filePath, updatedTask, { spaces: 2 });
78
+ await logger.info(`Task updated: ${taskId}`);
79
+ return updatedTask;
80
+ } catch (error) {
81
+ await logger.error(`Failed to update task ${taskId}`, error);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 更新任务进度
88
+ * @param {string} taskId - 任务 ID
89
+ * @param {string} progress - 进度描述
90
+ * @param {number} percent - 进度百分比
91
+ */
92
+ async updateTaskProgress(taskId, progress, percent) {
93
+ return await this.updateTask(taskId, {
94
+ progress,
95
+ percent,
96
+ last_updated: new Date().toISOString()
97
+ });
98
+ }
99
+
100
+ /**
101
+ * 标记任务为完成
102
+ * @param {string} taskId - 任务 ID
103
+ * @param {Object} result - 结果数据
104
+ */
105
+ async completeTask(taskId, result = {}) {
106
+ const task = await this.getTask(taskId);
107
+ if (!task) return null;
108
+
109
+ const startTime = new Date(task.created_at);
110
+ const endTime = new Date();
111
+ const durationMinutes = Math.round((endTime - startTime) / (1000 * 60));
112
+
113
+ return await this.updateTask(taskId, {
114
+ status: TaskStatus.COMPLETED,
115
+ completed_at: endTime.toISOString(),
116
+ duration: durationMinutes,
117
+ result,
118
+ progress: '已完成',
119
+ percent: 100
120
+ });
121
+ }
122
+
123
+ /**
124
+ * 标记任务为失败
125
+ * @param {string} taskId - 任务 ID
126
+ * @param {string} error - 错误信息
127
+ */
128
+ async failTask(taskId, error) {
129
+ return await this.updateTask(taskId, {
130
+ status: TaskStatus.FAILED,
131
+ error,
132
+ completed_at: new Date().toISOString()
133
+ });
134
+ }
135
+
136
+ /**
137
+ * 标记任务为超时
138
+ * @param {string} taskId - 任务 ID
139
+ */
140
+ async timeoutTask(taskId) {
141
+ return await this.updateTask(taskId, {
142
+ status: TaskStatus.TIMEOUT,
143
+ completed_at: new Date().toISOString()
144
+ });
145
+ }
146
+
147
+ /**
148
+ * 获取任务
149
+ * @param {string} taskId - 任务 ID
150
+ * @returns {Promise<Object|null>}
151
+ */
152
+ async getTask(taskId) {
153
+ const filePath = getTaskFilePath(this.chatId, taskId);
154
+
155
+ try {
156
+ return await fs.readJson(filePath);
157
+ } catch (error) {
158
+ if (error.code === 'ENOENT') {
159
+ return null;
160
+ }
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * 获取任务列表
167
+ * @param {number} limit - 限制数量
168
+ * @returns {Promise<Array>}
169
+ */
170
+ async getTasks(limit = 10) {
171
+ const files = await listJsonFiles(this.tasksDir);
172
+ const tasks = [];
173
+
174
+ for (const file of files.slice(0, limit)) {
175
+ try {
176
+ const task = await fs.readJson(path.join(this.tasksDir, file));
177
+ tasks.push(task);
178
+ } catch (error) {
179
+ await logger.error(`Failed to read task ${file}`, error);
180
+ }
181
+ }
182
+
183
+ return tasks;
184
+ }
185
+
186
+ /**
187
+ * 获取运行中的任务
188
+ * @returns {Promise<Array>}
189
+ */
190
+ async getRunningTasks() {
191
+ const tasks = await this.getTasks(100);
192
+ return tasks.filter(t => t.status === TaskStatus.RUNNING);
193
+ }
194
+
195
+ /**
196
+ * 获取最近的任务
197
+ * @returns {Promise<Object|null>}
198
+ */
199
+ async getLatestTask() {
200
+ const tasks = await this.getTasks(1);
201
+ return tasks[0] || null;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * 创建任务管理器实例
207
+ * @param {string} chatId
208
+ * @returns {TaskManager}
209
+ */
210
+ export function createTaskManager(chatId) {
211
+ return new TaskManager(chatId);
212
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * 用户状态管理
3
+ * 管理用户初始化状态和版本信息
4
+ */
5
+
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { getUserDir, ensureDir } from '../utils/fileUtils.js';
9
+ import { createLogger } from './logger.js';
10
+
11
+ const logger = createLogger('UserState');
12
+
13
+ const CURRENT_VERSION = '1.0.4';
14
+
15
+ /**
16
+ * 用户状态类
17
+ */
18
+ export class UserState {
19
+ constructor(chatId) {
20
+ this.chatId = chatId;
21
+ this.userDir = getUserDir(chatId);
22
+ this.initializedFile = path.join(this.userDir, '.initialized');
23
+ this.versionFile = path.join(this.userDir, '.version');
24
+ }
25
+
26
+ /**
27
+ * 检查是否首次使用
28
+ * @returns {Promise<boolean>}
29
+ */
30
+ async isFirstTime() {
31
+ try {
32
+ await fs.access(this.initializedFile);
33
+ return false;
34
+ } catch {
35
+ return true;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 标记用户已初始化
41
+ */
42
+ async markInitialized() {
43
+ await ensureDir(this.userDir);
44
+ await fs.writeFile(this.initializedFile, '');
45
+ await fs.writeFile(this.versionFile, CURRENT_VERSION);
46
+ await logger.info(`User ${this.chatId} initialized`);
47
+ }
48
+
49
+ /**
50
+ * 检查版本状态
51
+ * @returns {Promise<'first_time'|'updated'|'normal'>}
52
+ */
53
+ async checkVersion() {
54
+ // 首次使用
55
+ if (await this.isFirstTime()) {
56
+ return 'first_time';
57
+ }
58
+
59
+ // 检查版本
60
+ try {
61
+ const savedVersion = await fs.readFile(this.versionFile, 'utf-8');
62
+ if (savedVersion.trim() !== CURRENT_VERSION) {
63
+ // 版本更新
64
+ await fs.writeFile(this.versionFile, CURRENT_VERSION);
65
+ return 'updated';
66
+ }
67
+ } catch (error) {
68
+ await logger.error('Failed to read version file', error);
69
+ }
70
+
71
+ return 'normal';
72
+ }
73
+
74
+ /**
75
+ * 获取用户数据目录
76
+ * @returns {string}
77
+ */
78
+ getUserDir() {
79
+ return this.userDir;
80
+ }
81
+
82
+ /**
83
+ * 获取任务目录
84
+ * @returns {string}
85
+ */
86
+ getTasksDir() {
87
+ return path.join(this.userDir, 'tasks');
88
+ }
89
+
90
+ /**
91
+ * 获取监控目录
92
+ * @returns {string}
93
+ */
94
+ getMonitorsDir() {
95
+ return path.join(this.userDir, 'monitors');
96
+ }
97
+
98
+ /**
99
+ * 获取通知目录
100
+ * @returns {string}
101
+ */
102
+ getNotificationsDir() {
103
+ return path.join(this.userDir, 'notifications');
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 创建用户状态实例
109
+ * @param {string} chatId
110
+ * @returns {UserState}
111
+ */
112
+ export function createUserState(chatId) {
113
+ return new UserState(chatId);
114
+ }
@@ -0,0 +1,279 @@
1
+ /**
2
+ * 监控守护进程
3
+ * 优先复用 OpenClaw Cron(Gateway 模式),降级使用内部调度
4
+ */
5
+
6
+ import cron from 'node-cron';
7
+ import { createLogger } from '../core/logger.js';
8
+ import { createMonitorManager } from '../core/monitorManager.js';
9
+ import { sendMonitorTriggerNotification } from '../notifier/index.js';
10
+ import { evaluateSmartTrigger } from '../utils/smartTrigger.js';
11
+ import { isOpenClawGateway, registerCronTask } from '../utils/openclawUtils.js';
12
+ import { execSync } from 'child_process';
13
+
14
+ const logger = createLogger('MonitorDaemon');
15
+
16
+ /**
17
+ * 检查单个监控项
18
+ * @param {Object} monitor - 监控项
19
+ * @param {string} chatId - 聊天 ID
20
+ */
21
+ async function checkMonitor(monitor, chatId) {
22
+ try {
23
+ await logger.info(`Checking monitor: ${monitor.monitor_id}`);
24
+
25
+ // 获取触发条件
26
+ const trigger = monitor.semantic_trigger || monitor.trigger;
27
+ if (!trigger) {
28
+ await logger.warn(`Monitor ${monitor.monitor_id} has no trigger condition`);
29
+ return;
30
+ }
31
+
32
+ // 执行搜索检查(使用 Tavily 或 QVeris)
33
+ const searchResult = await searchForTrigger(trigger, monitor);
34
+
35
+ // 使用智能触发评估
36
+ const evaluation = await evaluateSmartTrigger(
37
+ trigger,
38
+ searchResult.content || JSON.stringify(searchResult),
39
+ { useLLM: true, threshold: 0.6 }
40
+ );
41
+
42
+ if (evaluation.shouldTrigger) {
43
+ await logger.info(`Monitor triggered: ${monitor.monitor_id} (confidence: ${evaluation.confidence})`);
44
+
45
+ // 发送通知
46
+ await sendMonitorTriggerNotification({
47
+ chatId,
48
+ monitorId: monitor.monitor_id,
49
+ monitorTitle: monitor.title,
50
+ message: `${evaluation.reason}\n\n相关内容:${searchResult.content?.slice(0, 200) || '详见报告'}...`,
51
+ category: monitor.category || 'Data'
52
+ });
53
+
54
+ // 更新监控触发记录
55
+ const monitorManager = createMonitorManager(chatId);
56
+ await monitorManager.updateMonitor(monitor.monitor_id, {
57
+ last_triggered_at: new Date().toISOString(),
58
+ trigger_count: (monitor.trigger_count || 0) + 1
59
+ });
60
+ }
61
+ } catch (error) {
62
+ await logger.error(`Failed to check monitor ${monitor.monitor_id}`, error);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * 搜索触发相关内容
68
+ * @param {string} trigger - 触发条件描述
69
+ * @param {Object} monitor - 监控项
70
+ * @returns {Promise<Object>}
71
+ */
72
+ async function searchForTrigger(trigger, monitor) {
73
+ try {
74
+ let searchResult;
75
+
76
+ // 方法1: 使用 Tavily 搜索
77
+ if (process.env.TAVILY_API_KEY) {
78
+ searchResult = await searchWithTavily(trigger);
79
+ }
80
+ // 方法2: 使用 QVeris
81
+ else if (process.env.QVERIS_API_KEY) {
82
+ searchResult = await searchWithQVeris(trigger);
83
+ }
84
+ // 方法3: 降级到空结果
85
+ else {
86
+ return { content: '', results: [] };
87
+ }
88
+
89
+ // 合并搜索结果为文本
90
+ const content = searchResult.results
91
+ ?.map(r => `${r.title} ${r.content}`)
92
+ .join(' ') || '';
93
+
94
+ return {
95
+ content,
96
+ results: searchResult.results || []
97
+ };
98
+ } catch (error) {
99
+ await logger.error('Trigger search failed', error);
100
+ return { content: '', results: [] };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * 使用 Tavily 搜索
106
+ * @param {string} query - 查询
107
+ * @returns {Promise<Object>}
108
+ */
109
+ async function searchWithTavily(query) {
110
+ const response = await fetch('https://api.tavily.com/search', {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ 'Authorization': `Bearer ${process.env.TAVILY_API_KEY}`
115
+ },
116
+ body: JSON.stringify({
117
+ query,
118
+ search_depth: 'basic',
119
+ max_results: 5
120
+ })
121
+ });
122
+
123
+ return await response.json();
124
+ }
125
+
126
+ /**
127
+ * 使用 QVeris 搜索
128
+ * @param {string} query - 查询
129
+ * @returns {Promise<Object>}
130
+ */
131
+ async function searchWithQVeris(query) {
132
+ const response = await fetch('https://api.qveris.ai/v1/search', {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ 'Authorization': `Bearer ${process.env.QVERIS_API_KEY}`
137
+ },
138
+ body: JSON.stringify({
139
+ query,
140
+ max_results: 5
141
+ })
142
+ });
143
+
144
+ return await response.json();
145
+ }
146
+
147
+ /**
148
+ * 分析搜索结果
149
+ * @param {Object} result - 搜索结果
150
+ * @param {string} trigger - 触发条件
151
+ * @returns {boolean}
152
+ */
153
+ function analyzeSearchResult(result, trigger) {
154
+ // 简化实现:检查结果中是否包含关键词
155
+ const results = result.results || [];
156
+ const triggerKeywords = trigger.toLowerCase().split(/\s+/);
157
+
158
+ for (const item of results) {
159
+ const content = (item.content || item.title || '').toLowerCase();
160
+ const matchCount = triggerKeywords.filter(kw => content.includes(kw)).length;
161
+
162
+ // 如果匹配超过一半关键词,认为触发
163
+ if (matchCount >= triggerKeywords.length / 2) {
164
+ return true;
165
+ }
166
+ }
167
+
168
+ return false;
169
+ }
170
+
171
+ /**
172
+ * 运行监控检查(单次)
173
+ * @param {string} chatId - 聊天 ID
174
+ */
175
+ export async function runMonitorCheck(chatId) {
176
+ await logger.info(`Running monitor check for ${chatId}`);
177
+
178
+ const monitorManager = createMonitorManager(chatId);
179
+ const monitors = await monitorManager.getActiveMonitors();
180
+
181
+ if (monitors.length === 0) {
182
+ await logger.info('No active monitors found');
183
+ return;
184
+ }
185
+
186
+ await logger.info(`Found ${monitors.length} active monitors`);
187
+
188
+ for (const monitor of monitors) {
189
+ await checkMonitor(monitor, chatId);
190
+ }
191
+
192
+ await logger.info('Monitor check completed');
193
+ }
194
+
195
+ /**
196
+ * 启动监控守护进程(使用 node-cron)
197
+ * @param {string} chatId - 聊天 ID
198
+ * @returns {Object} cron job
199
+ */
200
+ export async function startMonitorDaemon(chatId) {
201
+ await logger.info(`Starting monitor daemon for ${chatId}`);
202
+
203
+ // 优先尝试 OpenClaw Gateway 模式
204
+ if (isOpenClawGateway()) {
205
+ await logger.info('OpenClaw Gateway detected, registering cron task...');
206
+ const registered = await registerCronTask({
207
+ name: `cue-monitor-${chatId}`,
208
+ message: `cue-monitor-check ${chatId}`,
209
+ cron: '*/30 * * * *',
210
+ chatId
211
+ });
212
+
213
+ if (registered) {
214
+ await logger.info(`OpenClaw cron registered successfully for ${chatId}`);
215
+ return { mode: 'openclaw', chatId };
216
+ }
217
+ }
218
+
219
+ // 降级:使用内部 node-cron
220
+ await logger.info('Using internal node-cron scheduler');
221
+ const job = cron.schedule('*/30 * * * *', async () => {
222
+ await runMonitorCheck(chatId);
223
+ });
224
+
225
+ return { mode: 'internal', job, chatId };
226
+ }
227
+
228
+ /**
229
+ * 使用 OpenClaw 外部 cron(备用方案)
230
+ * @param {string} chatId - 聊天 ID
231
+ */
232
+
233
+ /**
234
+ * 使用浏览器获取动态网页数据
235
+ * @param {string} url - 网页 URL
236
+ * @returns {Promise<string>}
237
+ */
238
+ async function fetchWithBrowser(url) {
239
+ try {
240
+ const { execSync } = await import('child_process');
241
+ const cmd = `openclaw browser snapshot --url "${url}" --format text`;
242
+ const result = execSync(cmd, { encoding: 'utf-8', timeout: 30000 });
243
+ return result || '';
244
+ } catch (error) {
245
+ logger.error('Browser fetch failed:', error.message);
246
+ return null;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * 智能数据获取
252
+ * 1. 搜索 → 2. 浏览器获取
253
+ * @param {string} trigger - 触发条件
254
+ * @param {Object} monitor - 监控项
255
+ * @returns {Promise<Object>}
256
+ */
257
+ async function smartFetchData(trigger, monitor) {
258
+ // 第一步:搜索
259
+ const searchResult = await searchForTrigger(trigger, monitor);
260
+
261
+ // 检查搜索结果是否足够
262
+ const hasContent = searchResult.content && searchResult.content.length > 100;
263
+
264
+ if (hasContent) {
265
+ return { source: 'search', data: searchResult };
266
+ }
267
+
268
+ // 第二步:需要浏览器获取
269
+ if (monitor.target_url) {
270
+ logger.info('Search insufficient, trying browser fetch...');
271
+ const browserContent = await fetchWithBrowser(monitor.target_url);
272
+
273
+ if (browserContent) {
274
+ return { source: 'browser', data: { content: browserContent } };
275
+ }
276
+ }
277
+
278
+ return { source: 'none', data: searchResult };
279
+ }
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 研究工作进程
4
+ * 在后台执行实际的研究任务(使用流式 API)
5
+ */
6
+
7
+ import { startResearch } from '../api/cuecueClient.js';
8
+ import { createTaskManager } from '../core/taskManager.js';
9
+ import { createLogger } from '../core/logger.js';
10
+
11
+ const logger = createLogger('ResearchWorker');
12
+
13
+ async function main() {
14
+ const [,, taskId, topic, mode, chatId] = process.argv;
15
+ const apiKey = process.env.CUECUE_API_KEY;
16
+
17
+ if (!taskId || !topic || !apiKey) {
18
+ console.error('Usage: research-worker.js <taskId> <topic> <mode> <chatId>');
19
+ console.error('Requires CUECUE_API_KEY environment variable');
20
+ process.exit(1);
21
+ }
22
+
23
+ try {
24
+ await logger.info(`Research worker started: ${taskId}`);
25
+
26
+ const taskManager = createTaskManager(chatId);
27
+
28
+ // 更新进度:初始化
29
+ await taskManager.updateTaskProgress(taskId, '正在连接 CueCue 服务...', 5);
30
+
31
+ // 启动研究(使用流式 API)
32
+ const result = await startResearch({
33
+ topic,
34
+ mode: mode || 'default',
35
+ userProfile,
36
+ chatId,
37
+ apiKey,
38
+ onProgress: async (progress) => {
39
+ // 根据进度阶段更新任务状态
40
+ const progressMap = {
41
+ 'start': { message: '正在启动研究...', percent: 10 },
42
+ 'coordinator': { message: '正在分配研究任务...', percent: 15 },
43
+ 'task': { message: progress.message, percent: 30 },
44
+ 'report': { message: '正在生成报告...', percent: 70 },
45
+ 'complete': { message: '报告生成完成', percent: 95 },
46
+ 'final': { message: '研究完成', percent: 100 }
47
+ };
48
+
49
+ const p = progressMap[progress.stage] || { message: progress.message, percent: 50 };
50
+ await taskManager.updateTaskProgress(taskId, p.message, p.percent);
51
+
52
+ logger.info(`Progress: ${progress.stage} - ${progress.message}`);
53
+ }
54
+ });
55
+
56
+ // 更新任务:已启动
57
+ await taskManager.updateTask(taskId, {
58
+ conversation_id: result.conversationId,
59
+ report_url: result.reportUrl,
60
+ progress: '研究进行中...',
61
+ percent: 50
62
+ });
63
+
64
+ await logger.info(`Research started on CueCue: ${result.conversationId}`);
65
+
66
+ // 注意:流式 API 会一直等待直到研究完成
67
+ // result 中已包含报告内容(如果流式已完成)
68
+ if (result.report) {
69
+ await taskManager.completeTask(taskId, {
70
+ conversation_id: result.conversationId,
71
+ report_url: result.reportUrl,
72
+ report: result.report,
73
+ tasks: result.tasks
74
+ });
75
+ logger.info(`Research completed with report: ${taskId}`);
76
+ } else {
77
+ // 如果没有报告内容,标记为完成但需要后续获取
78
+ await taskManager.completeTask(taskId, {
79
+ conversation_id: result.conversationId,
80
+ report_url: result.reportUrl,
81
+ tasks: result.tasks
82
+ });
83
+ logger.info(`Research stream completed, report pending: ${taskId}`);
84
+ }
85
+
86
+ process.exit(0);
87
+ } catch (error) {
88
+ await logger.error(`Research worker failed: ${taskId}`, error);
89
+
90
+ const taskManager = createTaskManager(chatId);
91
+ await taskManager.failTask(taskId, error.message);
92
+
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ main();