From 3f1bf9905d3319578b494cc6c78bfda22b3364a8 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 5 Jun 2026 13:59:17 +0800 Subject: [PATCH] fix: AI review list UTC filter, vision timeout, and stuck loading state Co-authored-by: Cursor --- ai_client.py | 11 +- crypto_monitor_binance/app.py | 5 +- crypto_monitor_binance/templates/index.html | 40 +++--- crypto_monitor_gate/app.py | 5 +- crypto_monitor_gate/templates/index.html | 40 +++--- crypto_monitor_gate_bot/.env.example | 4 +- crypto_monitor_gate_bot/app.py | 123 ++++++++++--------- crypto_monitor_gate_bot/templates/index.html | 40 +++--- crypto_monitor_okx/app.py | 5 +- crypto_monitor_okx/templates/index.html | 40 +++--- history_window_lib.py | 8 ++ 11 files changed, 199 insertions(+), 122 deletions(-) diff --git a/ai_client.py b/ai_client.py index fca4be1..4f87bbe 100644 --- a/ai_client.py +++ b/ai_client.py @@ -19,7 +19,12 @@ def _env_str(name: str, default: str = "") -> str: return str(v).strip() -def _ai_timeout_seconds() -> int: +def _ai_timeout_seconds(*, image_count: int = 0) -> int: + if image_count > 0: + try: + return max(30, int(_env_str("AI_REVIEW_TIMEOUT_SECONDS", "300") or "300")) + except ValueError: + return 300 try: return max(10, int(_env_str("AI_TIMEOUT_SECONDS", "120") or "120")) except ValueError: @@ -129,7 +134,7 @@ def _generate_openai(prompt: str, images: List[tuple], temperature: float) -> st _openai_chat_url(), headers=headers, json=body, - timeout=_ai_timeout_seconds(), + timeout=_ai_timeout_seconds(image_count=len(images)), ) r.raise_for_status() data = r.json() @@ -149,7 +154,7 @@ def _generate_ollama(prompt: str, images: List[tuple], temperature: float) -> st } if images: payload["images"] = [b64 for b64, _mime in images] - r = requests.post(_ollama_api(), json=payload, timeout=_ai_timeout_seconds()) + r = requests.post(_ollama_api(), json=payload, timeout=_ai_timeout_seconds(image_count=len(images))) r.raise_for_status() return (r.json().get("response") or "").strip() or "AI 生成失败" diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index cd3cd80..c9bd0fa 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -138,6 +138,7 @@ from history_window_lib import ( resolve_window, sql_list_time_field, utc_window_to_bj_sql_strings, + utc_window_to_utc_sql_strings, ) def load_env_file(path): @@ -7791,11 +7792,11 @@ def delete_journal(jid): @login_required def api_reviews(): win = _list_window_from_request() - start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ) + start_sql, end_sql = utc_window_to_utc_sql_strings(win["start_utc"], win["end_utc"]) conn = get_db() rows = conn.execute( "SELECT * FROM ai_reviews WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT 200", - (start_bj, end_bj), + (start_sql, end_sql), ).fetchall() conn.close() return jsonify([row_to_dict(r) for r in rows]) diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 787aa09..3021671 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -1245,7 +1245,9 @@ function genDaily(){ btnLabel:"日复盘生成中…" }); } - fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`}) + const ac = new AbortController(); + const timer = setTimeout(()=>ac.abort(), 360000); + fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`,signal:ac.signal}) .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) .then(data=>{ if(!data || data.result == null) throw new Error("返回数据为空"); @@ -1254,17 +1256,21 @@ function genDaily(){ setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else if(el){ el.style.display="block"; } - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); loadReviews(); }) - .catch(e=>{ - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); + .catch(err=>{ const el=document.getElementById("daily_result"); - if(el){ + if(el && el.classList.contains("is-loading")){ el.classList.remove("is-loading","ai-result-md"); - el.innerText="生成失败,请重试。"; + el.innerText = err.name === "AbortError" + ? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS。" + : "生成失败,请重试。"; } - alert("生成日复盘失败:"+(e.message||e)); + alert("生成日复盘失败:"+(err.message||err)); + }) + .finally(()=>{ + clearTimeout(timer); + if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); }); } @@ -1282,7 +1288,9 @@ function genWeekly(){ btnLabel:"周复盘生成中…" }); } - fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`}) + const ac = new AbortController(); + const timer = setTimeout(()=>ac.abort(), 360000); + fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`,signal:ac.signal}) .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) .then(data=>{ if(!data || data.result == null) throw new Error("返回数据为空"); @@ -1291,17 +1299,21 @@ function genWeekly(){ setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else if(el){ el.style.display="block"; } - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); loadReviews(); }) - .catch(e=>{ - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); + .catch(err=>{ const el=document.getElementById("weekly_result"); - if(el){ + if(el && el.classList.contains("is-loading")){ el.classList.remove("is-loading","ai-result-md"); - el.innerText="生成失败,请重试。"; + el.innerText = err.name === "AbortError" + ? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS。" + : "生成失败,请重试。"; } - alert("生成周复盘失败:"+(e.message||e)); + alert("生成周复盘失败:"+(err.message||err)); + }) + .finally(()=>{ + clearTimeout(timer); + if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); }); } diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index 9285e0d..263169c 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -137,6 +137,7 @@ from history_window_lib import ( resolve_window, sql_list_time_field, utc_window_to_bj_sql_strings, + utc_window_to_utc_sql_strings, ) @@ -7841,11 +7842,11 @@ def delete_journal(jid): @login_required def api_reviews(): win = _list_window_from_request() - start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ) + start_sql, end_sql = utc_window_to_utc_sql_strings(win["start_utc"], win["end_utc"]) conn = get_db() rows = conn.execute( "SELECT * FROM ai_reviews WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT 200", - (start_bj, end_bj), + (start_sql, end_sql), ).fetchall() conn.close() return jsonify([row_to_dict(r) for r in rows]) diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 787aa09..3021671 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -1245,7 +1245,9 @@ function genDaily(){ btnLabel:"日复盘生成中…" }); } - fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`}) + const ac = new AbortController(); + const timer = setTimeout(()=>ac.abort(), 360000); + fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`,signal:ac.signal}) .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) .then(data=>{ if(!data || data.result == null) throw new Error("返回数据为空"); @@ -1254,17 +1256,21 @@ function genDaily(){ setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else if(el){ el.style.display="block"; } - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); loadReviews(); }) - .catch(e=>{ - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); + .catch(err=>{ const el=document.getElementById("daily_result"); - if(el){ + if(el && el.classList.contains("is-loading")){ el.classList.remove("is-loading","ai-result-md"); - el.innerText="生成失败,请重试。"; + el.innerText = err.name === "AbortError" + ? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS。" + : "生成失败,请重试。"; } - alert("生成日复盘失败:"+(e.message||e)); + alert("生成日复盘失败:"+(err.message||err)); + }) + .finally(()=>{ + clearTimeout(timer); + if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); }); } @@ -1282,7 +1288,9 @@ function genWeekly(){ btnLabel:"周复盘生成中…" }); } - fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`}) + const ac = new AbortController(); + const timer = setTimeout(()=>ac.abort(), 360000); + fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`,signal:ac.signal}) .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) .then(data=>{ if(!data || data.result == null) throw new Error("返回数据为空"); @@ -1291,17 +1299,21 @@ function genWeekly(){ setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else if(el){ el.style.display="block"; } - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); loadReviews(); }) - .catch(e=>{ - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); + .catch(err=>{ const el=document.getElementById("weekly_result"); - if(el){ + if(el && el.classList.contains("is-loading")){ el.classList.remove("is-loading","ai-result-md"); - el.innerText="生成失败,请重试。"; + el.innerText = err.name === "AbortError" + ? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS。" + : "生成失败,请重试。"; } - alert("生成周复盘失败:"+(e.message||e)); + alert("生成周复盘失败:"+(err.message||err)); + }) + .finally(()=>{ + clearTimeout(timer); + if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); }); } diff --git a/crypto_monitor_gate_bot/.env.example b/crypto_monitor_gate_bot/.env.example index 894a438..018cb09 100644 --- a/crypto_monitor_gate_bot/.env.example +++ b/crypto_monitor_gate_bot/.env.example @@ -125,11 +125,13 @@ FORCE_CLOSE_ENABLED=false # 推送与AI超时(秒) WECHAT_TIMEOUT_SECONDS=10 AI_TIMEOUT_SECONDS=120 +AI_REVIEW_TIMEOUT_SECONDS=300 AI_PROVIDER=openai OPENAI_API_BASE=https://op.bz121.com/v1 OPENAI_API_KEY=你的密钥 -OPENAI_MODEL=gemma4:e4b +# 须与网关「模型分布」里已启用节点的 Model ID 完全一致(示例为 4070s 节点) +OPENAI_MODEL=huihui_ai/gemma-4-abliterated:e4b OLLAMA_API=http://127.0.0.1:11434/api/generate AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py index 34bc1f2..ff3205e 100644 --- a/crypto_monitor_gate_bot/app.py +++ b/crypto_monitor_gate_bot/app.py @@ -100,6 +100,7 @@ from history_window_lib import ( resolve_window, sql_list_time_field, utc_window_to_bj_sql_strings, + utc_window_to_utc_sql_strings, ) @@ -7720,11 +7721,11 @@ def delete_journal(jid): @login_required def api_reviews(): win = _list_window_from_request() - start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ) + start_sql, end_sql = utc_window_to_utc_sql_strings(win["start_utc"], win["end_utc"]) conn = get_db() rows = conn.execute( "SELECT * FROM ai_reviews WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT 200", - (start_bj, end_bj), + (start_sql, end_sql), ).fetchall() conn.close() return jsonify([row_to_dict(r) for r in rows]) @@ -7992,35 +7993,40 @@ def _journal_ai_chart_builder(row): @login_required def ai_daily_review(): date = request.form.get("date", "") - conn = get_db() - rows = conn.execute( - "SELECT * FROM journal_entries WHERE substr(open_datetime, 1, 10)=? ORDER BY open_datetime ASC", - (date,) - ).fetchall() - conn.close() - if not rows: - return jsonify({"result": "该日无交易记录"}) + try: + conn = get_db() + rows = conn.execute( + "SELECT * FROM journal_entries WHERE substr(open_datetime, 1, 10)=? ORDER BY open_datetime ASC", + (date,), + ).fetchall() + conn.close() + if not rows: + return jsonify({"result": "该日无交易记录"}) - text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n" - for idx, row in enumerate(rows, 1): - text += journal_row_lines_for_ai(idx, row) - text += "\n" + text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n" + for idx, row in enumerate(rows, 1): + text += journal_row_lines_for_ai(idx, row) + text += "\n" - image_paths = collect_images_for_ai_review( - rows, - app.config["UPLOAD_FOLDER"], - build_chart_if_missing=_journal_ai_chart_builder, - ) - ai_result = ai_review(text, "每日", image_paths=image_paths) - full = f"【AI日复盘 {date}】\n{ai_result}\n\n原始记录:\n{text}" - conn = get_db() - conn.execute( - "INSERT INTO ai_reviews (id, review_type, target_date, content) VALUES (?,?,?,?)", - (uuid.uuid4().hex, "daily", date, full) - ) - conn.commit() - conn.close() - return jsonify({"result": full}) + image_paths = collect_images_for_ai_review( + rows, + app.config["UPLOAD_FOLDER"], + build_chart_if_missing=_journal_ai_chart_builder, + ) + print(f"[ai_daily_review] date={date} rows={len(rows)} images={len(image_paths)}") + ai_result = ai_review(text, "每日", image_paths=image_paths) + full = f"【AI日复盘 {date}】\n{ai_result}\n\n原始记录:\n{text}" + conn = get_db() + conn.execute( + "INSERT INTO ai_reviews (id, review_type, target_date, content) VALUES (?,?,?,?)", + (uuid.uuid4().hex, "daily", date, full), + ) + conn.commit() + conn.close() + return jsonify({"result": full}) + except Exception as e: + print(f"[ai_daily_review] date={date} failed: {e}") + return jsonify({"ok": False, "result": f"生成失败:{e}"}), 500 @app.route("/ai_weekly_review", methods=["POST"]) @@ -8028,35 +8034,40 @@ def ai_daily_review(): def ai_weekly_review(): start_date = request.form.get("start_date", "") end_date = request.form.get("end_date", "") - conn = get_db() - rows = conn.execute( - "SELECT * FROM journal_entries WHERE substr(open_datetime,1,10) >= ? AND substr(open_datetime,1,10) <= ? ORDER BY open_datetime ASC", - (start_date, end_date) - ).fetchall() - conn.close() - if not rows: - return jsonify({"result": "该时间段无交易记录"}) + try: + conn = get_db() + rows = conn.execute( + "SELECT * FROM journal_entries WHERE substr(open_datetime,1,10) >= ? AND substr(open_datetime,1,10) <= ? ORDER BY open_datetime ASC", + (start_date, end_date), + ).fetchall() + conn.close() + if not rows: + return jsonify({"result": "该时间段无交易记录"}) - text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n" - for idx, row in enumerate(rows, 1): - text += journal_row_lines_for_ai(idx, row) - text += "\n" + text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n" + for idx, row in enumerate(rows, 1): + text += journal_row_lines_for_ai(idx, row) + text += "\n" - image_paths = collect_images_for_ai_review( - rows, - app.config["UPLOAD_FOLDER"], - build_chart_if_missing=_journal_ai_chart_builder, - ) - ai_result = ai_review(text, "周度", image_paths=image_paths) - full = f"【AI周复盘 {start_date}~{end_date}】\n{ai_result}\n\n原始记录:\n{text}" - conn = get_db() - conn.execute( - "INSERT INTO ai_reviews (id, review_type, target_date, content) VALUES (?,?,?,?)", - (uuid.uuid4().hex, "weekly", f"{start_date}~{end_date}", full) - ) - conn.commit() - conn.close() - return jsonify({"result": full}) + image_paths = collect_images_for_ai_review( + rows, + app.config["UPLOAD_FOLDER"], + build_chart_if_missing=_journal_ai_chart_builder, + ) + print(f"[ai_weekly_review] range={start_date}~{end_date} rows={len(rows)} images={len(image_paths)}") + ai_result = ai_review(text, "周度", image_paths=image_paths) + full = f"【AI周复盘 {start_date}~{end_date}】\n{ai_result}\n\n原始记录:\n{text}" + conn = get_db() + conn.execute( + "INSERT INTO ai_reviews (id, review_type, target_date, content) VALUES (?,?,?,?)", + (uuid.uuid4().hex, "weekly", f"{start_date}~{end_date}", full), + ) + conn.commit() + conn.close() + return jsonify({"result": full}) + except Exception as e: + print(f"[ai_weekly_review] range={start_date}~{end_date} failed: {e}") + return jsonify({"ok": False, "result": f"生成失败:{e}"}), 500 def _hub_meta_bundle(): return { diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index b07cc29..0aece14 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -1208,7 +1208,9 @@ function genDaily(){ btnLabel:"日复盘生成中…" }); } - fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`}) + const ac = new AbortController(); + const timer = setTimeout(()=>ac.abort(), 360000); + fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`,signal:ac.signal}) .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) .then(data=>{ if(!data || data.result == null) throw new Error("返回数据为空"); @@ -1217,17 +1219,21 @@ function genDaily(){ setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else if(el){ el.style.display="block"; } - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); loadReviews(); }) - .catch(e=>{ - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); + .catch(err=>{ const el=document.getElementById("daily_result"); - if(el){ + if(el && el.classList.contains("is-loading")){ el.classList.remove("is-loading","ai-result-md"); - el.innerText="生成失败,请重试。"; + el.innerText = err.name === "AbortError" + ? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS。" + : "生成失败,请重试。"; } - alert("生成日复盘失败:"+(e.message||e)); + alert("生成日复盘失败:"+(err.message||err)); + }) + .finally(()=>{ + clearTimeout(timer); + if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); }); } @@ -1245,7 +1251,9 @@ function genWeekly(){ btnLabel:"周复盘生成中…" }); } - fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`}) + const ac = new AbortController(); + const timer = setTimeout(()=>ac.abort(), 360000); + fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`,signal:ac.signal}) .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) .then(data=>{ if(!data || data.result == null) throw new Error("返回数据为空"); @@ -1254,17 +1262,21 @@ function genWeekly(){ setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else if(el){ el.style.display="block"; } - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); loadReviews(); }) - .catch(e=>{ - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); + .catch(err=>{ const el=document.getElementById("weekly_result"); - if(el){ + if(el && el.classList.contains("is-loading")){ el.classList.remove("is-loading","ai-result-md"); - el.innerText="生成失败,请重试。"; + el.innerText = err.name === "AbortError" + ? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS。" + : "生成失败,请重试。"; } - alert("生成周复盘失败:"+(e.message||e)); + alert("生成周复盘失败:"+(err.message||err)); + }) + .finally(()=>{ + clearTimeout(timer); + if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); }); } diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 543c2f0..388f4f9 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -137,6 +137,7 @@ from history_window_lib import ( resolve_window, sql_list_time_field, utc_window_to_bj_sql_strings, + utc_window_to_utc_sql_strings, ) @@ -7473,11 +7474,11 @@ def delete_journal(jid): @login_required def api_reviews(): win = _list_window_from_request() - start_bj, end_bj = utc_window_to_bj_sql_strings(win["start_utc"], win["end_utc"], APP_TZ) + start_sql, end_sql = utc_window_to_utc_sql_strings(win["start_utc"], win["end_utc"]) conn = get_db() rows = conn.execute( "SELECT * FROM ai_reviews WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT 200", - (start_bj, end_bj), + (start_sql, end_sql), ).fetchall() conn.close() return jsonify([row_to_dict(r) for r in rows]) diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index 1280c22..bf18afe 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -1254,7 +1254,9 @@ function genDaily(){ btnLabel:"日复盘生成中…" }); } - fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`}) + const ac = new AbortController(); + const timer = setTimeout(()=>ac.abort(), 360000); + fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`,signal:ac.signal}) .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) .then(data=>{ if(!data || data.result == null) throw new Error("返回数据为空"); @@ -1263,17 +1265,21 @@ function genDaily(){ setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else if(el){ el.style.display="block"; } - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); loadReviews(); }) - .catch(e=>{ - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); + .catch(err=>{ const el=document.getElementById("daily_result"); - if(el){ + if(el && el.classList.contains("is-loading")){ el.classList.remove("is-loading","ai-result-md"); - el.innerText="生成失败,请重试。"; + el.innerText = err.name === "AbortError" + ? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS。" + : "生成失败,请重试。"; } - alert("生成日复盘失败:"+(e.message||e)); + alert("生成日复盘失败:"+(err.message||err)); + }) + .finally(()=>{ + clearTimeout(timer); + if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-daily-btn"); }); } @@ -1291,7 +1297,9 @@ function genWeekly(){ btnLabel:"周复盘生成中…" }); } - fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`}) + const ac = new AbortController(); + const timer = setTimeout(()=>ac.abort(), 360000); + fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`,signal:ac.signal}) .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) .then(data=>{ if(!data || data.result == null) throw new Error("返回数据为空"); @@ -1300,17 +1308,21 @@ function genWeekly(){ setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else if(el){ el.style.display="block"; } - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); loadReviews(); }) - .catch(e=>{ - if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); + .catch(err=>{ const el=document.getElementById("weekly_result"); - if(el){ + if(el && el.classList.contains("is-loading")){ el.classList.remove("is-loading","ai-result-md"); - el.innerText="生成失败,请重试。"; + el.innerText = err.name === "AbortError" + ? "生成超时(>6分钟),请检查 OPENAI_MODEL 是否与网关已启用模型一致,或增大 AI_REVIEW_TIMEOUT_SECONDS。" + : "生成失败,请重试。"; } - alert("生成周复盘失败:"+(e.message||e)); + alert("生成周复盘失败:"+(err.message||err)); + }) + .finally(()=>{ + clearTimeout(timer); + if(window.AiReviewRender && AiReviewRender.clearGenerating) AiReviewRender.clearGenerating("gen-weekly-btn"); }); } diff --git a/history_window_lib.py b/history_window_lib.py index fc800c1..07aaabc 100644 --- a/history_window_lib.py +++ b/history_window_lib.py @@ -75,6 +75,14 @@ def utc_window_to_bj_sql_strings(start_utc, end_utc, app_tz): return start_bj, end_bj +def utc_window_to_utc_sql_strings(start_utc, end_utc): + """SQLite CURRENT_TIMESTAMP 写入 UTC 时,用于 created_at 范围比较。""" + return ( + start_utc.strftime("%Y-%m-%d %H:%M:%S"), + end_utc.strftime("%Y-%m-%d %H:%M:%S"), + ) + + def normalize_bj_datetime_storage(raw): """表单 datetime-local(含 T)入库前统一为 YYYY-MM-DD HH:MM:SS(北京时间)。""" s = (raw or "").strip().replace("T", " ").replace("Z", "").strip()