@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,851 @@
1
+ /**
2
+ * CueCue API 客户端
3
+ * 基于官方 SDK 的流式 API 实现
4
+ */
5
+
6
+ import https from 'https';
7
+ import { randomUUID } from 'crypto';
8
+ import { createLogger } from '../core/logger.js';
9
+
10
+ const logger = createLogger('CueCueClient');
11
+
12
+ const BASE_URL = 'https://cuecue.cn';
13
+
14
+ /**
15
+ * 角色模式配置
16
+ */
17
+ const MODE_CONFIGS = {
18
+ trader: {
19
+ role: '短线交易分析师',
20
+ focus: '资金流向、席位动向、市场情绪、技术形态、游资博弈',
21
+ framework: '市场微观结构与资金流向分析框架(Timeline Reconstruction)',
22
+ method: '追踪龙虎榜席位动向,分析大单资金流向,识别市场情绪拐点,研判技术形态支撑压力位',
23
+ sources: '交易所龙虎榜、Level-2 行情数据、东方财富/同花顺资金数据、游资席位追踪、实时财经快讯'
24
+ },
25
+ 'fund-manager': {
26
+ role: '基金经理',
27
+ focus: '估值模型、财务分析、投资决策、风险收益比',
28
+ framework: '基本面分析与估值模型框架',
29
+ method: '深度分析财务报表,构建估值模型(DCF/PE/PB 等),评估内在价值与市场价格偏离度',
30
+ sources: '上市公司财报、交易所公告、Wind/同花顺数据、券商深度研报、管理层访谈纪要'
31
+ },
32
+ researcher: {
33
+ role: '行业研究员',
34
+ focus: '产业链分析、竞争格局、技术趋势、市场空间',
35
+ framework: '产业链拆解与竞争力评估框架(Peer Benchmarking)',
36
+ method: '梳理上下游产业链结构,对比主要竞争对手的核心能力,研判技术演进趋势',
37
+ sources: '上市公司公告、券商研报、行业协会数据、专利数据库、技术白皮书'
38
+ },
39
+ advisor: {
40
+ role: '资深理财顾问',
41
+ focus: '投资建议、资产配置、风险控制、收益预期',
42
+ framework: '资产配置与风险收益评估框架',
43
+ method: '根据用户财务状况,提供个性化的投资组合建议,分析各类资产的风险收益特征',
44
+ sources: '公募基金报告、保险产品说明书、银行理财公告、权威财经媒体'
45
+ }
46
+ };
47
+
48
+ /**
49
+ * 构建 rewritten_mandate 格式的提示词
50
+ * @param {string} topic - 研究主题
51
+ * @param {string} mode - 研究模式
52
+ * @returns {string}
53
+ */
54
+ export function buildPrompt(topic, mode = 'default') {
55
+ // 输入验证
56
+ if (!topic || typeof topic !== 'string' || topic.trim().length === 0) {
57
+ throw new Error('Invalid topic: must be non-empty string');
58
+ }
59
+
60
+ const config = MODE_CONFIGS[mode];
61
+
62
+ if (!config) {
63
+ logger.warn(`Unknown mode: ${mode}, using default researcher`);
64
+ }
65
+
66
+ const safeTopic = topic.trim().slice(0, 500); // 限制长度防止过大
67
+
68
+ return `**【调研目标】**
69
+ 以${config.role}的专业视角,针对"${safeTopic}"进行全网深度信息搜集与分析,旨在回答该主题下的核心投资/交易问题。
70
+
71
+ **【信息搜集与整合框架】**
72
+ 1. **${config.framework}**:${config.method}。
73
+ 2. **关键证据锚定**:针对核心争议点或关键数据,查找并引用权威信源(如官方公告、交易所数据、权威研报)的原始信息。
74
+ 3. **多维视角交叉**:汇总不同利益相关方(如买方机构、卖方分析师、产业从业者)的观点差异与共识。
75
+
76
+ **【信源与边界】**
77
+ - 优先信源:${config.sources}。
78
+ - 时间窗口:结合当前日期,优先近 6 个月内的最新动态与数据。
79
+ - 排除信源:无明确来源的小道消息、未经证实的社交媒体传言。
80
+
81
+ **【核心关注】**
82
+ ${config.focus}`;
83
+ }
84
+
85
+ /**
86
+ * 自动检测研究模式
87
+ * @param {string} topic - 研究主题
88
+ * @returns {string}
89
+ */
90
+ export function autoDetectMode(topic) {
91
+ // 输入验证
92
+ if (!topic || typeof topic !== 'string') {
93
+ return 'researcher';
94
+ }
95
+
96
+ const topicLower = topic.toLowerCase().slice(0, 200); // 限制长度防止正则爆炸
97
+
98
+ // 短线交易
99
+ if (/龙虎榜|涨停|游资|资金流向|短线|打板|连板|换手率|主力资金/.test(topicLower)) {
100
+ return 'trader';
101
+ }
102
+
103
+ // 基金经理
104
+ if (/财报|估值|业绩|年报|季报|投资|财务|ROE|PE|PB|现金流|盈利/.test(topicLower)) {
105
+ return 'fund-manager';
106
+ }
107
+
108
+ // 研究员
109
+ if (/产业链|竞争格局|技术路线|市场格局|行业分析|市场份额|供应链|上下游/.test(topicLower)) {
110
+ return 'researcher';
111
+ }
112
+
113
+ // 理财顾问
114
+ if (/投资建议|资产配置|风险控制|适合买|怎么买|定投|组合/.test(topicLower)) {
115
+ return 'advisor';
116
+ }
117
+
118
+ // 默认研究员
119
+ return 'researcher';
120
+ }
121
+
122
+ /**
123
+ * 启动深度研究(流式 API)
124
+ * @param {Object} options - 选项
125
+ * @param {string} options.topic - 研究主题
126
+ * @param {string} options.mode - 研究模式
127
+ * @param {string} options.chatId - 聊天 ID
128
+ * @param {string} options.apiKey - API Key
129
+ * @param {Function} options.onProgress - 进度回调
130
+ * @returns {Promise<Object>}
131
+ */
132
+ export async function startResearch({ topic, mode, chatId, apiKey, userProfile, onProgress }) {
133
+ const conversationId = `conv_${randomUUID().replace(/-/g, '')}`;
134
+ const messageId = `msg_${randomUUID().replace(/-/g, '')}`;
135
+ const reportUrl = `${BASE_URL}/c/${conversationId}`;
136
+
137
+ // 自动检测模式
138
+ if (!mode || mode === 'default') {
139
+ mode = autoDetectMode(topic);
140
+ }
141
+
142
+ // 构建提示词
143
+ const prompt = buildPromptLayered(topic, { mode, userProfile });
144
+
145
+ logger.info(`Starting research: ${topic} (mode: ${mode})`);
146
+
147
+ // 发送进度更新
148
+ if (onProgress) {
149
+ onProgress({
150
+ stage: 'start',
151
+ message: `🔬 开始深度研究:${topic}`,
152
+ reportUrl
153
+ });
154
+ }
155
+
156
+ try {
157
+ // 使用流式 API
158
+ const result = await makeStreamRequest({
159
+ conversationId,
160
+ messageId,
161
+ prompt,
162
+ chatId,
163
+ apiKey,
164
+ onProgress
165
+ });
166
+
167
+ return {
168
+ conversationId,
169
+ chatId,
170
+ reportUrl,
171
+ topic,
172
+ mode,
173
+ ...result
174
+ };
175
+ } catch (error) {
176
+ logger.error('Failed to start research', error);
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * 发送流式 HTTPS 请求
183
+ * @param {Object} options - 选项
184
+ * @returns {Promise<Object>}
185
+ */
186
+ function makeStreamRequest({ conversationId, messageId, prompt, chatId, apiKey, onProgress }) {
187
+ return new Promise((resolve, reject) => {
188
+ const url = new URL('/api/chat/stream', BASE_URL);
189
+
190
+ const requestData = {
191
+ messages: [
192
+ {
193
+ role: 'user',
194
+ content: prompt,
195
+ id: messageId,
196
+ type: 'text'
197
+ }
198
+ ],
199
+ chat_id: chatId,
200
+ conversation_id: conversationId,
201
+ need_confirm: false,
202
+ need_analysis: false,
203
+ need_underlying: false,
204
+ need_recommend: false
205
+ };
206
+
207
+ const body = JSON.stringify(requestData);
208
+
209
+ const options = {
210
+ hostname: url.hostname,
211
+ port: 443,
212
+ path: url.pathname + url.search,
213
+ method: 'POST',
214
+ headers: {
215
+ 'Content-Type': 'application/json',
216
+ 'Content-Length': Buffer.byteLength(body),
217
+ 'Authorization': `Bearer ${apiKey}`
218
+ },
219
+ timeout: 3600000 // 1小时超时
220
+ };
221
+
222
+ const req = https.request(options, (res) => {
223
+ // 解析 SSE 流
224
+ parseSSEStream(res, { conversationId, onProgress }, resolve, reject);
225
+ });
226
+
227
+ req.on('error', (error) => {
228
+ reject(error);
229
+ });
230
+
231
+ req.on('timeout', () => {
232
+ req.destroy();
233
+ reject(new Error('Request timeout'));
234
+ });
235
+
236
+ req.write(body);
237
+ req.end();
238
+ });
239
+ }
240
+
241
+ /**
242
+ * 解析 SSE 流
243
+ * @param {Object} res - HTTP 响应
244
+ * @param {Object} params - 参数
245
+ * @param {Function} resolve - Promise resolve
246
+ * @param {Function} reject - Promise reject
247
+ */
248
+ function parseSSEStream(res, params, resolve, reject) {
249
+ const { conversationId, onProgress } = params;
250
+
251
+ let buffer = '';
252
+ let currentEvent = {
253
+ type: 'message',
254
+ data: null,
255
+ id: null
256
+ };
257
+
258
+ const result = {
259
+ conversationId,
260
+ tasks: [],
261
+ report: ''
262
+ };
263
+
264
+ const state = {
265
+ isReporter: false,
266
+ currentAgent: null
267
+ };
268
+
269
+ const reportContent = [];
270
+
271
+ res.on('data', (chunk) => {
272
+ buffer += chunk.toString('utf-8');
273
+ const lines = buffer.split('\n');
274
+ buffer = lines.pop() || '';
275
+
276
+ for (const line of lines) {
277
+ processSSELine(line, currentEvent, result, reportContent, state, onProgress);
278
+
279
+ if (line === '') {
280
+ currentEvent = { type: 'message', data: null, id: null };
281
+ }
282
+ }
283
+ });
284
+
285
+ res.on('end', () => {
286
+ // 处理剩余数据
287
+ if (buffer) {
288
+ const lines = buffer.split('\n');
289
+ for (const line of lines) {
290
+ processSSELine(line, currentEvent, result, reportContent, state, onProgress);
291
+ }
292
+ }
293
+
294
+ // 确保报告内容设置
295
+ if (!result.report && reportContent.length > 0) {
296
+ result.report = reportContent.join('');
297
+ }
298
+
299
+ logger.info(`Research stream completed: ${conversationId}`);
300
+ resolve(result);
301
+ });
302
+
303
+ res.on('error', reject);
304
+ }
305
+
306
+ /**
307
+ * 处理单条 SSE 行
308
+ * @param {string} line - SSE 行
309
+ * @param {Object} currentEvent - 当前事件
310
+ * @param {Object} result - 结果对象
311
+ * @param {Array} reportContent - 报告内容数组
312
+ * @param {Object} state - 状态
313
+ * @param {Function} onProgress - 进度回调
314
+ */
315
+ function processSSELine(line, currentEvent, result, reportContent, state, onProgress) {
316
+ if (line.startsWith('id: ')) {
317
+ currentEvent.id = line.slice(4);
318
+ } else if (line.startsWith('event: ')) {
319
+ currentEvent.type = line.slice(7);
320
+ } else if (line.startsWith('data: ')) {
321
+ try {
322
+ currentEvent.data = JSON.parse(line.slice(6));
323
+ } catch (e) {
324
+ logger.warn(`Failed to parse SSE data: ${e}`);
325
+ }
326
+ } else if (line === '' && currentEvent.data !== null) {
327
+ handleSSEEvent(currentEvent, result, reportContent, state, onProgress);
328
+ }
329
+ }
330
+
331
+ /**
332
+ * 处理完整的 SSE 事件
333
+ * @param {Object} event - SSE 事件
334
+ * @param {Object} result - 结果对象
335
+ * @param {Array} reportContent - 报告内容数组
336
+ * @param {Object} state - 状态
337
+ * @param {Function} onProgress - 进度回调
338
+ */
339
+ function handleSSEEvent(event, result, reportContent, state, onProgress) {
340
+ const eventType = event.type;
341
+ const eventData = event.data;
342
+
343
+ if (!eventData) return;
344
+
345
+ const agentName = eventData.agent_name;
346
+
347
+ if (eventType === 'start_of_agent') {
348
+ state.currentAgent = agentName;
349
+
350
+ if (agentName === 'coordinator') {
351
+ logger.info('Research started by coordinator');
352
+ if (onProgress) {
353
+ onProgress({ stage: 'coordinator', message: '🔍 正在分配研究任务...' });
354
+ }
355
+ } else if (agentName === 'supervisor') {
356
+ const taskRequirement = eventData.task_requirement;
357
+ if (taskRequirement) {
358
+ result.tasks.push(taskRequirement);
359
+ logger.info(`Task assigned: ${taskRequirement}`);
360
+ if (onProgress) {
361
+ onProgress({ stage: 'task', message: `📋 ${taskRequirement}` });
362
+ }
363
+ }
364
+ } else if (agentName === 'reporter') {
365
+ state.isReporter = true;
366
+ logger.info('Reporter agent started');
367
+ if (onProgress) {
368
+ onProgress({ stage: 'report', message: '📝 正在生成报告...' });
369
+ }
370
+ }
371
+ } else if (eventType === 'end_of_agent') {
372
+ if (agentName === 'reporter') {
373
+ state.isReporter = false;
374
+ result.report = reportContent.join('');
375
+ logger.info('Reporter agent completed');
376
+ if (onProgress) {
377
+ onProgress({ stage: 'complete', message: '✅ 报告生成完成' });
378
+ }
379
+ }
380
+ state.currentAgent = null;
381
+ } else if (eventType === 'message' && state.isReporter) {
382
+ const delta = eventData.delta;
383
+ if (delta && delta.content) {
384
+ const content = delta.content;
385
+ // 移除引用标记如 【4-4】
386
+ const cleanedContent = content.replace(/【\d+-\d+】/g, '');
387
+ reportContent.push(cleanedContent);
388
+ }
389
+ } else if (eventType === 'final_session_state') {
390
+ logger.info('Research session completed');
391
+ if (onProgress) {
392
+ onProgress({ stage: 'final', message: '✨ 研究完成' });
393
+ }
394
+ }
395
+ }
396
+
397
+ /**
398
+ * 获取报告内容(备用方法)
399
+ * @param {string} conversationId - 对话 ID
400
+ * @param {string} apiKey - API Key
401
+ * @returns {Promise<Object>}
402
+ */
403
+ export async function getReportContent(conversationId, apiKey) {
404
+ return new Promise((resolve, reject) => {
405
+ const url = new URL(`/api/v1/conversations/${conversationId}`, BASE_URL);
406
+
407
+ const options = {
408
+ hostname: url.hostname,
409
+ port: 443,
410
+ path: url.pathname + url.search,
411
+ method: 'GET',
412
+ headers: {
413
+ 'Authorization': `Bearer ${apiKey}`,
414
+ 'Content-Type': 'application/json'
415
+ }
416
+ };
417
+
418
+ const req = https.request(options, (res) => {
419
+ let responseData = '';
420
+
421
+ res.on('data', (chunk) => {
422
+ responseData += chunk;
423
+ });
424
+
425
+ res.on('end', () => {
426
+ try {
427
+ const result = JSON.parse(responseData);
428
+ resolve(result);
429
+ } catch (error) {
430
+ reject(new Error(`Failed to parse response: ${error.message}`));
431
+ }
432
+ });
433
+ });
434
+
435
+ req.on('error', (error) => {
436
+ reject(error);
437
+ });
438
+
439
+ req.end();
440
+ });
441
+ }
442
+
443
+ /**
444
+ * 提取报告核心结论
445
+ * @param {Object} report - 报告数据
446
+ * @returns {Object}
447
+ */
448
+ export function extractReportInsights(report) {
449
+ const messages = report.messages || [];
450
+ const assistantMessages = messages.filter(m => m.role === 'assistant');
451
+
452
+ if (assistantMessages.length === 0) {
453
+ return {
454
+ summary: '暂无报告内容',
455
+ keyPoints: [],
456
+ recommendations: []
457
+ };
458
+ }
459
+
460
+ const lastMessage = assistantMessages[assistantMessages.length - 1];
461
+ const content = lastMessage.content || '';
462
+
463
+ // 提取摘要(前500字符)
464
+ const summary = content.slice(0, 500) + (content.length > 500 ? '...' : '');
465
+
466
+ // 提取关键点
467
+ const keyPoints = content
468
+ .split('\n')
469
+ .filter(line => line.match(/^[-•*]\s+/))
470
+ .map(line => line.replace(/^[-•*]\s+/, ''))
471
+ .slice(0, 5);
472
+
473
+ // 提取监控建议关键词
474
+ const recommendationKeywords = [
475
+ '建议关注', '值得监控', '需要跟踪', '风险提示', '机会',
476
+ 'recommend', 'monitor', 'track', 'risk', 'opportunity'
477
+ ];
478
+
479
+ const recommendations = [];
480
+ const lines = content.split('\n');
481
+
482
+ for (const line of lines) {
483
+ for (const keyword of recommendationKeywords) {
484
+ if (line.toLowerCase().includes(keyword.toLowerCase())) {
485
+ recommendations.push(line.trim());
486
+ break;
487
+ }
488
+ }
489
+ }
490
+
491
+ return {
492
+ summary,
493
+ keyPoints,
494
+ recommendations: recommendations.slice(0, 5),
495
+ fullContent: content
496
+ };
497
+ }
498
+
499
+ /**
500
+ * 生成监控建议
501
+ * @param {Object} insights - 报告洞察
502
+ * @param {string} topic - 研究主题
503
+ * @returns {Array}
504
+ */
505
+ /**
506
+ * 从文本中提取股票代码/名称
507
+ * @param {string} text - 文本内容
508
+ * @returns {Array} - 标的列表
509
+ */
510
+ function extractAssets(text) {
511
+ const assets = new Set();
512
+
513
+ // A股代码匹配 (如 600519, 300750)
514
+ const aStockMatch = text.match(/[36]\d{5}/g);
515
+ if (aStockMatch) {
516
+ aStockMatch.forEach(code => {
517
+ if (!['300000', '600000', '000000'].includes(code)) {
518
+ assets.add(code);
519
+ }
520
+ });
521
+ }
522
+
523
+ // 股票名称匹配 (如 宁德时代, 茅台, 比亚迪)
524
+ const names = text.match(/[\u4e00-\u9fa5]{2,6}(时代|股份|集团|科技|实业|控股|能源|电力|汽车|电池|银行|保险|证券)/g);
525
+ if (names) {
526
+ names.forEach(name => assets.add(name));
527
+ }
528
+
529
+ return Array.from(assets).slice(0, 5);
530
+ }
531
+
532
+ /**
533
+ * 判断监控分类
534
+ * @param {string} text - 文本内容
535
+ * @returns {string} - 分类
536
+ */
537
+ function detectCategory(text) {
538
+ const lower = text.toLowerCase();
539
+
540
+ if (/财报|业绩|利润|营收|PE|PB|ROE|分红|增发|回购/.test(lower)) return 'Data';
541
+ if (/公告|发布会|签约|合作|诉讼|处罚|减持|增持/.test(lower)) return 'Event';
542
+ if (/政策|监管|新规|试点|补贴|关税|制裁/.test(lower)) return 'Policy';
543
+ if (/情绪|舆论|机构|评级|目标价|外资/.test(lower)) return 'Sentiment';
544
+
545
+ return 'Data';
546
+ }
547
+
548
+ /**
549
+ * 判断显著性
550
+ * @param {string} text - 文本内容
551
+ * @returns {string} - 显著性
552
+ */
553
+ function detectSignificance(text) {
554
+ const lower = text.toLowerCase();
555
+
556
+ if (/风险|利空|减持|诉讼|处罚|警告|亏损|违约/.test(lower)) return 'Risk';
557
+ if (/利好|突破|增长|获批|合作|增持|订单|回购/.test(lower)) return 'Opportunity';
558
+
559
+ return 'Structural';
560
+ }
561
+
562
+ export function generateMonitorSuggestions(insights, topic) {
563
+ const suggestions = [];
564
+ const fullContent = insights?.fullContent || insights?.summary || '';
565
+
566
+ // 提取标的
567
+ const assets = extractAssets(fullContent + topic);
568
+
569
+ // 基于关键点生成监控
570
+ for (const point of (insights.keyPoints || [])) {
571
+ if (point.length > 10) {
572
+ const category = detectCategory(point);
573
+ const significance = detectSignificance(point);
574
+
575
+ suggestions.push({
576
+ title: `${topic} - ${category === 'Data' ? '数据更新' : category === 'Event' ? '事件驱动' : category === 'Policy' ? '政策变化' : '市场情绪'}`,
577
+ related_asset_symbol: assets[0] || null,
578
+ asset_name: assets[0] || topic,
579
+ category,
580
+ significance,
581
+ description: point.slice(0, 100),
582
+ trigger_keywords: point.split(/\s+/).slice(0, 5),
583
+ frequency_cron: category === 'Data' ? '0 9 * * 1-5' : '0 9,15 * * *',
584
+ start_date: calcSmartStartDate(category, point).date,
585
+ semantic_trigger: point
586
+ });
587
+ }
588
+ }
589
+
590
+ // 基于推荐生成监控
591
+ for (const rec of (insights.recommendations || [])) {
592
+ if (rec.length > 10) {
593
+ const category = detectCategory(rec);
594
+ const significance = detectSignificance(rec);
595
+
596
+ suggestions.push({
597
+ title: `${topic} - 推荐关注`,
598
+ related_asset_symbol: assets[0] || null,
599
+ asset_name: assets[0] || topic,
600
+ category,
601
+ significance,
602
+ description: rec.slice(0, 100),
603
+ trigger_keywords: ['建议', '推荐', '关注'],
604
+ frequency_cron: '0 9 * * 1-5',
605
+ start_date: calcSmartStartDate(category, point).date,
606
+ semantic_trigger: rec
607
+ });
608
+ }
609
+ }
610
+
611
+ // 如果太少,添加通用监控
612
+ if (suggestions.length < 2) {
613
+ suggestions.push({
614
+ title: `${topic} - 最新动态`,
615
+ related_asset_symbol: assets[0] || null,
616
+ asset_name: assets[0] || topic,
617
+ category: 'Data',
618
+ significance: 'Structural',
619
+ description: `跟踪${topic}的最新发展和市场反应`,
620
+ trigger_keywords: [topic, '最新', '动态'],
621
+ frequency_cron: '0 9 * * 1-5',
622
+ start_date: calcSmartStartDate("Data", topic).date,
623
+ semantic_trigger: `${topic}相关的重大新闻和公告`
624
+ });
625
+ }
626
+
627
+ return suggestions.slice(0, 5);
628
+ }
629
+
630
+ /**
631
+ * 使用专业模板生成监控建议(信号架构师版本)
632
+ * @param {Object} options - 选项
633
+ * @param {string} options.reportContent - 报告内容
634
+ * @param {string} options.topic - 研究主题
635
+ * @param {string} options.userProfile - 用户画像
636
+ * @param {Array} options.assetWhitelist - 标的清单
637
+ * @param {string} options.apiKey - API Key
638
+ * @returns {Promise<Array>}
639
+ */
640
+ export async function generateProfessionalMonitorSuggestions({ reportContent, topic, userProfile = '投资者', assetWhitelist = [], apiKey }) {
641
+ const template = `你是一位世界级的**战略情报与信号架构师**。
642
+ 你的核心任务是将一份非结构化的"深度研究报告",提取出**高价值,可量化、第一性信源**的可监控信号。
643
+
644
+ 请根据以下信息生成监控建议:
645
+
646
+ **User Profile**: ${userProfile}
647
+ **Asset Whitelist**: ${assetWhitelist.join(', ') || '无特定标的'}
648
+ **Report Content**: ${reportContent.slice(0, 5000)}
649
+
650
+ 请严格按以下JSON格式输出:
651
+ {
652
+ "monitoring_suggestions": [
653
+ {
654
+ "title": "简明扼要的监控标题",
655
+ "related_asset_symbol": "从Whitelist中选择,如无则填null",
656
+ "asset_name": "标的名称",
657
+ "category": "Data | Event | Policy | Sentiment",
658
+ "significance": "Opportunity | Risk | Structural",
659
+ "target_source": "具体的信源",
660
+ "frequency_cron": "Cron表达式",
661
+ "start_date": "YYYY-MM-DD",
662
+ "start_date_reason": "解释为什么从这个日期开始",
663
+ "semantic_trigger": "详细描述触发条件逻辑",
664
+ "reason_for_user": "一句话告诉用户为什么需要关注"
665
+ }
666
+ ]
667
+ }
668
+
669
+ 请只输出JSON,不要有其他文字。`;
670
+
671
+ try {
672
+ // 调用 CueCue API 生成专业建议
673
+ const result = await makeRequest('/api/v1/chat/completions', {
674
+ messages: [{ role: 'user', content: template, id: 'monitor_' + Date.now() }],
675
+ temperature: 0.3
676
+ }, apiKey);
677
+
678
+ // 解析 JSON 响应
679
+ const content = result.choices?.[0]?.message?.content || '';
680
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
681
+
682
+ if (jsonMatch) {
683
+ const parsed = JSON.parse(jsonMatch[0]);
684
+ return parsed.monitoring_suggestions || [];
685
+ }
686
+
687
+ return [];
688
+ } catch (error) {
689
+ logger.error('Failed to generate professional monitor suggestions', error);
690
+ return [];
691
+ }
692
+ }
693
+
694
+ /**
695
+ * 统一的监控建议生成入口
696
+ * 根据配置选择使用本地实现或 API 调用
697
+ *
698
+ * @param {Object} options - 选项
699
+ * @param {Object} options.insights - 报告洞察(用于本地模式)
700
+ * @param {string} options.topic - 研究主题
701
+ * @param {string} options.reportContent - 报告内容(用于 API 模式)
702
+ * @param {string} options.userProfile - 用户画像
703
+ * @param {Array} options.assetWhitelist - 标的清单
704
+ * @param {string} options.apiKey - API Key
705
+ * @param {boolean} options.useProfessionalApi - 是否使用专业 API(预留)
706
+ * @returns {Promise<Array>}
707
+ */
708
+ export async function createMonitorSuggestions(options) {
709
+ const { insights, topic, reportContent, userProfile, assetWhitelist, apiKey, useProfessionalApi = false } = options;
710
+
711
+ // 预留:未来可以通过配置启用专业 API
712
+ if (useProfessionalApi && apiKey) {
713
+ logger.info('Using professional API for monitor suggestions');
714
+ return generateProfessionalMonitorSuggestions({
715
+ reportContent: reportContent || insights?.fullContent || '',
716
+ topic,
717
+ userProfile: userProfile || '投资者',
718
+ assetWhitelist: assetWhitelist || [],
719
+ apiKey
720
+ });
721
+ }
722
+
723
+ // 当前:使用本地简单实现
724
+ logger.info('Using local simple implementation for monitor suggestions');
725
+ return generateMonitorSuggestions(insights, topic);
726
+ }
727
+
728
+ /**
729
+ * 根据监控类型智能计算开始日期
730
+ */
731
+ function calcSmartStartDate(category, title) {
732
+ const titleLower = title.toLowerCase();
733
+ let days = 7;
734
+ let reason = '7天静默期后开始';
735
+
736
+ if (/财报|业绩|年报|季报|半年报/.test(titleLower)) {
737
+ days = 30; reason = '等待财报季';
738
+ } else if (/政策|监管|新规/.test(titleLower)) {
739
+ days = 0; reason = '政策立即跟踪';
740
+ } else if (/公告|发布会|签约/.test(titleLower)) {
741
+ days = 3; reason = '等公告发布';
742
+ } else if (category === 'Data') {
743
+ days = 1; reason = '下一交易日';
744
+ }
745
+
746
+ const date = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
747
+ return { date: date.toISOString().slice(0, 10), reason };
748
+ }
749
+
750
+ /**
751
+ * 构建带用户画像的提示词
752
+ * @param {string} topic - 研究主题
753
+ * @param {string} mode - 研究模式
754
+ * @param {Object} userProfile - 用户画像
755
+ * @returns {string}
756
+ */
757
+ export function buildPromptWithProfile(topic, mode, userProfile) {
758
+ let prompt = buildPrompt(topic, mode);
759
+
760
+ if (!userProfile) return prompt;
761
+
762
+ // 构建用户画像部分
763
+ const parts = [];
764
+ if (userProfile.investmentStyle) parts.push(`投资风格:${userProfile.investmentStyle}`);
765
+ if (userProfile.riskPreference) parts.push(`风险偏好:${userProfile.riskPreference}`);
766
+ if (userProfile.focusAreas?.length) parts.push(`关注领域:${userProfile.focusAreas.join('、')}`);
767
+ if (userProfile.holdings?.length) parts.push(`持仓标的:${userProfile.holdings.join('、')}`);
768
+
769
+ if (parts.length === 0) return prompt;
770
+
771
+ // 插入用户画像
772
+ const profileSection = `\n\n**【用户画像】**\n${parts.join('\n')}\n`;
773
+
774
+ return prompt.replace('**【调研目标】**', '**【调研目标】**' + profileSection);
775
+ }
776
+
777
+ /**
778
+ * 构建带分层的提示词(意图优先策略)
779
+ * @param {string} topic - 研究主题
780
+ * @param {Object} options - 选项
781
+ * @returns {string}
782
+ */
783
+ export function buildPromptLayered(topic, options = {}) {
784
+ const {
785
+ explicitMode = null, // 显式指定的模式
786
+ userProfile = null, // 用户画像
787
+ autoDetectedMode = null // 自动检测的模式
788
+ } = options;
789
+
790
+ // 1. 显式模式优先
791
+ let mode = explicitMode || autoDetectedMode || 'researcher';
792
+
793
+ // 2. 构建提示词
794
+ let prompt = buildPrompt(topic, mode);
795
+
796
+ // 3. 画像作为背景参考(不覆盖显式意图)
797
+ if (userProfile) {
798
+ const parts = [];
799
+ if (userProfile.investmentStyle) parts.push(`投资风格:${userProfile.investmentStyle}`);
800
+ if (userProfile.riskPreference) parts.push(`风险偏好:${userProfile.riskPreference}`);
801
+ if (userProfile.focusAreas?.length) parts.push(`关注领域:${userProfile.focusAreas.join('、')}`);
802
+ if (userProfile.holdings?.length) parts.push(`持仓标的:${userProfile.holdings.join('、')}`);
803
+
804
+ if (parts.length > 0) {
805
+ const profileSection = `\n\n**【用户背景参考】**\n${parts.join('\n')}\n(此信息仅作参考,系统会根据您的具体问题调整分析重点)`;
806
+ prompt = prompt.replace('**【调研目标】**', '**【调研目标】**' + profileSection);
807
+ }
808
+ }
809
+
810
+ return prompt;
811
+ }
812
+
813
+ /**
814
+ * 记录用户模式偏好(用于自动学习)
815
+ * @param {string} chatId - 用户ID
816
+ * @param {string} mode - 使用的模式
817
+ */
818
+ export function recordUserPreference(chatId, mode) {
819
+ try {
820
+ const profileDir = path.join(process.env.HOME || '/root', '.cuecue', chatId);
821
+ const prefFile = path.join(profileDir, 'preferences.json');
822
+
823
+ let prefs = { modeHistory: [], lastUpdated: null };
824
+ if (fs.existsSync(prefFile)) {
825
+ prefs = JSON.parse(fs.readFileSync(prefFile, 'utf-8'));
826
+ }
827
+
828
+ // 添加历史记录
829
+ prefs.modeHistory.push({ mode, timestamp: Date.now() });
830
+
831
+ // 只保留最近10条
832
+ if (prefs.modeHistory.length > 10) {
833
+ prefs.modeHistory = prefs.modeHistory.slice(-10);
834
+ }
835
+
836
+ // 检查是否需要更新画像
837
+ const recentModes = prefs.modeHistory.slice(-3).map(p => p.mode);
838
+ const allSame = recentModes.every(m => m === recentModes[0]);
839
+
840
+ if (allSame && recentModes.length >= 3) {
841
+ prefs.suggestedMode = recentModes[0];
842
+ prefs.lastUpdated = Date.now();
843
+ }
844
+
845
+ fs.ensureDirSync(profileDir);
846
+ fs.writeFileSync(prefFile, JSON.stringify(prefs, null, 2));
847
+
848
+ } catch (e) {
849
+ logger.warn('Failed to record preference:', e.message);
850
+ }
851
+ }