feat(hub): enrich AI coach with fund history, closed trades, and chat uploads

- Add 15-day fund snapshot store and /api/hub/account on all instances

- Summary includes yesterday/today trades, fund columns, and section 5 操作建议

- Chat context distinguishes empty positions from local monitors

- Support image/document attachments in AI chat

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-07 08:54:20 +08:00
parent 51c59b073b
commit 62e48dab92
19 changed files with 947 additions and 106 deletions
+34 -7
View File
@@ -1,11 +1,12 @@
"""中控 AI:单会话聊天(直到用户点击新开)。"""
from __future__ import annotations
from typing import Any
from typing import Any, Optional
from hub_ai.attachments import parse_chat_attachments
from hub_ai.client import generate_text, model_label
from hub_ai.config import CHAT_MAX_HISTORY_TURNS, CHAT_TEMPERATURE
from hub_ai.context import build_daily_context, format_chat_context_brief
from hub_ai.context import build_daily_context, format_chat_context_for_chat
from hub_ai.prompts import CHAT_SYSTEM, build_chat_user_prompt
from hub_ai.store import (
append_chat_message,
@@ -23,7 +24,12 @@ def _history_lines(messages: list[dict], max_turns: int = CHAT_MAX_HISTORY_TURNS
lines = []
for m in rows:
role = "用户" if m.get("role") == "user" else "搭档"
lines.append(f"{role}{m.get('content') or ''}")
content = m.get("content") or ""
att = m.get("attachments") or []
if att:
names = "".join(str(a.get("name") or "附件") for a in att[:3])
content = f"{content} [附件: {names}]".strip()
lines.append(f"{role}{content}")
return "\n".join(lines)
@@ -47,20 +53,35 @@ def send_chat_message(
message: str,
*,
trading_day: str | None = None,
raw_attachments: Optional[list[dict]] = None,
) -> dict[str, Any]:
text = (message or "").strip()
if not text:
parsed = parse_chat_attachments(raw_attachments or [])
if parsed.get("errors") and not text and not parsed.get("images_b64"):
return {"ok": False, "msg": "".join(parsed["errors"])}
if not text and not parsed.get("images_b64") and not parsed.get("text_append"):
return {"ok": False, "msg": "消息不能为空"}
user_visible = text
if parsed.get("text_append"):
user_visible = (user_visible + "\n\n" + parsed["text_append"]).strip()
if not user_visible and parsed.get("attachment_note"):
user_visible = f"(上传了 {parsed['attachment_note']}"
ctx = build_daily_context(exchanges, trading_day=trading_day)
day = ctx["trading_day"]
session = ensure_active_session(trading_day=day)
sid = session["id"]
history = _history_lines(session.get("messages") or [])
append_chat_message(sid, "user", text)
append_chat_message(
sid,
"user",
user_visible,
attachments=parsed.get("attachment_meta") or [],
)
brief_ctx = format_chat_context_brief(ctx)
brief_ctx = format_chat_context_for_chat(ctx)
excerpt = summary_excerpt_for_chat(day)
user_prompt = build_chat_user_prompt(
@@ -68,12 +89,17 @@ def send_chat_message(
trading_day=day,
summary_excerpt=excerpt,
history_lines=history,
user_message=text,
user_message=text or user_visible,
attachment_note=str(parsed.get("attachment_note") or ""),
)
if parsed.get("text_append"):
user_prompt += "\n\n【附件正文】\n" + parsed["text_append"]
reply = generate_text(
system=CHAT_SYSTEM,
user=user_prompt,
temperature=CHAT_TEMPERATURE,
images_b64=parsed.get("images_b64") or None,
)
if reply.startswith("AI 调用失败"):
return {"ok": False, "msg": reply, "session_id": sid}
@@ -85,4 +111,5 @@ def send_chat_message(
"session": session,
"reply": reply,
"model": model_label(),
"attachment_warnings": parsed.get("errors") or [],
}