feat(hub): render AI summary account breakdown as icon table

Replace pipe-separated account lines with a structured table from stats_snapshot, including exchange icons and position remarks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-07 00:28:24 +08:00
parent 8417784dd8
commit a5f5239be9
6 changed files with 195 additions and 7 deletions
+99 -4
View File
@@ -2972,10 +2972,105 @@
return renderHubMarkdown(text);
}
function setAiSummaryMarkdown(body, contentMd) {
const AI_EX_SVG = {
binance:
'<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><rect width="24" height="24" rx="5" fill="#F0B90B"/><path fill="#1E2329" d="M12 4.5l2.2 2.2L12 8.9 9.8 6.7 12 4.5zm0 14.6l2.2-2.2L12 15.1l-2.2 2.2 2.2 2.2zM4.5 12l2.2-2.2L8.9 12l-2.2 2.2L4.5 12zm14.6 0l2.2-2.2L15.1 12l2.2-2.2 2.2 2.2z"/></svg>',
okx:
'<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><rect width="24" height="24" rx="5" fill="#000"/><rect x="5" y="5" width="6" height="6" fill="#fff"/><rect x="13" y="5" width="6" height="6" fill="#fff"/><rect x="5" y="13" width="6" height="6" fill="#fff"/><rect x="13" y="13" width="6" height="6" fill="#fff"/></svg>',
gate:
'<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><rect width="24" height="24" rx="5" fill="#2354E6"/><path fill="#fff" d="M7 17V7h4.2c2.2 0 3.6 1.1 3.6 2.9 0 1.2-.6 2.1-1.6 2.5 1.2.4 2 1.4 2 2.8 0 2-1.5 3.1-3.8 3.1H7zm2.5-2.1h1.5c1.1 0 1.7-.5 1.7-1.3s-.6-1.3-1.7-1.3H9.5v2.6zm0-4.4h1.4c1 0 1.5-.4 1.5-1.2s-.5-1.2-1.5-1.2H9.5v2.4z"/></svg>',
gate_bot:
'<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><rect width="24" height="24" rx="5" fill="#17E6A1"/><path fill="#102820" d="M7 17V7h4.2c2.2 0 3.6 1.1 3.6 2.9 0 1.2-.6 2.1-1.6 2.5 1.2.4 2 1.4 2 2.8 0 2-1.5 3.1-3.8 3.1H7zm2.5-2.1h1.5c1.1 0 1.7-.5 1.7-1.3s-.6-1.3-1.7-1.3H9.5v2.6zm0-4.4h1.4c1 0 1.5-.4 1.5-1.2s-.5-1.2-1.5-1.2H9.5v2.4z"/></svg>',
other:
'<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><rect width="24" height="24" rx="5" fill="var(--border-soft)"/><circle cx="12" cy="12" r="3" fill="var(--muted)"/></svg>',
};
function resolveExchangeIconId(key, name) {
const k = String(key || "").toLowerCase();
const n = String(name || "").toLowerCase();
if (k === "gate_bot" || k.includes("gate_bot") || n.includes("趋势") || n.includes("gate_bot"))
return "gate_bot";
if (k === "binance" || k.includes("binance") || n.includes("币安")) return "binance";
if (k === "okx" || k.includes("okx")) return "okx";
if (k === "gate" || k.includes("gate") || n.includes("gate")) return "gate";
return "other";
}
function hubExchangeIconHtml(key, name) {
const id = resolveExchangeIconId(key, name);
const title =
id === "binance"
? "Binance"
: id === "okx"
? "OKX"
: id === "gate_bot"
? "Gate Bot"
: id === "gate"
? "Gate"
: "";
return `<span class="ai-ex-icon ai-ex-${id}" title="${esc(title)}">${AI_EX_SVG[id] || AI_EX_SVG.other}</span>`;
}
function aiAccountStatusClass(status) {
const s = String(status || "");
if (s === "未监控") return "ai-ac-unmon";
if (s.includes("异常")) return "ai-ac-err";
if (s.includes("需关注")) return "ai-ac-warn";
return "";
}
function renderAiAccountTable(snapshot) {
const accounts = snapshot && snapshot.by_account;
if (!accounts || typeof accounts !== "object") return "";
const rows = Object.values(accounts);
if (!rows.length) return "";
const head =
"<thead><tr>" +
"<th>账户</th><th>状态</th><th>平仓盈亏</th><th>笔数</th><th>浮盈亏</th><th>备注</th>" +
"</tr></thead>";
const body = rows
.map((ac) => {
const closedPnl = Number(ac.pnl_u);
const floatPnl = Number(ac.float_pnl_u);
const remark =
ac.remark ||
(Array.isArray(ac.issues) && ac.issues.length ? ac.issues.join("") : "无");
const statusCls = aiAccountStatusClass(ac.status);
return (
"<tr>" +
`<td class="ai-ac-name">${hubExchangeIconHtml(ac.key, ac.name)}<span>${esc(ac.name || "—")}</span></td>` +
`<td class="${statusCls}">${esc(ac.status || "—")}</td>` +
`<td class="${aiPnlClass(closedPnl)}">${aiPnlSigned(closedPnl, 2)}</td>` +
`<td>${Number(ac.closed_count) || 0}</td>` +
`<td class="${aiPnlClass(floatPnl)}">${aiPnlSigned(floatPnl, 2)}</td>` +
`<td class="ai-ac-remark">${esc(remark)}</td>` +
"</tr>"
);
})
.join("");
return `<div class="ai-ac-table-wrap"><table class="ai-ac-table">${head}<tbody>${body}</tbody></table></div>`;
}
function renderAiSummaryBody(contentMd, snapshot) {
const md = String(contentMd || "");
const sec2 = /\*\*2\.\s*分户明细\*\*/;
const sec3 = /\*\*3\.\s*需关注\*\*/;
const i2 = md.search(sec2);
const i3 = md.search(sec3);
const tableHtml = renderAiAccountTable(snapshot);
if (i2 >= 0 && i3 > i2 && tableHtml) {
const headEnd = i2 + md.slice(i2).match(sec2)[0].length;
const part1 = md.slice(0, headEnd);
const part2 = md.slice(i3);
return renderHubMarkdown(part1) + tableHtml + renderHubMarkdown(part2);
}
return renderHubMarkdown(md) + (tableHtml ? tableHtml : "");
}
function setAiSummaryMarkdown(body, contentMd, snapshot) {
if (!body) return;
body.classList.add("ai-result-md");
body.innerHTML = renderHubMarkdown(contentMd);
body.innerHTML = renderAiSummaryBody(contentMd, snapshot);
}
function setAiSummaryPlaceholder(body, html) {
@@ -3075,7 +3170,7 @@
const j = await r.json();
const latest = j.latest;
if (latest && latest.content_md) {
if (body) setAiSummaryMarkdown(body, latest.content_md);
if (body) setAiSummaryMarkdown(body, latest.content_md, latest.stats_snapshot);
renderAiSummaryStats(latest.stats_snapshot);
const sm = document.getElementById("ai-summary-meta");
if (sm && latest.generated_at) {
@@ -3117,7 +3212,7 @@
if (!j.ok && j.detail) throw new Error(j.detail);
const sum = j.summary;
if (sum && sum.content_md && body) {
setAiSummaryMarkdown(body, sum.content_md);
setAiSummaryMarkdown(body, sum.content_md, sum.stats_snapshot);
renderAiSummaryStats(sum.stats_snapshot);
}
showToast(j.cached ? "已是最新上下文,返回缓存总结" : "今日总结已生成");