增加大模型
This commit is contained in:
+151
-9
@@ -1,5 +1,3 @@
|
||||
const REFRESH_MS = 60_000;
|
||||
|
||||
const PERIOD_API = {
|
||||
today: "/api/today/top30",
|
||||
yesterday: "/api/yesterday/top30",
|
||||
@@ -12,8 +10,13 @@ const tableState = {
|
||||
daybefore: { items: [], meta: {}, sortKey: "rank", sortDir: "asc" },
|
||||
};
|
||||
|
||||
const PERIOD_LS_PREFIX = "ba_period_";
|
||||
const PERIOD_TTL_MS = 4 * 60 * 60 * 1000;
|
||||
|
||||
let statsData = null;
|
||||
let currentView = "today";
|
||||
let llmPollTimer = null;
|
||||
let llmInterpretMap = {};
|
||||
|
||||
const SORT_KEYS = {
|
||||
rank: (r) => Number(r.rank) || 0,
|
||||
@@ -171,15 +174,49 @@ function setPeriodData(periodId, data) {
|
||||
renderPeriodTable(periodId);
|
||||
}
|
||||
|
||||
async function loadPeriod(periodId) {
|
||||
function loadPeriodFromLS(periodId) {
|
||||
try {
|
||||
const raw = localStorage.getItem(PERIOD_LS_PREFIX + periodId);
|
||||
if (!raw) return null;
|
||||
const obj = JSON.parse(raw);
|
||||
if (!obj?.data || Date.now() - (obj.ts || 0) > PERIOD_TTL_MS) return null;
|
||||
return obj.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function savePeriodToLS(periodId, data) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
PERIOD_LS_PREFIX + periodId,
|
||||
JSON.stringify({ ts: Date.now(), data })
|
||||
);
|
||||
} catch {
|
||||
/* quota */
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPeriod(periodId, force = false) {
|
||||
const tbody = ensurePeriodTable(periodId);
|
||||
if (!force) {
|
||||
const cached = loadPeriodFromLS(periodId);
|
||||
if (cached?.items?.length) {
|
||||
setPeriodData(periodId, cached);
|
||||
if (periodId === "today") {
|
||||
document.getElementById("status").textContent = "今日数据(浏览器缓存)";
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="7" class="loading">加载中…</td></tr>';
|
||||
try {
|
||||
const res = await fetch(PERIOD_API[periodId]);
|
||||
const data = await res.json();
|
||||
savePeriodToLS(periodId, data);
|
||||
setPeriodData(periodId, data);
|
||||
if (periodId === "today") {
|
||||
document.getElementById("status").textContent = "今日数据已刷新";
|
||||
document.getElementById("status").textContent = force ? "今日数据已手动刷新" : "今日数据已加载";
|
||||
}
|
||||
} catch (e) {
|
||||
if (tbody) tbody.innerHTML = `<tr><td colspan="7" class="error">${e.message}</td></tr>`;
|
||||
@@ -258,6 +295,7 @@ function renderStatsTable() {
|
||||
<th>昨日排名</th><th>昨日涨跌</th><th>昨日成交额</th>
|
||||
<th>前日排名</th><th>前日涨跌</th><th>前日成交额</th>
|
||||
<th>三日总成交额</th>
|
||||
<th>AI解读</th>
|
||||
</tr></thead>
|
||||
<tbody id="stats-body"></tbody>
|
||||
</table>`;
|
||||
@@ -271,30 +309,128 @@ 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">
|
||||
<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>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s || "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
function formatVol(v) {
|
||||
if (v >= 1e8) return (v / 1e8).toFixed(2) + "亿";
|
||||
if (v >= 1e4) return (v / 1e4).toFixed(2) + "万";
|
||||
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 renderLlmList(items) {
|
||||
const el = document.getElementById("llm-interpret-list");
|
||||
if (!el) return;
|
||||
if (!items.length) {
|
||||
el.innerHTML = '<p class="loading">暂无解读记录</p>';
|
||||
return;
|
||||
}
|
||||
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("");
|
||||
}
|
||||
|
||||
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) {
|
||||
clearInterval(llmPollTimer);
|
||||
llmPollTimer = null;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function runLlmInterpret() {
|
||||
const btn = document.getElementById("btn-llm-run");
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch("/api/llm/interpret/run", { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (!data.ok) alert(data.message || "启动失败");
|
||||
await refreshLlmStatus();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
document.getElementById("stats-table-wrap").innerHTML =
|
||||
'<p class="loading">统计中…</p>';
|
||||
try {
|
||||
const res = await fetch("/api/stats/three-day");
|
||||
statsData = await res.json();
|
||||
await loadLlmInterpretations();
|
||||
renderStatsTable();
|
||||
await refreshLlmStatus();
|
||||
} catch (e) {
|
||||
document.getElementById("stats-table-wrap").innerHTML = `<p class="error">${e.message}</p>`;
|
||||
}
|
||||
@@ -330,6 +466,10 @@ function switchView(view) {
|
||||
|
||||
if (view === "stats") {
|
||||
if (!statsData) loadStats();
|
||||
else {
|
||||
refreshLlmStatus();
|
||||
loadLlmInterpretations();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -358,10 +498,16 @@ document.querySelectorAll("[data-reset]").forEach((btn) => {
|
||||
document.getElementById("btn-refresh").addEventListener("click", async () => {
|
||||
document.getElementById("status").textContent = "刷新中…";
|
||||
await fetch("/api/refresh/today", { method: "POST" });
|
||||
await loadPeriod("today");
|
||||
await loadPeriod("today", true);
|
||||
if (currentView === "stats") await loadStats();
|
||||
});
|
||||
|
||||
document.getElementById("btn-llm-run")?.addEventListener("click", runLlmInterpret);
|
||||
document.getElementById("btn-llm-refresh")?.addEventListener("click", async () => {
|
||||
await loadLlmInterpretations();
|
||||
await refreshLlmStatus();
|
||||
});
|
||||
|
||||
document.getElementById("btn-reload-stats")?.addEventListener("click", () => {
|
||||
statsData = null;
|
||||
loadStats();
|
||||
@@ -371,7 +517,3 @@ document.getElementById("btn-export-stats")?.addEventListener("click", exportSta
|
||||
loadPeriod("today");
|
||||
loadPeriod("yesterday");
|
||||
loadPeriod("daybefore");
|
||||
|
||||
setInterval(() => {
|
||||
if (currentView === "today") loadPeriod("today");
|
||||
}, REFRESH_MS);
|
||||
|
||||
Reference in New Issue
Block a user