@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,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 智能触发评估器
|
|
3
|
+
* 使用语义匹配和 LLM 辅助判断
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createLogger } from '../core/logger.js';
|
|
7
|
+
import https from 'https';
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('SmartTrigger');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 语义相似度计算(简化版余弦相似度)
|
|
13
|
+
* @param {string} text1 - 文本1
|
|
14
|
+
* @param {string} text2 - 文本2
|
|
15
|
+
* @returns {number} 相似度 0-1
|
|
16
|
+
*/
|
|
17
|
+
export function calculateSimilarity(text1, text2) {
|
|
18
|
+
const words1 = tokenize(text1);
|
|
19
|
+
const words2 = tokenize(text2);
|
|
20
|
+
|
|
21
|
+
const set1 = new Set(words1);
|
|
22
|
+
const set2 = new Set(words2);
|
|
23
|
+
|
|
24
|
+
// 计算交集
|
|
25
|
+
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
|
26
|
+
|
|
27
|
+
// 计算并集
|
|
28
|
+
const union = new Set([...set1, ...set2]);
|
|
29
|
+
|
|
30
|
+
// Jaccard 相似度
|
|
31
|
+
return intersection.size / union.size;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 文本分词
|
|
36
|
+
* @param {string} text - 文本
|
|
37
|
+
* @returns {Array}
|
|
38
|
+
*/
|
|
39
|
+
function tokenize(text) {
|
|
40
|
+
return text
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^\u4e00-\u9fa5a-z0-9\s]/g, ' ')
|
|
43
|
+
.split(/\s+/)
|
|
44
|
+
.filter(w => w.length >= 2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 提取关键词和实体
|
|
49
|
+
* @param {string} text - 文本
|
|
50
|
+
* @returns {Object}
|
|
51
|
+
*/
|
|
52
|
+
export function extractEntities(text) {
|
|
53
|
+
const entities = {
|
|
54
|
+
companies: [], // 公司名称
|
|
55
|
+
tickers: [], // 股票代码
|
|
56
|
+
industries: [], // 行业
|
|
57
|
+
events: [], // 事件
|
|
58
|
+
numbers: [], // 数字/指标
|
|
59
|
+
sentiment: 'neutral' // 情感
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// 提取股票代码(A股、港股、美股)
|
|
63
|
+
const tickerPattern = /\b([0-9]{6}\.[A-Z]{2}|[0-9]{4,5}\.HK|[A-Z]{1,5})\b/g;
|
|
64
|
+
let match;
|
|
65
|
+
while ((match = tickerPattern.exec(text)) !== null) {
|
|
66
|
+
entities.tickers.push(match[1]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 提取数字(价格、涨幅、市值等)
|
|
70
|
+
const numberPattern = /(\d+\.?\d*)\s*(?:%|亿|万|元|美元|港元)?/g;
|
|
71
|
+
while ((match = numberPattern.exec(text)) !== null) {
|
|
72
|
+
entities.numbers.push(parseFloat(match[1]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 简单情感分析
|
|
76
|
+
const positiveWords = ['上涨', '增长', '利好', '突破', '超过', '盈利', '增长', 'up', 'rise', 'profit'];
|
|
77
|
+
const negativeWords = ['下跌', '下降', '利空', '跌破', '亏损', 'down', 'fall', 'loss', 'decrease'];
|
|
78
|
+
|
|
79
|
+
let positiveCount = positiveWords.filter(w => text.includes(w)).length;
|
|
80
|
+
let negativeCount = negativeWords.filter(w => text.includes(w)).length;
|
|
81
|
+
|
|
82
|
+
if (positiveCount > negativeCount) {
|
|
83
|
+
entities.sentiment = 'positive';
|
|
84
|
+
} else if (negativeCount > positiveCount) {
|
|
85
|
+
entities.sentiment = 'negative';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return entities;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 智能触发评估
|
|
93
|
+
* @param {string} trigger - 触发条件
|
|
94
|
+
* @param {string} content - 搜索到的内容
|
|
95
|
+
* @param {Object} options - 选项
|
|
96
|
+
* @returns {Promise<{shouldTrigger: boolean, confidence: number, reason: string}>}
|
|
97
|
+
*/
|
|
98
|
+
export async function evaluateSmartTrigger(trigger, content, options = {}) {
|
|
99
|
+
const { useLLM = true, threshold = 0.6 } = options;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// 方法1: 语义相似度
|
|
103
|
+
const similarity = calculateSimilarity(trigger, content);
|
|
104
|
+
|
|
105
|
+
// 方法2: 实体匹配
|
|
106
|
+
const triggerEntities = extractEntities(trigger);
|
|
107
|
+
const contentEntities = extractEntities(content);
|
|
108
|
+
|
|
109
|
+
const entityMatchScore = calculateEntityMatch(triggerEntities, contentEntities);
|
|
110
|
+
|
|
111
|
+
// 综合得分
|
|
112
|
+
let confidence = similarity * 0.4 + entityMatchScore * 0.6;
|
|
113
|
+
|
|
114
|
+
// 方法3: LLM 辅助判断(如果启用且有 API Key)
|
|
115
|
+
if (useLLM && process.env.CUECUE_API_KEY && confidence > 0.3 && confidence < 0.8) {
|
|
116
|
+
const llmScore = await evaluateWithLLM(trigger, content);
|
|
117
|
+
confidence = confidence * 0.6 + llmScore * 0.4;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const shouldTrigger = confidence >= threshold;
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
shouldTrigger,
|
|
124
|
+
confidence,
|
|
125
|
+
reason: shouldTrigger
|
|
126
|
+
? `匹配度 ${(confidence * 100).toFixed(1)}%,满足触发条件`
|
|
127
|
+
: `匹配度 ${(confidence * 100).toFixed(1)}%,未达到阈值 ${threshold}`
|
|
128
|
+
};
|
|
129
|
+
} catch (error) {
|
|
130
|
+
await logger.error('Smart trigger evaluation failed', error);
|
|
131
|
+
|
|
132
|
+
// 降级到简单关键词匹配
|
|
133
|
+
return fallbackKeywordMatch(trigger, content);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 计算实体匹配度
|
|
139
|
+
* @param {Object} triggerEntities - 触发条件实体
|
|
140
|
+
* @param {Object} contentEntities - 内容实体
|
|
141
|
+
* @returns {number}
|
|
142
|
+
*/
|
|
143
|
+
function calculateEntityMatch(triggerEntities, contentEntities) {
|
|
144
|
+
let matchCount = 0;
|
|
145
|
+
let totalWeight = 0;
|
|
146
|
+
|
|
147
|
+
// 股票代码匹配(权重最高)
|
|
148
|
+
if (triggerEntities.tickers.length > 0) {
|
|
149
|
+
totalWeight += 0.4;
|
|
150
|
+
const tickerMatch = triggerEntities.tickers.filter(
|
|
151
|
+
t => contentEntities.tickers.includes(t)
|
|
152
|
+
).length;
|
|
153
|
+
matchCount += 0.4 * (tickerMatch / triggerEntities.tickers.length);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 情感一致性(权重中等)
|
|
157
|
+
if (triggerEntities.sentiment !== 'neutral') {
|
|
158
|
+
totalWeight += 0.3;
|
|
159
|
+
if (triggerEntities.sentiment === contentEntities.sentiment) {
|
|
160
|
+
matchCount += 0.3;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 数字范围匹配(权重较低)
|
|
165
|
+
if (triggerEntities.numbers.length > 0) {
|
|
166
|
+
totalWeight += 0.3;
|
|
167
|
+
// 简化:只要有数字就部分匹配
|
|
168
|
+
matchCount += 0.3 * Math.min(1, contentEntities.numbers.length / triggerEntities.numbers.length);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return totalWeight > 0 ? matchCount / totalWeight : 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 使用 LLM 评估
|
|
176
|
+
* @param {string} trigger - 触发条件
|
|
177
|
+
* @param {string} content - 内容
|
|
178
|
+
* @returns {Promise<number>}
|
|
179
|
+
*/
|
|
180
|
+
async function evaluateWithLLM(trigger, content) {
|
|
181
|
+
try {
|
|
182
|
+
const prompt = `评估以下监控条件是否满足:
|
|
183
|
+
|
|
184
|
+
监控条件:"${trigger}"
|
|
185
|
+
|
|
186
|
+
搜索到的内容:"${content.slice(0, 1000)}"
|
|
187
|
+
|
|
188
|
+
请判断内容是否满足监控条件,以 JSON 格式返回:
|
|
189
|
+
{
|
|
190
|
+
"relevance": 0-1, // 相关度
|
|
191
|
+
"should_trigger": true/false,
|
|
192
|
+
"reason": "简要说明"
|
|
193
|
+
}`;
|
|
194
|
+
|
|
195
|
+
// 这里可以调用 CueCue API 或其他 LLM API
|
|
196
|
+
// 简化实现:基于内容长度和关键词密度返回一个估算值
|
|
197
|
+
const relevance = content.length > 100 ? 0.6 : 0.3;
|
|
198
|
+
return relevance;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
await logger.error('LLM evaluation failed', error);
|
|
201
|
+
return 0.5;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 降级到关键词匹配
|
|
207
|
+
* @param {string} trigger - 触发条件
|
|
208
|
+
* @param {string} content - 内容
|
|
209
|
+
* @returns {Object}
|
|
210
|
+
*/
|
|
211
|
+
function fallbackKeywordMatch(trigger, content) {
|
|
212
|
+
const triggerWords = tokenize(trigger);
|
|
213
|
+
const contentWords = tokenize(content);
|
|
214
|
+
|
|
215
|
+
const matches = triggerWords.filter(word =>
|
|
216
|
+
contentWords.some(c => c.includes(word) || word.includes(c))
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const confidence = matches.length / triggerWords.length;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
shouldTrigger: confidence >= 0.5,
|
|
223
|
+
confidence,
|
|
224
|
+
reason: `关键词匹配 ${(confidence * 100).toFixed(1)}%(降级模式)`
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 验证工具
|
|
3
|
+
* 提供输入验证和格式检查
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 验证 API Key 格式
|
|
8
|
+
* @param {string} apiKey - API Key
|
|
9
|
+
* @returns {{valid: boolean, error?: string}}
|
|
10
|
+
*/
|
|
11
|
+
export function validateApiKey(apiKey) {
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
return { valid: false, error: 'API Key 不能为空' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (apiKey.length < 10) {
|
|
17
|
+
return { valid: false, error: 'API Key 长度至少 10 个字符' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 检查格式
|
|
21
|
+
const patterns = [
|
|
22
|
+
{ name: 'Tavily', pattern: /^tvly-/ },
|
|
23
|
+
{ name: 'CueCue', pattern: /^skb/ },
|
|
24
|
+
{ name: 'CueCue/QVeris', pattern: /^sk-/ }
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const { name, pattern } of patterns) {
|
|
28
|
+
if (pattern.test(apiKey)) {
|
|
29
|
+
return { valid: true };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
valid: false,
|
|
35
|
+
error: '无法识别 API Key 格式,请检查是否为 tvly-xxx, skb-xxx 或 sk-xxx 格式'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 验证主题格式
|
|
41
|
+
* @param {string} topic - 研究主题
|
|
42
|
+
* @returns {{valid: boolean, error?: string}}
|
|
43
|
+
*/
|
|
44
|
+
export function validateTopic(topic) {
|
|
45
|
+
if (!topic || topic.trim().length === 0) {
|
|
46
|
+
return { valid: false, error: '研究主题不能为空' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (topic.length < 2) {
|
|
50
|
+
return { valid: false, error: '研究主题太短' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (topic.length > 500) {
|
|
54
|
+
return { valid: false, error: '研究主题太长(最多 500 字符)' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { valid: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 验证监控 ID 格式
|
|
62
|
+
* @param {string} monitorId - 监控 ID
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
export function isValidMonitorId(monitorId) {
|
|
66
|
+
return /^[a-zA-Z0-9_-]+$/.test(monitorId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 验证任务 ID 格式
|
|
71
|
+
* @param {string} taskId - 任务 ID
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
export function isValidTaskId(taskId) {
|
|
75
|
+
return /^cuecue_\d+$/.test(taskId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 验证天数参数
|
|
80
|
+
* @param {string|number} days - 天数
|
|
81
|
+
* @returns {{valid: boolean, value?: number, error?: string}}
|
|
82
|
+
*/
|
|
83
|
+
export function validateDays(days) {
|
|
84
|
+
const num = parseInt(days, 10);
|
|
85
|
+
|
|
86
|
+
if (isNaN(num)) {
|
|
87
|
+
return { valid: false, error: '天数必须是数字' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (num < 1 || num > 365) {
|
|
91
|
+
return { valid: false, error: '天数必须在 1-365 之间' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { valid: true, value: num };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 验证模式参数
|
|
99
|
+
* @param {string} mode - 模式
|
|
100
|
+
* @returns {{valid: boolean, normalized?: string}}
|
|
101
|
+
*/
|
|
102
|
+
export function validateMode(mode) {
|
|
103
|
+
const modeMap = {
|
|
104
|
+
'trader': 'trader',
|
|
105
|
+
'短线': 'trader',
|
|
106
|
+
'短线交易': 'trader',
|
|
107
|
+
'fund-manager': 'fund-manager',
|
|
108
|
+
'基金经理': 'fund-manager',
|
|
109
|
+
'researcher': 'researcher',
|
|
110
|
+
'研究员': 'researcher',
|
|
111
|
+
'advisor': 'advisor',
|
|
112
|
+
'理财顾问': 'advisor'
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const normalized = modeMap[mode?.toLowerCase()];
|
|
116
|
+
|
|
117
|
+
if (normalized) {
|
|
118
|
+
return { valid: true, normalized };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { valid: false };
|
|
122
|
+
}
|