fi-pool-server 0.1.0
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 +26 -0
- package/dist/db/index.d.ts +18 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +58 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +11 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +42 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema.d.ts +1101 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +149 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/services/analysis.d.ts +62 -0
- package/dist/services/analysis.d.ts.map +1 -0
- package/dist/services/analysis.js +74 -0
- package/dist/services/analysis.js.map +1 -0
- package/dist/services/daily-info.d.ts +150 -0
- package/dist/services/daily-info.d.ts.map +1 -0
- package/dist/services/daily-info.js +293 -0
- package/dist/services/daily-info.js.map +1 -0
- package/dist/services/embedding.d.ts +89 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +227 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/services/llm.d.ts +64 -0
- package/dist/services/llm.d.ts.map +1 -0
- package/dist/services/llm.js +109 -0
- package/dist/services/llm.js.map +1 -0
- package/dist/services/pipeline.d.ts +161 -0
- package/dist/services/pipeline.d.ts.map +1 -0
- package/dist/services/pipeline.js +844 -0
- package/dist/services/pipeline.js.map +1 -0
- package/dist/services/pool.d.ts +142 -0
- package/dist/services/pool.d.ts.map +1 -0
- package/dist/services/pool.js +208 -0
- package/dist/services/pool.js.map +1 -0
- package/dist/services/sentiment.d.ts +31 -0
- package/dist/services/sentiment.d.ts.map +1 -0
- package/dist/services/sentiment.js +76 -0
- package/dist/services/sentiment.js.map +1 -0
- package/dist/services/session.d.ts +117 -0
- package/dist/services/session.d.ts.map +1 -0
- package/dist/services/session.js +176 -0
- package/dist/services/session.js.map +1 -0
- package/dist/services/stock.d.ts +82 -0
- package/dist/services/stock.d.ts.map +1 -0
- package/dist/services/stock.js +99 -0
- package/dist/services/stock.js.map +1 -0
- package/dist/services/word-count.d.ts +61 -0
- package/dist/services/word-count.d.ts.map +1 -0
- package/dist/services/word-count.js +120 -0
- package/dist/services/word-count.js.map +1 -0
- package/dist/tools/auxiliary.d.ts +93 -0
- package/dist/tools/auxiliary.d.ts.map +1 -0
- package/dist/tools/auxiliary.js +204 -0
- package/dist/tools/auxiliary.js.map +1 -0
- package/dist/tools/command.d.ts +193 -0
- package/dist/tools/command.d.ts.map +1 -0
- package/dist/tools/command.js +263 -0
- package/dist/tools/command.js.map +1 -0
- package/dist/tools/execute.d.ts +109 -0
- package/dist/tools/execute.d.ts.map +1 -0
- package/dist/tools/execute.js +112 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/manager.d.ts +150 -0
- package/dist/tools/manager.d.ts.map +1 -0
- package/dist/tools/manager.js +200 -0
- package/dist/tools/manager.js.map +1 -0
- package/dist/tools/query.d.ts +163 -0
- package/dist/tools/query.d.ts.map +1 -0
- package/dist/tools/query.js +190 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/utils/http-client.d.ts +87 -0
- package/dist/utils/http-client.d.ts.map +1 -0
- package/dist/utils/http-client.js +211 -0
- package/dist/utils/http-client.js.map +1 -0
- package/dist/utils/indicators.d.ts +194 -0
- package/dist/utils/indicators.d.ts.map +1 -0
- package/dist/utils/indicators.js +395 -0
- package/dist/utils/indicators.js.map +1 -0
- package/dist/utils/signals.d.ts +65 -0
- package/dist/utils/signals.d.ts.map +1 -0
- package/dist/utils/signals.js +171 -0
- package/dist/utils/signals.js.map +1 -0
- package/drizzle/0000_equal_marvel_apes.sql +124 -0
- package/drizzle/meta/0000_snapshot.json +858 -0
- package/drizzle/meta/_journal.json +13 -0
- package/package.json +58 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日行情数据服务
|
|
3
|
+
*
|
|
4
|
+
* 提供上游职责:
|
|
5
|
+
* 1. 从腾讯财经接口获取历史 K 线数据和实时报价
|
|
6
|
+
* 2. 将获取的数据批量写入 daily_info 表(upsert)
|
|
7
|
+
* 3. 查询已入库的日行情数据
|
|
8
|
+
* 4. 刷新数据入口(单只或全量)
|
|
9
|
+
*
|
|
10
|
+
* 内置请求频率控制:相邻两次 HTTP 调用至少间隔 1200ms。
|
|
11
|
+
*
|
|
12
|
+
* @module services/daily-info
|
|
13
|
+
*/
|
|
14
|
+
import { getDatabase } from '../db/index.js';
|
|
15
|
+
import { dailyInfo, stock } from '../db/schema.js';
|
|
16
|
+
import { eq, and, between, sql } from 'drizzle-orm';
|
|
17
|
+
// ─── 模块级频率控制 ────────────────────────────────────────
|
|
18
|
+
/** 上一次 HTTP 请求的时间戳,用于频率控制 */
|
|
19
|
+
let lastFetchTime = 0;
|
|
20
|
+
/** 两次请求之间的最小间隔(毫秒),从 .env DATA_FETCH_INTERVAL_MS 读取,默认 1200ms */
|
|
21
|
+
const MIN_FETCH_INTERVAL = Math.max(200, parseInt(process.env.DATA_FETCH_INTERVAL_MS || '1200', 10));
|
|
22
|
+
/**
|
|
23
|
+
* 确保两次请求之间满足最小间隔。
|
|
24
|
+
* 如果上次请求至今不足 MIN_FETCH_INTERVAL,则等待剩余时间。
|
|
25
|
+
*/
|
|
26
|
+
async function enforceRateLimit() {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const elapsed = now - lastFetchTime;
|
|
29
|
+
if (elapsed < MIN_FETCH_INTERVAL) {
|
|
30
|
+
const delay = MIN_FETCH_INTERVAL - elapsed;
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
32
|
+
}
|
|
33
|
+
lastFetchTime = Date.now();
|
|
34
|
+
}
|
|
35
|
+
// ─── 市场前缀 ──────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* 根据股票代码判断所属市场,返回前缀标识。
|
|
38
|
+
*
|
|
39
|
+
* 上海:600/601/603/605/688/900
|
|
40
|
+
* 深圳:000/001/002/300/301/200
|
|
41
|
+
*
|
|
42
|
+
* @param code - 六位股票代码
|
|
43
|
+
* @returns 'sh' 或 'sz'
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* getMarketPrefix('600519') // => 'sh'
|
|
47
|
+
* getMarketPrefix('000001') // => 'sz'
|
|
48
|
+
*/
|
|
49
|
+
export function getMarketPrefix(code) {
|
|
50
|
+
const prefix = code.substring(0, 3);
|
|
51
|
+
if (['600', '601', '603', '605', '688', '900'].includes(prefix)) {
|
|
52
|
+
return 'sh';
|
|
53
|
+
}
|
|
54
|
+
return 'sz'; // 含 000/001/002/300/301/200 及未归类代码默认深圳
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 构建带市场前缀的完整代码(如 'sh600519')。
|
|
58
|
+
*
|
|
59
|
+
* @param code - 六位股票代码
|
|
60
|
+
* @returns 带前缀的代码
|
|
61
|
+
*/
|
|
62
|
+
function marketCode(code) {
|
|
63
|
+
return getMarketPrefix(code) + code;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 从腾讯财经接口获取最近 60 个交易日的日 K 线数据。
|
|
67
|
+
*
|
|
68
|
+
* 使用的接口:
|
|
69
|
+
* http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=${marketCode},day,,,60,qfq
|
|
70
|
+
*
|
|
71
|
+
* 返回数据的 day/qfqday 数组格式为 [date, open, close, high, low, volume]。
|
|
72
|
+
*
|
|
73
|
+
* @param code - 六位股票代码
|
|
74
|
+
* @returns 日 K 线数据数组(按日期升序,最近的在最后)
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const data = await fetchFromTencent('600519');
|
|
78
|
+
* console.log(data.length); // <= 60
|
|
79
|
+
*/
|
|
80
|
+
export async function fetchFromTencent(code) {
|
|
81
|
+
const url = `http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=${marketCode(code)},day,,,60,qfq`;
|
|
82
|
+
await enforceRateLimit();
|
|
83
|
+
const response = await fetch(url);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`腾讯 K 线接口返回错误状态 ${response.status} (${code})`);
|
|
86
|
+
}
|
|
87
|
+
const rawText = await response.text();
|
|
88
|
+
const json = parseJsonp(rawText);
|
|
89
|
+
// 提取数据:data -> {marketCode} -> day 或 qfqday
|
|
90
|
+
const codeKey = marketCode(code);
|
|
91
|
+
const stockData = json?.data?.[codeKey];
|
|
92
|
+
if (!stockData) {
|
|
93
|
+
throw new Error(`腾讯 K 线接口返回数据中无 ${codeKey} 节点 (${code})`);
|
|
94
|
+
}
|
|
95
|
+
// 优先使用 qfqday(前复权),否则用 day
|
|
96
|
+
const rawEntries = stockData.qfqday ?? stockData.day ?? [];
|
|
97
|
+
if (!Array.isArray(rawEntries) || rawEntries.length === 0) {
|
|
98
|
+
throw new Error(`腾讯 K 线接口返回空数组 (${code})`);
|
|
99
|
+
}
|
|
100
|
+
return rawEntries.map((entry) => {
|
|
101
|
+
const row = entry;
|
|
102
|
+
return {
|
|
103
|
+
date: row[0],
|
|
104
|
+
open: parseFloat(row[1]),
|
|
105
|
+
close: parseFloat(row[2]),
|
|
106
|
+
high: parseFloat(row[3]),
|
|
107
|
+
low: parseFloat(row[4]),
|
|
108
|
+
volume: parseInt(row[5], 10),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 从腾讯实时行情接口获取当前价格和名称。
|
|
114
|
+
* 使用的接口:
|
|
115
|
+
* https://web.sqt.gtimg.cn/q=sh${code} 或 sz${code}
|
|
116
|
+
*
|
|
117
|
+
* 返回 JSONP 格式文本,解析为股票数据字段数组后提取所需信息。
|
|
118
|
+
*
|
|
119
|
+
* @param code - 六位股票代码
|
|
120
|
+
* @returns 实时行情 { price, name }
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const { price, name } = await fetchRealTimeQuote('600519');
|
|
124
|
+
* // => { price: 1915.00, name: '贵州茅台' }
|
|
125
|
+
*/
|
|
126
|
+
export async function fetchRealTimeQuote(code) {
|
|
127
|
+
const url = `https://web.sqt.gtimg.cn/q=${marketCode(code)}`;
|
|
128
|
+
await enforceRateLimit();
|
|
129
|
+
const response = await fetch(url);
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(`腾讯实时行情接口返回错误状态 ${response.status} (${code})`);
|
|
132
|
+
}
|
|
133
|
+
const rawText = await response.text();
|
|
134
|
+
// 格式: v_sh600519="1~贵州茅台~600519~...";
|
|
135
|
+
// 提取引号内的内容
|
|
136
|
+
const match = rawText.match(/"([^"]+)"/);
|
|
137
|
+
if (!match) {
|
|
138
|
+
throw new Error(`解析实时行情响应失败: 未找到引号内容 (${code})`);
|
|
139
|
+
}
|
|
140
|
+
const fields = match[1].split('~');
|
|
141
|
+
// 标准字段索引(从 0 开始):
|
|
142
|
+
// 1 = 名称, 3 = 当前价
|
|
143
|
+
const name = fields[1];
|
|
144
|
+
const price = parseFloat(fields[3]);
|
|
145
|
+
if (!name || isNaN(price)) {
|
|
146
|
+
throw new Error(`解析实时行情数据字段失败 (${code}): name=${name}, price=${fields[3]}`);
|
|
147
|
+
}
|
|
148
|
+
return { price, name };
|
|
149
|
+
}
|
|
150
|
+
// ─── JSONP 解析 ────────────────────────────────────────────
|
|
151
|
+
/**
|
|
152
|
+
* 解析可能包含 JSONP 回调包装的响应文本为对象。
|
|
153
|
+
* 如果文本以 `函数名(` 开头,则剥离函数调用和末尾的 `;` 后解析 JSON。
|
|
154
|
+
* 否则直接解析为 JSON。
|
|
155
|
+
*
|
|
156
|
+
* @param text - 原始响应文本
|
|
157
|
+
* @returns 解析后的对象
|
|
158
|
+
*/
|
|
159
|
+
function parseJsonp(text) {
|
|
160
|
+
const trimmed = text.trim();
|
|
161
|
+
// 检查是否以字母开头(JSONP 回调函数名)
|
|
162
|
+
if (/^[a-zA-Z_\$]/.test(trimmed)) {
|
|
163
|
+
// 去掉函数名和左括号
|
|
164
|
+
const inner = trimmed.replace(/^[a-zA-Z_\$][a-zA-Z0-9_\$]*\(/, '').replace(/\);?\s*$/, '');
|
|
165
|
+
return JSON.parse(inner);
|
|
166
|
+
}
|
|
167
|
+
return JSON.parse(trimmed);
|
|
168
|
+
}
|
|
169
|
+
// ─── 数据库操作 ────────────────────────────────────────────
|
|
170
|
+
/**
|
|
171
|
+
* 批量插入或更新日行情数据。
|
|
172
|
+
* 按 (code, date) 唯一约束执行 upsert:
|
|
173
|
+
* - 已存在记录:更新 open/high/low/close/volume
|
|
174
|
+
* - 不存在记录:新增
|
|
175
|
+
*
|
|
176
|
+
* @param records - 行情数据记录数组
|
|
177
|
+
* @returns 成功处理的记录数
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* const n = await upsertDailyInfo([
|
|
181
|
+
* { code: '600519', date: '2024-06-01', open: 1500, high: 1510, low: 1490, close: 1505, volume: 1000000 },
|
|
182
|
+
* ]);
|
|
183
|
+
* // n === 1
|
|
184
|
+
*/
|
|
185
|
+
export async function upsertDailyInfo(records) {
|
|
186
|
+
if (records.length === 0)
|
|
187
|
+
return 0;
|
|
188
|
+
const db = getDatabase();
|
|
189
|
+
for (const record of records) {
|
|
190
|
+
db.insert(dailyInfo)
|
|
191
|
+
.values(record)
|
|
192
|
+
.onConflictDoUpdate({
|
|
193
|
+
target: [dailyInfo.code, dailyInfo.date],
|
|
194
|
+
set: {
|
|
195
|
+
open: record.open,
|
|
196
|
+
high: record.high,
|
|
197
|
+
low: record.low,
|
|
198
|
+
close: record.close,
|
|
199
|
+
volume: record.volume,
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
.run();
|
|
203
|
+
}
|
|
204
|
+
return records.length;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 查询指定股票的日行情数据。
|
|
208
|
+
* 可选的日期范围过滤。
|
|
209
|
+
*
|
|
210
|
+
* @param code - 股票代码
|
|
211
|
+
* @param startDate - 起始日期 'yyyy-MM-dd'(含),不指定则不限制起始
|
|
212
|
+
* @param endDate - 结束日期 'yyyy-MM-dd'(含),不指定则不限制结束
|
|
213
|
+
* @returns 日行情数据数组(按日期升序)
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* // 获取最近 10 天的数据
|
|
217
|
+
* const data = await getDailyInfo('600519', '2024-05-01', '2024-06-01');
|
|
218
|
+
*/
|
|
219
|
+
export async function getDailyInfo(code, startDate, endDate) {
|
|
220
|
+
const db = getDatabase();
|
|
221
|
+
const conditions = [eq(dailyInfo.code, code)];
|
|
222
|
+
if (startDate && endDate) {
|
|
223
|
+
conditions.push(between(dailyInfo.date, startDate, endDate));
|
|
224
|
+
}
|
|
225
|
+
else if (startDate) {
|
|
226
|
+
conditions.push(sql `${dailyInfo.date} >= ${startDate}`);
|
|
227
|
+
}
|
|
228
|
+
else if (endDate) {
|
|
229
|
+
conditions.push(sql `${dailyInfo.date} <= ${endDate}`);
|
|
230
|
+
}
|
|
231
|
+
return db
|
|
232
|
+
.select()
|
|
233
|
+
.from(dailyInfo)
|
|
234
|
+
.where(and(...conditions))
|
|
235
|
+
.orderBy(dailyInfo.date)
|
|
236
|
+
.all();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 刷新最新行情数据。
|
|
240
|
+
*
|
|
241
|
+
* - 当指定 code 时:获取该股票的最新实时行情和 K 线数据,更新 stock 表和 daily_info 表。
|
|
242
|
+
* - 当不指定 code 时:遍历数据库中所有股票,逐个刷新。
|
|
243
|
+
*
|
|
244
|
+
* @param code - 可选股票代码。不传则刷新全部
|
|
245
|
+
* @returns 更新的 daily_info 记录总数
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* // 刷新单只
|
|
249
|
+
* const { updated } = await refreshData('600519');
|
|
250
|
+
* // 刷新全部
|
|
251
|
+
* const { updated } = await refreshData();
|
|
252
|
+
*/
|
|
253
|
+
export async function refreshData(code) {
|
|
254
|
+
const db = getDatabase();
|
|
255
|
+
let codes = [];
|
|
256
|
+
if (code) {
|
|
257
|
+
codes = [code];
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const stocks = db.select({ code: stock.code }).from(stock).all();
|
|
261
|
+
codes = stocks.map((s) => s.code);
|
|
262
|
+
}
|
|
263
|
+
let totalUpdated = 0;
|
|
264
|
+
for (const c of codes) {
|
|
265
|
+
try {
|
|
266
|
+
// 1. 获取实时行情并更新 stock 表
|
|
267
|
+
const { price, name } = await fetchRealTimeQuote(c);
|
|
268
|
+
db.update(stock)
|
|
269
|
+
.set({ currentPrice: price, updatedAt: sql `datetime('now')` })
|
|
270
|
+
.where(eq(stock.code, c))
|
|
271
|
+
.run();
|
|
272
|
+
// 2. 获取历史 K 线并 upsert daily_info
|
|
273
|
+
const ohlcvList = await fetchFromTencent(c);
|
|
274
|
+
const records = ohlcvList.map((ohlcv) => ({
|
|
275
|
+
code: c,
|
|
276
|
+
date: ohlcv.date,
|
|
277
|
+
open: ohlcv.open,
|
|
278
|
+
high: ohlcv.high,
|
|
279
|
+
low: ohlcv.low,
|
|
280
|
+
close: ohlcv.close,
|
|
281
|
+
volume: ohlcv.volume,
|
|
282
|
+
}));
|
|
283
|
+
const n = await upsertDailyInfo(records);
|
|
284
|
+
totalUpdated += n;
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
// 单只股票失败不应影响后续股票的刷新
|
|
288
|
+
console.warn(`[daily-info] 刷新 ${c} 失败:`, err.message);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { updated: totalUpdated };
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=daily-info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daily-info.js","sourceRoot":"","sources":["../../src/services/daily-info.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 向量嵌入服务
|
|
3
|
+
*
|
|
4
|
+
* 对接 OpenAI 兼容的 Embedding API 生成文本向量,
|
|
5
|
+
* 存入 SQLite 数据库(BLOB 字段),
|
|
6
|
+
* 并提供基于余弦相似度的语义检索能力。
|
|
7
|
+
*
|
|
8
|
+
* sqlite-vec 扩展可用时优先使用,否则回退到 JS 内存计算。
|
|
9
|
+
*
|
|
10
|
+
* @module services/embedding
|
|
11
|
+
*/
|
|
12
|
+
/** 语义搜索结果条目 */
|
|
13
|
+
export interface SearchResult {
|
|
14
|
+
type: string;
|
|
15
|
+
code: string;
|
|
16
|
+
date: string;
|
|
17
|
+
relevance: number;
|
|
18
|
+
snippet: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 计算两个向量之间的余弦相似度。
|
|
22
|
+
*
|
|
23
|
+
* 余弦相似度 = (A · B) / (|A| * |B|),值域 [-1, 1]。
|
|
24
|
+
* 当任一向量为零向量时返回 0。
|
|
25
|
+
*
|
|
26
|
+
* @param a - 第一个向量
|
|
27
|
+
* @param b - 第二个向量
|
|
28
|
+
* @returns 余弦相似度
|
|
29
|
+
*/
|
|
30
|
+
export declare function cosineSimilarity(a: number[], b: number[]): number;
|
|
31
|
+
/**
|
|
32
|
+
* 调用 Embedding API 获取文本的向量表示。
|
|
33
|
+
*
|
|
34
|
+
* @param text - 输入文本
|
|
35
|
+
* @returns 浮点数向量数组
|
|
36
|
+
*
|
|
37
|
+
* @throws 当 API 不可达或返回空结果时抛出
|
|
38
|
+
*/
|
|
39
|
+
export declare function getEmbedding(text: string): Promise<number[]>;
|
|
40
|
+
/**
|
|
41
|
+
* 将向量嵌入存入 vec_embedding 表。
|
|
42
|
+
*
|
|
43
|
+
* 自动将 number[] 转换为 BLOB 存储。
|
|
44
|
+
*
|
|
45
|
+
* @param params.contentType - 内容类型('analysis' | 'final')
|
|
46
|
+
* @param params.contentCode - 股票代码
|
|
47
|
+
* @param params.contentDate - 报告日期(yyyy-MM-dd)
|
|
48
|
+
* @param params.contentText - 原始文本
|
|
49
|
+
* @param params.embedding - 向量数据
|
|
50
|
+
*/
|
|
51
|
+
export declare function storeEmbedding(params: {
|
|
52
|
+
contentType: string;
|
|
53
|
+
contentCode: string;
|
|
54
|
+
contentDate: string;
|
|
55
|
+
contentText: string;
|
|
56
|
+
embedding: number[];
|
|
57
|
+
}): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* 查询与给定文本语义相似的已存储向量。
|
|
60
|
+
*
|
|
61
|
+
* 实现策略:
|
|
62
|
+
* 1. 获取查询文本的嵌入向量
|
|
63
|
+
* 2. 从 vec_embedding 表中加载匹配条件的候选记录
|
|
64
|
+
* 3. 尝试使用 sqlite-vec 扩展加速(暂未集成,统一使用 JS 计算)
|
|
65
|
+
* 4. 逐条计算余弦相似度
|
|
66
|
+
* 5. 按相似度降序返回 top-k 结果
|
|
67
|
+
*
|
|
68
|
+
* @param params.query - 搜索查询文本
|
|
69
|
+
* @param params.type - 可选,按 content_type 过滤
|
|
70
|
+
* @param params.code - 可选,按股票代码过滤
|
|
71
|
+
* @param params.limit - 返回最大条数,默认 10
|
|
72
|
+
* @param params.minScore - 最低相似度阈值,默认 0.7
|
|
73
|
+
* @returns 搜索结果数组,按相关性降序排列
|
|
74
|
+
*/
|
|
75
|
+
export declare function searchSimilar(params: {
|
|
76
|
+
query: string;
|
|
77
|
+
type?: string;
|
|
78
|
+
code?: string;
|
|
79
|
+
limit?: number;
|
|
80
|
+
minScore?: number;
|
|
81
|
+
}): Promise<SearchResult[]>;
|
|
82
|
+
/**
|
|
83
|
+
* 删除指定股票和类型的向量嵌入。
|
|
84
|
+
*
|
|
85
|
+
* @param contentCode - 股票代码
|
|
86
|
+
* @param contentType - 可选内容类型,不传则删除该股票的所有向量
|
|
87
|
+
*/
|
|
88
|
+
export declare function deleteEmbeddings(contentCode: string, contentType?: string): Promise<void>;
|
|
89
|
+
//# sourceMappingURL=embedding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedding.d.ts","sourceRoot":"","sources":["../../src/services/embedding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA8BH,eAAe;AACf,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAID;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAejE;AA6BD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA0BlE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA0F1B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAWf"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 向量嵌入服务
|
|
3
|
+
*
|
|
4
|
+
* 对接 OpenAI 兼容的 Embedding API 生成文本向量,
|
|
5
|
+
* 存入 SQLite 数据库(BLOB 字段),
|
|
6
|
+
* 并提供基于余弦相似度的语义检索能力。
|
|
7
|
+
*
|
|
8
|
+
* sqlite-vec 扩展可用时优先使用,否则回退到 JS 内存计算。
|
|
9
|
+
*
|
|
10
|
+
* @module services/embedding
|
|
11
|
+
*/
|
|
12
|
+
import { fetchJson, fetchWithRetry } from '../utils/http-client.js';
|
|
13
|
+
import { getDatabase } from '../db/index.js';
|
|
14
|
+
import { vecEmbedding } from '../db/schema.js';
|
|
15
|
+
import { eq, and } from 'drizzle-orm';
|
|
16
|
+
// ─── 配置 ────────────────────────────────────────────────────────
|
|
17
|
+
/** Embedding API 地址 */
|
|
18
|
+
const API_URL = process.env.EMBEDDING_API_URL ||
|
|
19
|
+
'http://127.0.0.1:1234/v1/embeddings';
|
|
20
|
+
/** Embedding API 认证密钥 */
|
|
21
|
+
const API_KEY = process.env.EMBEDDING_API_KEY || 'not-needed';
|
|
22
|
+
/** 嵌入模型名称 */
|
|
23
|
+
const MODEL = process.env.EMBEDDING_MODEL || 'text-embedding-baai-bge-m3-568m';
|
|
24
|
+
// ─── 工具函数 ────────────────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* 计算两个向量之间的余弦相似度。
|
|
27
|
+
*
|
|
28
|
+
* 余弦相似度 = (A · B) / (|A| * |B|),值域 [-1, 1]。
|
|
29
|
+
* 当任一向量为零向量时返回 0。
|
|
30
|
+
*
|
|
31
|
+
* @param a - 第一个向量
|
|
32
|
+
* @param b - 第二个向量
|
|
33
|
+
* @returns 余弦相似度
|
|
34
|
+
*/
|
|
35
|
+
export function cosineSimilarity(a, b) {
|
|
36
|
+
if (a.length !== b.length || a.length === 0)
|
|
37
|
+
return 0;
|
|
38
|
+
let dotProduct = 0;
|
|
39
|
+
let normA = 0;
|
|
40
|
+
let normB = 0;
|
|
41
|
+
for (let i = 0; i < a.length; i++) {
|
|
42
|
+
dotProduct += a[i] * b[i];
|
|
43
|
+
normA += a[i] * a[i];
|
|
44
|
+
normB += b[i] * b[i];
|
|
45
|
+
}
|
|
46
|
+
const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
|
|
47
|
+
return magnitude === 0 ? 0 : dotProduct / magnitude;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 将 number[] 向量转换为 Buffer(Float32LE),用于 BLOB 存储。
|
|
51
|
+
*
|
|
52
|
+
* @param embedding - 浮点数向量
|
|
53
|
+
* @returns Buffer 对象
|
|
54
|
+
*/
|
|
55
|
+
function embeddingToBuffer(embedding) {
|
|
56
|
+
return Buffer.from(new Float32Array(embedding).buffer);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 将 Buffer(Float32LE)转换回 number[] 向量。
|
|
60
|
+
*
|
|
61
|
+
* @param buffer - 数据库读取的 BLOB
|
|
62
|
+
* @returns 浮点数向量
|
|
63
|
+
*/
|
|
64
|
+
function bufferToEmbedding(buffer) {
|
|
65
|
+
const floatArray = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / Float32Array.BYTES_PER_ELEMENT);
|
|
66
|
+
return Array.from(floatArray);
|
|
67
|
+
}
|
|
68
|
+
// ─── 公开 API ────────────────────────────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* 调用 Embedding API 获取文本的向量表示。
|
|
71
|
+
*
|
|
72
|
+
* @param text - 输入文本
|
|
73
|
+
* @returns 浮点数向量数组
|
|
74
|
+
*
|
|
75
|
+
* @throws 当 API 不可达或返回空结果时抛出
|
|
76
|
+
*/
|
|
77
|
+
export async function getEmbedding(text) {
|
|
78
|
+
const data = await fetchWithRetry(() => fetchJson(API_URL, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
input: text,
|
|
86
|
+
model: MODEL,
|
|
87
|
+
}),
|
|
88
|
+
}), 2, 1000);
|
|
89
|
+
if (!data.data || data.data.length === 0) {
|
|
90
|
+
throw new Error(`Embedding API 返回空结果 (model=${MODEL})`);
|
|
91
|
+
}
|
|
92
|
+
return data.data[0].embedding;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 将向量嵌入存入 vec_embedding 表。
|
|
96
|
+
*
|
|
97
|
+
* 自动将 number[] 转换为 BLOB 存储。
|
|
98
|
+
*
|
|
99
|
+
* @param params.contentType - 内容类型('analysis' | 'final')
|
|
100
|
+
* @param params.contentCode - 股票代码
|
|
101
|
+
* @param params.contentDate - 报告日期(yyyy-MM-dd)
|
|
102
|
+
* @param params.contentText - 原始文本
|
|
103
|
+
* @param params.embedding - 向量数据
|
|
104
|
+
*/
|
|
105
|
+
export async function storeEmbedding(params) {
|
|
106
|
+
const db = getDatabase();
|
|
107
|
+
const buffer = embeddingToBuffer(params.embedding);
|
|
108
|
+
await db.insert(vecEmbedding).values({
|
|
109
|
+
contentType: params.contentType,
|
|
110
|
+
contentCode: params.contentCode,
|
|
111
|
+
contentDate: params.contentDate,
|
|
112
|
+
contentText: params.contentText,
|
|
113
|
+
embedding: buffer,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 查询与给定文本语义相似的已存储向量。
|
|
118
|
+
*
|
|
119
|
+
* 实现策略:
|
|
120
|
+
* 1. 获取查询文本的嵌入向量
|
|
121
|
+
* 2. 从 vec_embedding 表中加载匹配条件的候选记录
|
|
122
|
+
* 3. 尝试使用 sqlite-vec 扩展加速(暂未集成,统一使用 JS 计算)
|
|
123
|
+
* 4. 逐条计算余弦相似度
|
|
124
|
+
* 5. 按相似度降序返回 top-k 结果
|
|
125
|
+
*
|
|
126
|
+
* @param params.query - 搜索查询文本
|
|
127
|
+
* @param params.type - 可选,按 content_type 过滤
|
|
128
|
+
* @param params.code - 可选,按股票代码过滤
|
|
129
|
+
* @param params.limit - 返回最大条数,默认 10
|
|
130
|
+
* @param params.minScore - 最低相似度阈值,默认 0.7
|
|
131
|
+
* @returns 搜索结果数组,按相关性降序排列
|
|
132
|
+
*/
|
|
133
|
+
export async function searchSimilar(params) {
|
|
134
|
+
const limit = params.limit ?? 10;
|
|
135
|
+
const minScore = params.minScore ?? 0.7;
|
|
136
|
+
// 1. 获取查询向量
|
|
137
|
+
let queryEmbedding;
|
|
138
|
+
try {
|
|
139
|
+
queryEmbedding = await getEmbedding(params.query);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
console.warn('[embedding] 获取查询向量失败,跳过向量检索:', err.message);
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
// 2. 构建查询条件
|
|
146
|
+
const db = getDatabase();
|
|
147
|
+
const conditions = [];
|
|
148
|
+
if (params.type) {
|
|
149
|
+
conditions.push(eq(vecEmbedding.contentType, params.type));
|
|
150
|
+
}
|
|
151
|
+
if (params.code) {
|
|
152
|
+
conditions.push(eq(vecEmbedding.contentCode, params.code));
|
|
153
|
+
}
|
|
154
|
+
// 3. 加载候选向量
|
|
155
|
+
let candidates;
|
|
156
|
+
if (conditions.length > 0) {
|
|
157
|
+
candidates = db
|
|
158
|
+
.select({
|
|
159
|
+
contentType: vecEmbedding.contentType,
|
|
160
|
+
contentCode: vecEmbedding.contentCode,
|
|
161
|
+
contentDate: vecEmbedding.contentDate,
|
|
162
|
+
contentText: vecEmbedding.contentText,
|
|
163
|
+
embedding: vecEmbedding.embedding,
|
|
164
|
+
})
|
|
165
|
+
.from(vecEmbedding)
|
|
166
|
+
.where(and(...conditions))
|
|
167
|
+
.all();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
candidates = db
|
|
171
|
+
.select({
|
|
172
|
+
contentType: vecEmbedding.contentType,
|
|
173
|
+
contentCode: vecEmbedding.contentCode,
|
|
174
|
+
contentDate: vecEmbedding.contentDate,
|
|
175
|
+
contentText: vecEmbedding.contentText,
|
|
176
|
+
embedding: vecEmbedding.embedding,
|
|
177
|
+
})
|
|
178
|
+
.from(vecEmbedding)
|
|
179
|
+
.all();
|
|
180
|
+
}
|
|
181
|
+
if (candidates.length === 0) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
// 4. 逐条计算余弦相似度
|
|
185
|
+
const results = [];
|
|
186
|
+
for (const row of candidates) {
|
|
187
|
+
if (!row.embedding)
|
|
188
|
+
continue;
|
|
189
|
+
try {
|
|
190
|
+
const candidateVector = bufferToEmbedding(row.embedding);
|
|
191
|
+
const score = cosineSimilarity(queryEmbedding, candidateVector);
|
|
192
|
+
if (score >= minScore) {
|
|
193
|
+
results.push({
|
|
194
|
+
type: row.contentType,
|
|
195
|
+
code: row.contentCode,
|
|
196
|
+
date: row.contentDate,
|
|
197
|
+
relevance: Math.round(score * 10000) / 10000, // 保留 4 位小数
|
|
198
|
+
snippet: row.contentText.slice(0, 200),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// 跳过无法解析的向量
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// 5. 排序并截取
|
|
208
|
+
results.sort((a, b) => b.relevance - a.relevance);
|
|
209
|
+
return results.slice(0, limit);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 删除指定股票和类型的向量嵌入。
|
|
213
|
+
*
|
|
214
|
+
* @param contentCode - 股票代码
|
|
215
|
+
* @param contentType - 可选内容类型,不传则删除该股票的所有向量
|
|
216
|
+
*/
|
|
217
|
+
export async function deleteEmbeddings(contentCode, contentType) {
|
|
218
|
+
const db = getDatabase();
|
|
219
|
+
const conditions = [
|
|
220
|
+
eq(vecEmbedding.contentCode, contentCode),
|
|
221
|
+
];
|
|
222
|
+
if (contentType) {
|
|
223
|
+
conditions.push(eq(vecEmbedding.contentType, contentType));
|
|
224
|
+
}
|
|
225
|
+
await db.delete(vecEmbedding).where(and(...conditions));
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=embedding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedding.js","sourceRoot":"","sources":["../../src/services/embedding.ts"],"names":[],"mappings":"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"}
|