增加大模型

This commit is contained in:
dekun
2026-05-26 10:04:36 +08:00
parent 86aa804c21
commit 1845018151
5 changed files with 351 additions and 104 deletions
+207 -74
View File
@@ -17,6 +17,16 @@ let statsData = null;
let currentView = "today";
let llmPollTimer = null;
let llmInterpretMap = {};
let llmRunState = {
running: false,
batch_id: "",
current_symbol: "",
done: 0,
total: 0,
};
let llmSymbolOrder = [];
const llmExpandedSymbols = new Set();
const llmSeenDone = new Set();
const SORT_KEYS = {
rank: (r) => Number(r.rank) || 0,
@@ -287,15 +297,17 @@ function renderStatsTable() {
return;
}
llmSymbolOrder = items.map((r) => r.symbol);
wrap.innerHTML = `
<table data-table="stats">
<table data-table="stats" class="stats-table">
<thead><tr>
<th>合约</th>
<th>今日排名</th><th>今日涨跌</th><th>今日成交额</th>
<th>昨日排名</th><th>昨日涨跌</th><th>昨日成交额</th>
<th>前日排名</th><th>前日涨跌</th><th>前日成交额</th>
<th>三日总成交额</th>
<th>AI解读</th>
<th class="llm-col-head">AI解读</th>
</tr></thead>
<tbody id="stats-body"></tbody>
</table>`;
@@ -309,20 +321,103 @@ function renderStatsTable() {
const pct = x.price_change_pct ?? 0;
return `<td class="${f === "pct" ? pctClass(pct) : ""}">${f === "pct" ? x.price_change_pct_fmt || "—" : f === "rank" ? x.rank ?? "—" : x.quote_volume_fmt || "—"}</td>`;
};
const llm = llmInterpretMap[row.symbol];
const llmCell = llm
? `<details class="llm-inline"><summary>AI解读</summary><div class="llm-text">${escapeHtml(llm.content)}</div><small>${llm.created_at?.slice(0, 19) || ""}</small></details>`
: '<span class="muted">—</span>';
return `<tr class="row-highlight">
return `<tr class="row-highlight stats-row" data-symbol="${row.symbol}">
<td><strong>${row.symbol}</strong></td>
${cell("today", "rank")}${cell("today", "pct")}${cell("today", "vol")}
${cell("yesterday", "rank")}${cell("yesterday", "pct")}${cell("yesterday", "vol")}
${cell("daybefore", "rank")}${cell("daybefore", "pct")}${cell("daybefore", "vol")}
<td>${formatVol(row.total_quote_volume)}</td>
<td class="llm-col">${llmCell}</td>
<td class="stats-total-vol">${formatVol(row.total_quote_volume)}</td>
<td class="llm-col">${buildLlmCellHtml(row.symbol)}</td>
</tr>`;
})
.join("");
bindLlmFoldHandlers();
}
function getLlmCellState(symbol) {
const llm = llmInterpretMap[symbol];
if (llm?.content) {
const failed = String(llm.content).startsWith("[解读失败]");
return { kind: failed ? "failed" : "done", llm };
}
if (!llmRunState.running) return { kind: "idle" };
if (symbol === llmRunState.current_symbol) return { kind: "running" };
const idx = llmSymbolOrder.indexOf(symbol);
if (idx >= 0 && idx < llmRunState.done) return { kind: "done", llm: null };
return { kind: "pending" };
}
function buildLlmCellHtml(symbol) {
const st = getLlmCellState(symbol);
if (st.kind === "done" && !st.llm) {
return `<div class="llm-placeholder done">已完成,点「刷新解读」</div>`;
}
if (st.kind === "done" && st.llm) {
const t = (st.llm.created_at || "").replace("T", " ").slice(0, 19);
const open = llmExpandedSymbols.has(symbol) ? " open" : "";
return `<details class="llm-fold"${open} data-symbol="${symbol}">
<summary class="llm-summary done">查看解读 <span class="llm-time">${t}</span></summary>
<div class="llm-text">${escapeHtml(st.llm.content)}</div>
</details>`;
}
if (st.kind === "failed" && st.llm) {
return `<details class="llm-fold" data-symbol="${symbol}">
<summary class="llm-summary failed">解读失败</summary>
<div class="llm-text llm-err">${escapeHtml(st.llm.content)}</div>
</details>`;
}
if (st.kind === "running") {
return `<div class="llm-placeholder running">解读中…</div>`;
}
if (st.kind === "pending") {
return `<div class="llm-placeholder pending">排队等待</div>`;
}
return `<span class="muted">—</span>`;
}
function bindLlmFoldHandlers() {
document.querySelectorAll("details.llm-fold").forEach((el) => {
el.ontoggle = () => {
const sym = el.dataset.symbol;
if (!sym) return;
if (el.open) llmExpandedSymbols.add(sym);
else llmExpandedSymbols.delete(sym);
};
});
}
function updateStatsLlmRows() {
const body = document.getElementById("stats-body");
if (!body) return;
document.querySelectorAll("tr.stats-row[data-symbol]").forEach((tr) => {
const sym = tr.dataset.symbol;
const td = tr.querySelector("td.llm-col");
if (!td) return;
const wasOpen = td.querySelector("details.llm-fold")?.open;
td.innerHTML = buildLlmCellHtml(sym);
const det = td.querySelector("details.llm-fold");
if (det) {
det.ontoggle = () => {
if (det.open) llmExpandedSymbols.add(sym);
else llmExpandedSymbols.delete(sym);
};
if (wasOpen || llmExpandedSymbols.has(sym)) det.open = true;
}
});
for (const sym of Object.keys(llmInterpretMap)) {
if (llmSeenDone.has(sym)) continue;
const llm = llmInterpretMap[sym];
if (llm?.content && !String(llm.content).startsWith("[解读失败]")) {
llmSeenDone.add(sym);
const det = document.querySelector(`details.llm-fold[data-symbol="${sym}"]`);
if (det) {
det.open = true;
llmExpandedSymbols.add(sym);
}
}
}
}
function escapeHtml(s) {
@@ -339,71 +434,106 @@ function formatVol(v) {
return String(Math.round(v));
}
async function loadLlmInterpretations() {
try {
const res = await fetch("/api/llm/interpretations");
const data = await res.json();
llmInterpretMap = {};
for (const item of data.items || []) {
llmInterpretMap[item.symbol] = item;
}
renderLlmList(data.items || []);
if (statsData?.ok) renderStatsTable();
} catch {
/* ignore */
function applyLlmPayload(data) {
if (data.batch_id) llmRunState.batch_id = data.batch_id;
if (data.running != null) {
llmRunState.running = !!data.running;
llmRunState.batch_id = data.batch_id || llmRunState.batch_id;
llmRunState.current_symbol = data.current_symbol || "";
llmRunState.done = data.done ?? llmRunState.done;
llmRunState.total = data.total ?? llmRunState.total;
}
const nextMap = { ...llmInterpretMap };
for (const item of data.items || []) {
if (item.symbol) nextMap[item.symbol] = item;
}
llmInterpretMap = nextMap;
if (document.getElementById("stats-body")) {
updateStatsLlmRows();
} else if (statsData?.ok) {
renderStatsTable();
}
}
function renderLlmList(items) {
const el = document.getElementById("llm-interpret-list");
if (!el) return;
if (!items.length) {
el.innerHTML = '<p class="loading">暂无解读记录</p>';
return;
async function loadLlmInterpretations() {
const q = llmRunState.batch_id
? `?batch_id=${encodeURIComponent(llmRunState.batch_id)}`
: "";
const res = await fetch(`/api/llm/interpretations${q}`);
if (!res.ok) throw new Error("加载解读失败");
const data = await res.json();
applyLlmPayload(data);
return data;
}
function updateLlmStatusText(st) {
const label = document.getElementById("llm-model-label");
const text = document.getElementById("llm-status-text");
if (label) label.textContent = st.enabled ? st.model : "未配置";
if (!text) return;
if (st.running) {
text.textContent = `解读中 ${st.done}/${st.total} · 当前 ${st.current_symbol || "—"}`;
} else {
text.textContent = st.enabled
? `就绪 · 已完成 ${Object.keys(llmInterpretMap).length} 条 · 批次 ${st.batch_id || dataBatchId(st)}`
: "请在 .env 配置 LLM_API_KEY";
}
el.innerHTML = items
.map(
(it) => `
<article class="llm-card">
<h4>${it.symbol} <small>${it.batch_id}</small></h4>
<div class="llm-text">${escapeHtml(it.content)}</div>
<time>${(it.created_at || "").replace("T", " ").slice(0, 19)}</time>
</article>`
)
.join("");
}
function dataBatchId(st) {
const keys = Object.keys(llmInterpretMap);
return keys.length ? llmInterpretMap[keys[0]]?.batch_id || "—" : "—";
}
async function refreshLlmStatus() {
try {
const res = await fetch("/api/llm/status");
const st = await res.json();
const label = document.getElementById("llm-model-label");
const text = document.getElementById("llm-status-text");
if (label) label.textContent = st.enabled ? st.model : "未配置";
if (!text) return;
if (st.running) {
text.textContent = `解读中 ${st.done}/${st.total} · 当前 ${st.current_symbol || "—"} · 批次 ${st.batch_id}`;
if (!llmPollTimer) {
llmPollTimer = setInterval(async () => {
await refreshLlmStatus();
await loadLlmInterpretations();
if (!(await fetch("/api/llm/status").then((r) => r.json())).running) {
clearInterval(llmPollTimer);
llmPollTimer = null;
}
}, 5000);
}
} else {
text.textContent = st.enabled
? `就绪 · 每币 ${st.interval_sec}s · 最近批次 ${st.batch_id || "—"}`
: "请在 .env 配置 LLM_API_KEY";
if (llmPollTimer) {
const res = await fetch("/api/llm/status");
if (!res.ok) throw new Error("状态获取失败");
const st = await res.json();
llmRunState.running = !!st.running;
llmRunState.batch_id = st.batch_id || llmRunState.batch_id;
llmRunState.current_symbol = st.current_symbol || "";
llmRunState.done = st.done ?? 0;
llmRunState.total = st.total ?? 0;
updateLlmStatusText(st);
return st;
}
function startLlmPolling() {
if (llmPollTimer) return;
llmPollTimer = setInterval(async () => {
try {
await refreshLlmStatus();
await loadLlmInterpretations();
const st = await fetch("/api/llm/status").then((r) => r.json());
if (!st.running) {
clearInterval(llmPollTimer);
llmPollTimer = null;
await loadLlmInterpretations();
}
} catch (e) {
console.warn("LLM poll:", e);
}
} catch {
/* ignore */
}, 3000);
}
function stopLlmPolling() {
if (llmPollTimer) {
clearInterval(llmPollTimer);
llmPollTimer = null;
}
}
async function refreshLlmAll() {
const text = document.getElementById("llm-status-text");
if (text) text.textContent = "刷新中…";
try {
const st = await refreshLlmStatus();
await loadLlmInterpretations();
if (st.running) startLlmPolling();
else stopLlmPolling();
} catch (e) {
if (text) text.textContent = "刷新失败";
console.error(e);
}
}
@@ -413,8 +543,18 @@ async function runLlmInterpret() {
try {
const res = await fetch("/api/llm/interpret/run", { method: "POST" });
const data = await res.json();
if (!data.ok) alert(data.message || "启动失败");
if (!data.ok) {
alert(data.message || "启动失败");
return;
}
llmInterpretMap = {};
llmSeenDone.clear();
if (data.batch_id) llmRunState.batch_id = data.batch_id;
llmRunState.running = true;
await refreshLlmStatus();
await loadLlmInterpretations();
startLlmPolling();
if (document.getElementById("stats-body")) updateStatsLlmRows();
} catch (e) {
alert(e.message);
} finally {
@@ -512,9 +652,8 @@ async function loadStats() {
try {
const res = await fetch("/api/stats/three-day");
statsData = await res.json();
await loadLlmInterpretations();
renderStatsTable();
await refreshLlmStatus();
await refreshLlmAll();
await loadWecomPreview();
} catch (e) {
document.getElementById("stats-table-wrap").innerHTML = `<p class="error">${e.message}</p>`;
@@ -551,10 +690,7 @@ function switchView(view) {
if (view === "stats") {
if (!statsData) loadStats();
else {
refreshLlmStatus();
loadLlmInterpretations();
}
else refreshLlmAll();
return;
}
@@ -588,10 +724,7 @@ document.getElementById("btn-refresh").addEventListener("click", async () => {
});
document.getElementById("btn-llm-run")?.addEventListener("click", runLlmInterpret);
document.getElementById("btn-llm-refresh")?.addEventListener("click", async () => {
await loadLlmInterpretations();
await refreshLlmStatus();
});
document.getElementById("btn-llm-refresh")?.addEventListener("click", () => refreshLlmAll());
document.getElementById("btn-reload-stats")?.addEventListener("click", () => {
statsData = null;