@cuebot/skill 1.0.5 → 1.0.6
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 +1 -1
- package/config/modes.json +32 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/api/cuecueClient.js +54 -13
- package/src/core/backgroundExecutor.js +8 -8
- package/src/core/monitorManager.js +14 -0
- package/src/cron/monitor-daemon.js +36 -24
- package/src/index.js +4 -4
- package/src/notifier/index.js +43 -3
- package/src/utils/dataSource.js +145 -20
- package/src/utils/envAdapter.js +233 -0
- package/src/utils/envUtils.js +57 -11
- package/src/utils/fileUtils.js +10 -3
- package/src/utils/notificationQueue.js +134 -246
- package/src/utils/openclawUtils.js +28 -1
- package/src/utils/subagentScheduler.js +147 -0
- package/src/utils/validators.js +0 -122
package/README.md
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"trader": {
|
|
3
|
+
"name": "短线交易视角",
|
|
4
|
+
"keywords": ["龙虎榜", "涨停", "游资", "资金流向", "短线", "打板", "连板", "换手率", "主力资金", "k线", "技术分析", "kdj", "macd", "成交量", "量价关系", "筹码", "支撑位", "压力位", "趋势", "牛股", "妖股", "跌停", "炸板", "首板", "二板", "三板"],
|
|
5
|
+
"description": "关注资金流向、技术信号、短期波动"
|
|
6
|
+
},
|
|
7
|
+
"fund-manager": {
|
|
8
|
+
"name": "基金经理视角",
|
|
9
|
+
"keywords": ["财报", "估值", "业绩", "年报", "季报", "投资", "财务", "ROE", "PE", "PB", "现金流", "盈利", "净利润", "营收", "毛利率", "净利率", "股息", "分红", "持仓", "重仓", "基金", "机构", "评级", "目标价", "买入", "卖出", "增持", "减持"],
|
|
10
|
+
"description": "关注基本面、估值、盈利能力"
|
|
11
|
+
},
|
|
12
|
+
"researcher": {
|
|
13
|
+
"name": "产业研究视角",
|
|
14
|
+
"keywords": ["产业链", "竞争格局", "技术路线", "市场格局", "行业分析", "市场份额", "供应链", "上下游", "产能", "产量", "出货量", "装机量", "渗透率", "国产替代", "技术突破", "专利", "研发", "产品", "客户", "竞争对手", "护城河", "壁垒"],
|
|
15
|
+
"description": "关注行业趋势、竞争格局、技术发展"
|
|
16
|
+
},
|
|
17
|
+
"advisor": {
|
|
18
|
+
"name": "理财顾问视角",
|
|
19
|
+
"keywords": ["投资建议", "资产配置", "风险控制", "适合买", "怎么买", "定投", "组合", "分散风险", "理财", "收益", "回撤", "最大回撤", "夏普比率", "波动", "稳健", "激进", "保守", "预期收益", "持有期", "卖出时机"],
|
|
20
|
+
"description": "关注资产配置、风险收益比"
|
|
21
|
+
},
|
|
22
|
+
"macro": {
|
|
23
|
+
"name": "宏观分析视角",
|
|
24
|
+
"keywords": ["宏观经济", "GDP", "CPI", "PPI", "利率", "货币政策", "财政政策", "降息", "加息", "降准", "流动性", "社融", "信贷", "M2", "汇率", "美元", "美债", "美联储", "央行", "经济周期", "复苏", "衰退", "通胀", "通缩"],
|
|
25
|
+
"description": "关注宏观经济、政策环境"
|
|
26
|
+
},
|
|
27
|
+
"industry": {
|
|
28
|
+
"name": "行业轮动视角",
|
|
29
|
+
"keywords": ["板块", "行业", "轮动", "热点", "风口", "赛道", "景气度", "估值修复", "戴维斯双击", "预期差", "逻辑", "催化剂", "政策利好", "业绩兑现", "景气上行", "周期", "上行", "下行", "拐点"],
|
|
30
|
+
"description": "关注行业周期、轮动机会"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/index.js
CHANGED
package/package.json
CHANGED
package/src/api/cuecueClient.js
CHANGED
|
@@ -82,6 +82,28 @@ export function buildPrompt(topic, mode = 'default') {
|
|
|
82
82
|
${config.focus}`;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
import fs from 'fs';
|
|
86
|
+
import path from 'path';
|
|
87
|
+
import { fileURLToPath } from 'url';
|
|
88
|
+
|
|
89
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
90
|
+
const __dirname = path.dirname(__filename);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 加载模式配置
|
|
94
|
+
*/
|
|
95
|
+
function loadModeConfigs() {
|
|
96
|
+
try {
|
|
97
|
+
const configPath = path.join(__dirname, '../../config/modes.json');
|
|
98
|
+
if (fs.existsSync(configPath)) {
|
|
99
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
logger.warn('Failed to load modes config:', e.message);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
/**
|
|
86
108
|
* 自动检测研究模式
|
|
87
109
|
* @param {string} topic - 研究主题
|
|
@@ -95,6 +117,35 @@ export function autoDetectMode(topic) {
|
|
|
95
117
|
|
|
96
118
|
const topicLower = topic.toLowerCase().slice(0, 200); // 限制长度防止正则爆炸
|
|
97
119
|
|
|
120
|
+
// 尝试从配置文件加载
|
|
121
|
+
const modeConfigs = loadModeConfigs();
|
|
122
|
+
|
|
123
|
+
if (modeConfigs) {
|
|
124
|
+
// 使用配置文件进行检测
|
|
125
|
+
let maxMatch = 0;
|
|
126
|
+
let bestMode = 'researcher';
|
|
127
|
+
|
|
128
|
+
for (const [mode, config] of Object.entries(modeConfigs)) {
|
|
129
|
+
if (config.keywords && Array.isArray(config.keywords)) {
|
|
130
|
+
let matchCount = 0;
|
|
131
|
+
for (const keyword of config.keywords) {
|
|
132
|
+
if (topicLower.includes(keyword.toLowerCase())) {
|
|
133
|
+
matchCount++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (matchCount > maxMatch) {
|
|
137
|
+
maxMatch = matchCount;
|
|
138
|
+
bestMode = mode;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (maxMatch > 0) {
|
|
144
|
+
return bestMode;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 备用:使用内置关键词(兼容旧逻辑)
|
|
98
149
|
// 短线交易
|
|
99
150
|
if (/龙虎榜|涨停|游资|资金流向|短线|打板|连板|换手率|主力资金/.test(topicLower)) {
|
|
100
151
|
return 'trader';
|
|
@@ -105,7 +156,7 @@ export function autoDetectMode(topic) {
|
|
|
105
156
|
return 'fund-manager';
|
|
106
157
|
}
|
|
107
158
|
|
|
108
|
-
//
|
|
159
|
+
// 产业研究
|
|
109
160
|
if (/产业链|竞争格局|技术路线|市场格局|行业分析|市场份额|供应链|上下游/.test(topicLower)) {
|
|
110
161
|
return 'researcher';
|
|
111
162
|
}
|
|
@@ -115,20 +166,10 @@ export function autoDetectMode(topic) {
|
|
|
115
166
|
return 'advisor';
|
|
116
167
|
}
|
|
117
168
|
|
|
118
|
-
//
|
|
169
|
+
// 默认产业研究
|
|
119
170
|
return 'researcher';
|
|
120
171
|
}
|
|
121
172
|
|
|
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
173
|
export async function startResearch({ topic, mode, chatId, apiKey, userProfile, onProgress }) {
|
|
133
174
|
const conversationId = `conv_${randomUUID().replace(/-/g, '')}`;
|
|
134
175
|
const messageId = `msg_${randomUUID().replace(/-/g, '')}`;
|
|
@@ -817,7 +858,7 @@ export function buildPromptLayered(topic, options = {}) {
|
|
|
817
858
|
*/
|
|
818
859
|
export function recordUserPreference(chatId, mode) {
|
|
819
860
|
try {
|
|
820
|
-
const profileDir = path.join(process.env.
|
|
861
|
+
const profileDir = path.join(process.env.OPENCLAW_WORKSPACE || '/root/.openclaw/workspaces', `\${process.env.OPENCLAW_CHANNEL || 'feishu'}-\${chatId}`, '.cuecue');
|
|
821
862
|
const prefFile = path.join(profileDir, 'preferences.json');
|
|
822
863
|
|
|
823
864
|
let prefs = { modeHistory: [], lastUpdated: null };
|
|
@@ -13,13 +13,13 @@ const logger = createLogger('BackgroundExecutor');
|
|
|
13
13
|
|
|
14
14
|
// 行业关键词映射
|
|
15
15
|
const industryKeywords = {
|
|
16
|
-
'新能源': [
|
|
17
|
-
'半导体': [
|
|
18
|
-
'医药': [
|
|
19
|
-
'消费': [
|
|
20
|
-
'金融': [
|
|
21
|
-
'地产': [
|
|
22
|
-
'汽车': [
|
|
16
|
+
'新能源': ['宁德时代', '比亚迪', '光伏', '锂电', '储能', '电动车', '特斯拉'],
|
|
17
|
+
'半导体': ['芯片', '集成电路', '中芯国际', '华为', 'AI芯片', 'GPU', '英伟达'],
|
|
18
|
+
'医药': ['药', '医疗', '生物', '疫苗', '创新药', '恒瑞'],
|
|
19
|
+
'消费': ['茅台', '五粮液', '食品', '饮料', '家电', '零售'],
|
|
20
|
+
'金融': ['银行', '保险', '证券', '理财', '基金', 'A股'],
|
|
21
|
+
'地产': ['房地产', '万科', '碧桂园', '保利', '房价'],
|
|
22
|
+
'汽车': ['汽车', '整车', '上险量', '销量', '车企']
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -29,7 +29,7 @@ function updateFocusIndustries(chatId, topic) {
|
|
|
29
29
|
try {
|
|
30
30
|
const fs = require('fs');
|
|
31
31
|
const path = require('path');
|
|
32
|
-
const profilePath = path.join(process.env.
|
|
32
|
+
const profilePath = path.join(process.env.OPENCLAW_WORKSPACE || '/root/.openclaw/workspaces', `${process.env.OPENCLAW_CHANNEL || 'feishu'}-${chatId}`, '.cuecue', 'profile.json');
|
|
33
33
|
if (!fs.existsSync(profilePath)) return;
|
|
34
34
|
|
|
35
35
|
const profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
|
|
@@ -43,6 +43,20 @@ export class MonitorManager {
|
|
|
43
43
|
const filePath = getMonitorFilePath(this.chatId, monitorId);
|
|
44
44
|
await fs.writeJson(filePath, monitor, { spaces: 2 });
|
|
45
45
|
|
|
46
|
+
// 自动注册 OpenClaw Cron 任务(如果存在)
|
|
47
|
+
try {
|
|
48
|
+
const { registerCronTask } = await import('../utils/openclawUtils.js');
|
|
49
|
+
await registerCronTask({
|
|
50
|
+
name: `cue-monitor-${this.chatId}`,
|
|
51
|
+
schedule: '*/30 * * * *',
|
|
52
|
+
message: '/cm check'
|
|
53
|
+
});
|
|
54
|
+
await logger.info('Monitor cron registered automatically');
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// 非 OpenClaw 环境忽略
|
|
57
|
+
await logger.info('Skipping cron registration (not in Gateway mode)');
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
await logger.info(`Monitor created: ${monitorId}`);
|
|
47
61
|
return monitor;
|
|
48
62
|
}
|
|
@@ -8,7 +8,9 @@ import { createLogger } from '../core/logger.js';
|
|
|
8
8
|
import { createMonitorManager } from '../core/monitorManager.js';
|
|
9
9
|
import { sendMonitorTriggerNotification } from '../notifier/index.js';
|
|
10
10
|
import { evaluateSmartTrigger } from '../utils/smartTrigger.js';
|
|
11
|
+
import { search } from '../utils/dataSource.js';
|
|
11
12
|
import { isOpenClawGateway, registerCronTask } from '../utils/openclawUtils.js';
|
|
13
|
+
import { startMonitorCheck, sendToSession } from '../utils/subagentScheduler.js';
|
|
12
14
|
import { execSync } from 'child_process';
|
|
13
15
|
|
|
14
16
|
const logger = createLogger('MonitorDaemon');
|
|
@@ -69,31 +71,19 @@ async function checkMonitor(monitor, chatId) {
|
|
|
69
71
|
* @param {Object} monitor - 监控项
|
|
70
72
|
* @returns {Promise<Object>}
|
|
71
73
|
*/
|
|
74
|
+
/**
|
|
75
|
+
* 搜索触发相关内容(统一接口)
|
|
76
|
+
* 自动选择可用数据源
|
|
77
|
+
*/
|
|
72
78
|
async function searchForTrigger(trigger, monitor) {
|
|
73
79
|
try {
|
|
74
|
-
|
|
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(' ') || '';
|
|
80
|
+
// 使用统一搜索接口,自动选择可用工具
|
|
81
|
+
const result = await search(trigger, { maxResults: 10 });
|
|
93
82
|
|
|
94
83
|
return {
|
|
95
|
-
content,
|
|
96
|
-
results:
|
|
84
|
+
content: result.content,
|
|
85
|
+
results: result.results || [],
|
|
86
|
+
source: result.source
|
|
97
87
|
};
|
|
98
88
|
} catch (error) {
|
|
99
89
|
await logger.error('Trigger search failed', error);
|
|
@@ -197,12 +187,16 @@ export async function runMonitorCheck(chatId) {
|
|
|
197
187
|
* @param {string} chatId - 聊天 ID
|
|
198
188
|
* @returns {Object} cron job
|
|
199
189
|
*/
|
|
190
|
+
/**
|
|
191
|
+
* 启动监控守护进程
|
|
192
|
+
* 使用 OpenClaw Subagent 执行检查
|
|
193
|
+
*/
|
|
200
194
|
export async function startMonitorDaemon(chatId) {
|
|
201
195
|
await logger.info(`Starting monitor daemon for ${chatId}`);
|
|
202
196
|
|
|
203
|
-
//
|
|
197
|
+
// 优先使用 OpenClaw 模式
|
|
204
198
|
if (isOpenClawGateway()) {
|
|
205
|
-
|
|
199
|
+
// ✅ 使用 OpenClaw Cron 触发
|
|
206
200
|
const registered = await registerCronTask({
|
|
207
201
|
name: `cue-monitor-${chatId}`,
|
|
208
202
|
message: `cue-monitor-check ${chatId}`,
|
|
@@ -211,7 +205,7 @@ export async function startMonitorDaemon(chatId) {
|
|
|
211
205
|
});
|
|
212
206
|
|
|
213
207
|
if (registered) {
|
|
214
|
-
await logger.info(`OpenClaw cron registered
|
|
208
|
+
await logger.info(`OpenClaw cron registered for ${chatId}, using subagent for checks`);
|
|
215
209
|
return { mode: 'openclaw', chatId };
|
|
216
210
|
}
|
|
217
211
|
}
|
|
@@ -225,6 +219,24 @@ export async function startMonitorDaemon(chatId) {
|
|
|
225
219
|
return { mode: 'internal', job, chatId };
|
|
226
220
|
}
|
|
227
221
|
|
|
222
|
+
/**
|
|
223
|
+
* 使用 Subagent 执行监控检查(可选增强)
|
|
224
|
+
* @param {string} chatId - 用户 ID
|
|
225
|
+
*/
|
|
226
|
+
export async function runMonitorCheckWithSubagent(chatId) {
|
|
227
|
+
// 优先使用 subagent
|
|
228
|
+
if (isOpenClawGateway()) {
|
|
229
|
+
const taskId = await startMonitorCheck(chatId);
|
|
230
|
+
if (taskId) {
|
|
231
|
+
await logger.info(`Monitor check started via subagent: ${taskId}`);
|
|
232
|
+
return { mode: 'subagent', taskId };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 降级到直接执行
|
|
237
|
+
return await runMonitorCheck(chatId);
|
|
238
|
+
}
|
|
239
|
+
|
|
228
240
|
/**
|
|
229
241
|
* 使用 OpenClaw 外部 cron(备用方案)
|
|
230
242
|
* @param {string} chatId - 聊天 ID
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Cue - 主入口
|
|
4
|
-
* Node.js 版本 v1.0.
|
|
4
|
+
* Node.js 版本 v1.0.6
|
|
5
5
|
*/
|
|
6
6
|
import { createLogger } from './core/logger.js';
|
|
7
7
|
import { createUserState } from './core/userState.js';
|
|
@@ -345,7 +345,7 @@ function showWelcome() {
|
|
|
345
345
|
*/
|
|
346
346
|
function showUpdateNotice() {
|
|
347
347
|
return `╔══════════════════════════════════════════╗
|
|
348
|
-
║ ✨ Cue 已更新至 v1.0.
|
|
348
|
+
║ ✨ Cue 已更新至 v1.0.6 (Node.js 版) ║
|
|
349
349
|
╠══════════════════════════════════════════╣
|
|
350
350
|
║ ║
|
|
351
351
|
║ 本次更新内容: ║
|
|
@@ -456,7 +456,7 @@ function detectResearchIntent(input) {
|
|
|
456
456
|
*/
|
|
457
457
|
function getUserProfile(chatId) {
|
|
458
458
|
try {
|
|
459
|
-
const baseDir = path.join(process.env.
|
|
459
|
+
const baseDir = path.join(process.env.OPENCLAW_WORKSPACE || '/root/.openclaw/workspaces', `\${process.env.OPENCLAW_CHANNEL || 'feishu'}-\${chatId}`, '.cuecue');
|
|
460
460
|
const profilePath = path.join(baseDir, 'profile.json');
|
|
461
461
|
|
|
462
462
|
if (fs.existsSync(profilePath)) {
|
|
@@ -493,7 +493,7 @@ function getUserProfile(chatId) {
|
|
|
493
493
|
*/
|
|
494
494
|
function updateFocusIndustries(chatId, topic) {
|
|
495
495
|
try {
|
|
496
|
-
const profilePath = path.join(process.env.
|
|
496
|
+
const profilePath = path.join(process.env.OPENCLAW_WORKSPACE || '/root/.openclaw/workspaces', `${process.env.OPENCLAW_CHANNEL || 'feishu'}-${chatId}`, '.cuecue', 'profile.json');
|
|
497
497
|
if (!fs.existsSync(profilePath)) return;
|
|
498
498
|
|
|
499
499
|
const profile = JSON.parse(fs.readFileSync(profilePath, 'utf-8'));
|
package/src/notifier/index.js
CHANGED
|
@@ -58,7 +58,44 @@ function formatNotification(notif) {
|
|
|
58
58
|
* @param {string} channel
|
|
59
59
|
* @returns {Promise<boolean>}
|
|
60
60
|
*/
|
|
61
|
-
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 发送交互式卡片消息(支持按钮)
|
|
64
|
+
* @param {string} chatId - 目标聊天 ID
|
|
65
|
+
* @param {string} text - 消息文本
|
|
66
|
+
* @param {Array} buttons - 按钮数组 [{ label, value }]
|
|
67
|
+
* @returns {Promise<boolean>}
|
|
68
|
+
*/
|
|
69
|
+
async function sendCardMessage(chatId, text, buttons = [], channel = null) {
|
|
70
|
+
// ✅ 通用化:从环境变量获取 channel
|
|
71
|
+
channel = channel || process.env.OPENCLAW_CHANNEL || 'feishu';
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const card = {
|
|
75
|
+
type: "AdaptiveCard",
|
|
76
|
+
version: "1.0",
|
|
77
|
+
body: [{ type: "TextBlock", text: text, wrap: true }],
|
|
78
|
+
actions: buttons.map(btn => ({
|
|
79
|
+
type: "Action.Execute",
|
|
80
|
+
title: btn.label,
|
|
81
|
+
data: { action: "send", value: btn.value }
|
|
82
|
+
}))
|
|
83
|
+
};
|
|
84
|
+
const cardJson = JSON.stringify(card).replace(/"/g, '\\"');
|
|
85
|
+
execSync(
|
|
86
|
+
`openclaw message send --channel ${channel} --target "${chatId}" --card "${cardJson}"`,
|
|
87
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
88
|
+
);
|
|
89
|
+
logger.info(`Card message sent to ${chatId}`);
|
|
90
|
+
return true;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logger.error(`Failed to send card message`, error);
|
|
93
|
+
return await sendMessageDirect(chatId, text, channel);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function sendMessageDirect(chatId, text, channel = null) {
|
|
98
|
+
channel = channel || process.env.OPENCLAW_CHANNEL || 'feishu';
|
|
62
99
|
try {
|
|
63
100
|
// 方法1: 使用 OpenClaw CLI
|
|
64
101
|
const result = execSync(
|
|
@@ -77,10 +114,13 @@ async function sendMessageDirect(chatId, text, channel = 'feishu') {
|
|
|
77
114
|
* 发送消息(经过可靠队列)
|
|
78
115
|
* @param {string} chatId - 目标聊天 ID
|
|
79
116
|
* @param {string} text - 消息内容
|
|
80
|
-
* @param {string} channel -
|
|
117
|
+
* @param {string} channel - 渠道(默认从环境变量 OPENCLAW_CHANNEL 获取)
|
|
81
118
|
* @returns {Promise<boolean>}
|
|
82
119
|
*/
|
|
83
|
-
export
|
|
120
|
+
export { sendCardMessage };
|
|
121
|
+
|
|
122
|
+
export async function sendMessage(chatId, text, channel = null) {
|
|
123
|
+
channel = channel || process.env.OPENCLAW_CHANNEL || 'feishu';
|
|
84
124
|
// 先尝试直接发送
|
|
85
125
|
const directSuccess = await sendMessageDirect(chatId, text, channel);
|
|
86
126
|
|
package/src/utils/dataSource.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 数据源管理器
|
|
3
|
-
*
|
|
3
|
+
* 统一接口,自动选择可用工具
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createLogger } from '../core/logger.js';
|
|
7
|
+
import { getApiKey } from './envUtils.js';
|
|
7
8
|
|
|
8
9
|
const logger = createLogger('DataSource');
|
|
9
10
|
|
|
@@ -28,7 +29,7 @@ export const DATA_SOURCES = {
|
|
|
28
29
|
// 免费股票数据 API
|
|
29
30
|
yahoo: {
|
|
30
31
|
name: 'Yahoo Finance',
|
|
31
|
-
envKey: null,
|
|
32
|
+
envKey: null,
|
|
32
33
|
priority: 10,
|
|
33
34
|
type: 'stock',
|
|
34
35
|
endpoints: {
|
|
@@ -42,7 +43,7 @@ export const DATA_SOURCES = {
|
|
|
42
43
|
priority: 11,
|
|
43
44
|
type: 'stock',
|
|
44
45
|
endpoints: {
|
|
45
|
-
quote: 'https://www.google.com/finance/quote/{symbol}:
|
|
46
|
+
quote: 'https://www.google.com/finance/quote/{symbol}:SHA'
|
|
46
47
|
}
|
|
47
48
|
},
|
|
48
49
|
tencent: {
|
|
@@ -81,6 +82,118 @@ export function getAvailableSources(type) {
|
|
|
81
82
|
return sources;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* 统一搜索接口
|
|
87
|
+
* 自动选择可用工具,按优先级尝试
|
|
88
|
+
* @param {string} query - 搜索查询
|
|
89
|
+
* @param {Object} options - 选项
|
|
90
|
+
* @returns {Promise<Object>}
|
|
91
|
+
*/
|
|
92
|
+
export async function search(query, options = {}) {
|
|
93
|
+
const { type = 'search', maxResults = 10 } = options;
|
|
94
|
+
|
|
95
|
+
const sources = getAvailableSources(type);
|
|
96
|
+
|
|
97
|
+
if (sources.length === 0) {
|
|
98
|
+
logger.warn(`No available sources for type: ${type}`);
|
|
99
|
+
return { content: '', results: [], source: null };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 按优先级尝试每个可用源
|
|
103
|
+
for (const source of sources) {
|
|
104
|
+
try {
|
|
105
|
+
logger.info(`Trying ${source.name} for search: ${query}`);
|
|
106
|
+
|
|
107
|
+
let result;
|
|
108
|
+
if (source.key === 'tavily') {
|
|
109
|
+
result = await searchWithTavily(query, maxResults);
|
|
110
|
+
} else if (source.key === 'qveris') {
|
|
111
|
+
result = await searchWithQVeris(query, maxResults);
|
|
112
|
+
} else {
|
|
113
|
+
logger.warn(`Unknown source: ${source.key}`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (result && result.results?.length > 0) {
|
|
118
|
+
logger.info(`Search successful with ${source.name}`);
|
|
119
|
+
return {
|
|
120
|
+
...result,
|
|
121
|
+
source: source.name
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
logger.warn(`${source.name} failed: ${e.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { content: '', results: [], source: null };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 使用 Tavily 搜索
|
|
134
|
+
*/
|
|
135
|
+
async function searchWithTavily(query, maxResults = 10) {
|
|
136
|
+
const apiKey = process.env.TAVILY_API_KEY;
|
|
137
|
+
if (!apiKey) return null;
|
|
138
|
+
|
|
139
|
+
const response = await fetch('https://api.tavily.com/search', {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'application/json'
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
api_key: apiKey,
|
|
146
|
+
query,
|
|
147
|
+
max_results: maxResults,
|
|
148
|
+
include_answer: true,
|
|
149
|
+
include_raw_content: false
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
throw new Error(`Tavily API error: ${response.status}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
results: data.results || [],
|
|
161
|
+
answer: data.answer,
|
|
162
|
+
content: data.results?.map(r => `${r.title} ${r.content}`).join(' ') || ''
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 使用 QVeris 搜索
|
|
168
|
+
*/
|
|
169
|
+
async function searchWithQVeris(query, maxResults = 10) {
|
|
170
|
+
const apiKey = process.env.QVERIS_API_KEY;
|
|
171
|
+
if (!apiKey) return null;
|
|
172
|
+
|
|
173
|
+
const response = await fetch('https://api.qveris.ai/v1/search', {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
'Authorization': `Bearer ${apiKey}`
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
query,
|
|
181
|
+
max_results: maxResults
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
throw new Error(`QVeris API error: ${response.status}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const data = await response.json();
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
results: data.results || [],
|
|
193
|
+
content: data.results?.map(r => `${r.title} ${r.content}`).join(' ') || ''
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
84
197
|
/**
|
|
85
198
|
* 获取股票数据
|
|
86
199
|
* @param {string} symbol - 股票代码
|
|
@@ -92,8 +205,8 @@ export async function fetchStockData(symbol) {
|
|
|
92
205
|
for (const source of sources) {
|
|
93
206
|
try {
|
|
94
207
|
logger.info(`Trying ${source.name} for ${symbol}`);
|
|
95
|
-
const data = await
|
|
96
|
-
if (data) return data;
|
|
208
|
+
const data = await fetchStockFromSource(source, symbol);
|
|
209
|
+
if (data) return { ...data, source: source.name };
|
|
97
210
|
} catch (e) {
|
|
98
211
|
logger.warn(`${source.name} failed: ${e.message}`);
|
|
99
212
|
}
|
|
@@ -102,7 +215,7 @@ export async function fetchStockData(symbol) {
|
|
|
102
215
|
return null;
|
|
103
216
|
}
|
|
104
217
|
|
|
105
|
-
async function
|
|
218
|
+
async function fetchStockFromSource(source, symbol) {
|
|
106
219
|
const endpoint = source.endpoints?.quote;
|
|
107
220
|
if (!endpoint) return null;
|
|
108
221
|
|
|
@@ -116,25 +229,37 @@ async function fetchFromSource(source, symbol) {
|
|
|
116
229
|
|
|
117
230
|
if (!response.ok) return null;
|
|
118
231
|
|
|
232
|
+
// 腾讯财经返回特殊格式
|
|
233
|
+
if (source.key === 'tencent') {
|
|
234
|
+
const text = await response.text();
|
|
235
|
+
return parseTencentResponse(text, symbol);
|
|
236
|
+
}
|
|
237
|
+
|
|
119
238
|
return await response.json();
|
|
120
239
|
}
|
|
121
240
|
|
|
122
241
|
/**
|
|
123
|
-
*
|
|
124
|
-
* @param {string} query - 查询
|
|
125
|
-
* @returns {Promise<Object>}
|
|
242
|
+
* 解析腾讯财经响应
|
|
126
243
|
*/
|
|
127
|
-
|
|
128
|
-
|
|
244
|
+
function parseTencentResponse(text, symbol) {
|
|
245
|
+
// 格式: "symbol="sh000001" ...~
|
|
246
|
+
const match = text.match(/="([^"]+)"/);
|
|
247
|
+
if (!match) return null;
|
|
129
248
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
logger.info(`Searching with ${source.name}`);
|
|
133
|
-
// 实现各源搜索
|
|
134
|
-
} catch (e) {
|
|
135
|
-
logger.warn(`${source.name} failed`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
249
|
+
const parts = match[1].split('~');
|
|
250
|
+
if (parts.length < 50) return null;
|
|
138
251
|
|
|
139
|
-
return
|
|
252
|
+
return {
|
|
253
|
+
symbol,
|
|
254
|
+
name: parts[1],
|
|
255
|
+
price: parseFloat(parts[3]),
|
|
256
|
+
change: parseFloat(parts[4]),
|
|
257
|
+
changePercent: parseFloat(parts[5]),
|
|
258
|
+
volume: parseInt(parts[6]),
|
|
259
|
+
amount: parseInt(parts[7]),
|
|
260
|
+
open: parseFloat(parts[8]),
|
|
261
|
+
close: parseFloat(parts[9]),
|
|
262
|
+
high: parseFloat(parts[10]),
|
|
263
|
+
low: parseFloat(parts[11])
|
|
264
|
+
};
|
|
140
265
|
}
|