diff --git a/manual_trading_hub/hub_ai/context.py b/manual_trading_hub/hub_ai/context.py
index 0492d28..f429b5b 100644
--- a/manual_trading_hub/hub_ai/context.py
+++ b/manual_trading_hub/hub_ai/context.py
@@ -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:
diff --git a/manual_trading_hub/hub_ai/prompts.py b/manual_trading_hub/hub_ai/prompts.py
index 818607e..6ab2d1d 100644
--- a/manual_trading_hub/hub_ai/prompts.py
+++ b/manual_trading_hub/hub_ai/prompts.py
@@ -19,7 +19,7 @@ SUMMARY_SYSTEM = """
- **当前持仓浮盈亏(U)**:…(仅汇总已监控且有数据的账户)
**2. 分户明细**
-每个账户一行:账户名 | 状态(已监控/未监控/连接异常) | 当日平仓盈亏 | 笔数 | 当前浮盈亏 | 备注
+中控页面会自动渲染分户表格,本节不要输出 pipe 分隔行或 Markdown 表格;可写一句「见下表」或直接留空。
**3. 需关注**
仅有依据时列出(如:某户当日亏损最大、浮亏偏大、Flask/Agent 异常、有持仓但无本地监控等);若无则写「无」。
diff --git a/manual_trading_hub/hub_ai/summary.py b/manual_trading_hub/hub_ai/summary.py
index d2dd066..badf87e 100644
--- a/manual_trading_hub/hub_ai/summary.py
+++ b/manual_trading_hub/hub_ai/summary.py
@@ -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 []
diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css
index d3f3caa..9e73648 100644
--- a/manual_trading_hub/static/app.css
+++ b/manual_trading_hub/static/app.css
@@ -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;
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index afbc21a..b8565e9 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -2972,10 +2972,105 @@
return renderHubMarkdown(text);
}
- function setAiSummaryMarkdown(body, contentMd) {
+ const AI_EX_SVG = {
+ binance:
+ '',
+ okx:
+ '',
+ gate:
+ '',
+ gate_bot:
+ '',
+ other:
+ '',
+ };
+
+ 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 ``;
+ }
+
+ 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 =
+ "" +
+ " ";
+ 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 (
+ "账户 状态 平仓盈亏 笔数 浮盈亏 备注 " +
+ "