@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,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 监控检查运行脚本
|
|
4
|
+
* 由 cron 调用执行单次检查
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { runMonitorCheck } from './monitor-daemon.js';
|
|
8
|
+
|
|
9
|
+
const chatId = process.argv[2] || process.env.CHAT_ID || 'default';
|
|
10
|
+
|
|
11
|
+
runMonitorCheck(chatId)
|
|
12
|
+
.then(() => {
|
|
13
|
+
console.log('Monitor check completed');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
})
|
|
16
|
+
.catch((error) => {
|
|
17
|
+
console.error('Monitor check failed:', error);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cue - 主入口
|
|
4
|
+
* Node.js 版本 v1.0.5
|
|
5
|
+
*/
|
|
6
|
+
import { createLogger } from './core/logger.js';
|
|
7
|
+
import { createUserState } from './core/userState.js';
|
|
8
|
+
import { createTaskManager } from './core/taskManager.js';
|
|
9
|
+
import { createMonitorManager } from './core/monitorManager.js';
|
|
10
|
+
import { getApiKey, detectServiceFromKey, setApiKey } from './utils/envUtils.js';
|
|
11
|
+
import { startResearch, autoDetectMode } from './api/cuecueClient.js';
|
|
12
|
+
import { startBackgroundResearch } from './core/backgroundExecutor.js';
|
|
13
|
+
import fs from 'fs-extra';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { listJsonFiles, getUserDir } from './utils/fileUtils.js';
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
17
|
+
const logger = createLogger('Cue');
|
|
18
|
+
// 获取环境配置
|
|
19
|
+
const chatId = process.env.CHAT_ID || process.env.FEISHU_CHAT_ID || 'default';
|
|
20
|
+
// 初始化核心模块
|
|
21
|
+
const userState = createUserState(chatId);
|
|
22
|
+
const taskManager = createTaskManager(chatId);
|
|
23
|
+
const monitorManager = createMonitorManager(chatId);
|
|
24
|
+
/**
|
|
25
|
+
* 处理命令
|
|
26
|
+
* @param {string} command - 命令
|
|
27
|
+
* @param {Array} args - 参数
|
|
28
|
+
*/
|
|
29
|
+
export async function handleCommand(command, args = [], rawInput = "") {
|
|
30
|
+
// 自动路由:检测自然语言研究意图
|
|
31
|
+
const rawInputFinal = process.env.MESSAGE_TEXT || process.env.INPUT_TEXT || process.env.USER_MESSAGE || rawInput || "";
|
|
32
|
+
if (!command && rawInputFinal) {
|
|
33
|
+
const researchIntent = detectResearchIntent(rawInputFinal);
|
|
34
|
+
if (researchIntent) {
|
|
35
|
+
logger.info(`Auto-routing to research: ${researchIntent.topic}`);
|
|
36
|
+
return await handleCue([researchIntent.topic]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
switch (command) {
|
|
41
|
+
case 'cue':
|
|
42
|
+
return await handleCue(args);
|
|
43
|
+
case 'ct':
|
|
44
|
+
return await handleCt();
|
|
45
|
+
case 'cm':
|
|
46
|
+
return await handleCm();
|
|
47
|
+
case 'cn':
|
|
48
|
+
return await handleCn(args[0]);
|
|
49
|
+
case 'key':
|
|
50
|
+
return await handleKey(args[0]);
|
|
51
|
+
case 'ch':
|
|
52
|
+
return handleCh();
|
|
53
|
+
default:
|
|
54
|
+
return handleCh();
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
logger.error(`Command failed: ${command}`, error);
|
|
58
|
+
return `❌ 错误:${error.message}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 处理 /cue 命令
|
|
63
|
+
*/
|
|
64
|
+
async function handleCue(args) {
|
|
65
|
+
// 解析主题(现在全部自动检测模式,简化用户操作)
|
|
66
|
+
let topic = args.join(' ');
|
|
67
|
+
|
|
68
|
+
// 读取用户画像
|
|
69
|
+
const userProfile = getUserProfile(chatId);
|
|
70
|
+
|
|
71
|
+
// 自动检测模式
|
|
72
|
+
let mode = autoDetectMode(topic);
|
|
73
|
+
|
|
74
|
+
// 检查用户状态
|
|
75
|
+
const status = await userState.checkVersion();
|
|
76
|
+
let output = '';
|
|
77
|
+
|
|
78
|
+
if (status === 'first_time') {
|
|
79
|
+
output += showWelcome();
|
|
80
|
+
await userState.markInitialized();
|
|
81
|
+
} else if (status === 'updated') {
|
|
82
|
+
output += showUpdateNotice();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 检查 API Key
|
|
86
|
+
const apiKey = await getApiKey('CUECUE_API_KEY');
|
|
87
|
+
if (!apiKey) {
|
|
88
|
+
output += '\n🔑 还差一步即可开始深度研究\n\n';
|
|
89
|
+
output += '发送 /key 配置你的 API Key\n\n';
|
|
90
|
+
output += '💡 现在先用示例体验:\n';
|
|
91
|
+
output += '"分析比亚迪 vs 特斯拉"\n';
|
|
92
|
+
return output;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 自动检测模式
|
|
96
|
+
if (!mode || mode === 'default') {
|
|
97
|
+
mode = autoDetectMode(topic);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const modeNames = {
|
|
101
|
+
trader: '短线交易视角',
|
|
102
|
+
'fund-manager': '基金经理视角',
|
|
103
|
+
researcher: '产业研究视角',
|
|
104
|
+
advisor: '理财顾问视角'
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
output += `\n🎯 根据主题自动匹配研究视角:${modeNames[mode]}\n\n`;
|
|
108
|
+
|
|
109
|
+
// 创建任务
|
|
110
|
+
const taskId = `cuecue_${Date.now()}`;
|
|
111
|
+
await taskManager.createTask({ taskId, topic, mode });
|
|
112
|
+
|
|
113
|
+
// 启动后台研究
|
|
114
|
+
try {
|
|
115
|
+
const result = await startBackgroundResearch({ taskId, topic, mode, chatId, apiKey, userProfile });
|
|
116
|
+
|
|
117
|
+
output += `✅ 研究任务已在后台启动!\n\n`;
|
|
118
|
+
output += `📋 任务信息:\n`;
|
|
119
|
+
output += ` 任务 ID:${taskId}\n`;
|
|
120
|
+
output += ` 进程 PID:${result.pid}\n\n`;
|
|
121
|
+
output += `⏳ 进度更新:每 5 分钟推送一次\n`;
|
|
122
|
+
output += `🔔 完成通知:研究完成后自动推送\n`;
|
|
123
|
+
output += `💡 使用 /ct 查看任务状态\n`;
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
output += `❌ 研究启动失败:${error.message}\n`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return output;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 处理 /ct 命令
|
|
133
|
+
*/
|
|
134
|
+
async function handleCt() {
|
|
135
|
+
const tasks = await taskManager.getTasks(10);
|
|
136
|
+
|
|
137
|
+
if (tasks.length === 0) {
|
|
138
|
+
return '📭 暂无研究任务\n';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let output = '📊 研究任务列表\n\n';
|
|
142
|
+
|
|
143
|
+
for (const task of tasks) {
|
|
144
|
+
const statusEmoji = {
|
|
145
|
+
running: '🔄',
|
|
146
|
+
completed: '✅',
|
|
147
|
+
failed: '❌',
|
|
148
|
+
timeout: '⏱️'
|
|
149
|
+
}[task.status] || '🔄';
|
|
150
|
+
|
|
151
|
+
output += `${statusEmoji} ${task.topic}\n`;
|
|
152
|
+
output += ` ID: ${task.task_id} | 状态:${task.status}\n\n`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return output;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 处理 /cm 命令
|
|
159
|
+
*/
|
|
160
|
+
async function handleCm() {
|
|
161
|
+
const monitors = await monitorManager.getMonitors(15);
|
|
162
|
+
|
|
163
|
+
if (monitors.length === 0) {
|
|
164
|
+
return '📭 暂无监控项\n\n💡 研究完成后回复 Y 可创建监控项\n';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let output = '🔔 监控项列表\n\n';
|
|
168
|
+
|
|
169
|
+
for (const monitor of monitors) {
|
|
170
|
+
const statusEmoji = monitor.is_active !== false ? '✅' : '⏸️';
|
|
171
|
+
const catEmoji = {
|
|
172
|
+
Price: '💰',
|
|
173
|
+
Event: '📅',
|
|
174
|
+
Data: '📊'
|
|
175
|
+
}[monitor.category] || '📊';
|
|
176
|
+
|
|
177
|
+
output += `${statusEmoji} ${catEmoji} ${monitor.title}\n`;
|
|
178
|
+
if (monitor.symbol) {
|
|
179
|
+
output += ` 标的:${monitor.symbol}\n`;
|
|
180
|
+
}
|
|
181
|
+
output += ` 触发:${monitor.semantic_trigger?.slice(0, 30) || '-'}\n\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return output;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 处理 /cn 命令
|
|
188
|
+
*/
|
|
189
|
+
async function handleCn(days = '3') {
|
|
190
|
+
const numDays = parseInt(days) || 3;
|
|
191
|
+
const notificationsDir = path.join(getUserDir(chatId), 'notifications');
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const files = await listJsonFiles(notificationsDir);
|
|
195
|
+
const cutoffTime = Date.now() - (numDays * 24 * 60 * 60 * 1000);
|
|
196
|
+
|
|
197
|
+
const notifications = [];
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
try {
|
|
200
|
+
const notif = await fs.readJson(path.join(notificationsDir, file));
|
|
201
|
+
const notifTime = new Date(notif.triggered_at || notif.timestamp).getTime();
|
|
202
|
+
if (notifTime >= cutoffTime) {
|
|
203
|
+
notifications.push(notif);
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// 忽略读取错误
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 按时间排序
|
|
211
|
+
notifications.sort((a, b) => new Date(b.triggered_at) - new Date(a.triggered_at));
|
|
212
|
+
|
|
213
|
+
if (notifications.length === 0) {
|
|
214
|
+
return `🔔 监控触发通知(最近${numDays}日)\n\n📭 暂无触发通知\n\n💡 当监控条件满足时,会自动发送通知到这里\n`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let output = `🔔 监控触发通知(最近${numDays}日)\n\n`;
|
|
218
|
+
output += `共 ${notifications.length} 条通知:\n\n`;
|
|
219
|
+
|
|
220
|
+
for (const notif of notifications.slice(0, 10)) {
|
|
221
|
+
output += `📊 ${notif.monitor_title || '未命名监控'}\n`;
|
|
222
|
+
output += ` ⏰ ${notif.triggered_at || new Date(notif.timestamp).toLocaleString('zh-CN')}\n`;
|
|
223
|
+
output += ` ${notif.message?.slice(0, 50) || '-'}...\n\n`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return output;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return `🔔 监控触发通知(最近${numDays}日)\n\n📭 暂无触发通知\n`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 处理 /key 命令
|
|
233
|
+
*/
|
|
234
|
+
async function handleKey(apiKey) {
|
|
235
|
+
if (!apiKey) {
|
|
236
|
+
// 查看状态
|
|
237
|
+
const { getApiKeyStatus } = await import('./utils/envUtils.js');
|
|
238
|
+
const status = await getApiKeyStatus();
|
|
239
|
+
|
|
240
|
+
let output = '╔══════════════════════════════════════════╗\n';
|
|
241
|
+
output += '║ 当前 API Key 配置状态 ║\n';
|
|
242
|
+
output += '╠══════════════════════════════════════════╣\n';
|
|
243
|
+
|
|
244
|
+
for (const s of status) {
|
|
245
|
+
if (s.configured) {
|
|
246
|
+
output += `║ ✅ ${s.name.padEnd(18)} ${s.masked.padEnd(24)} ║\n`;
|
|
247
|
+
} else {
|
|
248
|
+
output += `║ ❌ ${s.name.padEnd(18)} 未配置 ║\n`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
output += '╠══════════════════════════════════════════╣\n';
|
|
253
|
+
output += '║ 直接发送 API Key 即可自动配置 ║\n';
|
|
254
|
+
output += '╚══════════════════════════════════════════╝\n';
|
|
255
|
+
|
|
256
|
+
return output;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 配置 API Key
|
|
260
|
+
const service = detectServiceFromKey(apiKey);
|
|
261
|
+
|
|
262
|
+
if (!service) {
|
|
263
|
+
let output = '❌ 无法识别 API Key 类型\n\n';
|
|
264
|
+
output += '支持的格式:\n';
|
|
265
|
+
output += ' • Tavily: tvly-xxxxx\n';
|
|
266
|
+
output += ' • CueCue: skb-xxxxx 或 sk-xxxxx\n';
|
|
267
|
+
output += ' • QVeris: sk-xxxxx (长格式)\n';
|
|
268
|
+
return output;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await setApiKey(service.key, apiKey);
|
|
272
|
+
|
|
273
|
+
return `✅ ${service.name} API Key 配置成功!\n\n密钥已保存并生效,无需重启。\n`;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 处理 /ch 命令
|
|
277
|
+
*/
|
|
278
|
+
function handleCh() {
|
|
279
|
+
return `╔══════════════════════════════════════════╗
|
|
280
|
+
║ Cue - 你的专属调研助理 ║
|
|
281
|
+
╠══════════════════════════════════════════╣
|
|
282
|
+
║ 使用方式: ║
|
|
283
|
+
║ • /cue <问题> 深度调研 ║
|
|
284
|
+
║ • /ct 查看任务列表 ║
|
|
285
|
+
║ • /cm 查看监控项列表 ║
|
|
286
|
+
║ • /cn [天数] 查看监控通知 ║
|
|
287
|
+
║ • /key 配置 API Key ║
|
|
288
|
+
║ • /ch 显示帮助 ║
|
|
289
|
+
║ ║
|
|
290
|
+
║ 研究视角模式: ║
|
|
291
|
+
║ • trader - 短线交易视角 ║
|
|
292
|
+
║ • fund-manager - 基金经理视角 ║
|
|
293
|
+
║ • researcher - 产业研究视角 ║
|
|
294
|
+
║ • advisor - 理财顾问视角 ║
|
|
295
|
+
╚══════════════════════════════════════════╝
|
|
296
|
+
`;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* 检测是否为群聊
|
|
300
|
+
* 通过环境变量或 chatId 格式判断
|
|
301
|
+
*/
|
|
302
|
+
function isGroupChat() {
|
|
303
|
+
// 优先使用环境变量
|
|
304
|
+
if (process.env.CHAT_TYPE === 'group') return true;
|
|
305
|
+
if (process.env.CHAT_TYPE === 'user') return false;
|
|
306
|
+
|
|
307
|
+
// 通过 chatId 格式判断(Feishu 群聊通常包含 @ 或特定格式)
|
|
308
|
+
if (chatId.includes('@') || chatId.includes('group')) return true;
|
|
309
|
+
|
|
310
|
+
// 默认私聊
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* 显示欢迎消息(私聊版)
|
|
315
|
+
*/
|
|
316
|
+
function showWelcomePrivate() {
|
|
317
|
+
return `🎉 嗨!我是 Cue,你的 AI 调研助理
|
|
318
|
+
我可以帮你:
|
|
319
|
+
🔍 深度研究 - 40-60分钟生成专业投资报告
|
|
320
|
+
📊 智能监控 - 自动追踪你关注的标的
|
|
321
|
+
💡 实时问答 - 随时解答投资问题
|
|
322
|
+
🚀 立即体验,发送:
|
|
323
|
+
"分析宁德时代竞争优势"
|
|
324
|
+
💡 更多命令:/ch 查看帮助
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 显示欢迎消息(群聊版)
|
|
329
|
+
*/
|
|
330
|
+
function showWelcomeGroup() {
|
|
331
|
+
return `👋 大家好!我是 Cue,可以帮群友做深度研究
|
|
332
|
+
用法:@Cue 分析 [主题]
|
|
333
|
+
示例:@Cue 分析新能源赛道
|
|
334
|
+
💡 私聊我体验完整功能
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 显示欢迎消息(自动判断场景)
|
|
339
|
+
*/
|
|
340
|
+
function showWelcome() {
|
|
341
|
+
return isGroupChat() ? showWelcomeGroup() : showWelcomePrivate();
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* 显示更新提示
|
|
345
|
+
*/
|
|
346
|
+
function showUpdateNotice() {
|
|
347
|
+
return `╔══════════════════════════════════════════╗
|
|
348
|
+
║ ✨ Cue 已更新至 v1.0.5 (Node.js 版) ║
|
|
349
|
+
╠══════════════════════════════════════════╣
|
|
350
|
+
║ ║
|
|
351
|
+
║ 本次更新内容: ║
|
|
352
|
+
║ 🔧 全面 Node.js 重构 ║
|
|
353
|
+
║ 🔧 自动角色匹配 ║
|
|
354
|
+
║ 🔧 /cn 监控通知查询 ║
|
|
355
|
+
║ 🔧 /key API Key 配置 ║
|
|
356
|
+
║ ║
|
|
357
|
+
╚══════════════════════════════════════════╝
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* 显示配额不足引导
|
|
362
|
+
*/
|
|
363
|
+
function showQuotaExceeded(type) {
|
|
364
|
+
const messages = {
|
|
365
|
+
research: `❌ 今日研究额度已用完 (3/3)
|
|
366
|
+
💡 三个选择:
|
|
367
|
+
1️⃣ 明天再来(每日 00:00 重置)
|
|
368
|
+
2️⃣ 配置个人 API Key 无限额 → 发送 /key
|
|
369
|
+
3️⃣ 邀请好友获得额度 → 发送 /invite
|
|
370
|
+
当前可用:智能对话(继续聊天不受限)`,
|
|
371
|
+
monitor: `❌ 监控项数量已达上限 (10/10)
|
|
372
|
+
💡 解决方案:
|
|
373
|
+
1️⃣ 删除不活跃的监控项 → 发送 /cm 查看列表
|
|
374
|
+
2️⃣ 或使用个人 API Key 提升限额 → 发送 /key`,
|
|
375
|
+
chat: `❌ 今日对话额度已用完 (100/100)
|
|
376
|
+
💡 提示:
|
|
377
|
+
• 配额每日 00:00 自动重置
|
|
378
|
+
• 配置个人 API Key 可解除限制 → 发送 /key`
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return messages[type] || messages.research;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* 显示研究完成后的转化引导
|
|
385
|
+
*/
|
|
386
|
+
function showResearchCompleteGuide(task) {
|
|
387
|
+
return `✅ 研究完成!《${task.topic}》
|
|
388
|
+
📋 报告已生成,包含:
|
|
389
|
+
• 多维度分析与市场洞察
|
|
390
|
+
• 可溯源的数据与信息来源
|
|
391
|
+
• 结构化的投资建议
|
|
392
|
+
💡 接下来可以:
|
|
393
|
+
• 回复 "监控" → 自动追踪该标的动态
|
|
394
|
+
• 回复 "对比 XX" → 增加对比分析(如:对比比亚迪)
|
|
395
|
+
• 回复 "深度" → 补充财务数据细节
|
|
396
|
+
• 回复 "分享" → 生成分享卡片
|
|
397
|
+
📊 使用 /ct 查看所有研究报告`;
|
|
398
|
+
}
|
|
399
|
+
// 导出 handleCommand 供外部调用
|
|
400
|
+
export default {
|
|
401
|
+
handleCommand,
|
|
402
|
+
showWelcomePrivate,
|
|
403
|
+
showWelcomeGroup,
|
|
404
|
+
showQuotaExceeded,
|
|
405
|
+
showResearchCompleteGuide
|
|
406
|
+
};
|
|
407
|
+
/**
|
|
408
|
+
* 检测自然语言研究意图
|
|
409
|
+
* @param {string} input - 用户输入
|
|
410
|
+
* @returns {Object|null} - {topic, mode} 或 null
|
|
411
|
+
*/
|
|
412
|
+
function detectResearchIntent(input) {
|
|
413
|
+
if (!input || typeof input !== 'string') return null;
|
|
414
|
+
|
|
415
|
+
const text = input.trim();
|
|
416
|
+
|
|
417
|
+
// 排除太短的输入
|
|
418
|
+
if (text.length < 3) return null;
|
|
419
|
+
|
|
420
|
+
// 研究意图关键词
|
|
421
|
+
const researchKeywords = [
|
|
422
|
+
'分析', '研究', '调研', '调查', '了解', '看看',
|
|
423
|
+
'怎么样', '如何', '好不好', '值不值',
|
|
424
|
+
'走势', '前景', '竞争力', '对比', '比较',
|
|
425
|
+
' analyze', 'research', 'investigate', 'report'
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
const isResearch = researchKeywords.some(kw => text.toLowerCase().includes(kw.toLowerCase()));
|
|
429
|
+
|
|
430
|
+
if (!isResearch) return null;
|
|
431
|
+
|
|
432
|
+
// 提取研究主题
|
|
433
|
+
let topic = text;
|
|
434
|
+
|
|
435
|
+
// 移除常见前缀
|
|
436
|
+
const prefixes = [
|
|
437
|
+
'帮我分析', '请分析', '分析一下', '帮我看看',
|
|
438
|
+
'研究一下', '调研', '帮我调研',
|
|
439
|
+
'分析', '看看', '了解'
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
for (const prefix of prefixes) {
|
|
443
|
+
if (topic.toLowerCase().startsWith(prefix)) {
|
|
444
|
+
topic = topic.slice(prefix.length).trim();
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 如果主题太短,返回 null
|
|
450
|
+
if (topic.length < 2) return null;
|
|
451
|
+
|
|
452
|
+
return { topic };
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* 读取用户画像
|
|
456
|
+
*/
|
|
457
|
+
function getUserProfile(chatId) {
|
|
458
|
+
try {
|
|
459
|
+
const baseDir = path.join(process.env.HOME || '/root', '.cuecue', chatId);
|
|
460
|
+
const profilePath = path.join(baseDir, 'profile.json');
|
|
461
|
+
|
|
462
|
+
if (fs.existsSync(profilePath)) {
|
|
463
|
+
return JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 自动创建默认用户画像
|
|
467
|
+
const defaultProfile = {
|
|
468
|
+
name: chatId || "default",
|
|
469
|
+
role: "个人投资者",
|
|
470
|
+
risk_tolerance: "中等",
|
|
471
|
+
investment_style: "稳健",
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
focus_industries: [],
|
|
475
|
+
created_at: new Date().toISOString()
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// 确保目录存在
|
|
479
|
+
fs.ensureDirSync(baseDir);
|
|
480
|
+
fs.writeFileSync(profilePath, JSON.stringify(defaultProfile, null, 2), 'utf-8');
|
|
481
|
+
|
|
482
|
+
return defaultProfile;
|
|
483
|
+
} catch (e) {
|
|
484
|
+
// 忽略错误
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* 从研究主题中提取行业,更新用户画像
|
|
491
|
+
* @param {string} chatId - 用户 ID
|
|
492
|
+
* @param {string} topic - 研究主题
|
|
493
|
+
*/
|
|
494
|
+
function updateFocusIndustries(chatId, topic) {
|
|
495
|
+
try {
|
|
496
|
+
const profilePath = path.join(process.env.HOME || '/root', '.cuecue', chatId, 'profile.json');
|
|
497
|
+
if (!fs.existsSync(profilePath)) return;
|
|
498
|
+
|
|
499
|
+
const profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
|
|
500
|
+
const industries = profile.focus_industries || [];
|
|
501
|
+
|
|
502
|
+
// 行业关键词映射
|
|
503
|
+
const industryKeywords = {
|
|
504
|
+
'新能源': ['宁德时代', '比亚迪', '光伏', '锂电', '储能', '电动车', '特斯拉'],
|
|
505
|
+
'半导体': ['芯片', '集成电路', '中芯国际', '华为', 'AI芯片', 'GPU', '英伟达'],
|
|
506
|
+
'医药': ['药', '医疗', '生物', '疫苗', '创新药', '恒瑞'],
|
|
507
|
+
'消费': ['茅台', '五粮液', '食品', '饮料', '家电', '零售'],
|
|
508
|
+
'金融': ['银行', '保险', '证券', '理财', '基金', 'A股'],
|
|
509
|
+
'地产': ['房地产', '万科', '碧桂园', '保利', '房价'],
|
|
510
|
+
'汽车': ['汽车', '整车', '上险量', '销量', '车企']
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// 提取行业
|
|
514
|
+
for (const [industry, keywords] of Object.entries(industryKeywords)) {
|
|
515
|
+
for (const kw of keywords) {
|
|
516
|
+
if (topic.includes(kw) && !industries.includes(industry)) {
|
|
517
|
+
industries.push(industry);
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 更新
|
|
524
|
+
if (industries.length > 0) {
|
|
525
|
+
profile.focus_industries = industries;
|
|
526
|
+
profile.updated_at = new Date().toISOString();
|
|
527
|
+
fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2), 'utf-8');
|
|
528
|
+
}
|
|
529
|
+
} catch (e) {
|
|
530
|
+
// 忽略错误
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* 获取欢迎消息(首次使用引导)
|
|
536
|
+
*/
|
|
537
|
+
function getWelcomeMessage() {
|
|
538
|
+
return `🎉 欢迎使用 Cue - 你的专属调研助理!
|
|
539
|
+
|
|
540
|
+
📋 使用指南:
|
|
541
|
+
|
|
542
|
+
1️⃣ 深度调研
|
|
543
|
+
发送 /cue <问题> 开始研究
|
|
544
|
+
例:/cue 宁德时代的竞争优势
|
|
545
|
+
|
|
546
|
+
2️⃣ 查看任务
|
|
547
|
+
/ct - 查看研究进度
|
|
548
|
+
/cm - 查看监控项
|
|
549
|
+
|
|
550
|
+
3️⃣ 监控通知
|
|
551
|
+
/cn - 查看触发通知
|
|
552
|
+
|
|
553
|
+
🔑 首次使用请先配置:
|
|
554
|
+
/key 您的CUECUE_API_KEY
|
|
555
|
+
|
|
556
|
+
💡 提示:研究完成后可选择创建监控项
|
|
557
|
+
`;
|
|
558
|
+
}
|