fix(ocr+bot): image download race, SenseNova context, log path, encoding

Root causes of the screenshot 404 incident:
1. RACE: client uploads image AND sends message concurrently; bot received
   the message before the upload finished writing, so its GET hit a 404
   error page (<100B treated as failure). FIX: _download_image now retries
   3x with 2s backoff.
2. Zhiwei mentioned tesseract/小果 because the failure text never told her
   the pipeline IS SenseNova. FIX: failure messages now name SenseNova
   explicitly and ask for resend.
3. log_xmpp never worked for the bot: sys.path used relative '../..' from
   a symlinked __file__ which resolved to '/' instead of MoFin root. This
   is why the '最近对话' panel never had bot chat data (only cron script
   entries). FIX: absolute path per red line #7. Verified: test message
   now lands in xmpp_messages.jsonl.
4. My PowerShell -replace corrupted the file encoding (UnicodeDecodeError
   crash loop on restart). Restored from git HEAD and re-applied edits with
   the edit tool. Lesson: never use PowerShell string replace on UTF-8
   source files with Chinese content.
5. functional_health: new sense_ocr module (OCR config presence +
   SenseNova API TCP reachability), no token cost.
This commit is contained in:
hmo
2026-07-20 21:42:59 +08:00
parent 60dbb64f92
commit 299ddc1796
5 changed files with 107 additions and 20 deletions
@@ -50,6 +50,9 @@ REGISTRY = [
{"module": "gateway_llm", "function": "LLM调用链可用(gateway agent.log)",
"check": {"type": "agent_log", "max_age_min": 30, "when": "always"},
"repair": {"action": "llm_diagnose"}},
{"module": "sense_ocr", "function": "识图服务(SenseNova OCR配置+API可达)",
"check": {"type": "ocr_health", "when": "always"},
"repair": {"action": "llm_diagnose"}},
{"module": "xmpp_bot", "function": "XMPP消息收发(bot journal)",
"check": {"type": "bot_activity", "when": "always"},
"repair": {"action": "llm_diagnose"}},
@@ -127,6 +130,31 @@ def check_bot_activity(chk, now):
return "fail", f"检查失败: {e}"
def check_ocr_health(chk, now):
"""识图服务健康:OCR 配置存在 + SenseNova API 可达(TCP 443)。
不发真实 OCR 请求(省钱),真实端到端验证走 K 测试。"""
import json as _json, socket
# 1. OCR 配置存在且含 key
cfg_path = '/home/hmo/.config/mofin/ocr_config.json'
if not os.path.exists(cfg_path):
return "fail", "OCR 配置文件不存在: /home/hmo/.config/mofin/ocr_config.json"
try:
cfg = _json.load(open(cfg_path))
if not cfg.get('key') or not cfg.get('base_url'):
return "fail", "OCR 配置缺 key 或 base_url"
except Exception as e:
return "fail", f"OCR 配置解析失败: {str(e)[:60]}"
# 2. SenseNova API TCP 可达
try:
host = cfg['base_url'].split('//')[1].split('/')[0].split(':')[0]
port = int(cfg['base_url'].split('//')[1].split('/')[0].split(':')[1]) if ':' in cfg['base_url'].split('//')[1].split('/')[0] else 443
s = socket.create_connection((host, port), timeout=5)
s.close()
except Exception as e:
return "fail", f"SenseNova API 不可达 ({host}): {str(e)[:60]}"
return "ok", f"配置 OK + {host}:{port} 可达"
def check_cron_engine(chk, now):
"""cron 引擎:最近 max_age_min 内是否有任何 job 运行过"""
try:
@@ -180,6 +208,8 @@ def main():
status, reason = check_agent_log(chk, now)
elif t == "bot_activity":
status, reason = check_bot_activity(chk, now)
elif t == "ocr_health":
status, reason = check_ocr_health(chk, now)
elif t == "cron_engine":
status, reason = check_cron_engine(chk, now)
else: