From 86a608109030d28cd6d00f28cab4cd1e1e7e5acb Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 5 Jun 2026 13:39:50 +0800 Subject: [PATCH] fix: AI daily review sqlite3.Row crash and error feedback across four exchanges Co-authored-by: Cursor --- ai_review_lib.py | 62 +++++++++++++------- crypto_monitor_binance/templates/index.html | 12 ++-- crypto_monitor_gate/templates/index.html | 12 ++-- crypto_monitor_gate_bot/templates/index.html | 12 ++-- crypto_monitor_okx/app.py | 2 + crypto_monitor_okx/templates/index.html | 12 ++-- tests/test_ai_review_lib.py | 22 +++++++ 7 files changed, 97 insertions(+), 37 deletions(-) diff --git a/ai_review_lib.py b/ai_review_lib.py index d4704d6..0a8293e 100644 --- a/ai_review_lib.py +++ b/ai_review_lib.py @@ -21,46 +21,66 @@ def _journal_nz(v: Any, default: str = "无") -> str: return s if s else default +def _row_get(row: Any, key: str, default: Any = None) -> Any: + """兼容 dict 与 sqlite3.Row(Row 无 .get 方法)。""" + if row is None: + return default + getter = getattr(row, "get", None) + if callable(getter): + return getter(key, default) + try: + keys = row.keys() if hasattr(row, "keys") else () + if key in keys: + return row[key] + except Exception: + pass + try: + return row[key] + except (KeyError, TypeError, IndexError): + return default + + def journal_row_lines_for_ai( idx: int, - row: Mapping[str, Any], + row: Any, *, include_hold_duration: bool = True, ) -> str: """把 journal 字段拼成给 AI 的文本;四所日复盘/周复盘共用。""" lines = [ ( - f"{idx}. {_journal_nz(row.get('coin'))} {_journal_nz(row.get('tf'))} " - f"| 盈亏:{_journal_nz(row.get('pnl'))}U " - f"| 实际RR:{_journal_nz(row.get('real_rr'))} " - f"| 预期RR:{_journal_nz(row.get('expect_rr'))}" + f"{idx}. {_journal_nz(_row_get(row, 'coin'))} {_journal_nz(_row_get(row, 'tf'))} " + f"| 盈亏:{_journal_nz(_row_get(row, 'pnl'))}U " + f"| 实际RR:{_journal_nz(_row_get(row, 'real_rr'))} " + f"| 预期RR:{_journal_nz(_row_get(row, 'expect_rr'))}" ), - f" 开仓逻辑:{_journal_nz(row.get('entry_reason'))}", - f" 平仓/离场(交易员自述):{_journal_nz(row.get('exit_reason'))}", + f" 开仓逻辑:{_journal_nz(_row_get(row, 'entry_reason'))}", + f" 平仓/离场(交易员自述):{_journal_nz(_row_get(row, 'exit_reason'))}", ] if include_hold_duration: - lines.append(f" 持仓时长:{_journal_nz(row.get('hold_duration'))}") + lines.append(f" 持仓时长:{_journal_nz(_row_get(row, 'hold_duration'))}") ee_bits = [ - _journal_nz(row.get("early_exit")), - _journal_nz(row.get("early_exit_reason")), - _journal_nz(row.get("early_exit_trigger")), - _journal_nz(row.get("early_exit_note")), + _journal_nz(_row_get(row, "early_exit")), + _journal_nz(_row_get(row, "early_exit_reason")), + _journal_nz(_row_get(row, "early_exit_trigger")), + _journal_nz(_row_get(row, "early_exit_note")), ] if any(x != "无" for x in ee_bits): lines.append( " 提前离场记录:" f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}" ) - mood_bits = f"心态标签:{_journal_nz(row.get('mood_issues'))}" - if row.get("mood_score") is not None: - mood_bits += f" | 自评心态分:{row.get('mood_score')}" + mood_bits = f"心态标签:{_journal_nz(_row_get(row, 'mood_issues'))}" + mood_score = _row_get(row, "mood_score") + if mood_score is not None: + mood_bits += f" | 自评心态分:{mood_score}" lines.append(f" {mood_bits}") - if _journal_nz(row.get("post_breakeven_stare")) != "无": - lines.append(f" 保本后盯盘:{_journal_nz(row.get('post_breakeven_stare'))}") - if _journal_nz(row.get("new_trade_while_occupied")) != "无": - lines.append(f" 占用时新开仓:{_journal_nz(row.get('new_trade_while_occupied'))}") - if _journal_nz(row.get("note")) != "无": - lines.append(f" 备注:{_journal_nz(row.get('note'))}") + if _journal_nz(_row_get(row, "post_breakeven_stare")) != "无": + lines.append(f" 保本后盯盘:{_journal_nz(_row_get(row, 'post_breakeven_stare'))}") + if _journal_nz(_row_get(row, "new_trade_while_occupied")) != "无": + lines.append(f" 占用时新开仓:{_journal_nz(_row_get(row, 'new_trade_while_occupied'))}") + if _journal_nz(_row_get(row, "note")) != "无": + lines.append(f" 备注:{_journal_nz(_row_get(row, 'note'))}") return "\n".join(lines) + "\n" diff --git a/crypto_monitor_binance/templates/index.html b/crypto_monitor_binance/templates/index.html index 37d6623..c5b6685 100644 --- a/crypto_monitor_binance/templates/index.html +++ b/crypto_monitor_binance/templates/index.html @@ -1236,14 +1236,16 @@ function genDaily(){ const d = document.getElementById("day_date").value; if(!d){alert("请选择日期");return;} fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`}) - .then(r=>r.json()).then(data=>{ + .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) + .then(data=>{ const el=document.getElementById("daily_result"); const wrap=document.getElementById("daily_result_wrap"); setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else { el.style.display="block"; } loadReviews(); - }); + }) + .catch(e=>alert("生成日复盘失败:"+(e.message||e))); } function genWeekly(){ @@ -1251,14 +1253,16 @@ function genWeekly(){ const e=document.getElementById("week_end").value; if(!s || !e){alert("请选择起止日期");return;} fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`}) - .then(r=>r.json()).then(data=>{ + .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) + .then(data=>{ const el=document.getElementById("weekly_result"); const wrap=document.getElementById("weekly_result_wrap"); setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else { el.style.display="block"; } loadReviews(); - }); + }) + .catch(e=>alert("生成周复盘失败:"+(e.message||e))); } function exportDailyBundleMd(){ diff --git a/crypto_monitor_gate/templates/index.html b/crypto_monitor_gate/templates/index.html index 37d6623..c5b6685 100644 --- a/crypto_monitor_gate/templates/index.html +++ b/crypto_monitor_gate/templates/index.html @@ -1236,14 +1236,16 @@ function genDaily(){ const d = document.getElementById("day_date").value; if(!d){alert("请选择日期");return;} fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`}) - .then(r=>r.json()).then(data=>{ + .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) + .then(data=>{ const el=document.getElementById("daily_result"); const wrap=document.getElementById("daily_result_wrap"); setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else { el.style.display="block"; } loadReviews(); - }); + }) + .catch(e=>alert("生成日复盘失败:"+(e.message||e))); } function genWeekly(){ @@ -1251,14 +1253,16 @@ function genWeekly(){ const e=document.getElementById("week_end").value; if(!s || !e){alert("请选择起止日期");return;} fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`}) - .then(r=>r.json()).then(data=>{ + .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) + .then(data=>{ const el=document.getElementById("weekly_result"); const wrap=document.getElementById("weekly_result_wrap"); setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else { el.style.display="block"; } loadReviews(); - }); + }) + .catch(e=>alert("生成周复盘失败:"+(e.message||e))); } function exportDailyBundleMd(){ diff --git a/crypto_monitor_gate_bot/templates/index.html b/crypto_monitor_gate_bot/templates/index.html index 9ea2c40..a3ed3ea 100644 --- a/crypto_monitor_gate_bot/templates/index.html +++ b/crypto_monitor_gate_bot/templates/index.html @@ -1199,14 +1199,16 @@ function genDaily(){ const d = document.getElementById("day_date").value; if(!d){alert("请选择日期");return;} fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`}) - .then(r=>r.json()).then(data=>{ + .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) + .then(data=>{ const el=document.getElementById("daily_result"); const wrap=document.getElementById("daily_result_wrap"); setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else { el.style.display="block"; } loadReviews(); - }); + }) + .catch(e=>alert("生成日复盘失败:"+(e.message||e))); } function genWeekly(){ @@ -1214,14 +1216,16 @@ function genWeekly(){ const e=document.getElementById("week_end").value; if(!s || !e){alert("请选择起止日期");return;} fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`}) - .then(r=>r.json()).then(data=>{ + .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) + .then(data=>{ const el=document.getElementById("weekly_result"); const wrap=document.getElementById("weekly_result_wrap"); setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else { el.style.display="block"; } loadReviews(); - }); + }) + .catch(e=>alert("生成周复盘失败:"+(e.message||e))); } function exportDailyBundleMd(){ diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index aad37f2..543c2f0 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -7757,6 +7757,7 @@ def ai_daily_review(): 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, @@ -7792,6 +7793,7 @@ def ai_weekly_review(): 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, diff --git a/crypto_monitor_okx/templates/index.html b/crypto_monitor_okx/templates/index.html index c7d87bc..5c01276 100644 --- a/crypto_monitor_okx/templates/index.html +++ b/crypto_monitor_okx/templates/index.html @@ -1245,14 +1245,16 @@ function genDaily(){ const d = document.getElementById("day_date").value; if(!d){alert("请选择日期");return;} fetch("/ai_daily_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`date=${encodeURIComponent(d)}`}) - .then(r=>r.json()).then(data=>{ + .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) + .then(data=>{ const el=document.getElementById("daily_result"); const wrap=document.getElementById("daily_result_wrap"); setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else { el.style.display="block"; } loadReviews(); - }); + }) + .catch(e=>alert("生成日复盘失败:"+(e.message||e))); } function genWeekly(){ @@ -1260,14 +1262,16 @@ function genWeekly(){ const e=document.getElementById("week_end").value; if(!s || !e){alert("请选择起止日期");return;} fetch("/ai_weekly_review",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`start_date=${encodeURIComponent(s)}&end_date=${encodeURIComponent(e)}`}) - .then(r=>r.json()).then(data=>{ + .then(r=>{ if(!r.ok) throw new Error("HTTP "+r.status); return r.json(); }) + .then(data=>{ const el=document.getElementById("weekly_result"); const wrap=document.getElementById("weekly_result_wrap"); setAiReviewMarkdown(el, data.result); if(wrap){ wrap.style.display="block"; } else { el.style.display="block"; } loadReviews(); - }); + }) + .catch(e=>alert("生成周复盘失败:"+(e.message||e))); } function exportDailyBundleMd(){ diff --git a/tests/test_ai_review_lib.py b/tests/test_ai_review_lib.py index 717139b..beb7690 100644 --- a/tests/test_ai_review_lib.py +++ b/tests/test_ai_review_lib.py @@ -1,6 +1,7 @@ """AI 复盘 journal 文本格式化(四所共用)。""" from __future__ import annotations +import sqlite3 import sys import unittest from pathlib import Path @@ -36,6 +37,27 @@ class TestAiReviewLib(unittest.TestCase): self.assertIn("备注:测试备注", text) self.assertNotIn("开仓类型", text) + def test_journal_row_accepts_sqlite_row(self): + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + conn.execute( + """CREATE TABLE journal_entries ( + coin TEXT, tf TEXT, pnl TEXT, real_rr TEXT, expect_rr TEXT, + entry_reason TEXT, exit_reason TEXT, hold_duration TEXT, + mood_issues TEXT, mood_score INTEGER, note TEXT + )""" + ) + conn.execute( + """INSERT INTO journal_entries VALUES (?,?,?,?,?,?,?,?,?,?,?)""", + ("BTC", "15m", "5", "1.2", "2.0", "突破", "止盈", "2小时", "", None, ""), + ) + row = conn.execute("SELECT * FROM journal_entries").fetchone() + conn.close() + text = journal_row_lines_for_ai(1, row) + self.assertIn("BTC 15m", text) + self.assertIn("实际RR:1.2", text) + self.assertIn("开仓逻辑:突破", text) + if __name__ == "__main__": unittest.main()