Fundamental_Analysis/services/data-persistence-service/assets/main.js
Lv, Qi 21155bc4f8 feat(realtime): 接入前端实时报价并完善后端缓存
前端: 新增 RealTimeQuoteResponse 类型;新增 useRealtimeQuote Hook 并在报告页图表旁展示价格与时间戳(严格 TTL,无兜底)

FastAPI: 新增 GET /financials/{market}/{symbol}/realtime?max_age_seconds=.. 只读端点;通过 DataPersistenceClient 读取 Rust 缓存

Rust: 新增 realtime_quotes hypertable 迁移;新增 POST /api/v1/market-data/quotes 与 GET /api/v1/market-data/quotes/{symbol}?market=..;新增 DTO/Model/DB 函数;修正 #[api] 宏与路径参数;生成 SQLx 离线缓存 (.sqlx) 以支持离线构建

Python: DataPersistenceClient 新增 upsert/get 实时报价,并调整 GET 路径与参数

说明: TradingView 图表是第三方 websocket,不受我们缓存控制;页面数值展示走自有缓存通路,统一且可控。
2025-11-09 05:12:14 +08:00

384 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import init, {
init_cli,
run_command_async,
get_completions,
get_history_item,
search_history
} from '/cli-ui/forge_cli_wasm.js';
async function main() {
// 1. Initialize xterm.js
const term = new Terminal({
cursorBlink: true,
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
},
cols: 120, // Set a reasonable terminal width
scrollback: 1000,
convertEol: true, // Convert \n to \r\n for proper line endings
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal'));
fitAddon.fit();
window.addEventListener('resize', () => fitAddon.fit());
term.writeln('Welcome to the Forge CLI (WASM Interface)');
term.writeln('------------------------------------------');
term.writeln('');
try {
// 2. Load and initialize the WASM module
term.write('Loading WASM module...');
await init();
term.writeln('\r✅ WASM module loaded successfully.');
// 3. Fetch OpenAPI spec and initialize the CLI
const baseUrl = window.location.origin; // 动态获取base URL
term.write(`Fetching OpenAPI spec from ${baseUrl}/api-docs/openapi.json...`);
const response = await fetch(`${baseUrl}/api-docs/openapi.json`);
if (!response.ok) {
throw new Error(`Failed to fetch spec: ${response.statusText}`);
}
const specJson = await response.text();
const spec = JSON.parse(specJson);
// 保存到全局以便 JS fallback 使用
window.__openapiSpec = spec;
window.__baseUrl = baseUrl;
init_cli(specJson, baseUrl);
term.writeln('\r✅ CLI initialized with OpenAPI spec.');
} catch (e) {
term.writeln(`\r\n❌ Error during initialization: ${e}`);
return;
}
// 4. Implement the REPL with enhanced functionality
let currentLine = '';
let cursorPosition = 0; // 光标在当前行中的位置
let historyIndex = -1; // -1表示当前输入>=0表示历史记录索引
let isInReverseSearch = false;
let reverseSearchQuery = '';
let completionMenu = null; // 当前显示的补全菜单
const prompt = '\r\n$ ';
const promptOnly = '$ '; // 不包含换行的提示符,用于重绘
// 重绘当前行
function redrawLine() {
// 移动到行首并清除从提示符后的所有内容
term.write('\r' + promptOnly);
term.write('\x1b[K'); // 清除从光标到行尾的内容
if (isInReverseSearch) {
// 在反向搜索模式下,替换整个提示符
term.write('\r\x1b[K'); // 清除整行
term.write(`(reverse-i-search)'${reverseSearchQuery}': ${currentLine}`);
} else {
term.write(currentLine);
}
// 移动光标到正确位置
if (cursorPosition < currentLine.length) {
const moveCursor = currentLine.length - cursorPosition;
term.write('\x1b[' + moveCursor + 'D'); // 向左移动光标
}
}
// 插入字符到当前位置
function insertChar(char) {
currentLine = currentLine.slice(0, cursorPosition) + char + currentLine.slice(cursorPosition);
cursorPosition++;
redrawLine();
}
// 删除字符
function deleteChar() {
if (cursorPosition > 0) {
currentLine = currentLine.slice(0, cursorPosition - 1) + currentLine.slice(cursorPosition);
cursorPosition--;
redrawLine();
}
}
// 移动光标
function moveCursor(direction) {
if (direction === 'left' && cursorPosition > 0) {
cursorPosition--;
term.write('\x1b[D');
} else if (direction === 'right' && cursorPosition < currentLine.length) {
cursorPosition++;
term.write('\x1b[C');
}
}
// 处理Tab补全
function handleTabCompletion() {
try {
const completionResult = get_completions(currentLine, cursorPosition);
const suggestions = JSON.parse(completionResult.suggestions);
if (suggestions.length === 0) {
return;
}
if (suggestions.length === 1) {
// 只有一个建议,直接补全
const suggestion = suggestions[0];
const beforeCursor = currentLine.slice(0, suggestion.start_pos);
const afterCursor = currentLine.slice(suggestion.end_pos);
currentLine = beforeCursor + suggestion.value + afterCursor;
cursorPosition = beforeCursor.length + suggestion.value.length;
redrawLine();
} else {
// 多个建议,显示补全菜单
term.writeln('');
suggestions.slice(0, 10).forEach(suggestion => {
const desc = suggestion.description ? ` - ${suggestion.description}` : '';
term.writeln(` ${suggestion.value}${desc}`);
});
redrawLine();
}
} catch (e) {
console.error('Tab completion error:', e);
}
}
// 处理历史记录导航
function navigateHistory(direction) {
if (direction === 'up') {
const item = get_history_item(historyIndex + 1);
if (item) {
historyIndex++;
currentLine = item;
cursorPosition = currentLine.length;
redrawLine();
}
} else if (direction === 'down') {
if (historyIndex > 0) {
historyIndex--;
const item = get_history_item(historyIndex);
if (item) {
currentLine = item;
cursorPosition = currentLine.length;
redrawLine();
}
} else if (historyIndex === 0) {
historyIndex = -1;
currentLine = '';
cursorPosition = 0;
redrawLine();
}
}
}
// 处理反向搜索
function handleReverseSearch(char) {
if (char) {
reverseSearchQuery += char;
}
try {
const searchResults = JSON.parse(search_history(reverseSearchQuery));
if (searchResults.length > 0) {
currentLine = searchResults[0];
cursorPosition = currentLine.length;
}
redrawLine();
} catch (e) {
console.error('Reverse search error:', e);
}
}
// 退出反向搜索模式
function exitReverseSearch() {
isInReverseSearch = false;
reverseSearchQuery = '';
cursorPosition = currentLine.length;
redrawLine();
}
// JS fallback当 wasm 返回 Path not found 时,用 JS 直接按 OpenAPI 执行
async function executeCommandJS(commandLine) {
try {
const spec = window.__openapiSpec;
const baseUrl = window.__baseUrl || '';
if (!spec) return 'Error: OpenAPI spec not loaded.';
const tokens = commandLine.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
if (tokens.length === 0) return '';
const cmd = tokens[0];
const args = {};
for (let i = 1; i < tokens.length; i++) {
const t = tokens[i];
if (t.startsWith('--')) {
const key = t.replace(/^--/, '');
const val = (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) ? tokens[++i] : '';
args[key] = val.replace(/^"|"$/g, '');
}
}
const parts = cmd.split('.');
const method = parts.pop().toUpperCase();
const cmdSegs = parts;
// 匹配路径模板
let matched = null;
for (const [key, item] of Object.entries(spec.paths || {})) {
const keySegs = key.split('/').filter(s => s);
if (keySegs.length !== cmdSegs.length) continue;
let ok = true;
for (let i = 0; i < keySegs.length; i++) {
const ks = keySegs[i];
const cs = cmdSegs[i];
const isParam = ks.startsWith('{') && ks.endsWith('}');
if (!isParam && ks !== cs) { ok = false; break; }
}
if (ok) { matched = [key, item]; break; }
}
if (!matched) {
return `API request failed (JS fallback): Path not found for /${cmdSegs.join('/')}`;
}
const [pathTemplate, pathItem] = matched;
const op = (pathItem[method.toLowerCase()]);
if (!op) return `API request failed (JS fallback): Operation not found for ${cmd}`;
// 构造路径和查询
let finalPath = pathTemplate;
const used = new Set();
if (Array.isArray(op.parameters)) {
for (const p of op.parameters) {
const prm = p && p.name ? p : (p && p.$ref ? null : null);
if (!prm) continue;
if (p.in === 'path' && args[p.name] != null) {
finalPath = finalPath.replace(`{${p.name}}`, encodeURIComponent(args[p.name]));
used.add(p.name);
}
}
}
const query = [];
for (const [k, v] of Object.entries(args)) {
if (!used.has(k)) query.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
}
let serverUrl = '';
if (Array.isArray(spec.servers) && spec.servers.length > 0 && spec.servers[0].url) {
serverUrl = spec.servers[0].url;
}
const url = `${baseUrl}${serverUrl}${finalPath}${query.length ? ('?' + query.join('&')) : ''}`;
const resp = await fetch(url, { method });
const text = await resp.text();
try {
return JSON.stringify(JSON.parse(text), null, 2);
} catch {
return text;
}
} catch (e) {
return `API request failed (JS fallback): ${e}`;
}
}
term.write(prompt);
term.onKey(({ key, domEvent }) => {
const { keyCode, ctrlKey, altKey, metaKey } = domEvent;
// Ctrl+R - 反向搜索
if (ctrlKey && keyCode === 82 && !isInReverseSearch) {
isInReverseSearch = true;
reverseSearchQuery = '';
currentLine = '';
cursorPosition = 0;
redrawLine();
return;
}
// 在反向搜索模式下的处理
if (isInReverseSearch) {
if (keyCode === 13) { // Enter - 接受搜索结果
exitReverseSearch();
return;
} else if (keyCode === 27) { // Esc - 取消搜索
isInReverseSearch = false;
reverseSearchQuery = '';
currentLine = '';
cursorPosition = 0;
redrawLine();
return;
} else if (keyCode === 8) { // Backspace - 删除搜索字符
if (reverseSearchQuery.length > 0) {
reverseSearchQuery = reverseSearchQuery.slice(0, -1);
handleReverseSearch();
}
return;
} else if (!ctrlKey && !altKey && !metaKey && key.length === 1) {
handleReverseSearch(key);
return;
}
return;
}
// 普通模式下的处理
if (keyCode === 13) { // Enter - 执行命令
if (currentLine.trim()) {
term.writeln('');
// 异步执行命令
(async () => {
try {
let result = await run_command_async(currentLine);
const plain = String(result);
if (plain.includes('Path not found for')) {
result = await executeCommandJS(currentLine);
}
// 清理ANSI转义序列
const cleanResult = String(result)
.replace(/\x1b\[[0-9;]*m/g, '')
.replace(/\x1b\[[0-9]*[A-Za-z]/g, '')
.replace(/\[\d+m/g, '');
const lines = cleanResult.split('\n');
lines.forEach((line, index) => {
if (index === lines.length - 1 && line === '') {
return;
}
term.writeln(line);
});
} catch (error) {
term.writeln(`Error: ${error}`);
}
term.write(prompt);
})();
currentLine = '';
cursorPosition = 0;
historyIndex = -1;
} else {
term.write(prompt);
}
} else if (keyCode === 9) { // Tab - 补全
domEvent.preventDefault();
handleTabCompletion();
} else if (keyCode === 8) { // Backspace
deleteChar();
} else if (keyCode === 37) { // 左箭头
moveCursor('left');
} else if (keyCode === 39) { // 右箭头
moveCursor('right');
} else if (keyCode === 38) { // 上箭头 - 历史记录上一个
navigateHistory('up');
} else if (keyCode === 40) { // 下箭头 - 历史记录下一个
navigateHistory('down');
} else if (keyCode === 36) { // Home - 移到行首
cursorPosition = 0;
redrawLine();
} else if (keyCode === 35) { // End - 移到行尾
cursorPosition = currentLine.length;
redrawLine();
} else if (!ctrlKey && !altKey && !metaKey && key.length === 1) {
// 普通字符输入
insertChar(key);
}
});
}
main();