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:
@@ -271,6 +271,28 @@ def format_context_text(payload: dict) -> str:
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def format_account_remark(ac: dict) -> str:
|
||||
"""分户表格备注:持仓摘要或关注点。"""
|
||||
positions = ac.get("positions") or []
|
||||
if not positions:
|
||||
issues = ac.get("issues") or []
|
||||
if issues:
|
||||
return ";".join(str(x) for x in issues[:2])
|
||||
return "无"
|
||||
parts: list[str] = []
|
||||
for p in positions[:3]:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
sym = p.get("symbol") or "?"
|
||||
side = p.get("side") or "?"
|
||||
contracts = p.get("contracts") if p.get("contracts") is not None else p.get("size")
|
||||
upnl = _position_float_pnl(p)
|
||||
parts.append(f"持仓: {sym} {side} 张数{contracts} 浮盈亏{upnl:.4f}U")
|
||||
if len(positions) > 3:
|
||||
parts.append(f"另有{len(positions) - 3}仓")
|
||||
return ";".join(parts) if parts else "无"
|
||||
|
||||
|
||||
def format_chat_context_brief(payload: dict, max_chars: int = 2500) -> str:
|
||||
text = format_context_text(payload)
|
||||
if len(text) <= max_chars:
|
||||
|
||||
@@ -19,7 +19,7 @@ SUMMARY_SYSTEM = """
|
||||
- **当前持仓浮盈亏(U)**:…(仅汇总已监控且有数据的账户)
|
||||
|
||||
**2. 分户明细**
|
||||
每个账户一行:账户名 | 状态(已监控/未监控/连接异常) | 当日平仓盈亏 | 笔数 | 当前浮盈亏 | 备注
|
||||
中控页面会自动渲染分户表格,本节不要输出 pipe 分隔行或 Markdown 表格;可写一句「见下表」或直接留空。
|
||||
|
||||
**3. 需关注**
|
||||
仅有依据时列出(如:某户当日亏损最大、浮亏偏大、Flask/Agent 异常、有持仓但无本地监控等);若无则写「无」。
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from hub_ai.client import generate_text, model_label
|
||||
from hub_ai.context import build_daily_context
|
||||
from hub_ai.context import build_daily_context, format_account_remark
|
||||
from hub_ai.prompts import SUMMARY_SYSTEM, build_summary_user_prompt
|
||||
from hub_ai.store import append_summary, get_latest_summary, list_summaries
|
||||
|
||||
@@ -38,11 +38,13 @@ def generate_daily_summary(
|
||||
"totals": ctx.get("totals"),
|
||||
"by_account": {
|
||||
str(ac.get("key") or ac.get("id")): {
|
||||
"key": ac.get("key"),
|
||||
"name": ac.get("name"),
|
||||
"status": ac.get("status"),
|
||||
"pnl_u": (ac.get("trade_stats") or {}).get("total_pnl_u"),
|
||||
"closed_count": (ac.get("trade_stats") or {}).get("closed_count"),
|
||||
"float_pnl_u": ac.get("float_pnl_u"),
|
||||
"remark": format_account_remark(ac),
|
||||
"issues": ac.get("issues") or [],
|
||||
}
|
||||
for ac in ctx.get("accounts") or []
|
||||
|
||||
@@ -3575,6 +3575,75 @@ body.hub-page-ai #page-ai {
|
||||
.ai-bubble-assistant.ai-result-md ol {
|
||||
margin: 4px 0 6px 1.15em;
|
||||
}
|
||||
.ai-ac-table-wrap {
|
||||
margin: 8px 0 12px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--inset-surface) 88%, transparent);
|
||||
}
|
||||
.ai-ac-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.ai-ac-table th,
|
||||
.ai-ac-table td {
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
.ai-ac-table th {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
background: color-mix(in srgb, var(--inset-surface) 60%, transparent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-ac-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.ai-ac-table tbody tr:hover td {
|
||||
background: color-mix(in srgb, var(--accent-dim) 35%, transparent);
|
||||
}
|
||||
.ai-ac-name {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.ai-ac-name > span:last-child {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.ai-ex-icon {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
line-height: 0;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ai-ac-remark {
|
||||
color: var(--muted);
|
||||
font-size: 0.74rem;
|
||||
max-width: 16rem;
|
||||
}
|
||||
.ai-ac-unmon {
|
||||
color: var(--muted);
|
||||
}
|
||||
.ai-ac-err {
|
||||
color: var(--red);
|
||||
}
|
||||
.ai-ac-warn {
|
||||
color: var(--amber, #d4a017);
|
||||
}
|
||||
.ai-ac-table .pos,
|
||||
.ai-ac-table .neg {
|
||||
font-weight: 600;
|
||||
}
|
||||
.ai-placeholder {
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
|
||||
@@ -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 ? "已是最新上下文,返回缓存总结" : "今日总结已生成");
|
||||
|
||||
@@ -288,6 +288,6 @@
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script src="/assets/chart.js?v=20260604-upnl-contracts"></script>
|
||||
<script src="/assets/ai_review_render.js?v=2"></script>
|
||||
<script src="/assets/app.js?v=20260606-hub-ai-md"></script>
|
||||
<script src="/assets/app.js?v=20260606-hub-ai-table"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user