@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 CHANGED
@@ -67,7 +67,7 @@ const taskId = await startBackgroundResearch({
67
67
 
68
68
  ## 版本
69
69
 
70
- v1.0.5
70
+ v1.0.6
71
71
 
72
72
  ## License
73
73
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Cue Core - AI Research Assistant Library
3
- * @version 1.0.5
3
+ * @version 1.0.6
4
4
  */
5
5
 
6
6
  // API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuebot/skill",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Cue AI Research Assistant - Core Library",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -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.HOME || '/root', '.cuecue', chatId);
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
- '半导体': [芯片, '集成电路', '中芯国际', '华为', 'AI芯片', 'GPU', '英伟达'],
18
- '医药': [药, '医疗', '生物', '疫苗', '创新药', '恒瑞'],
19
- '消费': [茅台, '五粮液', '食品', '饮料', '家电', '零售'],
20
- '金融': [银行, '保险', '证券', '理财', '基金', 'A股'],
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.HOME || '/root', ' .cuecue', chatId, 'profile.json');
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
- let searchResult;
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: searchResult.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
- // 优先尝试 OpenClaw Gateway 模式
197
+ // 优先使用 OpenClaw 模式
204
198
  if (isOpenClawGateway()) {
205
- await logger.info('OpenClaw Gateway detected, registering cron task...');
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 successfully for ${chatId}`);
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.5
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.5 (Node.js 版) ║
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.HOME || '/root', '.cuecue', chatId);
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.HOME || '/root', '.cuecue', chatId, 'profile.json');
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'));
@@ -58,7 +58,44 @@ function formatNotification(notif) {
58
58
  * @param {string} channel
59
59
  * @returns {Promise<boolean>}
60
60
  */
61
- async function sendMessageDirect(chatId, text, channel = 'feishu') {
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 - 渠道(默认 feishu)
117
+ * @param {string} channel - 渠道(默认从环境变量 OPENCLAW_CHANNEL 获取)
81
118
  * @returns {Promise<boolean>}
82
119
  */
83
- export async function sendMessage(chatId, text, channel = 'feishu') {
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
 
@@ -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}:EX'
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 fetchFromSource(source, symbol);
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 fetchFromSource(source, symbol) {
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
- export async function searchNews(query) {
128
- const sources = getAvailableSources('search');
244
+ function parseTencentResponse(text, symbol) {
245
+ // 格式: "symbol="sh000001" ...~
246
+ const match = text.match(/="([^"]+)"/);
247
+ if (!match) return null;
129
248
 
130
- for (const source of sources) {
131
- try {
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 null;
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
  }