@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.
- package/README.md +74 -0
- package/index.js +28 -0
- package/package.json +28 -0
- package/src/api/cuecueClient.js +851 -0
- package/src/core/backgroundExecutor.js +331 -0
- package/src/core/logger.js +130 -0
- package/src/core/monitorManager.js +139 -0
- package/src/core/taskManager.js +212 -0
- package/src/core/userState.js +114 -0
- package/src/cron/monitor-daemon.js +279 -0
- package/src/cron/research-worker.js +97 -0
- package/src/cron/run-check.js +19 -0
- package/src/index.js +558 -0
- package/src/notifier/index.js +319 -0
- package/src/utils/dataSource.js +140 -0
- package/src/utils/envUtils.js +243 -0
- package/src/utils/fileUtils.js +136 -0
- package/src/utils/notificationQueue.js +311 -0
- package/src/utils/openclawUtils.js +135 -0
- package/src/utils/smartTrigger.js +226 -0
- package/src/utils/validators.js +122 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 后台执行管理器
|
|
3
|
+
* 管理研究任务的后台运行、进度推送和完成检测
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { createLogger } from './logger.js';
|
|
8
|
+
import { createTaskManager } from './taskManager.js';
|
|
9
|
+
import { sendProgressNotification, sendResearchCompleteNotification, sendMonitorSuggestionNotification } from '../notifier/index.js';
|
|
10
|
+
import { getReportContent, extractReportInsights, generateMonitorSuggestions } from '../api/cuecueClient.js';
|
|
11
|
+
|
|
12
|
+
const logger = createLogger('BackgroundExecutor');
|
|
13
|
+
|
|
14
|
+
// 行业关键词映射
|
|
15
|
+
const industryKeywords = {
|
|
16
|
+
'新能源': [宁德时代, '比亚迪', '光伏', '锂电', '储能', '电动车', '特斯拉'],
|
|
17
|
+
'半导体': [芯片, '集成电路', '中芯国际', '华为', 'AI芯片', 'GPU', '英伟达'],
|
|
18
|
+
'医药': [药, '医疗', '生物', '疫苗', '创新药', '恒瑞'],
|
|
19
|
+
'消费': [茅台, '五粮液', '食品', '饮料', '家电', '零售'],
|
|
20
|
+
'金融': [银行, '保险', '证券', '理财', '基金', 'A股'],
|
|
21
|
+
'地产': [房地产, '万科', '碧桂园', '保利', '房价'],
|
|
22
|
+
'汽车': [汽车, '整车', '上险量', '销量', '车企']
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 更新用户关注行业
|
|
27
|
+
*/
|
|
28
|
+
function updateFocusIndustries(chatId, topic) {
|
|
29
|
+
try {
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const profilePath = path.join(process.env.HOME || '/root', ' .cuecue', chatId, 'profile.json');
|
|
33
|
+
if (!fs.existsSync(profilePath)) return;
|
|
34
|
+
|
|
35
|
+
const profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
|
|
36
|
+
const industries = profile.focus_industries || [];
|
|
37
|
+
|
|
38
|
+
for (const [industry, keywords] of Object.entries(industryKeywords)) {
|
|
39
|
+
for (const kw of keywords) {
|
|
40
|
+
if (topic.includes(kw) && !industries.includes(industry)) {
|
|
41
|
+
industries.push(industry);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (industries.length > 0) {
|
|
48
|
+
profile.focus_industries = industries;
|
|
49
|
+
profile.updated_at = new Date().toISOString();
|
|
50
|
+
fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2), 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// 忽略
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 存储正在运行的任务
|
|
58
|
+
const runningTasks = new Map();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 启动后台研究任务
|
|
62
|
+
* @param {Object} options
|
|
63
|
+
* @param {string} options.taskId - 任务 ID
|
|
64
|
+
* @param {string} options.topic - 研究主题
|
|
65
|
+
* @param {string} options.mode - 研究模式
|
|
66
|
+
* @param {string} options.chatId - 聊天 ID
|
|
67
|
+
* @param {string} options.apiKey - API Key
|
|
68
|
+
* @returns {Promise<Object>}
|
|
69
|
+
*/
|
|
70
|
+
export async function startBackgroundResearch({
|
|
71
|
+
userProfile = null,
|
|
72
|
+
taskId,
|
|
73
|
+
topic,
|
|
74
|
+
mode,
|
|
75
|
+
chatId,
|
|
76
|
+
apiKey
|
|
77
|
+
}) {
|
|
78
|
+
try {
|
|
79
|
+
await logger.info(`Starting background research: ${taskId}`);
|
|
80
|
+
|
|
81
|
+
const taskManager = createTaskManager(chatId);
|
|
82
|
+
|
|
83
|
+
// 启动研究进程(API Key 通过环境变量传递,避免命令行暴露)
|
|
84
|
+
const researchProcess = spawn('node', [
|
|
85
|
+
'src/cron/research-worker.js',
|
|
86
|
+
taskId,
|
|
87
|
+
topic,
|
|
88
|
+
mode,
|
|
89
|
+
chatId
|
|
90
|
+
], {
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: 'ignore',
|
|
93
|
+
env: {
|
|
94
|
+
...process.env,
|
|
95
|
+
CUECUE_USER_PROFILE: JSON.stringify(userProfile),
|
|
96
|
+
CUECUE_API_KEY: apiKey
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 保存进程信息
|
|
101
|
+
runningTasks.set(taskId, {
|
|
102
|
+
pid: researchProcess.pid,
|
|
103
|
+
startTime: Date.now(),
|
|
104
|
+
chatId,
|
|
105
|
+
topic
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 保存 PID 到任务文件
|
|
109
|
+
await taskManager.updateTask(taskId, {
|
|
110
|
+
pid: researchProcess.pid,
|
|
111
|
+
status: 'running'
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 启动进度推送定时器
|
|
115
|
+
startProgressNotifier(taskId, chatId, topic);
|
|
116
|
+
|
|
117
|
+
// 启动完成检测(传递 apiKey 以便获取报告)
|
|
118
|
+
startCompletionWatcher(taskId, chatId, topic, researchProcess, apiKey);
|
|
119
|
+
|
|
120
|
+
await logger.info(`Background research started: ${taskId} (PID: ${researchProcess.pid})`);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
taskId,
|
|
124
|
+
pid: researchProcess.pid,
|
|
125
|
+
status: 'started'
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
await logger.error(`Failed to start background research: ${taskId}`, error);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 启动进度推送定时器
|
|
135
|
+
* @param {string} taskId - 任务 ID
|
|
136
|
+
* @param {string} chatId - 聊天 ID
|
|
137
|
+
* @param {string} topic - 研究主题
|
|
138
|
+
*/
|
|
139
|
+
function startProgressNotifier(taskId, chatId, topic) {
|
|
140
|
+
const startTime = Date.now();
|
|
141
|
+
|
|
142
|
+
// 每5分钟推送一次进度
|
|
143
|
+
const intervalId = setInterval(async () => {
|
|
144
|
+
try {
|
|
145
|
+
// 检查任务是否还在运行
|
|
146
|
+
if (!runningTasks.has(taskId)) {
|
|
147
|
+
clearInterval(intervalId);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const elapsedMinutes = Math.round((Date.now() - startTime) / (1000 * 60));
|
|
152
|
+
|
|
153
|
+
// 超过60分钟自动停止
|
|
154
|
+
if (elapsedMinutes >= 60) {
|
|
155
|
+
clearInterval(intervalId);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 发送进度通知
|
|
160
|
+
await sendProgressNotification({
|
|
161
|
+
chatId,
|
|
162
|
+
taskId,
|
|
163
|
+
topic,
|
|
164
|
+
progress: getStageDescription(elapsedMinutes),
|
|
165
|
+
elapsedMinutes
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await logger.info(`Progress notification sent: ${taskId} (${elapsedMinutes}min)`);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
await logger.error(`Progress notification failed: ${taskId}`, error);
|
|
171
|
+
}
|
|
172
|
+
}, 5 * 60 * 1000); // 5分钟
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 启动完成检测
|
|
177
|
+
* @param {string} taskId - 任务 ID
|
|
178
|
+
* @param {string} chatId - 聊天 ID
|
|
179
|
+
* @param {string} topic - 研究主题
|
|
180
|
+
* @param {Object} process - 子进程
|
|
181
|
+
* @param {string} apiKey - API Key
|
|
182
|
+
*/
|
|
183
|
+
function startCompletionWatcher(taskId, chatId, topic, process, apiKey) {
|
|
184
|
+
const startTime = Date.now();
|
|
185
|
+
const timeoutMs = 60 * 60 * 1000; // 60分钟超时
|
|
186
|
+
|
|
187
|
+
process.on('exit', async (code) => {
|
|
188
|
+
try {
|
|
189
|
+
const elapsedMinutes = Math.round((Date.now() - startTime) / (1000 * 60));
|
|
190
|
+
const taskManager = createTaskManager(chatId);
|
|
191
|
+
|
|
192
|
+
runningTasks.delete(taskId);
|
|
193
|
+
|
|
194
|
+
if (code === 0) {
|
|
195
|
+
// 成功完成 - 获取报告内容
|
|
196
|
+
try {
|
|
197
|
+
// 从任务管理器获取 conversation_id
|
|
198
|
+
const task = await taskManager.getTask(taskId);
|
|
199
|
+
const conversationId = task?.conversation_id;
|
|
200
|
+
|
|
201
|
+
if (!conversationId) {
|
|
202
|
+
throw new Error('conversation_id not found in task');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 使用传入的 apiKey 参数
|
|
206
|
+
const report = await getReportContent(conversationId, apiKey);
|
|
207
|
+
const insights = extractReportInsights(report);
|
|
208
|
+
const monitorSuggestions = generateMonitorSuggestions(insights, topic);
|
|
209
|
+
|
|
210
|
+
// 保存报告内容到任务
|
|
211
|
+
await taskManager.completeTask(taskId, {
|
|
212
|
+
conversation_id: conversationId,
|
|
213
|
+
report_url: `https://cuecue.cn/c/${conversationId}`,
|
|
214
|
+
summary: insights.summary,
|
|
215
|
+
key_points: insights.keyPoints,
|
|
216
|
+
monitor_suggestions: monitorSuggestions
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// 清理运行中任务记录
|
|
220
|
+
runningTasks.delete(taskId);
|
|
221
|
+
|
|
222
|
+
// 发送完成通知(带监控建议)
|
|
223
|
+
await updateFocusIndustries(chatId, topic);
|
|
224
|
+
await sendResearchCompleteNotification({
|
|
225
|
+
chatId,
|
|
226
|
+
taskId,
|
|
227
|
+
topic,
|
|
228
|
+
reportUrl: `https://cuecue.cn/c/${conversationId}`,
|
|
229
|
+
duration: elapsedMinutes,
|
|
230
|
+
monitorSuggestions: monitorSuggestions.map(s => s.title)
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// 发送监控建议通知
|
|
234
|
+
if (monitorSuggestions.length > 0) {
|
|
235
|
+
await sendMonitorSuggestionNotification({
|
|
236
|
+
chatId,
|
|
237
|
+
taskId,
|
|
238
|
+
topic,
|
|
239
|
+
suggestions: monitorSuggestions
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await logger.info(`Research completed with report: ${taskId}`);
|
|
244
|
+
} catch (reportError) {
|
|
245
|
+
// 如果获取报告失败,仍然标记完成
|
|
246
|
+
await logger.error(`Failed to get report content: ${taskId}`, reportError);
|
|
247
|
+
|
|
248
|
+
// 尝试从任务获取 conversation_id
|
|
249
|
+
const task = await taskManager.getTask(taskId);
|
|
250
|
+
const conversationId = task?.conversation_id || 'unknown';
|
|
251
|
+
|
|
252
|
+
await taskManager.completeTask(taskId, {
|
|
253
|
+
conversation_id: conversationId,
|
|
254
|
+
report_url: `https://cuecue.cn/c/${conversationId}`
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// 清理运行中任务记录
|
|
258
|
+
runningTasks.delete(taskId);
|
|
259
|
+
await updateFocusIndustries(chatId, topic);
|
|
260
|
+
await sendResearchCompleteNotification({
|
|
261
|
+
chatId,
|
|
262
|
+
taskId,
|
|
263
|
+
topic,
|
|
264
|
+
reportUrl: `https://cuecue.cn/c/${conversationId}`,
|
|
265
|
+
duration: elapsedMinutes
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
// 失败
|
|
270
|
+
await taskManager.failTask(taskId, `Process exited with code ${code}`);
|
|
271
|
+
await logger.error(`Research failed: ${taskId} (code: ${code})`);
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
await logger.error(`Completion handling failed: ${taskId}`, error);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// 超时检测
|
|
279
|
+
setTimeout(async () => {
|
|
280
|
+
if (runningTasks.has(taskId)) {
|
|
281
|
+
try {
|
|
282
|
+
process.kill();
|
|
283
|
+
runningTasks.delete(taskId);
|
|
284
|
+
|
|
285
|
+
const taskManager = createTaskManager(chatId);
|
|
286
|
+
await taskManager.timeoutTask(taskId);
|
|
287
|
+
|
|
288
|
+
await logger.warn(`Research timeout: ${taskId}`);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
await logger.error(`Timeout handling failed: ${taskId}`, error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}, timeoutMs);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 获取阶段描述
|
|
298
|
+
* @param {number} elapsedMinutes - 已耗时(分钟)
|
|
299
|
+
* @returns {string}
|
|
300
|
+
*/
|
|
301
|
+
function getStageDescription(elapsedMinutes) {
|
|
302
|
+
if (elapsedMinutes < 10) {
|
|
303
|
+
return '全网信息搜集与初步筛选';
|
|
304
|
+
} else if (elapsedMinutes < 30) {
|
|
305
|
+
return '多源交叉验证与事实核查';
|
|
306
|
+
} else if (elapsedMinutes < 50) {
|
|
307
|
+
return '深度分析与逻辑推理';
|
|
308
|
+
} else {
|
|
309
|
+
return '报告生成与质量检查';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 检查任务是否正在运行
|
|
315
|
+
* @param {string} taskId - 任务 ID
|
|
316
|
+
* @returns {boolean}
|
|
317
|
+
*/
|
|
318
|
+
export function isTaskRunning(taskId) {
|
|
319
|
+
return runningTasks.has(taskId);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 获取运行中的任务列表
|
|
324
|
+
* @returns {Array}
|
|
325
|
+
*/
|
|
326
|
+
export function getRunningTasks() {
|
|
327
|
+
return Array.from(runningTasks.entries()).map(([taskId, info]) => ({
|
|
328
|
+
taskId,
|
|
329
|
+
...info
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日志系统
|
|
3
|
+
* 提供统一的日志记录功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { getLogDir } from '../utils/fileUtils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 日志级别
|
|
12
|
+
*/
|
|
13
|
+
export const LogLevel = {
|
|
14
|
+
DEBUG: 0,
|
|
15
|
+
INFO: 1,
|
|
16
|
+
WARN: 2,
|
|
17
|
+
ERROR: 3
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 日志类
|
|
22
|
+
*/
|
|
23
|
+
export class Logger {
|
|
24
|
+
constructor(moduleName) {
|
|
25
|
+
this.moduleName = moduleName;
|
|
26
|
+
this.logDir = getLogDir();
|
|
27
|
+
this.level = LogLevel.INFO;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 获取日志文件路径
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
getLogFilePath() {
|
|
35
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
36
|
+
return path.join(this.logDir, `cue-${today}.log`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 格式化日志消息
|
|
41
|
+
* @param {string} level - 日志级别
|
|
42
|
+
* @param {string} message - 消息
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
formatMessage(level, message) {
|
|
46
|
+
const timestamp = new Date().toISOString();
|
|
47
|
+
return `[${timestamp}] [${level}] [${this.moduleName}] ${message}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 写入日志
|
|
52
|
+
* @param {string} level - 日志级别
|
|
53
|
+
* @param {string} message - 消息
|
|
54
|
+
*/
|
|
55
|
+
async log(level, message) {
|
|
56
|
+
try {
|
|
57
|
+
await fs.ensureDir(this.logDir);
|
|
58
|
+
const logLine = this.formatMessage(level, message) + '\n';
|
|
59
|
+
await fs.appendFile(this.getLogFilePath(), logLine);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Failed to write log:', error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 调试日志
|
|
67
|
+
* @param {string} message
|
|
68
|
+
*/
|
|
69
|
+
async debug(message) {
|
|
70
|
+
if (this.level <= LogLevel.DEBUG) {
|
|
71
|
+
await this.log('DEBUG', message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 信息日志
|
|
77
|
+
* @param {string} message
|
|
78
|
+
*/
|
|
79
|
+
async info(message) {
|
|
80
|
+
if (this.level <= LogLevel.INFO) {
|
|
81
|
+
await this.log('INFO', message);
|
|
82
|
+
console.log(message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 警告日志
|
|
88
|
+
* @param {string} message
|
|
89
|
+
*/
|
|
90
|
+
async warn(message) {
|
|
91
|
+
if (this.level <= LogLevel.WARN) {
|
|
92
|
+
await this.log('WARN', message);
|
|
93
|
+
console.warn('⚠️', message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 错误日志
|
|
99
|
+
* @param {string} message
|
|
100
|
+
* @param {Error} [error]
|
|
101
|
+
*/
|
|
102
|
+
async error(message, error = null) {
|
|
103
|
+
if (this.level <= LogLevel.ERROR) {
|
|
104
|
+
const fullMessage = error ? `${message}: ${error.message}` : message;
|
|
105
|
+
await this.log('ERROR', fullMessage);
|
|
106
|
+
console.error('❌', fullMessage);
|
|
107
|
+
|
|
108
|
+
if (error?.stack) {
|
|
109
|
+
await this.log('ERROR', error.stack);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 设置日志级别
|
|
116
|
+
* @param {number} level
|
|
117
|
+
*/
|
|
118
|
+
setLevel(level) {
|
|
119
|
+
this.level = level;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 创建日志实例
|
|
125
|
+
* @param {string} moduleName - 模块名称
|
|
126
|
+
* @returns {Logger}
|
|
127
|
+
*/
|
|
128
|
+
export function createLogger(moduleName) {
|
|
129
|
+
return new Logger(moduleName);
|
|
130
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 监控管理
|
|
3
|
+
* 管理监控项的创建、更新和查询
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { getMonitorFilePath, listJsonFiles, ensureDir, getUserDir } from '../utils/fileUtils.js';
|
|
9
|
+
import { createLogger } from './logger.js';
|
|
10
|
+
|
|
11
|
+
const logger = createLogger('MonitorManager');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 监控管理类
|
|
15
|
+
*/
|
|
16
|
+
export class MonitorManager {
|
|
17
|
+
constructor(chatId) {
|
|
18
|
+
this.chatId = chatId;
|
|
19
|
+
this.monitorsDir = path.join(getUserDir(chatId), 'monitors');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 创建监控项
|
|
24
|
+
* @param {Object} monitorData - 监控数据
|
|
25
|
+
* @returns {Promise<Object>}
|
|
26
|
+
*/
|
|
27
|
+
async createMonitor(monitorData) {
|
|
28
|
+
const { monitorId, title, symbol, category, trigger } = monitorData;
|
|
29
|
+
|
|
30
|
+
await ensureDir(this.monitorsDir);
|
|
31
|
+
|
|
32
|
+
const monitor = {
|
|
33
|
+
monitor_id: monitorId,
|
|
34
|
+
title,
|
|
35
|
+
symbol,
|
|
36
|
+
category,
|
|
37
|
+
semantic_trigger: trigger,
|
|
38
|
+
is_active: true,
|
|
39
|
+
created_at: new Date().toISOString(),
|
|
40
|
+
...monitorData
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const filePath = getMonitorFilePath(this.chatId, monitorId);
|
|
44
|
+
await fs.writeJson(filePath, monitor, { spaces: 2 });
|
|
45
|
+
|
|
46
|
+
await logger.info(`Monitor created: ${monitorId}`);
|
|
47
|
+
return monitor;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 更新监控项
|
|
52
|
+
* @param {string} monitorId - 监控 ID
|
|
53
|
+
* @param {Object} updates - 更新数据
|
|
54
|
+
* @returns {Promise<Object|null>}
|
|
55
|
+
*/
|
|
56
|
+
async updateMonitor(monitorId, updates) {
|
|
57
|
+
const filePath = getMonitorFilePath(this.chatId, monitorId);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const monitor = await fs.readJson(filePath);
|
|
61
|
+
const updatedMonitor = { ...monitor, ...updates };
|
|
62
|
+
await fs.writeJson(filePath, updatedMonitor, { spaces: 2 });
|
|
63
|
+
await logger.info(`Monitor updated: ${monitorId}`);
|
|
64
|
+
return updatedMonitor;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
await logger.error(`Failed to update monitor ${monitorId}`, error);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 获取监控项
|
|
73
|
+
* @param {string} monitorId - 监控 ID
|
|
74
|
+
* @returns {Promise<Object|null>}
|
|
75
|
+
*/
|
|
76
|
+
async getMonitor(monitorId) {
|
|
77
|
+
const filePath = getMonitorFilePath(this.chatId, monitorId);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
return await fs.readJson(filePath);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (error.code === 'ENOENT') {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 获取监控列表
|
|
91
|
+
* @param {number} limit - 限制数量
|
|
92
|
+
* @returns {Promise<Array>}
|
|
93
|
+
*/
|
|
94
|
+
async getMonitors(limit = 15) {
|
|
95
|
+
const files = await listJsonFiles(this.monitorsDir);
|
|
96
|
+
const monitors = [];
|
|
97
|
+
|
|
98
|
+
for (const file of files.slice(0, limit)) {
|
|
99
|
+
try {
|
|
100
|
+
const monitor = await fs.readJson(path.join(this.monitorsDir, file));
|
|
101
|
+
monitors.push(monitor);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
await logger.error(`Failed to read monitor ${file}`, error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return monitors;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 获取激活的监控
|
|
112
|
+
* @returns {Promise<Array>}
|
|
113
|
+
*/
|
|
114
|
+
async getActiveMonitors() {
|
|
115
|
+
const monitors = await this.getMonitors(100);
|
|
116
|
+
return monitors.filter(m => m.is_active !== false);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 统计监控数量
|
|
121
|
+
* @returns {Promise<{total: number, active: number}>}
|
|
122
|
+
*/
|
|
123
|
+
async getStats() {
|
|
124
|
+
const monitors = await this.getMonitors(1000);
|
|
125
|
+
return {
|
|
126
|
+
total: monitors.length,
|
|
127
|
+
active: monitors.filter(m => m.is_active !== false).length
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 创建监控管理器实例
|
|
134
|
+
* @param {string} chatId
|
|
135
|
+
* @returns {MonitorManager}
|
|
136
|
+
*/
|
|
137
|
+
export function createMonitorManager(chatId) {
|
|
138
|
+
return new MonitorManager(chatId);
|
|
139
|
+
}
|