diff --git a/manual_trading_hub/hub_ai/archive_quote.py b/manual_trading_hub/hub_ai/archive_quote.py index 0df8297..69131c5 100644 --- a/manual_trading_hub/hub_ai/archive_quote.py +++ b/manual_trading_hub/hub_ai/archive_quote.py @@ -3,11 +3,24 @@ from __future__ import annotations from typing import Any -from hub_ai.chat import _is_ai_error_reply from hub_ai.client import generate_text, model_label -from hub_ai.config import CHAT_MAX_CONTINUATIONS, CHAT_MAX_OUTPUT_TOKENS, CHAT_TEMPERATURE +from hub_ai.rolling_summary import refresh_session_rolling_summary +from hub_ai.text_util import clip_text, is_ai_error_reply +from hub_ai.config import ( + CHAT_MAX_CONTINUATIONS, + CHAT_MAX_OUTPUT_TOKENS, + CHAT_TEMPERATURE, + CHAT_USER_MESSAGE_MAX_CHARS, +) from hub_ai.prompts import CHAT_SYSTEM, build_archive_quote_review_prompt -from hub_ai.store import CHAT_BOT_TRADING, append_chat_message, create_new_session, list_chat_sessions +from hub_ai.store import ( + CHAT_BOT_TRADING, + append_chat_message, + create_new_session, + delete_chat_session, + get_active_session, + list_chat_sessions, +) from hub_symbol_archive_lib import list_daily_trades @@ -43,7 +56,10 @@ def format_archive_trades_for_ai(payload: dict[str, Any]) -> str: if not trades: lines.append("(该日无交易记录)") return "\n".join(lines) - for i, t in enumerate(trades, 1): + max_rows = 50 + if len(trades) > max_rows: + lines.append(f"(共 {len(trades)} 笔,以下展示最近 {max_rows} 笔)") + for i, t in enumerate(trades[:max_rows], 1): ex = str(t.get("exchange_key") or t.get("account_exchange_key") or "—") sym = str(t.get("symbol") or "—") direction = str(t.get("direction") or "—") @@ -54,7 +70,7 @@ def format_archive_trades_for_ai(payload: dict[str, Any]) -> str: pnl = _fmt_pnl(t.get("pnl_amount")) entry = str(t.get("entry_type") or t.get("entry_reason") or t.get("monitor_type") or "—") tag = _tag_label(str(t.get("behavior_tag") or "")) - note = str(t.get("note") or "").strip() + note = clip_text(str(t.get("note") or "").strip(), 80) line = ( f"{i}. {ex} | {sym} | {direction} | 开仓类型 {entry} | " f"开 {opened} | 平 {closed} | 持仓 {hold} | 结果 {result} | " @@ -87,13 +103,12 @@ def send_archive_quote_review( archive_payload = list_daily_trades(trading_day=day, period="today") archive_trades_text = format_archive_trades_for_ai(archive_payload) - - append_chat_message(sid, "user", text) + user_for_prompt = clip_text(text, CHAT_USER_MESSAGE_MAX_CHARS) user_prompt = build_archive_quote_review_prompt( quote_date=day, archive_trades_text=archive_trades_text, - user_message=text, + user_message=user_for_prompt, ) reply = generate_text( system=CHAT_SYSTEM, @@ -102,10 +117,20 @@ def send_archive_quote_review( max_tokens=CHAT_MAX_OUTPUT_TOKENS, max_continuations=CHAT_MAX_CONTINUATIONS, ) - if _is_ai_error_reply(reply): - return {"ok": False, "msg": reply, "session_id": sid} + if is_ai_error_reply(reply): + delete_chat_session(sid) + return {"ok": False, "msg": reply} + append_chat_message(sid, "user", text) session = append_chat_message(sid, "assistant", reply) + refresh_session_rolling_summary( + sid, + prior_summary="", + user_text=text, + assistant_text=reply, + bot_mode=CHAT_BOT_TRADING, + ) + session = get_active_session() or session return { "ok": True, "trading_day": day, diff --git a/manual_trading_hub/hub_ai/chat.py b/manual_trading_hub/hub_ai/chat.py index e37ea56..f937249 100644 --- a/manual_trading_hub/hub_ai/chat.py +++ b/manual_trading_hub/hub_ai/chat.py @@ -7,22 +7,30 @@ from hub_ai.attachments import parse_chat_attachments from hub_ai.client import generate_text, model_label from hub_ai.config import ( CHAT_CONTEXT_MAX_CHARS, + CHAT_FOLLOWUP_CONTEXT_MAX_CHARS, CHAT_HISTORY_MAX_CHARS_PER_MSG, CHAT_MAX_CONTINUATIONS, CHAT_MAX_HISTORY_TURNS, CHAT_MAX_OUTPUT_TOKENS, + CHAT_PROMPT_MAX_CHARS, CHAT_SUMMARY_EXCERPT_MAX_CHARS, CHAT_TEMPERATURE, + CHAT_USER_MESSAGE_MAX_CHARS, trading_day_reset_hour, ) from hub_trades_lib import current_trading_day -from hub_ai.context import build_daily_context, format_chat_context_for_chat +from hub_ai.context import ( + build_daily_context, + format_chat_context_for_chat, + format_chat_position_overview, +) from hub_ai.prompts import ( CHAT_GENERAL_SYSTEM, CHAT_SYSTEM, build_chat_user_prompt, build_general_chat_user_prompt, ) +from hub_ai.rolling_summary import refresh_session_rolling_summary from hub_ai.store import ( CHAT_BOT_GENERAL, CHAT_BOT_TRADING, @@ -36,18 +44,23 @@ from hub_ai.store import ( set_active_session, summary_excerpt_for_chat, ) +from hub_ai.text_util import clip_text, is_ai_error_reply def _is_ai_error_reply(text: str) -> bool: - t = (text or "").strip() - return t.startswith("AI 调用失败") or t.startswith("AI 生成失败") + return is_ai_error_reply(text) + + +def _clip_text(text: str, max_chars: int) -> str: + return clip_text(text, max_chars) def _history_lines( messages: list[dict], max_turns: int = CHAT_MAX_HISTORY_TURNS, *, - max_chars_per_msg: int = 1500, + max_chars_per_msg: int = CHAT_HISTORY_MAX_CHARS_PER_MSG, + total_max_chars: int | None = None, ) -> str: rows = [m for m in (messages or []) if m.get("role") in ("user", "assistant")] rows = rows[-max_turns * 2 :] @@ -61,17 +74,60 @@ def _history_lines( if att: names = "、".join(str(a.get("name") or "附件") for a in att[:3]) content = f"{content} [附件: {names}]".strip() - if len(content) > max_chars_per_msg: - content = content[: max_chars_per_msg - 1].rstrip() + "…" - lines.append(f"{role}:{content}") + content = _clip_text(content, max_chars_per_msg) + if content: + lines.append(f"{role}:{content}") + if total_max_chars and total_max_chars > 0: + while lines and len("\n".join(lines)) > total_max_chars: + lines.pop(0) return "\n".join(lines) +def _trading_context_bundle(ctx: dict[str, Any], *, prior_count: int) -> tuple[str, str]: + day = str(ctx.get("trading_day") or (ctx.get("totals") or {}).get("trading_day") or "") + if prior_count <= 0: + brief = format_chat_context_for_chat(ctx, max_chars=CHAT_CONTEXT_MAX_CHARS) + excerpt = summary_excerpt_for_chat(day, max_chars=CHAT_SUMMARY_EXCERPT_MAX_CHARS) + return brief, excerpt + totals = ctx.get("totals") or {} + overview = format_chat_position_overview(ctx) + slim = ( + f"【续聊快照 {day}】平仓盈亏 {totals.get('total_pnl_u')}U | " + f"笔数 {totals.get('closed_count')} | " + f"持仓 {totals.get('open_position_count', 0)} 仓 | " + f"浮盈亏 {totals.get('float_pnl_u')}U" + ) + brief = _clip_text(overview + "\n" + slim, CHAT_FOLLOWUP_CONTEXT_MAX_CHARS) + return brief, "" + + +def _history_budget(*sizes: int) -> int: + used = sum(int(s or 0) for s in sizes) + 2200 + return max(1200, CHAT_PROMPT_MAX_CHARS - used) + + +def _prompt_memory(session: dict, prior_msgs: list[dict]) -> tuple[str, str]: + """续聊优先用滚动摘要;旧会话无摘要时仅带最近 1 轮兜底。""" + rolling = str(session.get("rolling_summary") or "").strip() + if rolling: + return rolling, "" + prior_count = len([m for m in prior_msgs if m.get("role") in ("user", "assistant")]) + if prior_count <= 0: + return "", "" + tail = _history_lines( + prior_msgs, + max_turns=1, + max_chars_per_msg=CHAT_HISTORY_MAX_CHARS_PER_MSG, + ) + return "", tail + + def get_chat_state() -> dict[str, Any]: store = load_chat_store() session = get_active_session() if session: session.setdefault("bot_mode", CHAT_BOT_TRADING) + session.setdefault("rolling_summary", "") return { "active_session_id": store.get("active_session_id"), "session": session, @@ -139,43 +195,38 @@ def send_chat_message( ) session = ensure_active_session(trading_day=day) sid = session["id"] - history = _history_lines( - session.get("messages") or [], - max_chars_per_msg=CHAT_HISTORY_MAX_CHARS_PER_MSG, - ) - - append_chat_message( - sid, - "user", - user_visible, - attachments=parsed.get("attachment_meta") or [], - ) + prior_rolling = str(session.get("rolling_summary") or "") + prior_msgs = session.get("messages") or [] + prior_count = len([m for m in prior_msgs if m.get("role") in ("user", "assistant")]) + user_for_prompt = _clip_text(text or user_visible, CHAT_USER_MESSAGE_MAX_CHARS) + rolling_summary, history_tail = _prompt_memory(session, prior_msgs) bot_mode = (session.get("bot_mode") or CHAT_BOT_TRADING).strip().lower() if bot_mode == CHAT_BOT_GENERAL: user_prompt = build_general_chat_user_prompt( - history_lines=history, - user_message=text or user_visible, + rolling_summary=rolling_summary, + history_lines=history_tail, + user_message=user_for_prompt, attachment_note=str(parsed.get("attachment_note") or ""), ) if parsed.get("text_append"): - user_prompt += "\n\n【附件正文】\n" + parsed["text_append"] + user_prompt += "\n\n【附件正文】\n" + _clip_text(parsed["text_append"], 3000) system_prompt = CHAT_GENERAL_SYSTEM else: ctx = build_daily_context(exchanges, trading_day=day) day = ctx["trading_day"] - brief_ctx = format_chat_context_for_chat(ctx, max_chars=CHAT_CONTEXT_MAX_CHARS) - excerpt = summary_excerpt_for_chat(day, max_chars=CHAT_SUMMARY_EXCERPT_MAX_CHARS) + brief_ctx, excerpt = _trading_context_bundle(ctx, prior_count=prior_count) user_prompt = build_chat_user_prompt( context_text=brief_ctx, trading_day=day, summary_excerpt=excerpt, - history_lines=history, - user_message=text or user_visible, + rolling_summary=rolling_summary, + history_lines=history_tail, + user_message=user_for_prompt, attachment_note=str(parsed.get("attachment_note") or ""), ) if parsed.get("text_append"): - user_prompt += "\n\n【附件正文】\n" + parsed["text_append"] + user_prompt += "\n\n【附件正文】\n" + _clip_text(parsed["text_append"], 3000) system_prompt = CHAT_SYSTEM reply = generate_text( @@ -189,7 +240,21 @@ def send_chat_message( if _is_ai_error_reply(reply): return {"ok": False, "msg": reply, "session_id": sid} + append_chat_message( + sid, + "user", + user_visible, + attachments=parsed.get("attachment_meta") or [], + ) session = append_chat_message(sid, "assistant", reply) + refresh_session_rolling_summary( + sid, + prior_summary=prior_rolling, + user_text=user_visible, + assistant_text=reply, + bot_mode=bot_mode, + ) + session = get_active_session() or session return { "ok": True, "trading_day": day, diff --git a/manual_trading_hub/hub_ai/config.py b/manual_trading_hub/hub_ai/config.py index a1a84e3..080cc3f 100644 --- a/manual_trading_hub/hub_ai/config.py +++ b/manual_trading_hub/hub_ai/config.py @@ -15,12 +15,18 @@ def _int_env(key: str, default: int) -> int: SUMMARY_TEMPERATURE = 0.15 CHAT_TEMPERATURE = 0.5 -CHAT_MAX_HISTORY_TURNS = 40 +CHAT_MAX_HISTORY_TURNS = _int_env("CHAT_MAX_HISTORY_TURNS", 16) CHAT_MAX_OUTPUT_TOKENS = _int_env("CHAT_MAX_OUTPUT_TOKENS", 8192) CHAT_MAX_CONTINUATIONS = _int_env("CHAT_MAX_CONTINUATIONS", 4) -CHAT_CONTEXT_MAX_CHARS = _int_env("CHAT_CONTEXT_MAX_CHARS", 20_000) -CHAT_SUMMARY_EXCERPT_MAX_CHARS = _int_env("CHAT_SUMMARY_EXCERPT_MAX_CHARS", 2000) -CHAT_HISTORY_MAX_CHARS_PER_MSG = _int_env("CHAT_HISTORY_MAX_CHARS_PER_MSG", 1500) +CHAT_CONTEXT_MAX_CHARS = _int_env("CHAT_CONTEXT_MAX_CHARS", 12_000) +CHAT_FOLLOWUP_CONTEXT_MAX_CHARS = _int_env("CHAT_FOLLOWUP_CONTEXT_MAX_CHARS", 4500) +CHAT_PROMPT_MAX_CHARS = _int_env("CHAT_PROMPT_MAX_CHARS", 28_000) +CHAT_USER_MESSAGE_MAX_CHARS = _int_env("CHAT_USER_MESSAGE_MAX_CHARS", 3500) +CHAT_SUMMARY_EXCERPT_MAX_CHARS = _int_env("CHAT_SUMMARY_EXCERPT_MAX_CHARS", 1200) +CHAT_HISTORY_MAX_CHARS_PER_MSG = _int_env("CHAT_HISTORY_MAX_CHARS_PER_MSG", 900) +CHAT_ROLLING_SUMMARY_MAX_CHARS = _int_env("CHAT_ROLLING_SUMMARY_MAX_CHARS", 900) +CHAT_ROLLING_SUMMARY_GEN_MAX_TOKENS = _int_env("CHAT_ROLLING_SUMMARY_GEN_MAX_TOKENS", 512) +CHAT_ROLLING_SUMMARY_TEMPERATURE = 0.2 SUMMARY_RETENTION_DAYS = 90 CHAT_SESSION_RETENTION_DAYS = 60 FUND_HISTORY_DAYS = 180 diff --git a/manual_trading_hub/hub_ai/prompts.py b/manual_trading_hub/hub_ai/prompts.py index 587e907..c2912b0 100644 --- a/manual_trading_hub/hub_ai/prompts.py +++ b/manual_trading_hub/hub_ai/prompts.py @@ -49,8 +49,8 @@ CHAT_SYSTEM = """ - 用户口述与快照冲突时,以快照为准并口语说明「我这边看到是空仓/有N仓」。 - 若附带「今日总结摘要」,那是较早生成的缓存,**实盘持仓以【当前多账户快照】里的「实盘持仓总览」为准**,摘要里若提到持仓可能已过时。 - 若用户上传图片,可结合图中可见信息讨论,看不清的明确说看不清。 -- **优先接住【用户现在说】和【此前对话】**:用户聊心态、悔单、某笔操作时,先顺着这个话题回应,不要每句都复述账户资金数字。 -- **接续对话**:有【此前对话】时须接着聊,不要重复开场白;整段回复必须写完,以句号/问号/感叹号收尾,不得停在半句话;编号列表每条单独一行。 +- **优先接住【用户现在说】和【对话核心摘要】**:用户聊心态、悔单、某笔操作时,先顺着这个话题回应,不要每句都复述账户资金数字。 +- **接续对话**:有【对话核心摘要】时须接着聊,不要重复开场白;整段回复必须写完,以句号/问号/感叹号收尾,不得停在半句话;编号列表每条单独一行。 - 快照里的盈亏/资金仅在需要核对事实时引用;用户口述与快照冲突时,以快照为准并口语说明。 """.strip() @@ -73,19 +73,60 @@ CHAT_GENERAL_SYSTEM = """ - 用户未主动聊交易时,不要主动扯合约、仓位、盈亏、盯盘。 - 你没有接入用户的交易账户数据;不要编造持仓、资金或监控状态。若被问到交易事实,说明这边看不到实盘,建议去中控监控区或实例页查看。 - 若用户上传图片或文档,结合可见内容回应;看不清的直说。 -- 接续【此前对话】,不要重复开场白;回复须写完整,以句号/问号/感叹号收尾。 +- 接续【对话核心摘要】,不要重复开场白;回复须写完整,以句号/问号/感叹号收尾。 """.strip() +ROLLING_SUMMARY_TRADING_SYSTEM = """ +你是交易教练的对话记录员。把「此前摘要」与「本轮用户+教练回复」压成一条极短中文摘要。 + +要求: +- 120~280 字,纯文本一段,不要标题、不要列表、不要寒暄。 +- 只保留:用户情绪/困扰、涉及的交易事实、教练核心建议、已达成的共识、待跟进事项。 +- 禁止编造未出现的信息;数字与账户名须来自原文。 +""".strip() + + +ROLLING_SUMMARY_GENERAL_SYSTEM = """ +你是对话记录员。把「此前摘要」与「本轮用户+助手回复」压成一条极短中文摘要。 + +要求: +- 100~240 字,纯文本一段,不要标题、不要列表。 +- 只保留:话题、用户诉求、助手给出的关键信息、待跟进事项。 +""".strip() + + +def build_rolling_summary_user_prompt( + *, + prior_summary: str, + user_text: str, + assistant_text: str, +) -> str: + parts: list[str] = [] + if prior_summary.strip(): + parts.extend(["【此前摘要】", prior_summary.strip()]) + parts.extend([ + "【本轮用户】", + user_text.strip() or "(空)", + "【本轮教练/助手】", + assistant_text.strip() or "(空)", + "请输出更新后的对话核心摘要:", + ]) + return "\n\n".join(parts) + + def build_general_chat_user_prompt( *, - history_lines: str, + rolling_summary: str = "", + history_lines: str = "", user_message: str, attachment_note: str = "", ) -> str: parts: list[str] = [] - if history_lines.strip(): - parts.extend(["【此前对话(须接续,勿重复开场)】", history_lines.strip()]) + if rolling_summary.strip(): + parts.extend(["【对话核心摘要(须接续,勿重复开场)】", rolling_summary.strip()]) + elif history_lines.strip(): + parts.extend(["【最近对话】", history_lines.strip()]) if attachment_note.strip(): parts.extend(["【用户附件说明】", attachment_note.strip()]) parts.extend(["【用户现在说(优先回应这一条)】", user_message.strip()]) @@ -97,13 +138,16 @@ def build_chat_user_prompt( context_text: str, trading_day: str, summary_excerpt: str, - history_lines: str, + rolling_summary: str = "", + history_lines: str = "", user_message: str, attachment_note: str = "", ) -> str: parts = [f"【交易日】{trading_day}"] - if history_lines.strip(): - parts.extend(["【此前对话(须接续,勿重复开场)】", history_lines.strip()]) + if rolling_summary.strip(): + parts.extend(["【对话核心摘要(须接续,勿重复开场)】", rolling_summary.strip()]) + elif history_lines.strip(): + parts.extend(["【最近对话】", history_lines.strip()]) parts.extend([ "【当前多账户快照(事实参考;持仓以「实盘持仓总览」为准)】", context_text.strip() or "(无监控数据)", diff --git a/manual_trading_hub/hub_ai/rolling_summary.py b/manual_trading_hub/hub_ai/rolling_summary.py new file mode 100644 index 0000000..24295c3 --- /dev/null +++ b/manual_trading_hub/hub_ai/rolling_summary.py @@ -0,0 +1,69 @@ +"""聊天滚动摘要:每轮后压缩历史,续聊只带摘要 + 当前消息。""" +from __future__ import annotations + +from hub_ai.text_util import clip_text, is_ai_error_reply +from hub_ai.client import generate_text +from hub_ai.config import ( + CHAT_ROLLING_SUMMARY_GEN_MAX_TOKENS, + CHAT_ROLLING_SUMMARY_MAX_CHARS, + CHAT_ROLLING_SUMMARY_TEMPERATURE, +) +from hub_ai.prompts import ( + ROLLING_SUMMARY_GENERAL_SYSTEM, + ROLLING_SUMMARY_TRADING_SYSTEM, + build_rolling_summary_user_prompt, +) +from hub_ai.store import CHAT_BOT_GENERAL, update_session_rolling_summary + + +def refresh_session_rolling_summary( + session_id: str, + *, + prior_summary: str, + user_text: str, + assistant_text: str, + bot_mode: str, +) -> str: + """合并旧摘要与本轮对话,生成新的短摘要并写入会话。""" + user_clip = clip_text(user_text, 1200) + assistant_clip = clip_text(assistant_text, 1800) + if not user_clip and not assistant_clip: + summary = clip_text(prior_summary, CHAT_ROLLING_SUMMARY_MAX_CHARS) + update_session_rolling_summary(session_id, summary) + return summary + + system = ( + ROLLING_SUMMARY_GENERAL_SYSTEM + if (bot_mode or "").strip().lower() == CHAT_BOT_GENERAL + else ROLLING_SUMMARY_TRADING_SYSTEM + ) + raw = generate_text( + system=system, + user=build_rolling_summary_user_prompt( + prior_summary=prior_summary, + user_text=user_clip, + assistant_text=assistant_clip, + ), + temperature=CHAT_ROLLING_SUMMARY_TEMPERATURE, + max_tokens=CHAT_ROLLING_SUMMARY_GEN_MAX_TOKENS, + max_continuations=1, + ) + if is_ai_error_reply(raw): + fallback = _fallback_summary(prior_summary, user_clip, assistant_clip) + update_session_rolling_summary(session_id, fallback) + return fallback + + summary = clip_text(raw, CHAT_ROLLING_SUMMARY_MAX_CHARS) + update_session_rolling_summary(session_id, summary) + return summary + + +def _fallback_summary(prior: str, user_text: str, assistant_text: str) -> str: + parts: list[str] = [] + if prior.strip(): + parts.append(prior.strip()) + if user_text.strip(): + parts.append(f"用户:{clip_text(user_text, 200)}") + if assistant_text.strip(): + parts.append(f"教练:{clip_text(assistant_text, 280)}") + return clip_text("\n".join(parts), CHAT_ROLLING_SUMMARY_MAX_CHARS) diff --git a/manual_trading_hub/hub_ai/store.py b/manual_trading_hub/hub_ai/store.py index 5197b15..1906ed5 100644 --- a/manual_trading_hub/hub_ai/store.py +++ b/manual_trading_hub/hub_ai/store.py @@ -165,6 +165,7 @@ def create_new_session( "created_at": _now_str(), "updated_at": _now_str(), "messages": [], + "rolling_summary": "", } store.setdefault("sessions", []).append(session) store["active_session_id"] = session["id"] @@ -179,6 +180,22 @@ def ensure_active_session(*, trading_day: str) -> dict: return create_new_session(trading_day=trading_day) +def update_session_rolling_summary(session_id: str, summary: str) -> dict: + store = load_chat_store() + target = None + for s in store.get("sessions") or []: + if str(s.get("id")) == str(session_id): + target = s + break + if not target: + raise KeyError("session_not_found") + target["rolling_summary"] = str(summary or "").strip() + target["updated_at"] = _now_str() + store["active_session_id"] = target["id"] + save_chat_store(store) + return target + + def append_chat_message( session_id: str, role: str, diff --git a/manual_trading_hub/hub_ai/text_util.py b/manual_trading_hub/hub_ai/text_util.py new file mode 100644 index 0000000..e310dc1 --- /dev/null +++ b/manual_trading_hub/hub_ai/text_util.py @@ -0,0 +1,14 @@ +"""中控 AI 文本小工具。""" + + +def is_ai_error_reply(text: str) -> bool: + t = (text or "").strip() + return t.startswith("AI 调用失败") or t.startswith("AI 生成失败") + + +def clip_text(text: str, max_chars: int) -> str: + s = str(text or "").strip() + limit = max(200, int(max_chars or 0)) + if len(s) <= limit: + return s + return s[: limit - 1].rstrip() + "…" diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index a89cc66..561912f 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -3498,8 +3498,10 @@ } const ARCHIVE_QUOTE_AI_KEY = "hub_archive_quote_ai"; + let archiveQuoteAiPending = false; async function consumeArchiveQuoteAiPending() { + if (archiveQuoteAiPending || aiChatLoading) return; let raw = ""; try { raw = sessionStorage.getItem(ARCHIVE_QUOTE_AI_KEY) || ""; @@ -3526,6 +3528,7 @@ applyAiMobileTab("trading"); } + archiveQuoteAiPending = true; setAiChatBusy(true); renderAiChatMessages(aiChatSessionCache, { pendingUser: content, @@ -3547,12 +3550,14 @@ showToast("复盘语录已发送给交易教练"); } catch (e) { showToast(String(e), true); + if (input) input.value = content; try { await loadAiChatSession(); } catch (_) { renderAiChatMessages(aiChatSessionCache); } } finally { + archiveQuoteAiPending = false; setAiChatBusy(false); } } @@ -3604,6 +3609,7 @@ const files = fileInput && fileInput.files ? Array.from(fileInput.files) : []; if (!text && !files.length) return; const pendingAttachments = files.map((f) => ({ name: f.name, kind: f.type.startsWith("image/") ? "image" : "text" })); + const savedText = text; if (input) input.value = ""; setAiChatBusy(true); renderAiChatMessages(aiChatSessionCache, { @@ -3629,6 +3635,7 @@ } } catch (e) { showToast(String(e), true); + if (input && savedText) input.value = savedText; try { await loadAiChatSession(); } catch (_) { diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index d706923..d7d9199 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -594,6 +594,6 @@ - +