| 时间 | 用户 | 意图 | 问题 | 币种 | 会话 |
|---|---|---|---|---|---|
| 加载中... | |||||
diff --git a/app/db/chat_assistant_db.py b/app/db/chat_assistant_db.py index 5cb8924..4508648 100644 --- a/app/db/chat_assistant_db.py +++ b/app/db/chat_assistant_db.py @@ -3,7 +3,7 @@ from __future__ import annotations import json -from datetime import datetime +from datetime import datetime, timedelta from app.db.llm_insights import repair_mojibake_json, repair_mojibake_text from app.db.postgres_connection import ensure_migrations_once @@ -310,6 +310,140 @@ def bootstrap_chat(user_id: int) -> dict: } +def get_chat_admin_overview(hours: int = 24) -> dict: + init_chat_tables() + hours = max(0, int(hours or 24)) + conn = get_conn() + try: + params = [] + where = "1=1" + if hours > 0: + where += " AND m.created_at >= %s" + params.append((datetime.now().replace(microsecond=0) - timedelta(hours=hours)).isoformat()) + totals = conn.execute( + f""" + SELECT + COUNT(*) AS total_messages, + COUNT(*) FILTER (WHERE role='user') AS total_questions, + COUNT(DISTINCT session_id) AS total_sessions, + COUNT(DISTINCT user_id) AS total_users + FROM chat_messages m + WHERE {where} + """, + tuple(params), + ).fetchone() + top_intents = conn.execute( + f""" + SELECT COALESCE(NULLIF(intent, ''), 'unknown') AS intent, COUNT(*) AS n + FROM chat_messages m + WHERE role='user' AND {where} + GROUP BY 1 + ORDER BY n DESC, intent ASC + LIMIT 8 + """, + tuple(params), + ).fetchall() + top_symbols = conn.execute( + f""" + SELECT COALESCE(NULLIF(symbol, ''), 'unknown') AS symbol, COUNT(*) AS n + FROM chat_messages m + WHERE role='user' AND {where} + GROUP BY 1 + ORDER BY n DESC, symbol ASC + LIMIT 10 + """, + tuple(params), + ).fetchall() + recent = conn.execute( + f""" + SELECT m.id, m.session_id, m.user_id, m.role, m.content_text, m.intent, m.symbol, m.model, m.created_at, + s.title AS session_title, u.email AS user_email + FROM chat_messages m + LEFT JOIN chat_sessions s ON s.id=m.session_id + LEFT JOIN app_user u ON u.id=m.user_id + WHERE m.role='user' AND {where} + ORDER BY m.created_at DESC, m.id DESC + LIMIT 50 + """, + tuple(params), + ).fetchall() + finally: + conn.close() + return { + "hours": hours, + "total_messages": int((totals[0] if totals else 0) or 0), + "total_questions": int((totals[1] if totals else 0) or 0), + "total_sessions": int((totals[2] if totals else 0) or 0), + "total_users": int((totals[3] if totals else 0) or 0), + "top_intents": [dict(row) for row in top_intents], + "top_symbols": [dict(row) for row in top_symbols], + "recent_questions": [dict(row) for row in recent], + } + + +def list_chat_admin_questions(hours: int = 24, intent: str = "", search: str = "", offset: int = 0, limit: int = 50) -> dict: + init_chat_tables() + hours = max(0, int(hours or 24)) + offset = max(0, int(offset or 0)) + limit = max(1, min(int(limit or 50), 200)) + intent = str(intent or "").strip() + search = str(search or "").strip() + conn = get_conn() + try: + params = [] + where = "m.role='user'" + if hours > 0: + where += " AND m.created_at >= %s" + params.append((datetime.now().replace(microsecond=0) - timedelta(hours=hours)).isoformat()) + if intent and intent != "all": + where += " AND COALESCE(NULLIF(m.intent, ''), 'unknown')=%s" + params.append(intent) + if search: + where += " AND (m.content_text ILIKE %s OR COALESCE(s.title,'') ILIKE %s OR COALESCE(u.email,'') ILIKE %s)" + like = f"%{search}%" + params.extend([like, like, like]) + total = conn.execute( + f""" + SELECT COUNT(*) + FROM chat_messages m + LEFT JOIN chat_sessions s ON s.id=m.session_id + LEFT JOIN app_user u ON u.id=m.user_id + WHERE {where} + """, + tuple(params), + ).fetchone()[0] + rows = conn.execute( + f""" + SELECT m.id, m.session_id, m.user_id, m.role, m.content_text, m.content_json, m.context_json, + m.intent, m.symbol, m.timeframe, m.model, m.created_at, + s.title AS session_title, u.email AS user_email + FROM chat_messages m + LEFT JOIN chat_sessions s ON s.id=m.session_id + LEFT JOIN app_user u ON u.id=m.user_id + WHERE {where} + ORDER BY m.created_at DESC, m.id DESC + LIMIT %s OFFSET %s + """, + tuple(params + [limit, offset]), + ).fetchall() + finally: + conn.close() + items = [] + for row in rows: + item = dict(row) + item["content"] = _loads(item.pop("content_json", "{}"), {}) + item["context"] = _loads(item.pop("context_json", "{}"), {}) + item["content_text"] = repair_mojibake_text(item.get("content_text", "")) + items.append(item) + return { + "items": items, + "total": int(total or 0), + "limit": limit, + "offset": offset, + "has_more": offset + len(items) < int(total or 0), + } + + __all__ = [ "append_chat_message", "bootstrap_chat", diff --git a/app/services/chat_assistant.py b/app/services/chat_assistant.py index 8ea01a2..e960f93 100644 --- a/app/services/chat_assistant.py +++ b/app/services/chat_assistant.py @@ -21,7 +21,6 @@ from app.db import chat_assistant_db from app.db.analytics import get_pipeline_runs from app.db.llm_insights import compute_input_hash, repair_mojibake_json, repair_mojibake_text from app.db.onchain_db import get_onchain_token_detail, get_onchain_overview -from app.db.paper_trading import get_paper_trading_summary, list_paper_trade_events, list_paper_trades from app.db.schema import get_conn from app.services.llm_insights import get_llm_params from app.services.market_overview import get_crypto_market_overview @@ -32,7 +31,7 @@ exchange = ccxt.binance({"enableRateLimit": True}) CRYPTO_TERMS = { "btc", "eth", "bnb", "sol", "xrp", "doge", "ada", "sui", "link", "qnt", "币", "加密", "crypto", "usdt", "binance", "行情", "链上", "舆情", "推荐", "复盘", - "k线", "k 线", "技术面", "止盈", "止损", "纸面", "模拟交易", "仓位", "山寨", + "k线", "k 线", "技术面", "止盈", "止损", "山寨", } INTENT_LABELS = { @@ -42,7 +41,7 @@ INTENT_LABELS = { "sentiment": "舆情解读", "onchain": "链上异动", "review": "复盘查询", - "paper_trading": "模拟交易", + "restricted": "受限内容", "help": "帮助", "unsupported": "范围外", } @@ -124,12 +123,12 @@ def extract_symbol(message: str, session=None, preferences=None) -> str: def detect_intent(message: str, symbol: str = "") -> str: text = str(message or "").lower() + if any(k in text for k in ("模拟交易", "纸面交易", "paper trading", "paper-trading", "paper")): + return "restricted" if not _is_crypto_question(text, symbol): return "unsupported" if any(k in text for k in ("怎么用", "能做什么", "帮助", "help", "问什么")): return "help" - if any(k in text for k in ("模拟交易", "paper", "开仓", "平仓", "持仓", "收益", "仓位")): - return "paper_trading" if any(k in text for k in ("链上", "onchain", "鲸鱼", "转账", "dex", "流动性", "合约")): return "onchain" if any(k in text for k in ("舆情", "新闻", "消息", "情绪", "热点", "叙事", "ai 舆情")): @@ -399,21 +398,6 @@ def _review_context(symbol: str = "", limit=8): conn.close() return [dict(row) for row in rows] - -def _paper_context(symbol: str = ""): - if symbol: - trades = list_paper_trades(limit=10, status="") - symbol_norm = _normalize_symbol(symbol) - items = [x for x in trades.get("items", []) if x.get("symbol") == symbol_norm][:5] - events = list_paper_trade_events(limit=20, symbol=symbol_norm) - return {"trades": items, "events": events.get("items", [])} - return { - "summary": get_paper_trading_summary(days=30), - "trades": list_paper_trades(limit=5).get("items", []), - "events": list_paper_trade_events(limit=8).get("items", []), - } - - def _market_context(): try: market = get_crypto_market_overview() @@ -444,6 +428,8 @@ def build_context(intent: str, message: str, symbol: str, preferences=None) -> d } if intent == "unsupported": return ctx + if intent == "restricted": + return ctx if intent in ("coin_analysis", "recommendation_explain", "onchain", "sentiment") and symbol: ctx["technicals"] = analyze_symbol_technicals(symbol) ctx["recommendations"] = _latest_recommendations(symbol=symbol, limit=5) @@ -453,20 +439,15 @@ def build_context(intent: str, message: str, symbol: str, preferences=None) -> d except Exception as exc: ctx["onchain"] = {"error": str(exc)[:200]} ctx["reviews"] = _review_context(symbol=symbol, limit=8) - ctx["paper"] = _paper_context(symbol=symbol) - ctx["sources"] = ["binance_klines", "recommendation", "event_news", "onchain", "review_log", "paper_trading"] + ctx["sources"] = ["binance_klines", "recommendation", "event_news", "onchain", "review_log"] elif intent == "market_overview": ctx.update(_market_context()) ctx["pipeline"] = get_pipeline_runs(limit=5, hours=24, offset=0) ctx["sources"] = ["market_overview", "onchain_overview", "llm_sentiment", "pipeline_runs"] - elif intent == "paper_trading": - ctx["paper"] = _paper_context(symbol=symbol) - ctx["sources"] = ["paper_trading"] elif intent == "review": ctx["reviews"] = _review_context(symbol=symbol, limit=12) ctx["recommendations"] = _latest_recommendations(symbol=symbol, limit=8) if symbol else _latest_recommendations(limit=8) - ctx["paper"] = _paper_context(symbol=symbol) - ctx["sources"] = ["review_log", "recommendation", "paper_trading"] + ctx["sources"] = ["review_log", "recommendation"] elif intent == "sentiment": ctx["sentiment_events"] = _sentiment_context(symbol=symbol, limit=20) try: @@ -490,16 +471,24 @@ def build_context(intent: str, message: str, symbol: str, preferences=None) -> d "单币技术面:自动拉取 Binance 15m/1h/4h/1d K 线", "推荐解释:结合当前看板状态、入场窗口、TP/SL、风险理由", "舆情和链上:读取当前系统已采集与 AI 解读的结果", - "复盘和模拟交易:区分信号表现与纸面交易收益", + "复盘:区分信号表现、失效样本和真实推荐结果", ] return ctx def _fallback_answer(intent: str, message: str, context: dict) -> dict: + if intent == "restricted": + return { + "summary": "内部模拟交易数据不可在智能问答中直接访问。", + "answer": "我不能读取或解释内部模拟交易数据。你可以继续问公开行情、单币技术面、推荐解释、链上异动、舆情影响或复盘结果(不含纸面交易明细)。", + "evidence": [], + "related_records": [], + "followups": ["分析 BTC/USDT 的技术面", "解释最新推荐为什么不是可买"], + } if intent == "unsupported": return { "summary": "我只能回答加密货币和 AlphaX 当前数据相关的问题。", - "answer": "这个问题超出了 Crypto 研究助手的范围。你可以问某个币的技术面、看板推荐原因、链上异动、舆情影响、复盘或模拟交易表现。", + "answer": "这个问题超出了 Crypto 研究助手的范围。你可以问某个币的技术面、看板推荐原因、链上异动、舆情影响或复盘表现。", "evidence": [], "related_records": [], "followups": ["分析 BTC/USDT 的技术面", "今天市场适合追强势币吗?"], @@ -510,7 +499,6 @@ def _fallback_answer(intent: str, message: str, context: dict) -> dict: recommendations = context.get("recommendations") or [] sentiment = context.get("sentiment_events") or [] onchain = context.get("onchain") or {} - paper = context.get("paper") or {} evidence = [] if tech_summary: evidence.append(f"技术面:{tech_summary.get('headline', '')}") @@ -521,9 +509,6 @@ def _fallback_answer(intent: str, message: str, context: dict) -> dict: evidence.append(f"舆情:近 72h 有 {len(sentiment)} 条相关事件,最新为「{sentiment[0].get('title', '')[:60]}」。") if isinstance(onchain, dict) and (onchain.get("events") or onchain.get("metrics")): evidence.append(f"链上:近 7 天有 {len(onchain.get('events') or [])} 条映射事件。") - if isinstance(paper, dict) and paper.get("trades"): - latest = paper["trades"][0] - evidence.append(f"模拟交易:最新状态 {latest.get('status')},收益 {latest.get('pnl_pct') or latest.get('realized_pnl_pct') or 0}%。") if not evidence: evidence.append("当前数据库没有足够样本,结论需要降级为观察。") @@ -534,7 +519,7 @@ def _fallback_answer(intent: str, message: str, context: dict) -> dict: answer = state.get("summary") or "我读取了全市场行情,但当前没有足够信息形成强结论。" elif symbol: summary = tech_summary.get("headline") or f"{symbol} 需要继续观察" - answer = "结论:" + summary + " 证据区已汇总技术面、推荐、舆情、链上和模拟交易上下文。" + answer = "结论:" + summary + " 证据区已汇总技术面、推荐、舆情、链上和复盘上下文。" else: summary = "已读取当前系统数据" answer = "我已经按你的问题读取了当前数据库,但没有识别到明确币种;可以继续追问具体币种。" @@ -554,8 +539,6 @@ def _related_records(context: dict) -> list[dict]: records.append({"type": "recommendation", "label": f"推荐 #{item.get('id')}", "symbol": item.get("symbol"), "status": item.get("execution_status")}) for item in (context.get("sentiment_events") or [])[:3]: records.append({"type": "sentiment", "label": item.get("title"), "symbol": item.get("symbol"), "status": item.get("importance")}) - for item in ((context.get("paper") or {}).get("trades") or [])[:3]: - records.append({"type": "paper_trade", "label": f"模拟交易 #{item.get('id')}", "symbol": item.get("symbol"), "status": item.get("status")}) return records @@ -602,6 +585,20 @@ def _followups(intent: str, symbol: str = "") -> list[str]: return ["分析 BTC/USDT 现在的技术面", "解释最新推荐为什么不是可买"] +def _answer_style_for_intent(intent: str) -> str: + return { + "coin_analysis": "technical", + "recommendation_explain": "decision", + "market_overview": "market", + "sentiment": "news", + "onchain": "onchain", + "review": "review", + "restricted": "notice", + "unsupported": "notice", + "help": "help", + }.get(intent or "", "default") + + def _call_chat_llm(message: str, context: dict, history=None) -> dict: cfg = llm_config() params = get_llm_params() @@ -617,10 +614,11 @@ def _call_chat_llm(message: str, context: dict, history=None) -> dict: "context": context, "recent_history": (history or [])[-8:], "rules": [ - "只回答加密货币、AlphaX 当前数据、技术面、链上、舆情、复盘和模拟交易相关问题。", + "只回答加密货币、AlphaX 当前数据、技术面、链上、舆情、复盘相关问题,不要访问内部模拟交易数据。", "不要给真实下单指令,不要修改推荐状态,不要承诺收益。", "回答使用中文,采用两段式:先结论,再证据。", - "输出严格 JSON:summary, answer, evidence[], risk_flags[], related_records[], followups[]。", + "根据 intent 选择 answer_style:technical/decision/market/news/onchain/review/notice/help/default。", + "输出严格 JSON:summary, answer, answer_style, evidence[], risk_flags[], related_records[], followups[]。", ], } system_prompt = "你是 AlphaX Agent 的 Crypto 研究助手。你只能基于提供的结构化数据回答,不能编造数据。" @@ -649,6 +647,7 @@ def _call_chat_llm(message: str, context: dict, history=None) -> dict: if not isinstance(parsed, dict): raise ValueError("llm_output_not_object") parsed.setdefault("summary", parsed.get("answer", "")[:80]) + parsed.setdefault("answer_style", _answer_style_for_intent(context.get("intent"))) parsed.setdefault("evidence", []) parsed.setdefault("risk_flags", []) parsed.setdefault("related_records", _related_records(context)) @@ -680,18 +679,24 @@ def answer_chat(user_id: int, message: str, session_id: int = 0) -> dict: ) history = chat_assistant_db.list_chat_messages(session["id"], user_id, limit=12).get("items", []) context = build_context(intent, message, symbol, preferences=prefs) - llm_result = _call_chat_llm(message, context, history=history) - if llm_result.get("status") == "success": - answer = repair_mojibake_json(llm_result.get("content") or {}) - model = llm_result.get("model") or "" - else: + if intent == "restricted": + llm_result = {"status": "skipped", "error": "restricted_data_scope"} answer = repair_mojibake_json(_fallback_answer(intent, message, context)) - answer["llm_status"] = llm_result.get("status") - answer["llm_error"] = llm_result.get("error", "") - model = llm_result.get("model") or "" + model = "" + else: + llm_result = _call_chat_llm(message, context, history=history) + if llm_result.get("status") == "success": + answer = repair_mojibake_json(llm_result.get("content") or {}) + model = llm_result.get("model") or "" + else: + answer = repair_mojibake_json(_fallback_answer(intent, message, context)) + answer["llm_status"] = llm_result.get("status") + answer["llm_error"] = llm_result.get("error", "") + model = llm_result.get("model") or "" answer.setdefault("related_records", _related_records(context)) answer.setdefault("followups", _followups(intent, symbol)) answer.setdefault("evidence", []) + answer.setdefault("answer_style", _answer_style_for_intent(intent)) assistant_text = repair_mojibake_text(answer.get("answer") or answer.get("summary") or "") assistant_msg = chat_assistant_db.append_chat_message( session["id"], diff --git a/app/web/routes_admin.py b/app/web/routes_admin.py index 7f9b8d7..301b86c 100644 --- a/app/web/routes_admin.py +++ b/app/web/routes_admin.py @@ -3,6 +3,7 @@ from fastapi.responses import HTMLResponse from app.config.system_config import seed_runtime_system_defaults from app.db import auth_db +from app.db import chat_assistant_db from app.db.scheduler_db import ( enqueue_manual_trigger, get_job_config, @@ -84,6 +85,23 @@ def build_router(templates): raise HTTPException(status_code=404, detail="日志不存在") return item + @router.get("/api/admin/chat-logs/overview") + async def api_admin_chat_logs_overview(hours: int = 24, altcoin_session: str = Cookie(default="")): + require_admin(altcoin_session) + return chat_assistant_db.get_chat_admin_overview(hours=hours) + + @router.get("/api/admin/chat-logs") + async def api_admin_chat_logs( + hours: int = 24, + intent: str = "all", + search: str = "", + offset: int = 0, + limit: int = 50, + altcoin_session: str = Cookie(default=""), + ): + require_admin(altcoin_session) + return chat_assistant_db.list_chat_admin_questions(hours=hours, intent=intent, search=search, offset=offset, limit=limit) + @router.get("/api/runtime-config") async def api_runtime_config(kind: str = "all", altcoin_session: str = Cookie(default="")): require_admin(altcoin_session) diff --git a/app/web/routes_pages.py b/app/web/routes_pages.py index 540b84a..4cff7c3 100644 --- a/app/web/routes_pages.py +++ b/app/web/routes_pages.py @@ -96,6 +96,17 @@ def build_router(templates, repo_root: Path, stock_report_template: str): return HTMLResponse(content=f"
{exc.detail}
返回看板", status_code=exc.status_code) return render_page("system_logs.html", request, active_nav="system_logs") + @router.get("/chat-logs", response_class=HTMLResponse) + async def chat_logs_page(request: Request): + user, redirect = require_page_user(request) + if redirect: + return redirect + try: + require_admin(request.cookies.get("altcoin_session", "")) + except HTTPException as exc: + return HTMLResponse(content=f"{exc.detail}
返回看板", status_code=exc.status_code) + return render_page("chat_logs.html", request, active_nav="chat_logs") + @router.get("/paper-trading", response_class=HTMLResponse) async def paper_trading_page(request: Request): user, redirect = require_page_user(request) diff --git a/static/base.html b/static/base.html index d590141..4f0af83 100644 --- a/static/base.html +++ b/static/base.html @@ -190,6 +190,7 @@ a { color: inherit; text-decoration: none; } AI 记录 策略 迭代 + 问答日志 配置中心 调度中心 diff --git a/static/chat.html b/static/chat.html index 7794bd4..f65476d 100644 --- a/static/chat.html +++ b/static/chat.html @@ -30,6 +30,9 @@ .answer-head b{font-size:16px;color:var(--ink);line-height:1.35} .tag{display:inline-flex;align-items:center;border-radius:999px;padding:4px 8px;background:rgba(66,98,255,.08);color:var(--blue);font-size:11px;font-weight:950;white-space:nowrap} .answer-text{font-size:14px;color:var(--slate);line-height:1.75;margin-bottom:12px} +.answer-body{display:grid;gap:10px;margin-bottom:12px} +.answer-block{font-size:14px;color:var(--slate);line-height:1.75;white-space:pre-wrap} +.answer-block.is-title{font-size:13px;font-weight:950;color:var(--ink);background:var(--surface);border:1px solid var(--hairline-soft);border-radius:10px;padding:8px 10px;line-height:1.5} .progress-box{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:11px 12px;margin-bottom:12px} .progress-row{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px;color:var(--slate);padding:5px 0} .progress-row strong{color:var(--ink);font-size:12px} @@ -45,6 +48,13 @@ .ev:before{content:"";position:absolute;left:0;top:.65em;width:4px;height:4px;border-radius:50%;background:var(--blue)} .record-list{display:flex;gap:6px;flex-wrap:wrap} .record{border:1px solid var(--hairline);background:#fff;border-radius:999px;padding:5px 8px;font-size:11px;font-weight:850;color:var(--slate)} +.mini-section{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:11px;margin-top:10px} +.mini-section h3{font-size:12px;font-weight:950;color:var(--ink);margin-bottom:8px} +.brief-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px} +.brief{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:#fff;padding:10px;min-width:0} +.brief span{display:block;font-size:10px;color:var(--stone);font-weight:950} +.brief b{display:block;margin-top:4px;font-size:14px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.notice-box{border:1px solid rgba(255,183,77,.42);background:rgba(255,183,77,.11);border-radius:var(--radius-md);padding:12px;color:#6d4700;font-size:13px;line-height:1.65;margin-top:10px} .tf-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:10px} .tf{border:1px solid var(--hairline-soft);background:#fff;border-radius:var(--radius-md);padding:9px;min-width:0} .tf span{display:block;color:var(--stone);font-size:10px;font-weight:950} @@ -63,7 +73,7 @@ .loading-dots i:nth-child(2){animation-delay:.15s} .loading-dots i:nth-child(3){animation-delay:.3s} @keyframes pulse{0%,80%,100%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-3px)}} -@media(max-width:980px){.chat-shell{grid-template-columns:1fr}.session-pane{display:none}.hero{padding:14px}.messages{padding:14px}.evidence-grid{grid-template-columns:1fr}.tf-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.composer{padding:10px}.chat-main{height:calc(100vh - 48px)}} +@media(max-width:980px){.chat-shell{grid-template-columns:1fr}.session-pane{display:none}.hero{padding:14px}.messages{padding:14px}.evidence-grid{grid-template-columns:1fr}.brief-grid{grid-template-columns:1fr}.tf-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.composer{padding:10px}.chat-main{height:calc(100vh - 48px)}} @media(max-width:520px){.composer-inner{grid-template-columns:1fr}.send{width:100%}.hero{display:block}.status-pill{margin-top:8px}.msg{grid-template-columns:30px minmax(0,1fr)}.avatar{width:30px;height:30px;border-radius:8px}.bubble{padding:12px}} {% endblock %} @@ -104,14 +114,22 @@ function short(v,n){v=String(v||'');return v.length>n?v.slice(0,n)+'...':v;} function fmtNum(v){v=Number(v||0);if(!v)return'--';if(v>=100)return v.toFixed(2);if(v>=1)return v.toFixed(3);if(v>=0.01)return v.toFixed(4);return v.toFixed(8);} function normMessages(items){return (items||[]).map(function(m){m=deepFix(m||{});return {id:m.id,role:m.role,text:m.content_text||'',content:m.content||{},context:m.context||{},created_at:m.created_at,intent:m.intent,symbol:m.symbol};});} function renderSessions(){if(!state.sessions.length){sessionList.innerHTML='直接输入你的问题即可,支持单币技术面、推荐解释、链上异动、舆情影响、复盘和模拟交易。
直接输入你的问题即可,支持单币技术面、推荐解释、链上异动、舆情影响和复盘结果。
| 时间 | 用户 | 意图 | 问题 | 币种 | 会话 |
|---|---|---|---|---|---|
| 加载中... | |||||