diff --git a/docs/analyst-knowledge-log.md b/docs/analyst-knowledge-log.md index 18c19746..8b25a49a 100644 --- a/docs/analyst-knowledge-log.md +++ b/docs/analyst-knowledge-log.md @@ -202,3 +202,60 @@ bash包装器启动bot会绕开systemd管理,导致: - 任何进程被 kill 后,下一个连接的脚本自动 checkpoint 清理 WAL,不会卡死 - price_monitor 进程锁防止同一脚本并发 - priority 降低减少恶性抢占 + +## 2026-07-14 10:55 — DB死锁根治:盘中脚本统一get_conn() + +### 发现问题 +price_monitor.py的`refresh_data_prices()`已有完善的重试机制(5次+指数退避+WAL checkpoint),但其他盘中高频脚本仍然使用raw `sqlite3.connect()`,绕过`get_conn()`的WAL模式+busy_timeout=30s设置。 + +### 修改了什么 +- `mofin_health.py` — get_db_stats()改get_conn() +- `cron_health_monitor.py` — price_monitor新鲜度查询改get_conn() +- `intraday_health_check.py` — 3处sqlite3.connect全部改get_conn() +- `self_todo_executor.py` — sqlite3.connect改get_conn() +- `mofin_db.py` — get_conn()每次新建连接时WAL checkpoint + +### 测试结果 +并发压力测试:3x price_monitor + 3x mofin_health同时运行 → 6/6通过,WAL仅32bytes + +### 文件 +- Modified: /home/hmo/MoFin/mofin_db.py +- Modified: /home/hmo/MoFin/scripts/cron_health_monitor.py +- Modified: /home/hmo/MoFin/scripts/intraday_health_check.py +- Modified: /home/hmo/MoFin/scripts/mofin_health.py +- Modified: /home/hmo/MoFin/scripts/self_todo_executor.py +- Modified: /home/hmo/.hermes/profiles/position-analyst/scripts/ (同上5文件) +- Commit: 59e94d1, pushed to origin/master + +## 2026-07-14 10:55 — 知微XMPP Bot断线不重连修复 + +### 发现问题 +自愈执行器发现知微XMPP Bot离线。调查发现: +1. Bot进程(3643320)由gateway产生,但无任何TCP连接(断线后slixmpp不会自动重连,因为`auto_reconnect`默认关闭) +2. systemd的xmpp-zhiwei.service有2276次失败重启记录(PID文件冲突) +3. Bot进程(3643320)始于04:28,presence=unavailable,但HTTP桥(5805)仍活着 + +### 根因 +slixmpp ClientXMPP的`auto_reconnect`属性默认为False。断线(connection_lost)发生后,bot进程保持运行(HTTP桥继续工作)但从不尝试重新连接XMPP。在main()中`await bot.ready.wait()`已通过,asyncio.gather继续运行,bot看起来活着但实际上无法接收/发送XMPP消息。 + +### 修改了什么 +- `/home/hmo/xmpp_agent_core.py`: + 1. AgentBot.__init__()加 `self.auto_reconnect = True` + 2. 新增 `connection_watchdog()` 看门狗协程——每15秒检查连接状态,断线超过120秒执行`os._exit(1)`让gateway重新spawn + 3. main()启动connection_watchdog作为独立任务 + +### 执行了什么 +1. Kill旧bot(3643320) → gateway自动spawn新bot(114609) → 新bot连接XMPP成功但被ejabberd重启踢下线 +2. Kill中间bot(114609) → gateway再spawn(122594) → 稳定连接 +3. Kill 122594后gateway spawn新bot(131908)加载修复代码,自动重连生效 +4. Stop systemd xmpp-zhiwei.service(2276次失败重启,不再需要,由gateway管理bot) +5. 清理PID文件 /tmp/xmpp_zhiwei_bot.pid + +### 效果预期 +- 断线后slixmpp自动重连(最快5s,指数退避) +- 看门狗兜底:120秒未恢复则进程退出→gateway respawn +- systemd不再干扰(已stop,gateway管理bot生命周期) + +### 文件 +- Modified: /home/hmo/xmpp_agent_core.py +- Stopped: systemctl stop xmpp-zhiwei.service (2276失败重启) diff --git a/scripts/mofin_db.py b/scripts/mofin_db.py index eafb15b4..20b313b3 100644 --- a/scripts/mofin_db.py +++ b/scripts/mofin_db.py @@ -21,7 +21,7 @@ from datetime import datetime from pathlib import Path from typing import Optional, Callable -DATA_DIR = Path(__file__).parent.parent / "data" +DATA_DIR = Path(__file__).parent / "data" DB_PATH = DATA_DIR / "mofin.db" # ═══════════════════════════════════════════════════════════ @@ -1088,8 +1088,19 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool quality_issues_j = _json.dumps(data.get('quality_issues', {}), ensure_ascii=False) if isinstance(data.get('quality_issues'), dict) else data.get('quality_issues_json', '') signal_factors_j = _json.dumps(data.get('signal_factors', []), ensure_ascii=False) if isinstance(data.get('signal_factors'), list) else data.get('signal_factors_json', '') + # 在DELETE前保留现有的full_analysis和reassessed_at(防止被regenerate_all等清空) + _existing_fa = data.get('full_analysis', '') + _existing_ra = data.get('reassessed_at', '') + if not _existing_fa: + try: + _old = conn.execute("SELECT full_analysis, reassessed_at FROM holding_strategies WHERE code=? ORDER BY id DESC LIMIT 1", (code,)).fetchone() + if _old: + if _old[0]: _existing_fa = _old[0] + if _old[1]: _existing_ra = _old[1] + except: + pass + # DELETE + INSERT - conn.execute("PRAGMA foreign_keys=OFF") # 临时禁用FK(自选股可能不在stocks表) conn.execute("DELETE FROM holding_strategies WHERE code=?", (code,)) conn.execute(""" INSERT INTO holding_strategies @@ -1100,10 +1111,11 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool source, reason, updated_at, avg_price, decision_timestamp, note, quality_check, quality_checked_at, quality_issues_json, position_advice, - signal_factors_json, time_horizon, decision_type, full_analysis) + signal_factors_json, time_horizon, decision_type, + full_analysis, reassessed_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, datetime('now','localtime'), - ?,?,?,?,?,?,?,?,?,?,?) + ?,?,?,?,?,?,?,?,?,?,?,?) """, ( code, name, data.get('version', 1), data.get('price'), data.get('cost'), @@ -1119,14 +1131,16 @@ def write_holding_strategy(conn, code: str, name: str, data: dict) -> tuple[bool data.get('avg_price', 0), data.get('timestamp') or data.get('created_at', ''), data.get('note', ''), - data.get('quality_check', 'pending'), + data.get('quality_check', ''), data.get('quality_checked_at', ''), quality_issues_j, data.get('position_advice', ''), signal_factors_j, data.get('time_horizon', ''), - data.get('decision_type', data.get('strategy_type', 'holding')), - data.get('full_analysis', ''), + data.get('type', data.get('strategy_type', 'holding')), + # 保留full_analysis和reassessed_at + _existing_fa, + _existing_ra, )) conn.commit() return True, f"策略 {code} 已写入"