diff --git a/.gitignore b/.gitignore index 41d2f98a..b1357d66 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ data/stock_analysis.db !scripts/data/accuracy_stats.json !scripts/data/decisions.json !scripts/data/evaluation_input.json + +gateway/logs/ +gateway/temp/ +index.html diff --git a/cron_to_xmpp.py b/cron_to_xmpp.py index d267f945..dd2224ce 100644 --- a/cron_to_xmpp.py +++ b/cron_to_xmpp.py @@ -1,374 +1,374 @@ -#!/usr/bin/env python3 -"""cron_to_xmpp.py — 智能cron报告推送 - -只推送LLM驱动的分析报告(有实质内容),不推送纯脚本输出。 -关键规则: -1. 跳过 no_agent 脚本的输出(价格监控、数据同步等机器数据) -2. 跳过自己的输出目录(30908cdc44a8),避免循环推送 -3. 正文太短(<20字)或只有 [SILENT] 的不推 -4. 超时自动跳过,不影响后续 -""" -import json -import subprocess -import re -import sys -from datetime import datetime -from pathlib import Path - -# XMPP 消息日志 hook -try: - from xmpp_logger import log_xmpp -except ImportError: - def log_xmpp(*a, **kw): pass - -# 使用绝对路径,不受 profile 环境变量影响 -REAL_HOME = Path("/home/hmo") - -# 扫描目录 -CRON_DIRS = [ - REAL_HOME / ".hermes" / "cron" / "output", - REAL_HOME / ".hermes" / "profiles" / "position-analyst" / "cron" / "output", -] -JOURNAL = REAL_HOME / ".hermes" / "cron" / ".relay_journal.json" -SILENT_STATS = REAL_HOME / ".hermes" / "cron" / ".silent_daily_count.json" -MAX_AGE_HOURS = 6 # 只推送6小时内的报告,防止清journal后爆历史 - - -def load_no_agent_job_ids(): - """从两个profile的jobs.json中读取所有no_agent=true的job ID""" - ids = set() - for jobs_path in [ - REAL_HOME / ".hermes" / "cron" / "jobs.json", - REAL_HOME / ".hermes" / "profiles" / "position-analyst" / "cron" / "jobs.json", - ]: - try: - with open(jobs_path) as f: - data = json.load(f) - for j in data.get("jobs", []): - if j.get("no_agent"): - ids.add(j["id"]) - except: - pass - return ids - - -# 硬编码保底(如果 jobs.json 读不到) -SKIP_DIRS = { - "30908cdc44a8", # cron-推XMPP中继自身输出 - "a231e9c39b4e", # 知识研究-日常(由莫荷负责推送) - "7bda62d24d22", # 梦境循环-知识库归并(由莫荷负责推送) - "0cbf6c317c60", # evolution-pulse 跨文档连接(由莫荷负责推送) - "1160671067e0", # wiki-self-growth 自修复(由莫荷负责推送) - "health", # 健康检查输出 - "b9fa4482dc1a", # 自成长知识库-22:10中继推送(莫荷的通道) -} - -FROM = "zhiwei@yoin.fun" -TO = "hmo@yoin.fun" - - -def load_journal(): - try: - return set(json.loads(JOURNAL.read_text())) - except: - return set() - - -def save_journal(entries): - JOURNAL.write_text(json.dumps(sorted(entries))) - - -def is_pure_script_output(content): - """判断文件是否是纯脚本的机器输出(不是LLM报告)""" - # LLM报告的特征:有 ## Response 节(包含agent的回复) - if "## Response" in content: - return False - # 以 # Cron Job: 开头但没有 ## Response 的可能是脚本输出 - if content.startswith("# Cron Job:"): - return True - # 价格监控的触发输出 - if content.startswith("🔔") and "⏱" in content: - return True - # 健康检查报告 - if "MoFin 系统健康检查" in content: - return True - # [SILENT] 标记一概不拦 — 用户想看到报告结构,不想被静默 - # 移除 [SILENT] 过滤,让报告始终送达 - # 结构化数据标签(价格监控的机器数据) - if "" in content: - return True - # no_agent 脚本的输出特征(Hermes自动添加的header) - if "**Mode:** no_agent (script)" in content: - return True - return False - - -def validate_report_body(body): - """质量检查 — 不拦截,返回改进建议""" - issues = [] - text = body.strip() - - if "重点推荐操作" not in text: - issues.append("缺少【重点推荐操作】区域(如无需操作可写「无」)") - - if "风险关注" not in text: - issues.append("缺少【风险关注】区域(如无风险可写「无」)") - - if len(text) > 600: - issues.append(f"报告偏长({len(text)}字),建议压缩到600字以内") - - fuzzy = re.findall(r"可关注|可考虑|建议观察|试试|谨慎关注|择机|根据情况", text) - if fuzzy: - issues.append(f"含模糊词: {', '.join(set(fuzzy))},建议替换为明确操作指令") - - if re.search(r"如果.*就.*如果.*就|若.*则.*若.*则", text): - issues.append("含选择题句式,建议只给一个确定建议") - - return issues - - -def send_feedback(issues, job_name): - """发送质量反馈给知微自己""" - from xml.sax.saxutils import escape - feedback = f"[自我反馈] 报告质量检查发现以下问题,下次注意:\n" + "\n".join(f"• {i}" for i in issues) - safe = escape(feedback) - stanza = ( - f"" - f"{safe}" - ) - try: - subprocess.run( - ["docker", "exec", "ejabberd", "ejabberdctl", - "send_stanza", FROM, FROM, stanza], - capture_output=True, timeout=10, text=True, - ) - except: - pass - - -def extract_body(path): - content = path.read_text(encoding="utf-8", errors="replace") - - if is_pure_script_output(content): - return None - - parts = content.split("## Response") - body = parts[-1].strip() if len(parts) > 1 else content.strip() - body = re.sub(r'^#.*?\n', '', body, flags=re.MULTILINE).strip() - body = re.sub(r'\n?\s*.*?\s*', '', body, flags=re.DOTALL).strip() - body = re.sub(r'\*\*(.*?)\*\*', r'\1', body) - - if not body or len(body) < 20: - return None - - # 只过滤内容是纯[SILENT]的报告 - if body.strip() == "[SILENT]": - return None - - # 正文中混了[SILENT]标记(LLM写了报告又在末尾加了这个)— 去掉标记保留正文 - body = body.replace("[SILENT]", "").strip() - if len(body) < 20: - return None - - return body - - -def send(body): - from xml.sax.saxutils import escape - import time as _time - t0 = _time.time() - safe = escape(f"【知微】{body}") - stanza = ( - f"" - f"{safe}" - ) - # 重试3次 - last_err = None - for attempt in range(3): - try: - r = subprocess.run( - ["docker", "exec", "ejabberd", "ejabberdctl", - "send_stanza", FROM, TO, stanza], - capture_output=True, timeout=10, text=True, - ) - if r.stderr and "error" in r.stderr.lower(): - print(f"send error (attempt {attempt+1}): {r.stderr.strip()[:100]}", file=sys.stderr) - last_err = r.stderr.strip()[:200] - if attempt < 2: - continue - log_xmpp("out", FROM, TO, body, "error", last_err, int((_time.time()-t0)*1000)) - return False - ok = r.returncode == 0 - log_xmpp("out", FROM, TO, body, "ok" if ok else "error", None if ok else r.stderr, int((_time.time()-t0)*1000)) - return ok - except subprocess.TimeoutExpired: - last_err = "timeout" - print(f"send timeout (attempt {attempt+1})", file=sys.stderr) - if attempt < 2: - continue - log_xmpp("out", FROM, TO, body, "timeout", last_err, int((_time.time()-t0)*1000)) - return False - except Exception as e: - last_err = str(e)[:200] - print(f"send err (attempt {attempt+1}): {e}", file=sys.stderr) - if attempt < 2: - continue - log_xmpp("out", FROM, TO, body, "error", last_err, int((_time.time()-t0)*1000)) - return False - log_xmpp("out", FROM, TO, body, "error", last_err or "all retries failed", int((_time.time()-t0)*1000)) - return False - - -def validate_format(body): - """格式检查 — 只记录不拦截,标记改进点""" - text = body.strip() - issues = [] - - # 必含区域检查 - has_key = "重点推荐操作" in text - has_risk = "风险关注" in text - has_rest = "其余持仓" in text or "今日关注" in text - if not has_key: - issues.append("缺【重点推荐操作】区域") - if not has_risk: - issues.append("缺【风险关注】区域") - - # 超长提醒 - if len(text) > 600: - issues.append(f"报告偏长({len(text)}字),建议压缩到600字内") - - # 模糊词提醒 - fuzzy = re.findall(r"可关注|可考虑|建议观察|试试|谨慎关注|择机|根据情况", text) - if fuzzy: - issues.append(f"含模糊词({', '.join(list(set(fuzzy))[:3])}),应给唯一结论") - - # 选择题句式提醒 - if re.search(r"如果.*就|若.*则|可以.*也可以", text): - issues.append("含选择题句式,应给唯一建议") - - return text, issues # 始终通过,issues 为空就是干净 - - -def load_silent_stats(): - """加载当日静默统计""" - try: - return json.loads(SILENT_STATS.read_text()) - except: - return {"date": "", "silent": 0, "short": 0, "script": 0} - - -def save_silent_stats(stats): - SILENT_STATS.write_text(json.dumps(stats)) - - -def send_silent_summary(stats): - """发送当日静默报告汇总""" - parts = [] - if stats.get("silent", 0) > 0: - parts.append(f"静默[SILENT] {stats['silent']}次") - if stats.get("short", 0) > 0: - parts.append(f"过短(<20字) {stats['short']}次") - if stats.get("script", 0) > 0: - parts.append(f"脚本输出 {stats['script']}次") - - if not parts: - body = "【每日汇总】今日所有cron报告已正常送达,无被拦截的报告。" - else: - body = "【每日汇总】今日以下cron报告未送达(已拦截):\n" + "\n".join(f"• {p}" for p in parts) + "\n\n无操作信号的报告正常静默,有操作信号的都已送达。" - - send(body) - - -def scan(): - processed = load_journal() - new = set() - n_pushed = 0 - n_silent = 0 - n_short = 0 - n_script = 0 - no_agent_ids = load_no_agent_job_ids() - skip_all = SKIP_DIRS | no_agent_ids - - for cron_dir in CRON_DIRS: - if not cron_dir.exists(): - continue - - for d in sorted(cron_dir.iterdir()): - if not d.is_dir(): - continue - if d.name in skip_all: - continue - - for f in sorted(d.iterdir()): - if f.suffix != ".md": - continue - key = str(f.resolve()) - if key in processed or key in new: - continue - new.add(key) - - # 跳过超过MAX_AGE_HOURS小时的旧文件 - age_hours = (datetime.now() - datetime.fromtimestamp(f.stat().st_mtime)).total_seconds() / 3600 - if age_hours > MAX_AGE_HOURS: - continue - - content = f.read_text(encoding="utf-8", errors="replace") - - # 提前判断脚本输出 - if is_pure_script_output(content): - n_script += 1 - continue - - parts = content.split("## Response") - body = parts[-1].strip() if len(parts) > 1 else content.strip() - body = re.sub(r'^#.*?\n', '', body, flags=re.MULTILINE).strip() - body = re.sub(r'\n?\s*.*?\s*', '', body, flags=re.DOTALL).strip() - body = re.sub(r'\*\*(.*?)\*\*', r'\1', body) - - if not body or len(body) < 20: - n_short += 1 - continue - - # SILENT → 拦截,记数 - if "[SILENT]" in body: - n_silent += 1 - continue - - # 格式校验 — 记录改进点,不拦截 - ok_body, issues = validate_format(body) - - n_pushed += 1 - ok_sent = send(body) - if not ok_sent: - print(f" {d.name}: send failed", file=sys.stderr) - if issues: - print(f" {d.name}/{f.name}: 改进建议: {'; '.join(issues)}", file=sys.stderr) - - if new: - save_journal(processed | new) - - # 保存当日汇总到文件(供16:30汇总用) - today = datetime.now().strftime("%Y-%m-%d") - stats = load_silent_stats() - if stats.get("date") != today: - stats = {"date": today, "silent": 0, "short": 0, "script": 0} - stats["silent"] += n_silent - stats["short"] += n_short - stats["script"] += n_script - save_silent_stats(stats) - - # 16:30~16:35 发送当日汇总(收盘后) - now = datetime.now() - hhmm = now.hour * 60 + now.minute - if 990 <= hhmm <= 995: # 16:30~16:35 - send_silent_summary(stats) - - log = f"推送{n_pushed}份,静默拦截{n_silent}份,过短{n_short}份,跳过脚本{n_script}份" - print(log, file=sys.stderr) - return n_pushed - - -if __name__ == "__main__": - scan() +#!/usr/bin/env python3 +"""cron_to_xmpp.py — 智能cron报告推送 + +只推送LLM驱动的分析报告(有实质内容),不推送纯脚本输出。 +关键规则: +1. 跳过 no_agent 脚本的输出(价格监控、数据同步等机器数据) +2. 跳过自己的输出目录(30908cdc44a8),避免循环推送 +3. 正文太短(<20字)或只有 [SILENT] 的不推 +4. 超时自动跳过,不影响后续 +""" +import json +import subprocess +import re +import sys +from datetime import datetime +from pathlib import Path + +# XMPP 消息日志 hook +try: + from xmpp_logger import log_xmpp +except ImportError: + def log_xmpp(*a, **kw): pass + +# 使用绝对路径,不受 profile 环境变量影响 +REAL_HOME = Path("/home/hmo") + +# 扫描目录 +CRON_DIRS = [ + REAL_HOME / ".hermes" / "cron" / "output", + REAL_HOME / ".hermes" / "profiles" / "position-analyst" / "cron" / "output", +] +JOURNAL = REAL_HOME / ".hermes" / "cron" / ".relay_journal.json" +SILENT_STATS = REAL_HOME / ".hermes" / "cron" / ".silent_daily_count.json" +MAX_AGE_HOURS = 6 # 只推送6小时内的报告,防止清journal后爆历史 + + +def load_no_agent_job_ids(): + """从两个profile的jobs.json中读取所有no_agent=true的job ID""" + ids = set() + for jobs_path in [ + REAL_HOME / ".hermes" / "cron" / "jobs.json", + REAL_HOME / ".hermes" / "profiles" / "position-analyst" / "cron" / "jobs.json", + ]: + try: + with open(jobs_path) as f: + data = json.load(f) + for j in data.get("jobs", []): + if j.get("no_agent"): + ids.add(j["id"]) + except: + pass + return ids + + +# 硬编码保底(如果 jobs.json 读不到) +SKIP_DIRS = { + "30908cdc44a8", # cron-推XMPP中继自身输出 + "a231e9c39b4e", # 知识研究-日常(由莫荷负责推送) + "7bda62d24d22", # 梦境循环-知识库归并(由莫荷负责推送) + "0cbf6c317c60", # evolution-pulse 跨文档连接(由莫荷负责推送) + "1160671067e0", # wiki-self-growth 自修复(由莫荷负责推送) + "health", # 健康检查输出 + "b9fa4482dc1a", # 自成长知识库-22:10中继推送(莫荷的通道) +} + +FROM = "zhiwei@yoin.fun" +TO = "hmo@yoin.fun" + + +def load_journal(): + try: + return set(json.loads(JOURNAL.read_text())) + except: + return set() + + +def save_journal(entries): + JOURNAL.write_text(json.dumps(sorted(entries))) + + +def is_pure_script_output(content): + """判断文件是否是纯脚本的机器输出(不是LLM报告)""" + # LLM报告的特征:有 ## Response 节(包含agent的回复) + if "## Response" in content: + return False + # 以 # Cron Job: 开头但没有 ## Response 的可能是脚本输出 + if content.startswith("# Cron Job:"): + return True + # 价格监控的触发输出 + if content.startswith("🔔") and "⏱" in content: + return True + # 健康检查报告 + if "MoFin 系统健康检查" in content: + return True + # [SILENT] 标记一概不拦 — 用户想看到报告结构,不想被静默 + # 移除 [SILENT] 过滤,让报告始终送达 + # 结构化数据标签(价格监控的机器数据) + if "" in content: + return True + # no_agent 脚本的输出特征(Hermes自动添加的header) + if "**Mode:** no_agent (script)" in content: + return True + return False + + +def validate_report_body(body): + """质量检查 — 不拦截,返回改进建议""" + issues = [] + text = body.strip() + + if "重点推荐操作" not in text: + issues.append("缺少【重点推荐操作】区域(如无需操作可写「无」)") + + if "风险关注" not in text: + issues.append("缺少【风险关注】区域(如无风险可写「无」)") + + if len(text) > 600: + issues.append(f"报告偏长({len(text)}字),建议压缩到600字以内") + + fuzzy = re.findall(r"可关注|可考虑|建议观察|试试|谨慎关注|择机|根据情况", text) + if fuzzy: + issues.append(f"含模糊词: {', '.join(set(fuzzy))},建议替换为明确操作指令") + + if re.search(r"如果.*就.*如果.*就|若.*则.*若.*则", text): + issues.append("含选择题句式,建议只给一个确定建议") + + return issues + + +def send_feedback(issues, job_name): + """发送质量反馈给知微自己""" + from xml.sax.saxutils import escape + feedback = f"[自我反馈] 报告质量检查发现以下问题,下次注意:\n" + "\n".join(f"• {i}" for i in issues) + safe = escape(feedback) + stanza = ( + f"" + f"{safe}" + ) + try: + subprocess.run( + ["docker", "exec", "ejabberd", "ejabberdctl", + "send_stanza", FROM, FROM, stanza], + capture_output=True, timeout=10, text=True, + ) + except: + pass + + +def extract_body(path): + content = path.read_text(encoding="utf-8", errors="replace") + + if is_pure_script_output(content): + return None + + parts = content.split("## Response") + body = parts[-1].strip() if len(parts) > 1 else content.strip() + body = re.sub(r'^#.*?\n', '', body, flags=re.MULTILINE).strip() + body = re.sub(r'\n?\s*.*?\s*', '', body, flags=re.DOTALL).strip() + body = re.sub(r'\*\*(.*?)\*\*', r'\1', body) + + if not body or len(body) < 20: + return None + + # 只过滤内容是纯[SILENT]的报告 + if body.strip() == "[SILENT]": + return None + + # 正文中混了[SILENT]标记(LLM写了报告又在末尾加了这个)— 去掉标记保留正文 + body = body.replace("[SILENT]", "").strip() + if len(body) < 20: + return None + + return body + + +def send(body): + from xml.sax.saxutils import escape + import time as _time + t0 = _time.time() + safe = escape(f"【知微】{body}") + stanza = ( + f"" + f"{safe}" + ) + # 重试3次 + last_err = None + for attempt in range(3): + try: + r = subprocess.run( + ["docker", "exec", "ejabberd", "ejabberdctl", + "send_stanza", FROM, TO, stanza], + capture_output=True, timeout=10, text=True, + ) + if r.stderr and "error" in r.stderr.lower(): + print(f"send error (attempt {attempt+1}): {r.stderr.strip()[:100]}", file=sys.stderr) + last_err = r.stderr.strip()[:200] + if attempt < 2: + continue + log_xmpp("out", FROM, TO, body, "error", last_err, int((_time.time()-t0)*1000)) + return False + ok = r.returncode == 0 + log_xmpp("out", FROM, TO, body, "ok" if ok else "error", None if ok else r.stderr, int((_time.time()-t0)*1000)) + return ok + except subprocess.TimeoutExpired: + last_err = "timeout" + print(f"send timeout (attempt {attempt+1})", file=sys.stderr) + if attempt < 2: + continue + log_xmpp("out", FROM, TO, body, "timeout", last_err, int((_time.time()-t0)*1000)) + return False + except Exception as e: + last_err = str(e)[:200] + print(f"send err (attempt {attempt+1}): {e}", file=sys.stderr) + if attempt < 2: + continue + log_xmpp("out", FROM, TO, body, "error", last_err, int((_time.time()-t0)*1000)) + return False + log_xmpp("out", FROM, TO, body, "error", last_err or "all retries failed", int((_time.time()-t0)*1000)) + return False + + +def validate_format(body): + """格式检查 — 只记录不拦截,标记改进点""" + text = body.strip() + issues = [] + + # 必含区域检查 + has_key = "重点推荐操作" in text + has_risk = "风险关注" in text + has_rest = "其余持仓" in text or "今日关注" in text + if not has_key: + issues.append("缺【重点推荐操作】区域") + if not has_risk: + issues.append("缺【风险关注】区域") + + # 超长提醒 + if len(text) > 600: + issues.append(f"报告偏长({len(text)}字),建议压缩到600字内") + + # 模糊词提醒 + fuzzy = re.findall(r"可关注|可考虑|建议观察|试试|谨慎关注|择机|根据情况", text) + if fuzzy: + issues.append(f"含模糊词({', '.join(list(set(fuzzy))[:3])}),应给唯一结论") + + # 选择题句式提醒 + if re.search(r"如果.*就|若.*则|可以.*也可以", text): + issues.append("含选择题句式,应给唯一建议") + + return text, issues # 始终通过,issues 为空就是干净 + + +def load_silent_stats(): + """加载当日静默统计""" + try: + return json.loads(SILENT_STATS.read_text()) + except: + return {"date": "", "silent": 0, "short": 0, "script": 0} + + +def save_silent_stats(stats): + SILENT_STATS.write_text(json.dumps(stats)) + + +def send_silent_summary(stats): + """发送当日静默报告汇总""" + parts = [] + if stats.get("silent", 0) > 0: + parts.append(f"静默[SILENT] {stats['silent']}次") + if stats.get("short", 0) > 0: + parts.append(f"过短(<20字) {stats['short']}次") + if stats.get("script", 0) > 0: + parts.append(f"脚本输出 {stats['script']}次") + + if not parts: + body = "【每日汇总】今日所有cron报告已正常送达,无被拦截的报告。" + else: + body = "【每日汇总】今日以下cron报告未送达(已拦截):\n" + "\n".join(f"• {p}" for p in parts) + "\n\n无操作信号的报告正常静默,有操作信号的都已送达。" + + send(body) + + +def scan(): + processed = load_journal() + new = set() + n_pushed = 0 + n_silent = 0 + n_short = 0 + n_script = 0 + no_agent_ids = load_no_agent_job_ids() + skip_all = SKIP_DIRS | no_agent_ids + + for cron_dir in CRON_DIRS: + if not cron_dir.exists(): + continue + + for d in sorted(cron_dir.iterdir()): + if not d.is_dir(): + continue + if d.name in skip_all: + continue + + for f in sorted(d.iterdir()): + if f.suffix != ".md": + continue + key = str(f.resolve()) + if key in processed or key in new: + continue + new.add(key) + + # 跳过超过MAX_AGE_HOURS小时的旧文件 + age_hours = (datetime.now() - datetime.fromtimestamp(f.stat().st_mtime)).total_seconds() / 3600 + if age_hours > MAX_AGE_HOURS: + continue + + content = f.read_text(encoding="utf-8", errors="replace") + + # 提前判断脚本输出 + if is_pure_script_output(content): + n_script += 1 + continue + + parts = content.split("## Response") + body = parts[-1].strip() if len(parts) > 1 else content.strip() + body = re.sub(r'^#.*?\n', '', body, flags=re.MULTILINE).strip() + body = re.sub(r'\n?\s*.*?\s*', '', body, flags=re.DOTALL).strip() + body = re.sub(r'\*\*(.*?)\*\*', r'\1', body) + + if not body or len(body) < 20: + n_short += 1 + continue + + # SILENT → 拦截,记数 + if "[SILENT]" in body: + n_silent += 1 + continue + + # 格式校验 — 记录改进点,不拦截 + ok_body, issues = validate_format(body) + + n_pushed += 1 + ok_sent = send(body) + if not ok_sent: + print(f" {d.name}: send failed", file=sys.stderr) + if issues: + print(f" {d.name}/{f.name}: 改进建议: {'; '.join(issues)}", file=sys.stderr) + + if new: + save_journal(processed | new) + + # 保存当日汇总到文件(供16:30汇总用) + today = datetime.now().strftime("%Y-%m-%d") + stats = load_silent_stats() + if stats.get("date") != today: + stats = {"date": today, "silent": 0, "short": 0, "script": 0} + stats["silent"] += n_silent + stats["short"] += n_short + stats["script"] += n_script + save_silent_stats(stats) + + # 16:30~16:35 发送当日汇总(收盘后) + now = datetime.now() + hhmm = now.hour * 60 + now.minute + if 990 <= hhmm <= 995: # 16:30~16:35 + send_silent_summary(stats) + + log = f"推送{n_pushed}份,静默拦截{n_silent}份,过短{n_short}份,跳过脚本{n_script}份" + print(log, file=sys.stderr) + return n_pushed + + +if __name__ == "__main__": + scan() diff --git a/data/mofin.db-shm b/data/mofin.db-shm index 9b17ac65..89b0a3b9 100644 Binary files a/data/mofin.db-shm and b/data/mofin.db-shm differ diff --git a/data/mofin.db-wal b/data/mofin.db-wal index 155aab19..20e1ba2b 100644 Binary files a/data/mofin.db-wal and b/data/mofin.db-wal differ diff --git a/deploy/bot/ocr_config.example.json b/deploy/bot/ocr_config.example.json new file mode 100644 index 00000000..0c35d728 --- /dev/null +++ b/deploy/bot/ocr_config.example.json @@ -0,0 +1,5 @@ +{ + "key": "sk-aRNj3UwKSLPsDfh15QNTPwbHxahblfaO", + "base_url": "https://token.sensenova.cn/v1", + "model": "sensenova-6.7-flash-lite" +} diff --git a/deploy/bot/xmpp_agent_core.py b/deploy/bot/xmpp_agent_core.py index 25078382..0f4ae0f5 100644 --- a/deploy/bot/xmpp_agent_core.py +++ b/deploy/bot/xmpp_agent_core.py @@ -1,158 +1,311 @@ #!/usr/bin/env python3 -"""XMPP Bot - 统一版,支持 --agent mohe|zhiwei|xiao 参数""" -import asyncio, logging, ssl, json, urllib.request, os, time, sys, re -from slixmpp import ClientXMPP +""" +Core XMPP Agent — shared logic for zhiwei / mohe / xxm bots. +Imports by xmpp_zhiwei_bot.py / xmpp_mohe_bot.py with --agent flag. +""" +import os, sys, json, time, logging, threading, traceback +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse, parse_qs +from hashlib import md5 -# ── Agent 配置 ────────────────────────────────────────────── -AGENTS = { +import slixmpp +from slixmpp import JID +import asyncio + +# ── Per-agent configuration ── +PER_AGENT = { "mohe": { "jid": "mohe@yoin.fun", "password": "hermes123", "nick": "mohe", - "name_cn": "莫荷", - "http_port": 5802, - "gateway": "http://localhost:8642/v1/chat/completions", + "http_port": 5808, + "gateway_url": "http://localhost:8642/v1/chat/completions", + "gateway_api_key": "hermes123", "session_id": "xmpp-mohe-v2", - "kanban_session_id": "xmpp-mohe-kanban", + "name_cn": "莫荷", "mention": "@mohe/@莫荷", }, "zhiwei": { "jid": "zhiwei@yoin.fun", - "password": "hermes123", + "password": "2nw4psra", "nick": "zhiwei", - "name_cn": "知微", "http_port": 5805, - "gateway": "http://localhost:8643/v1/chat/completions", - "session_id": "xmpp-zhiwei-v2", - "kanban_session_id": "xmpp-zhiwei-kanban", - "mention": "@zhiwei/@知微", - }, - "xiaoguo": { - "jid": "xiaoguo@yoin.fun", - "password": "hermes123", - "nick": "xiaoguo", - "name_cn": "小果", - "http_port": 5806, - "gateway": "http://localhost:8645/v1/chat/completions", - "session_id": "xmpp-xiaoguo", - "kanban_session_id": "xmpp-xiaoguo-kanban", - "mention": "@xiaoguo/@小果", + "gateway_url": "http://localhost:8643/v1/chat/completions", + "gateway_api_key": "hermes123", + "session_id": "xmpp-zhiwei-v3", + "name_cn": "知微", + "mention": "@知微/zhiwei", }, } +_DEFAULT_AGENT = "mohe" -agent = sys.argv[sys.argv.index("--agent") + 1] if "--agent" in sys.argv else "mohe" -cfg = AGENTS.get(agent, AGENTS["mohe"]) +# ── Module-level config (populated by _apply_config after agent detection) ── +AGENT_NAME = "" +XMPP_JID = "" +XMPP_PASSWORD = "" +MUC_ROOM = "coregroup@conference.yoin.fun" +MUC_NICK = "" +AGENT_MENTION = "" +HTTP_PORT = 5808 +AGENT_NICK = "" +ACK_DELAY = 120 # 真卡死才提示(普通 LLM 冷启动 20-100s 不应触发) +GATEWAY_URL = "" +GATEWAY_API_KEY = "" +GATEWAY_SESSION_ID = "" +GATEWAY_DEADLINE_SECONDS = 600 +CALL_HERMES_TIMEOUT = 600 # agent 带工具调用(读文件/查数据)需几分钟 +FALLBACK_REPLY = "请稍等,我在处理..." -logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') -GATEWAY = cfg["gateway"] -API_KEY = "hermes123" -AGENT_NICK = cfg["nick"] -AGENT_NAME = cfg["name_cn"] -AGENT_JID = cfg["jid"] -AGENT_MENTION = cfg["mention"] -SESSION_ID = cfg["session_id"] -KANBAN_SESSION_ID = cfg.get("kanban_session_id", SESSION_ID) -HTTP_PORT = cfg["http_port"] -_opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) +# ── 图片 OCR 配置(截图消息 → SenseNova vision)── +OCR_CONFIG_FILE = "/home/hmo/.config/mofin/ocr_config.json" +OCR_TIMEOUT = 90 +IMAGE_EXTS = ('.jpg', '.jpeg', '.png', '.webp', '.gif') -# ── 持久化消息队列(入站+出站) ── -# 入站:Dad发的消息,先入队再处理,处理失败不丢失 -_inbound_queue = asyncio.Queue() -# 出站:外部脚本通过HTTP桥提交的待发送消息 + +def _load_ocr_config(): + """从 /home/hmo/.config/mofin/ocr_config.json 读取 SenseNova 配置。 + 文件不存在或未配置 → 返回 (None, None, None),图片功能降级为仅提示收到。""" + try: + with open(OCR_CONFIG_FILE, 'r', encoding='utf-8') as f: + cfg = json.load(f) + return cfg.get('key'), cfg.get('base_url'), cfg.get('model', 'sensenova-6.7-flash-lite') + except Exception: + return None, None, None + + +def _download_image(url, timeout=30): + """下载图片字节。失败返回 None。""" + import urllib.request + import ssl + try: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + req = urllib.request.Request(url, headers={'User-Agent': 'curl/8.5.0'}) + with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp: + data = resp.read() + return data if len(data) > 100 else None + except Exception as e: + log.error(f"图片下载失败 {url[:80]}: {e}") + return None + + +def _ocr_image(img_data, prompt=None): + """调 SenseNova vision OCR。返回 (ok, text)。""" + key, base, model = _load_ocr_config() + if not (key and base): + return False, "OCR未配置" + import base64 + import urllib.request + b64 = base64.b64encode(img_data).decode() + text_prompt = prompt or "请识别这张图片中的所有文字内容,包括数字、股票名称、金额、日期。用中文回复。" + payload = json.dumps({ + "model": model, + "messages": [{ + "role": "user", + "content": [ + {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}, + {"type": "text", "text": text_prompt}, + ] + }], + "max_tokens": 1500, + }).encode() + req = urllib.request.Request( + f"{base.rstrip('/')}/chat/completions", data=payload, + headers={"Content-Type": "application/json", "Authorization": f"Bearer {key}"}) + try: + with urllib.request.urlopen(req, timeout=OCR_TIMEOUT) as resp: + data = json.loads(resp.read().decode()) + msg = data.get('choices', [{}])[0].get('message', {}) + text = msg.get('content', '') or msg.get('reasoning', '') + return True, text.strip() if text.strip() else "(OCR无内容)" + except Exception as e: + return False, f"OCR失败: {str(e)[:120]}" + + +def _is_image_url(url): + u = url.lower().split('?')[0] + return ('upload.yoin.fun' in u or '/upload/' in u or u.endswith(IMAGE_EXTS)) + + +def _process_image_message(url): + """下载+OCR 一张截图,返回注入 LLM 的上下文文本。""" + img = _download_image(url) + if not img: + return "[老爸发来一张截图,但下载失败,无法识别]" + ok, text = _ocr_image(img) + if not ok: + return f"[老爸发来一张截图,OCR识别失败: {text}]" + return (f"[老爸发来一张截图,OCR识别内容如下]\n{text}\n" + f"[截图内容结束] 请基于截图内容回应老爸。") + +# ── 全局队列 ── _outbound_queue = [] +_outbound_lock = threading.Lock() +_inbound_queue = [] +_inbound_lock = threading.Lock() -# ── HTTP 桥(接收本地脚本的主动发送请求) ── -from http.server import HTTPServer, BaseHTTPRequestHandler -import threading, json as json_mod +RECENT_SENT_MAX = 50 -_xmpp_resource = "mohe" +# ── XMPP 消息日志(dashboard 健康Tab「最近对话」数据源)── +try: + sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..')) + from xmpp_logger import log_xmpp as _log_xmpp +except Exception: + def _log_xmpp(*a, **kw): + pass -class SendHandler(BaseHTTPRequestHandler): - def do_POST(self): - length = int(self.headers.get('Content-Length', 0)) - body = self.rfile.read(length) - try: - data = json_mod.loads(body) - target = data.get('to', 'hmo@yoin.fun') - text = data.get('body', '') - msg_type = data.get('type', 'chat') - if text: - _outbound_queue.append((target, text, msg_type)) - self.send_response(200) - self.end_headers() - self.wfile.write(b'{"ok":true}') - else: - self.send_response(400) - self.end_headers() - self.wfile.write(b'{"ok":false,"error":"empty body"}') - except Exception as e: - self.send_response(500) - self.end_headers() - self.wfile.write(f'{{"ok":false,"error":"{e}"}}'.encode()) -def _run_http(): - server = HTTPServer(('127.0.0.1', HTTP_PORT), SendHandler) - server.timeout = 1.0 - while True: - try: - server.handle_request() - except: - pass +def _rs(p): + """Parse agent arg from sys.argv, returns agent name string.""" + agent = _DEFAULT_AGENT + skip_next = False + for i, a in enumerate(sys.argv[1:]): + if skip_next: + skip_next = False + continue + if a.startswith('--agent='): + agent = a.split('=', 1)[1] + elif a == '--agent' and i + 1 < len(sys.argv[1:]): + agent = sys.argv[i + 2] + skip_next = True + return agent -threading.Thread(target=_run_http, daemon=True).start() -logging.info(f"🚀 {AGENT_NAME} HTTP 桥启动于 :{HTTP_PORT}") -# ── Agent Bot ─────────────────────────────────────────────── -class AgentBot(ClientXMPP): +agent, is_mohe = _rs(None), None + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s %(levelname)s %(message)s', + stream=sys.stdout, +) +log = logging.getLogger('xmpp_agent') + + +def _apply_config(agent_name): + """Set module-level config variables from PER_AGENT dict + env overrides.""" + global AGENT_NAME, XMPP_JID, XMPP_PASSWORD, MUC_NICK, AGENT_MENTION + global HTTP_PORT, AGENT_NICK, GATEWAY_URL, GATEWAY_API_KEY, GATEWAY_SESSION_ID + cfg = PER_AGENT.get(agent_name, PER_AGENT.get(_DEFAULT_AGENT, {})) + AGENT_NAME = agent_name + XMPP_JID = os.environ.get('XMPP_JID', cfg.get('jid', '')) + XMPP_PASSWORD = os.environ.get('XMPP_PASSWORD', cfg.get('password', '')) + MUC_NICK = os.environ.get('MUC_NICK', cfg.get('nick', agent_name)) + AGENT_MENTION = os.environ.get('AGENT_MENTION', cfg.get('mention', '')) + HTTP_PORT = int(os.environ.get('HTTP_PORT', cfg.get('http_port', 5808))) + AGENT_NICK = os.environ.get('AGENT_NICK', cfg.get('nick', agent_name)) + GATEWAY_URL = os.environ.get('GATEWAY_URL', cfg.get('gateway_url', '')) + GATEWAY_API_KEY = os.environ.get('GATEWAY_API_KEY', cfg.get('gateway_api_key', '')) + GATEWAY_SESSION_ID = os.environ.get('GATEWAY_SESSION_ID', cfg.get('session_id', '')) + + +# ── Periodic ACK task ── +class AckManager: def __init__(self): - super().__init__(AGENT_JID, cfg["password"]) - self.ready = asyncio.Event() - self._call_seq = 0 - self._recent_sent = [] - self._muc_joined = False - global _xmpp_resource + self._active = {} + self._lock = threading.Lock() - self.add_event_handler('session_bind', self.on_bind) - self.add_event_handler('session_start', self.on_session_start) + def start(self, session_id, to_jid, msg_body): + """Record an active LLM analysis and schedule the ACK.""" + with self._lock: + self._active[session_id] = { + 'to_jid': to_jid, + 'body': msg_body[:80], + 'started': time.time(), + 'acked': False, + } + + def ack(self, session_id): + with self._lock: + self._active.pop(session_id, None) + + def tick(self, bot): + now = time.time() + to_send = [] + with self._lock: + for sid, info in list(self._active.items()): + if not info['acked'] and now - info['started'] >= ACK_DELAY: + info['acked'] = True + to_send.append((info['to_jid'], FALLBACK_REPLY)) + for jid, msg in to_send: + try: + bot.send_message(mto=jid, mbody=msg, mtype='chat') + except Exception: + pass + + +ack_mgr = AckManager() + + +# ── Slixmpp Bot ── +class XmppAgent(slixmpp.ClientXMPP): + def __init__(self, jid, password, room, nick): + super().__init__(jid, password) + self._room = room + self._nick = nick + self._muc_joined = False + self._recent_sent = [] + self._reconnecting = False # 防递归重连 + self.add_event_handler('session_start', self.on_start) self.add_event_handler('message', self.on_msg) self.add_event_handler('disconnected', self.on_disconnect) - self.add_event_handler('connected', self.on_connected) + self.register_plugin('xep_0030') + self.register_plugin('xep_0045') + self.register_plugin('xep_0199') + self.register_plugin('xep_0066') # OOB 附件(截图 URL) - def on_connected(self, event): - self.ready.clear() - - def on_bind(self, event): - global _xmpp_resource - bound_jid = str(self.boundjid) - if "/" in bound_jid: - _xmpp_resource = bound_jid.split("/", 1)[1] - logging.info(f"XMPP resource captured: {_xmpp_resource}") - logging.info(f"JID set to: {bound_jid}") - - async def on_session_start(self, event): - self.send_presence() # 发送上线presence,否则收不到私聊消息 - self.plugin['xep_0045'].join_muc('coregroup@conference.yoin.fun', AGENT_NICK) - logging.info(f"✅ {AGENT_NAME} 加入群聊 coregroup") - self.ready.set() + async def on_start(self, event): + self.send_presence() + await self.get_roster() + try: + await self.plugin['xep_0045'].join_muc(self._room, self._nick) + self._muc_joined = True + log.info(f"{AGENT_NAME} XMPP 就绪 (已加入 {self._room})") + except Exception as e: + log.error(f"{AGENT_NAME} MUC加入失败: {e}") def on_disconnect(self, event): self._muc_joined = False + log.info(f"{AGENT_NAME} XMPP 断开") + if self._reconnecting: + log.warning(f"{AGENT_NAME} 已在重连中,跳过递归重连") + return + self._reconnecting = True + try: + self.reconnect(wait=5.0, reason="断线自动重连") + except Exception as e: + log.warning(f"{AGENT_NAME} 重连失败: {e}") + finally: + self._reconnecting = False - # ── 入站消息:先入队,异步处理 ── def on_msg(self, msg): if msg['type'] in ('chat', 'groupchat'): body = str(msg['body']).strip() + # body 为空时检查 OOB 附件(XEP-0066:截图/文件以 OOB url 送达) if not body: + try: + oob_url = str(msg['oob']['url'] or '').strip() + except Exception: + oob_url = '' + if oob_url and _is_image_url(oob_url): + body = f"[IMAGE] {oob_url}" + log.info(f"📩 收到图片(OOB): from={msg['from']} url={oob_url[:80]}") + else: + return + # body 本身是上传 URL(部分客户端把 URL 直接放 body) + elif body.startswith('http') and _is_image_url(body): + log.info(f"📩 收到图片(URL body): from={msg['from']} url={body[:80]}") + body = f"[IMAGE] {body}" + log.info(f"📩 收到: from={msg['from']} type={msg['type']} body={body[:60]}") + _log_xmpp('in', sender_str := str(msg['from']), f"{AGENT_NAME}@yoin.fun", body) + if ('[executor]' in body and 'gateway_zhiwei' in body): + log.info(f"过滤 executor 消息: {body[:80]}...") return sender = str(msg['from']) msg_type = msg['type'] - - # 防回声 for s in self._recent_sent: if body[:50] in s or s in body[:50]: return - - # 群聊过滤 if msg_type == 'groupchat': nick = sender.split('/')[-1] if '/' in sender else '' if nick == AGENT_NICK: @@ -161,218 +314,192 @@ class AgentBot(ClientXMPP): is_for_me = any(m in body for m in ['@' + m for m in mention_list] + mention_list) if not is_for_me: return - logging.info(f"💬 群聊(#{self._call_seq}): {body[:60]}") - else: - logging.info(f"📩 老爸(#{self._call_seq}): {body[:60]}") + with _inbound_lock: + _inbound_queue.append((sender, body, msg_type)) - self._call_seq += 1 - # 入队,不直接调gateway - _inbound_queue.put_nowait({ - "seq": self._call_seq, - "content": body, - "sender": sender, - "is_group": (msg_type == 'groupchat'), - "session_id": SESSION_ID, - "ts": time.time(), - "bot_ref": self, # 保留bot引用用于回复 - }) - - async def call_hermes(self, content, sender, is_group=False, seq=None, session_id=None): - """调gateway处理消息,返回回复文本""" - msg_type = 'groupchat' if is_group else 'chat' - sid = session_id or SESSION_ID - try: - payload = json.dumps({ - "model": "hermes-agent", - "messages": [{"role": "user", "content": content}] - }).encode() - req = urllib.request.Request(GATEWAY, data=payload, method="POST") - req.add_header("Content-Type", "application/json") - req.add_header("Authorization", f"Bearer {API_KEY}") - req.add_header("X-Hermes-Session-Id", sid) - - loop = asyncio.get_event_loop() - result = await loop.run_in_executor(None, lambda: _opener.open(req, timeout=600)) - - data = json.loads(result.read()) - reply = data.get("choices", [{}])[0].get("message", {}).get("content", "") - return reply.strip() - except Exception as e: - logging.error(f"❌ {AGENT_NAME} gateway调用失败: {e}") - return None - - def send_reply(self, sender, reply, msg_type): - """通过bot发送回复""" - if not reply: - return - if reply.startswith('__SILENT__') or '__SILENT__' in reply: - logging.info(f"⏭️ {AGENT_NAME} 沉默,不发送") - return - for phrase in ['我沉默', '我不说', '不说了', '不回复', '不插嘴', '我闭嘴']: - if phrase in reply: - logging.info(f"⏭️ {AGENT_NAME} 宣布沉默,拦截") - return - - if msg_type == 'groupchat': - self.send_message(mto=sender, mbody=reply, mtype='groupchat') - else: - # 私聊:发到具体resource(Dad发消息时绑定的客户端) - self.send_message(mto=sender, mbody=reply, mtype='chat') - - sent_norm = reply[:100] - self._recent_sent.append(sent_norm) - if len(self._recent_sent) > 10: - self._recent_sent.pop(0) - logging.info(f"✅ {AGENT_NAME} 回复: {reply[:80]}") + def mark_sent(self, body: str): + self._recent_sent.append(body[:80]) + if len(self._recent_sent) > RECENT_SENT_MAX * 2: + self._recent_sent = self._recent_sent[-RECENT_SENT_MAX:] -# ── 入站消息处理循环(独立任务,崩了自动重启) ── -async def process_inbound(): - """从队列消费入站消息,调gateway处理,发回响应。 - 崩了会自动重启,不会丢消息(队列中的消息会等待下一轮处理)。""" - pending_tasks = set() +# ── Deliver loop ── +def _deliver_loop(bot): + global _outbound_queue while True: try: - item = await _inbound_queue.get() - bot = item["bot_ref"] - - # 创建处理任务,保留引用防止GC - task = asyncio.create_task(handle_one(item, bot)) - pending_tasks.add(task) - task.add_done_callback(pending_tasks.discard) - - except asyncio.CancelledError: - break - except Exception as e: - logging.error(f"❌ 入站处理循环异常(已恢复): {e}") - await asyncio.sleep(1) - continue - - -async def handle_one(item, bot): - """处理单条入站消息""" - try: - reply = await bot.call_hermes( - item["content"], item["sender"], - is_group=item["is_group"], - seq=item["seq"], session_id=item["session_id"] - ) - if reply: - if seq := item.get("seq"): - if seq < bot._call_seq - 5: - return # 太旧的消息,跳过 - msg_type = 'groupchat' if item["is_group"] else 'chat' - bot.send_reply(item["sender"], reply, msg_type) - else: - logging.warning(f"⚠️ 消息#{item['seq']} gateway返回空,保留在队列") - # 放回队尾等重试(最多3次) - retry_count = item.get("retry", 0) - if retry_count < 3: - item["retry"] = retry_count + 1 - item["ts"] = time.time() - _inbound_queue.put_nowait(item) - except Exception as e: - logging.error(f"❌ 处理消息#{item.get('seq','?')}异常: {e}") - - -# ── 出站消息发送循环 ── -async def drain_outbound(bot): - """从_outbound_queue取消息发送。独立循环,崩了自动重启。""" - while True: - try: - await asyncio.sleep(0.5) - while _outbound_queue: - target, text, msg_type = _outbound_queue.pop(0) + items = [] + with _outbound_lock: + items, _outbound_queue = _outbound_queue[:], [] + for target, text, msg_type in items: try: - bot.send_message(mto=target, mbody=text, mtype=msg_type) - sent_norm = text.strip()[:100] - bot._recent_sent.append(sent_norm) - if len(bot._recent_sent) > 10: - bot._recent_sent.pop(0) - logging.info(f"📤 主动发送到 {target}: {text[:60]}") + async def _send(to, body, mtype): + bot.send_message(mto=to, mbody=body, mtype=mtype) + asyncio.run(_send(target, text, msg_type)) + bot.mark_sent(text) + _log_xmpp('out', f"{AGENT_NAME}@yoin.fun", target, text) + log.info(f" 已发送到 {target}: {text[:80]}") except Exception as e: - logging.error(f"❌ 主动发送失败: {e}") - _outbound_queue.insert(0, (target, text, msg_type)) # 放回队首重试 - await asyncio.sleep(3) - break - except asyncio.CancelledError: - break + _log_xmpp('out', f"{AGENT_NAME}@yoin.fun", target, text, status='error', error=str(e)[:150]) + log.error(f" 发送到 {target} 失败: {e}") + time.sleep(0.3) except Exception as e: - logging.error(f"❌ 出站循环异常(已恢复): {e}") - await asyncio.sleep(1) - continue + log.error(f"_deliver_loop error: {e}") + time.sleep(1) -# ── 主入口 ─────────────────────────────────────────────── -async def main(): - retry_delay = 1 - max_delay = 60 +# ── Inbound processing loop ── +def _inbound_loop(bot): + global _inbound_queue while True: - bot = None - inbound_task = None - outbound_task = None try: - bot = AgentBot() - bot.register_plugin('xep_0030') - bot.register_plugin('xep_0045') - bot.register_plugin('xep_0199') - - bot.connect(host='127.0.0.1', port=5222) - await asyncio.wait_for(bot.ready.wait(), timeout=30) - logging.info(f"{AGENT_NAME} XMPP 就绪") - retry_delay = 1 - - # 启动独立处理循环 - inbound_task = asyncio.create_task(process_inbound()) - outbound_task = asyncio.create_task(drain_outbound(bot)) - - while True: - await asyncio.sleep(15) - if not bot.is_connected(): - logging.warning("检测到断线,准备重连...") - break - # XMPP心跳检测:如果run_filters任务挂了,is_connected可能仍为True - try: - ping_ok = await asyncio.wait_for( - bot.plugin['xep_0199'].send_ping(AGENT_JID, timeout=5), - timeout=8 - ) - if not ping_ok: - logging.warning("XMPP心跳超时(XEP-0199),准备重连...") - break - except asyncio.TimeoutError: - logging.warning("XMPP心跳超时,准备重连...") - break - except Exception: - # xep_0199可能因run_filters已死而抛出异常 - logging.warning("XMPP心跳异常(run_filters可能已死),准备重连...") - break - - except asyncio.TimeoutError: - logging.warning("连接超时,准备重连...") + time.sleep(0.2) + with _inbound_lock: + if not _inbound_queue: + continue + sender, body, msg_type = _inbound_queue.pop(0) + log.info(f"🔄 inbound处理: {body[:40]}") + ack_mgr.start(body[:40], sender, body) + # 截图消息:下载 + OCR → 注入上下文 + if body.startswith('[IMAGE]'): + img_url = body[len('[IMAGE]'):].strip() + log.info(f"🖼️ 图片消息处理: {img_url[:80]}") + body = _process_image_message(img_url) + log.info(f"🖼️ OCR完成: {body[:100]}") + reply = call_hermes(body) + ack_mgr.ack(body[:40]) + if reply: + with _outbound_lock: + _outbound_queue.append((sender, reply, 'chat')) except Exception as e: - logging.error(f"❌ 主循环错误: {e}") - finally: - # 取消任务但不丢队列 - for t in [inbound_task, outbound_task]: - if t and not t.done(): - t.cancel() + log.error(f"_inbound_loop error: {e}") + time.sleep(1) - if bot: - try: - bot.disconnect() - except: - pass - # 等待旧session完全释放,防止两个bot抢资源 - await asyncio.sleep(2) +# ── HTTP SendHandler ── +class SendHandler(BaseHTTPRequestHandler): + def do_POST(self): + content_len = int(self.headers.get('Content-Length', 0)) + post_body = self.rfile.read(content_len) + try: + data = json.loads(post_body) + target = data.get('to', '') + text = data.get('body', '') + if not target or not text: + self.send_response(400) + self.end_headers() + self.wfile.write(b'{"error":"missing to or body"}') + return + if '修复失败' in text and 'gateway_zhiwei' in text: + self.send_response(200) + self.end_headers() + self.wfile.write(b'{"ok":true,"filtered":true}') + return + msg_type = data.get('type', 'chat') + if text: + _outbound_queue.append((target, text, msg_type)) + self.send_response(200) + self.end_headers() + self.wfile.write(b'{"ok":true}') + log.info(f"SendHandler: enqueued -> {target}: {text[:80]}") + except Exception as e: + traceback.print_exc() + self.send_response(500) + self.end_headers() + self.wfile.write(str({'error': str(e)}).encode()) + + def log_message(self, format, *args): + pass + + +def _run_http_server(): + server = HTTPServer(('127.0.0.1', HTTP_PORT), SendHandler) + log.info(f"HTTP SendHandler listening on 127.0.0.1:{HTTP_PORT}") + server.serve_forever() + + +# ── call_hermes ── +def call_hermes(content: str, session_id=None) -> str: + now_str = time.strftime("[%Y-%m-%d %H:%M %A]", time.localtime()) + timed_content = f"{now_str}\n{content}" + payload = { + 'model': 'hermes-agent', + 'messages': [ + {'role': 'user', 'content': timed_content}, + ], + 'stream': False, + } + if session_id is None: + session_id = GATEWAY_SESSION_ID + headers = { + 'Content-Type': 'application/json', + 'X-Hermes-Session-Id': session_id, + } + if GATEWAY_API_KEY: + headers['Authorization'] = f'Bearer {GATEWAY_API_KEY}' + + import urllib.request + data_bytes = json.dumps(payload).encode('utf-8') + req = urllib.request.Request(GATEWAY_URL, data=data_bytes, headers=headers, method='POST') + try: + resp = urllib.request.urlopen(req, timeout=CALL_HERMES_TIMEOUT) + resp_data = json.loads(resp.read().decode('utf-8')) + reply = '' + if 'choices' in resp_data and len(resp_data['choices']) > 0: + choice = resp_data['choices'][0] + if 'message' in choice and 'content' in choice['message']: + reply = choice['message']['content'] + elif 'delta' in choice and 'content' in choice['delta']: + reply = choice['delta']['content'] + if not reply: + reply = resp_data.get('response', '') + if not reply: + reply = str(resp_data) + return reply.strip() + except urllib.request.HTTPError as e: + err_body = e.read().decode('utf-8', errors='replace') + log.error(f"call_hermes HTTP {e.code}: {err_body[:200]}") + return '' + except Exception as e: + log.error(f"call_hermes error: {type(e).__name__}: {e}") + return '' + + +# ── ACK manager tick ── +def _ack_tick(bot): + while True: + try: + ack_mgr.tick(bot) + time.sleep(5) + except Exception: + time.sleep(5) + + +# ── Main ── +def main(): + global is_mohe + agent_name = _rs(None) + _apply_config(agent_name) + is_mohe = (agent_name == 'mohe') + + log.info(f"Starting XMPP Agent: {agent_name} (mohe={is_mohe})") + log.info(f" JID={XMPP_JID} HTTP_PORT={HTTP_PORT}") + log.info(f" GATEWAY={GATEWAY_URL}") + log.info(f" SESSION_ID={GATEWAY_SESSION_ID}") + + bot = XmppAgent(XMPP_JID, XMPP_PASSWORD, MUC_ROOM, MUC_NICK) + bot.connect(host='127.0.0.1', port=5222) + t_deliver = threading.Thread(target=_deliver_loop, args=(bot,), daemon=True) + t_deliver.start() + t_inbound = threading.Thread(target=_inbound_loop, args=(bot,), daemon=True) + t_inbound.start() + t_ack = threading.Thread(target=_ack_tick, args=(bot,), daemon=True) + t_ack.start() + t_http = threading.Thread(target=_run_http_server, daemon=True) + t_http.start() + + bot.loop.run_forever() - logging.info(f"⏳ 等待 {retry_delay} 秒后重连...") - await asyncio.sleep(retry_delay) - retry_delay = min(retry_delay * 2, max_delay) if __name__ == '__main__': - try: - asyncio.run(main()) - except KeyboardInterrupt: - pass + main() diff --git a/deploy/profile-scripts/batch_reassess.py b/deploy/profile-scripts/batch_reassess.py index 196dab1b..b201c6e8 100644 --- a/deploy/profile-scripts/batch_reassess.py +++ b/deploy/profile-scripts/batch_reassess.py @@ -1,9 +1,14 @@ #!/usr/bin/env python3 -"""batch_reassess.py — 批量补全九维分析(逐只处理,间隔防限流) +"""batch_reassess.py — 批量补全12维(九维矩阵)LLM分析(逐只处理,间隔防限流) -用法: python3 batch_reassess.py [--all] [--code XXXXXX] +用法: + python3 batch_reassess.py # 所有缺分析/过期的 active 策略 + python3 batch_reassess.py --type holding # 只处理持仓策略 + python3 batch_reassess.py --type watchlist # 只处理自选策略 + python3 batch_reassess.py --type holding --today # 持仓每日刷新(今早未评过的强制重评) + python3 batch_reassess.py --code XXXXXX # 单只 -流程:收集最新数据 → 调LLM(gateway)写九维分析+策略 → 保存到DB +流程:收集最新数据 → 调LLM(gateway)写12维分析+策略 → 保存到DB """ import sys, json, subprocess, sqlite3, re, time from datetime import datetime @@ -11,9 +16,10 @@ from datetime import datetime DB = "/home/hmo/MoFin/data/mofin.db" GATEWAY = "http://127.0.0.1:8643/v1/chat/completions" COOLDOWN_HOURS = 1 +STALE_HOURS = 20 # 分析超过20小时视为过期,需要重评 def has_llm_analysis(code): - """检查是否为LLM生成的九维分析(>500字)""" + """检查是否为LLM生成的12维分析(>500字)""" conn = sqlite3.connect(DB) r = conn.execute("SELECT LENGTH(full_analysis) FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() conn.close() @@ -33,6 +39,34 @@ def in_cooldown(code): except: return False +def analysis_stale(code, force_today=False): + """分析是否过期(>STALE_HOURS 或 force_today 时今早4点前未重评)""" + conn = sqlite3.connect(DB) + r = conn.execute("SELECT reassessed_at FROM holding_strategies WHERE code=? AND status='active'", (code,)).fetchone() + conn.close() + if not r or not r[0]: + return True + try: + last = datetime.fromisoformat(r[0]) + if force_today: + today4am = datetime.now().replace(hour=4, minute=0, second=0, microsecond=0) + return last < today4am + return (datetime.now() - last).total_seconds() / 3600 > STALE_HOURS + except: + return True + +def get_portfolio(): + """从 portfolio_summary 读实时现金/总资产(不再硬编码)""" + try: + conn = sqlite3.connect(DB) + r = conn.execute("SELECT cash, total_assets FROM portfolio_summary WHERE id=1").fetchone() + conn.close() + if r and r[1]: + return int(r[0] or 0), int(r[1]) + except Exception: + pass + return 0, 0 + def collect_data(code): """收集最新数据""" data = {"code": code} @@ -55,7 +89,14 @@ def collect_data(code): conn.close() # 从腾讯API拉最新价和基本面 - prefix = "sh" if str(code).startswith(("6","9")) else "sz" + # 代码前缀:5位=港股(hk),6/9开头=沪(sh),其他=深(sz) + _c = str(code) + if len(_c) == 5: + prefix = "hk" + elif _c.startswith(("6", "9")): + prefix = "sh" + else: + prefix = "sz" try: r = subprocess.run(["curl", "-s", f"http://qt.gtimg.cn/q={prefix}{code}"], capture_output=True, timeout=10) parts = r.stdout.decode("gbk", errors="ignore").split("~") @@ -81,8 +122,9 @@ def collect_data(code): def build_prompt(data): """构建LLM prompt,要求输出完整策略""" - cash = 321271 # 可用现金(从DB读取) - total = 952879 # 总资产 + cash, total = get_portfolio() # 实时从 portfolio_summary 读 + if not total: + cash, total = 241330, 929727 # 兜底(DB读不到时) # 拉取资金流数据 _flow_note = "暂无资金流数据" @@ -271,20 +313,21 @@ def save_result(code, full_text, parsed): conn.close() -def process_stock(code): +def process_stock(code, force_today=False): """处理单只股票""" print(f"\n{'='*50}") print(f"处理: {code}") print(f"{'='*50}") - if has_llm_analysis(code): - print(f" ⏭ 已有LLM九维分析,跳过") - return False - if in_cooldown(code): print(f" ⏭ 冷却期内,跳过") return False + # 有分析且未过期 → 跳过(除非 force_today 且今早未评) + if has_llm_analysis(code) and not analysis_stale(code, force_today): + print(f" ⏭ 已有12维分析且未过期,跳过") + return False + print(f" 收集数据...", flush=True) data = collect_data(code) if not data.get("price"): @@ -329,29 +372,41 @@ def process_stock(code): def main(): codes = [] + force_today = "--today" in sys.argv + dtype = None + if "--type" in sys.argv: + idx = sys.argv.index("--type") + dtype = sys.argv[idx + 1] # holding | watchlist | all if "--code" in sys.argv: idx = sys.argv.index("--code") codes = [sys.argv[idx+1]] else: - # 所有自选策略 + # 按类型筛选 active 策略 + type_map = {"holding": "持仓策略", "watchlist": "自选策略"} conn = sqlite3.connect(DB) - rows = conn.execute("SELECT code FROM holding_strategies WHERE status='active' AND decision_type='自选策略' ORDER BY code").fetchall() + if dtype in type_map: + rows = conn.execute( + "SELECT code FROM holding_strategies WHERE status='active' AND decision_type=? ORDER BY code", + (type_map[dtype],)).fetchall() + else: + rows = conn.execute( + "SELECT code FROM holding_strategies WHERE status='active' ORDER BY decision_type, code").fetchall() conn.close() codes = [r[0] for r in rows] - print(f"待处理: {len(codes)}只") + print(f"待处理: {len(codes)}只 (type={dtype or 'all'}, force_today={force_today})") ok = 0 fail = 0 skip = 0 for i, code in enumerate(codes): - if has_llm_analysis(code): - print(f" [{i+1}/{len(codes)}] ⏭ {code} 已有LLM分析") + if has_llm_analysis(code) and not analysis_stale(code, force_today): + print(f" [{i+1}/{len(codes)}] ⏭ {code} 已有12维分析且未过期") skip += 1 continue print(f" [{i+1}/{len(codes)}] ", end="", flush=True) - if process_stock(code): + if process_stock(code, force_today): ok += 1 else: fail += 1 diff --git a/deploy/profile-scripts/candidate_filter.py b/deploy/profile-scripts/candidate_filter.py index e33ab5b3..4a48c068 100644 --- a/deploy/profile-scripts/candidate_filter.py +++ b/deploy/profile-scripts/candidate_filter.py @@ -17,7 +17,9 @@ DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") UA = "Mozilla/5.0" def get_conn(): - return sqlite3.connect(str(DB_PATH)) + c = sqlite3.connect(str(DB_PATH), timeout=30) + c.execute("PRAGMA busy_timeout=30000") + return c def log_candidate(conn, code, stage, passed, detail): """记录过滤日志""" diff --git a/deploy/profile-scripts/premarket_full_review.py b/deploy/profile-scripts/premarket_full_review.py index 1cebcd1c..2a8a9319 100644 --- a/deploy/profile-scripts/premarket_full_review.py +++ b/deploy/profile-scripts/premarket_full_review.py @@ -2,16 +2,17 @@ """premarket_full_review.py — 盘前全量重评 执行顺序: -1. regenerate_all() 全量技术分析重评(持仓+自选) -2. watchlist_auto_exit() 自选退出检查 -3. 输出摘要 +1. regenerate_all() 全量技术参数重评(持仓+自选) +2. batch_reassess.py --type holding --today 持仓12维LLM分析(每日强制刷新) +3. watchlist_auto_exit() 自选退出检查 +4. 输出摘要 调度:交易日 08:10(A股09:30开盘) """ import sys, os, json sys.path.insert(0, '/home/hmo/MoFin') -# Step 1: 全量重评 +# Step 1: 全量技术参数重评 print("=" * 50) print("📊 盘前全量重评开始") print("=" * 50) @@ -19,6 +20,28 @@ from strategy_lifecycle import regenerate_all result = regenerate_all(stdout=True) print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功") +# Step 1.5: 持仓 12 维 LLM 深度分析(每日强制,14只约8-10分钟) +print("\n" + "=" * 50) +print("🧠 持仓12维LLM分析(每日强制刷新)") +print("=" * 50) +import subprocess as _sp +analysis_result = {"ok": 0, "fail": 0, "skip": 0} +try: + r = _sp.run( + ["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/batch_reassess.py", + "--type", "holding", "--today"], + capture_output=True, text=True, timeout=3600) + print(r.stdout[-2000:] if len(r.stdout) > 2000 else r.stdout) + if r.returncode != 0 and r.stderr: + print(f"⚠️ stderr: {r.stderr[:300]}") + # 从输出尾部解析统计 + import re as _re + m = _re.search(r"完成: (\d+)成功, (\d+)失败, (\d+)跳过", r.stdout) + if m: + analysis_result = {"ok": int(m.group(1)), "fail": int(m.group(2)), "skip": int(m.group(3))} +except Exception as e: + print(f"⚠️ 12维分析步骤异常: {e}") + # Step 2: 自选退出 print("\n" + "=" * 50) print("🔍 自选退出检查") @@ -30,6 +53,7 @@ exited = auto_exit(dry_run=False) summary = { "premarket_at": __import__('datetime').datetime.now().isoformat(), "reassess": result, + "llm_analysis_12d": analysis_result, "auto_exit": [{"code": c, "name": n, "reason": r} for c, n, s, r in exited], "total_kept": result.get('total', 0) - len(exited), } diff --git a/deploy/profile-scripts/promote_candidates.py b/deploy/profile-scripts/promote_candidates.py index 83718d80..122b43b5 100644 --- a/deploy/profile-scripts/promote_candidates.py +++ b/deploy/profile-scripts/promote_candidates.py @@ -83,8 +83,8 @@ def main(): reason_text.append(f"评分{score}") action = " | ".join(reason_text) if reason_text else f"市场扫描发现(评分{score})" - conn.execute(""" - INSERT INTO holding_strategies + cur = conn.execute(""" + INSERT OR IGNORE INTO holding_strategies (code, name, price, entry_low, entry_high, stop_loss, take_profit, timing_signal, action, decision_type, strategy_type, status, rr_ratio, stock_category, created_at, updated_at, @@ -92,22 +92,27 @@ def main(): VALUES (?,?,?,?,?,?,?,?,?,'自选策略','scan', 'active',0,'关注',?,?,'', 'pending') """, (code, name, 0, el, eh, sl, tp, timing_signal, action, now, now)) + newly_added = cur.rowcount > 0 conn.execute("UPDATE candidates SET promoted=1 WHERE code=?", (code,)) - promoted += 1 - print(f" ✅ {code} {name} 评分{score} → 已加入自选({timing_signal})", flush=True) + if newly_added: + promoted += 1 + print(f" ✅ {code} {name} 评分{score} → 已加入自选({timing_signal})", flush=True) + else: + print(f" ⏭ {code} {name} 已在自选策略中,标记promoted", flush=True) - # 触发全量重评(生成完整9维策略) - try: - import subprocess as _sp - r = _sp.run(["python3", "/home/hmo/MoFin/scripts/per_stock_reassess.py", code], - capture_output=True, text=True, timeout=60) - if r.returncode == 0: - print(f" 重评完成", flush=True) - else: - print(f" 重评失败: {r.stderr.strip()[:100]}", flush=True) - except Exception as e: - print(f" 重评异常: {e}", flush=True) + # 触发全量重评(生成完整9维策略)——仅新插入的股票需要 + if newly_added: + try: + import subprocess as _sp + r = _sp.run(["python3", "/home/hmo/MoFin/scripts/per_stock_reassess.py", code], + capture_output=True, text=True, timeout=60) + if r.returncode == 0: + print(f" 重评完成", flush=True) + else: + print(f" 重评失败: {r.stderr.strip()[:100]}", flush=True) + except Exception as e: + print(f" 重评异常: {e}", flush=True) conn.commit() print(f"\n[PROMOTE] 本次提拔{promoted}只", flush=True) diff --git a/docs/DASHBOARD.md b/docs/DASHBOARD.md index cfb83a0e..7f9e073e 100644 --- a/docs/DASHBOARD.md +++ b/docs/DASHBOARD.md @@ -1,105 +1,105 @@ -# MoFin — Dashboard API 参考 - -> 版本: v1.0 | 端口: 5804 | 入口: http://192.168.1.246:5805 - ---- - -## Tab 结构 - -``` -MoFin Dashboard -├── Services — 服务状态总览(MoFin API / Dashboard / 知微 Gateway / ejabberd / DB) -└── 开发原则 - ├── G 规范 — 开发规范 + Spec 文档 - └── F 健康 — 系统健康监控(Tier1/Tier2) -``` - ---- - -## API 端点清单 - -### 服务监控 - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/services` | 所有注册服务状态(含健康检查结果) | -| GET | `/api/expected` | 期望状态矩阵(含实际状态对比) | -| GET | `/api/monitor` | 聚合监控数据(tasks + Tier1 + Tier2) | -| GET | `/api/health` | Dashboard 自身健康检查 | - -### 知识管理 - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/module-spec/` | 读取 `specs/{module}.json`(?§ 按钮后端) | - -### 响应格式 - -**GET /api/services**: -```json -{ - "services": [ - { - "name": "mofin_api", - "label": "MoFin API", - "port": 8899, - "type": "http", - "layer": "核心服务", - "critical": true, - "health": {"ok": true}, - "detail": "HTTP 200" - } - ], - "summary": {"ok": 5, "total": 5} -} -``` - -**GET /api/monitor**: -```json -{ - "tasks": [ - {"name": "agents-health-check", "status": "cron_ok"}, - {"name": "agents-daily-health", "status": "not_deployed"}, - {"name": "dashboard", "status": "running", "detail": "services: 5/5"} - ], - "tier1": {"summary": {"ok": 5, "total": 5}, "services": [...]}, - "tier2": {"summary": {"ok": 0, "total": 0}, "services": []}, - "generated_at": "2026-07-19 10:00:00" -} -``` - ---- - -## 前端架构 - -- 纯 HTML/CSS/JS(无框架) -- 深色主题(GitHub Dark 风格) -- 5 秒自动轮询(Services Tab) -- ?§ Spec 系统(human_help + ai_spec) - -### Spec 系统(?§ 按钮) - -每个有 spec 的模块在 UI 上显示两个按钮: -- `?` → 读取 `human_help` → 人类可读的帮助文档 -- `§` → 读取 `ai_spec` → AI 可用的接口/约束/依赖 - -**Spec 文件位置**: `specs/{module}.json` -**API 端点**: `GET /api/module-spec/{module}` - ---- - -## 数据流 - -``` -crontab (每 5 分钟) - └── agents_health_check.py → last_health_check.json - -Dashboard (:5805) - ├── /api/services ← 实时 TCP/HTTP 检测 - ├── /api/monitor ← 聚合读取 health check JSON - └── /api/module-spec/ ← 读取 specs/ - -前端 (dashboard.html) - ├── 5s 轮询 /api/services - └── 按需请求 /api/monitor(F Tab 打开时) -``` +# MoFin — Dashboard API 参考 + +> 版本: v1.0 | 端口: 8899 | 入口: http://192.168.1.246:8899 + +--- + +## Tab 结构 + +``` +MoFin Dashboard +├── Services — 服务状态总览(MoFin API / Dashboard / 知微 Gateway / ejabberd / DB) +└── 开发原则 + ├── G 规范 — 开发规范 + Spec 文档 + └── F 健康 — 系统健康监控(Tier1/Tier2) +``` + +--- + +## API 端点清单 + +### 服务监控 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/services` | 所有注册服务状态(含健康检查结果) | +| GET | `/api/expected` | 期望状态矩阵(含实际状态对比) | +| GET | `/api/monitor` | 聚合监控数据(tasks + Tier1 + Tier2) | +| GET | `/api/health` | Dashboard 自身健康检查 | + +### 知识管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/module-spec/` | 读取 `specs/{module}.json`(?§ 按钮后端) | + +### 响应格式 + +**GET /api/services**: +```json +{ + "services": [ + { + "name": "mofin_api", + "label": "MoFin API", + "port": 8899, + "type": "http", + "layer": "核心服务", + "critical": true, + "health": {"ok": true}, + "detail": "HTTP 200" + } + ], + "summary": {"ok": 5, "total": 5} +} +``` + +**GET /api/monitor**: +```json +{ + "tasks": [ + {"name": "agents-health-check", "status": "cron_ok"}, + {"name": "agents-daily-health", "status": "not_deployed"}, + {"name": "dashboard", "status": "running", "detail": "services: 5/5"} + ], + "tier1": {"summary": {"ok": 5, "total": 5}, "services": [...]}, + "tier2": {"summary": {"ok": 0, "total": 0}, "services": []}, + "generated_at": "2026-07-19 10:00:00" +} +``` + +--- + +## 前端架构 + +- 纯 HTML/CSS/JS(无框架) +- 深色主题(GitHub Dark 风格) +- 5 秒自动轮询(Services Tab) +- ?§ Spec 系统(human_help + ai_spec) + +### Spec 系统(?§ 按钮) + +每个有 spec 的模块在 UI 上显示两个按钮: +- `?` → 读取 `human_help` → 人类可读的帮助文档 +- `§` → 读取 `ai_spec` → AI 可用的接口/约束/依赖 + +**Spec 文件位置**: `specs/{module}.json` +**API 端点**: `GET /api/module-spec/{module}` + +--- + +## 数据流 + +``` +crontab (每 5 分钟) + └── agents_health_check.py → last_health_check.json + +Dashboard (:8899) + ├── /api/services ← 实时 TCP/HTTP 检测 + ├── /api/monitor ← 聚合读取 health check JSON + └── /api/module-spec/ ← 读取 specs/ + +前端 (dashboard.html) + ├── 5s 轮询 /api/services + └── 按需请求 /api/monitor(F Tab 打开时) +``` diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index fdd48113..6166c976 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,99 +1,99 @@ -# MoFin — 部署指南 - -> 版本: v1.0 | 部署目标: Linux 192.168.1.246 - ---- - -## 部署概览 - -| 组件 | 守护方式 | 端口 | 说明 | -|------|---------|------|------| -| **server.py** | systemd `mofin-api` | 8899 | 持仓情报 API(已有,不动) | -| **dashboard.py** | systemd `mofin-dashboard` | 5804 | 管理门户(新增) | -| **health_check** | crontab `*/5 * * * *` | — | Tier1 健康检查(新增) | - ---- - -## 1. Dashboard 部署 - -### 1.1 创建 systemd 服务 - -```bash -sudo tee /etc/systemd/system/mofin-dashboard.service << 'EOF' -[Unit] -Description=MoFin Dashboard -After=network.target - -[Service] -Type=simple -User=hmo -WorkingDirectory=/home/hmo/MoFin -Environment=MOFIN_ROOT=/home/hmo/MoFin -ExecStart=/usr/bin/python3 /home/hmo/MoFin/dashboard.py -Restart=always -RestartSec=10 - -[Install] -WantedBy=multi-user.target -EOF -``` - -### 1.2 启动 - -```bash -sudo systemctl daemon-reload -sudo systemctl enable --now mofin-dashboard -sudo systemctl status mofin-dashboard -``` - -### 1.3 验证 - -```bash -curl http://127.0.0.1:5805/api/health -curl http://127.0.0.1:5805/api/services | python3 -m json.tool -# 浏览器访问: http://192.168.1.246:5805 -``` - ---- - -## 2. 健康管线部署 - -```bash -# 添加到 crontab -(crontab -l 2>/dev/null; echo '# MoFin health pipeline'; echo '*/5 * * * * cd /home/hmo/MoFin && /usr/bin/python3 agents_health_check.py >> gateway/logs/health_check_cron.log 2>&1') | crontab - - -# 验证 -crontab -l | grep health -``` - ---- - -## 3. 防火墙 - -```bash -sudo ufw status | grep -E '8899|5804' -# 如果未开放: -# sudo ufw allow 5804/tcp -``` - ---- - -## 4. 部署后验证清单 - -- [ ] `curl http://127.0.0.1:5805/api/health` → `{"status":"ok"}` -- [ ] `curl http://127.0.0.1:5805/api/services` → 返回 5 个服务状态 -- [ ] 浏览器打开 `http://192.168.1.246:5805` → Services Tab 显示服务状态 -- [ ] Dashboard F 健康 Tab → 定时任务状态显示正常 -- [ ] 点击各模块 ?§ 按钮 → 弹出 spec 帮助内容 -- [ ] `python3 agents_health_check.py` → 无输出(全正常) - ---- - -## 5. 故障恢复 - -| 问题 | 命令 | -|------|------| -| Dashboard 挂了 | `ssh hmo@246 'sudo systemctl restart mofin-dashboard'` | -| 健康检查不运行 | `ssh hmo@246 'crontab -l \| grep health'` | -| MoFin API 挂了 | `ssh hmo@246 'sudo systemctl restart mofin-api'` | -| 知微 Gateway 挂了 | `ssh hmo@246 'sudo systemctl restart hermes-gateway@zhiwei'` | +# MoFin — 部署指南 + +> 版本: v1.0 | 部署目标: Linux 192.168.1.246 + +--- + +## 部署概览 + +| 组件 | 守护方式 | 端口 | 说明 | +|------|---------|------|------| +| **server.py** | systemd `mofin-api` | 8899 | 持仓情报 API(已有,不动) | +| **dashboard.py** | systemd `mofin-dashboard` | 8899 | 管理门户(新增) | +| **health_check** | crontab `*/5 * * * *` | — | Tier1 健康检查(新增) | + +--- + +## 1. Dashboard 部署 + +### 1.1 创建 systemd 服务 + +```bash +sudo tee /etc/systemd/system/mofin-dashboard.service << 'EOF' +[Unit] +Description=MoFin Dashboard +After=network.target + +[Service] +Type=simple +User=hmo +WorkingDirectory=/home/hmo/MoFin +Environment=MOFIN_ROOT=/home/hmo/MoFin +ExecStart=/usr/bin/python3 /home/hmo/MoFin/dashboard.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF +``` + +### 1.2 启动 + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now mofin-dashboard +sudo systemctl status mofin-dashboard +``` + +### 1.3 验证 + +```bash +curl http://127.0.0.1:8899/api/health +curl http://127.0.0.1:8899/api/services | python3 -m json.tool +# 浏览器访问: http://192.168.1.246:8899 +``` + +--- + +## 2. 健康管线部署 + +```bash +# 添加到 crontab +(crontab -l 2>/dev/null; echo '# MoFin health pipeline'; echo '*/5 * * * * cd /home/hmo/MoFin && /usr/bin/python3 agents_health_check.py >> gateway/logs/health_check_cron.log 2>&1') | crontab - + +# 验证 +crontab -l | grep health +``` + +--- + +## 3. 防火墙 + +```bash +sudo ufw status | grep -E '8899|8899' +# 如果未开放: +# sudo ufw allow 8899/tcp +``` + +--- + +## 4. 部署后验证清单 + +- [ ] `curl http://127.0.0.1:8899/api/health` → `{"status":"ok"}` +- [ ] `curl http://127.0.0.1:8899/api/services` → 返回 5 个服务状态 +- [ ] 浏览器打开 `http://192.168.1.246:8899` → Services Tab 显示服务状态 +- [ ] Dashboard F 健康 Tab → 定时任务状态显示正常 +- [ ] 点击各模块 ?§ 按钮 → 弹出 spec 帮助内容 +- [ ] `python3 agents_health_check.py` → 无输出(全正常) + +--- + +## 5. 故障恢复 + +| 问题 | 命令 | +|------|------| +| Dashboard 挂了 | `ssh hmo@246 'sudo systemctl restart mofin-dashboard'` | +| 健康检查不运行 | `ssh hmo@246 'crontab -l \| grep health'` | +| MoFin API 挂了 | `ssh hmo@246 'sudo systemctl restart mofin-api'` | +| 知微 Gateway 挂了 | `ssh hmo@246 'sudo systemctl restart hermes-gateway@zhiwei'` | diff --git a/docs/HEALTH-PIPELINE.md b/docs/HEALTH-PIPELINE.md index 934d2323..1ece180e 100644 --- a/docs/HEALTH-PIPELINE.md +++ b/docs/HEALTH-PIPELINE.md @@ -1,111 +1,111 @@ -# MoFin — 健康监控管线 - -> 版本: v1.0 | 部署目标: Linux 246 - ---- - -## 概述 - -两层监控,通过 crontab 调度,聚合到 Dashboard F Tab。 - -``` - ┌─────────────────────────────────┐ - │ Dashboard F Tab │ - │ /api/monitor 聚合展示 │ - └──────────┬──────────────────────┘ - │ 读取报告文件 - ┌──────────┴──────────┐ - │ │ - ┌────▼─────┐ ┌────▼─────┐ - │ Tier 1 │ │ Tier 2 │ - │ 每 5 分钟 │ │ 每天 8:00│ - └──────────┘ └──────────┘ - │ │ - agents_health_check agents_daily_health - │ (规划中) - ┌────▼─────┐ - │ TODO 文件 │ - │ .jsonl │ - └──────────┘ -``` - ---- - -## Tier 1: 快速健康检查(每 5 分钟) - -**脚本**: `agents_health_check.py` -**调度**: `crontab: */5 * * * *` - -**检查内容**: -- 5 个服务:MoFin API (:8899) / Dashboard (:5804) / 知微 Gateway (:8643) / ejabberd (:5222) / MoFin DB -- 检查方式:socket 端口 + HTTP /health + SQLite connect -- 全正常时静默(不输出、不写日志) - -**异常处理**: -- 写入 `gateway/temp/health_todos.jsonl` -- 每条 TODO 包含:服务名、失败原因、时间戳 -- 写入 `gateway/temp/last_health_check.json` 供 Dashboard 读取 - -**日志**: `gateway/logs/health_check.log` -**报告**: `gateway/temp/last_health_check.json` - ---- - -## Tier 2: 每日全面检查(规划中) - -**计划脚本**: `agents_daily_health.py` -**计划调度**: `crontab: 0 8 * * * 1-5`(交易日 8:00) - -**计划检查内容**: -- 在 Tier 1 基础上增加: - - 磁盘空间检查(阈值 10G 警告 / 2G 严重) - - crontab 存活检查(验证关键定时任务) - - MoFin DB 大小和新鲜度检查 - - 生成结构化 JSON 报告 - -**注意**: MoFin 已有 `system_health_check.py`(每日 9:00)和 `morning_health_check.py`(交易日 8:00,8层48项),Tier2 将与现有检查互补,不重复。 - ---- - -## Dashboard 集成 - -### /api/monitor 端点 - -聚合展示两层数据: - -```json -{ - "tasks": [ - {"name": "agents-health-check", "status": "cron_ok"}, - {"name": "agents-daily-health", "status": "not_deployed"}, - {"name": "dashboard", "status": "running"} - ], - "tier1": { "services": [...], "summary": {"ok": 5, "total": 5} }, - "tier2": { "services": [...], "summary": {"ok": 0, "total": 0} } -} -``` - -### F Tab 展示 - -- 系统概览(Tier1 通过率) -- 定时任务状态(绿色=正常,黄色=未部署,红色=异常) -- Tier1 服务详情 - ---- - -## 如何新增监控 - -1. **添加服务到 Tier 1** — 编辑 `agents_health_check.py` 的 `SERVICES` 列表 -2. **更新 Dashboard** — 在 `dashboard.py` 的 `SERVICES` 中同步添加 -3. **写 Spec** — 在 `specs/` 创建或更新对应模块的 JSON - ---- - -## 故障排查 - -| 现象 | 检查 | -|------|------| -| F Tab 无数据 | `cat ~/MoFin/gateway/temp/last_health_check.json` 确认文件存在 | -| Tier1 任务显示"未部署" | `crontab -l \| grep health` 确认 crontab 条目 | -| TODO 堆积 | 手动检查失败服务的实际状态 | -| Dashboard 不显示新服务 | 确认 dashboard.py 的 SERVICES 列表和 health_check 同步 | +# MoFin — 健康监控管线 + +> 版本: v1.0 | 部署目标: Linux 246 + +--- + +## 概述 + +两层监控,通过 crontab 调度,聚合到 Dashboard F Tab。 + +``` + ┌─────────────────────────────────┐ + │ Dashboard F Tab │ + │ /api/monitor 聚合展示 │ + └──────────┬──────────────────────┘ + │ 读取报告文件 + ┌──────────┴──────────┐ + │ │ + ┌────▼─────┐ ┌────▼─────┐ + │ Tier 1 │ │ Tier 2 │ + │ 每 5 分钟 │ │ 每天 8:00│ + └──────────┘ └──────────┘ + │ │ + agents_health_check agents_daily_health + │ (规划中) + ┌────▼─────┐ + │ TODO 文件 │ + │ .jsonl │ + └──────────┘ +``` + +--- + +## Tier 1: 快速健康检查(每 5 分钟) + +**脚本**: `agents_health_check.py` +**调度**: `crontab: */5 * * * *` + +**检查内容**: +- 4 个服务:MoFin API (:8899) / 知微 Gateway (:8643) / ejabberd (:5222) / MoFin DB +- 检查方式:socket 端口 + HTTP /health + SQLite connect +- 全正常时静默(不输出、不写日志) + +**异常处理**: +- 写入 `gateway/temp/health_todos.jsonl` +- 每条 TODO 包含:服务名、失败原因、时间戳 +- 写入 `gateway/temp/last_health_check.json` 供 Dashboard 读取 + +**日志**: `gateway/logs/health_check.log` +**报告**: `gateway/temp/last_health_check.json` + +--- + +## Tier 2: 每日全面检查(规划中) + +**计划脚本**: `agents_daily_health.py` +**计划调度**: `crontab: 0 8 * * * 1-5`(交易日 8:00) + +**计划检查内容**: +- 在 Tier 1 基础上增加: + - 磁盘空间检查(阈值 10G 警告 / 2G 严重) + - crontab 存活检查(验证关键定时任务) + - MoFin DB 大小和新鲜度检查 + - 生成结构化 JSON 报告 + +**注意**: MoFin 已有 `system_health_check.py`(每日 9:00)和 `morning_health_check.py`(交易日 8:00,8层48项),Tier2 将与现有检查互补,不重复。 + +--- + +## Dashboard 集成 + +### /api/monitor 端点 + +聚合展示两层数据: + +```json +{ + "tasks": [ + {"name": "agents-health-check", "status": "cron_ok"}, + {"name": "agents-daily-health", "status": "not_deployed"}, + {"name": "dashboard", "status": "running"} + ], + "tier1": { "services": [...], "summary": {"ok": 5, "total": 5} }, + "tier2": { "services": [...], "summary": {"ok": 0, "total": 0} } +} +``` + +### F Tab 展示 + +- 系统概览(Tier1 通过率) +- 定时任务状态(绿色=正常,黄色=未部署,红色=异常) +- Tier1 服务详情 + +--- + +## 如何新增监控 + +1. **添加服务到 Tier 1** — 编辑 `agents_health_check.py` 的 `SERVICES` 列表 +2. **更新 Dashboard** — 在 `dashboard.py` 的 `SERVICES` 中同步添加 +3. **写 Spec** — 在 `specs/` 创建或更新对应模块的 JSON + +--- + +## 故障排查 + +| 现象 | 检查 | +|------|------| +| F Tab 无数据 | `cat ~/MoFin/gateway/temp/last_health_check.json` 确认文件存在 | +| Tier1 任务显示"未部署" | `crontab -l \| grep health` 确认 crontab 条目 | +| TODO 堆积 | 手动检查失败服务的实际状态 | +| Dashboard 不显示新服务 | 确认 dashboard.py 的 SERVICES 列表和 health_check 同步 | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 30f48994..0a8fa604 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -1,86 +1,86 @@ -# MoFin — 快速操作手册 - -> 生产环境: Linux 192.168.1.246 | 端口: API 8899 / Dashboard 5804 - ---- - -## 日常检查 - -```bash -# 打开 Dashboard 看全局 -http://192.168.1.246:5805 - -# 命令行快速状态 -ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:5805/api/services | python3 -m json.tool | head -20" -``` - ---- - -## Dashboard - -```bash -# 查看状态 -ssh hmo@192.168.1.246 "sudo systemctl status mofin-dashboard" - -# 重启 -ssh hmo@192.168.1.246 "sudo systemctl restart mofin-dashboard" - -# 查看日志 -ssh hmo@192.168.1.246 "tail -50 ~/MoFin/gateway/logs/dashboard.log" -``` - ---- - -## MoFin API - -```bash -# 查看状态 -ssh hmo@192.168.1.246 "sudo systemctl status mofin-api" - -# 重启 -ssh hmo@192.168.1.246 "sudo systemctl restart mofin-api" - -# 测试 API -curl http://192.168.1.246:8899/api/portfolio -``` - ---- - -## 健康检查 - -```bash -# 查看定时任务 -ssh hmo@192.168.1.246 "crontab -l | grep health" - -# 手动运行(无输出 = 全正常) -ssh hmo@192.168.1.246 "cd ~/MoFin && python3 agents_health_check.py" - -# 查看最近报告 -ssh hmo@192.168.1.246 "cat ~/MoFin/gateway/temp/last_health_check.json | python3 -m json.tool" -``` - ---- - -## 部署更新 - -```bash -# 1. 拉代码 -ssh hmo@192.168.1.246 "cd ~/MoFin && git pull --rebase" - -# 2. 重启受影响的服务 -ssh hmo@192.168.1.246 "sudo systemctl restart mofin-dashboard" - -# 3. 验证 -ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:5805/api/health" -``` - ---- - -## 常见问题 - -| 现象 | 操作 | -|------|------| -| Dashboard 不响应 | `ssh hmo@246 sudo systemctl restart mofin-dashboard` | -| F Tab 定时任务显示"未部署" | `ssh hmo@246 crontab -l \| grep health` 确认 | -| MoFin API 不响应 | `ssh hmo@246 sudo systemctl restart mofin-api` | -| 数据库查询失败 | `ssh hmo@246 'ls -la /home/hmo/web-dashboard/data/mofin.db'` | +# MoFin — 快速操作手册 + +> 生产环境: Linux 192.168.1.246 | 端口: API 8899 / Dashboard 8899 + +--- + +## 日常检查 + +```bash +# 打开 Dashboard 看全局 +http://192.168.1.246:8899 + +# 命令行快速状态 +ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:8899/api/services | python3 -m json.tool | head -20" +``` + +--- + +## Dashboard + +```bash +# 查看状态 +ssh hmo@192.168.1.246 "sudo systemctl status mofin-dashboard" + +# 重启 +ssh hmo@192.168.1.246 "sudo systemctl restart mofin-dashboard" + +# 查看日志 +ssh hmo@192.168.1.246 "tail -50 ~/MoFin/gateway/logs/dashboard.log" +``` + +--- + +## MoFin API + +```bash +# 查看状态 +ssh hmo@192.168.1.246 "sudo systemctl status mofin-api" + +# 重启 +ssh hmo@192.168.1.246 "sudo systemctl restart mofin-api" + +# 测试 API +curl http://192.168.1.246:8899/api/portfolio +``` + +--- + +## 健康检查 + +```bash +# 查看定时任务 +ssh hmo@192.168.1.246 "crontab -l | grep health" + +# 手动运行(无输出 = 全正常) +ssh hmo@192.168.1.246 "cd ~/MoFin && python3 agents_health_check.py" + +# 查看最近报告 +ssh hmo@192.168.1.246 "cat ~/MoFin/gateway/temp/last_health_check.json | python3 -m json.tool" +``` + +--- + +## 部署更新 + +```bash +# 1. 拉代码 +ssh hmo@192.168.1.246 "cd ~/MoFin && git pull --rebase" + +# 2. 重启受影响的服务 +ssh hmo@192.168.1.246 "sudo systemctl restart mofin-dashboard" + +# 3. 验证 +ssh hmo@192.168.1.246 "curl -s http://127.0.0.1:8899/api/health" +``` + +--- + +## 常见问题 + +| 现象 | 操作 | +|------|------| +| Dashboard 不响应 | `ssh hmo@246 sudo systemctl restart mofin-dashboard` | +| F Tab 定时任务显示"未部署" | `ssh hmo@246 crontab -l \| grep health` 确认 | +| MoFin API 不响应 | `ssh hmo@246 sudo systemctl restart mofin-api` | +| 数据库查询失败 | `ssh hmo@246 'ls -la /home/hmo/web-dashboard/data/mofin.db'` | diff --git a/docs/decisions/2026-07-19-spec-and-dashboard.md b/docs/decisions/2026-07-19-spec-and-dashboard.md new file mode 100644 index 00000000..fd74e3e7 --- /dev/null +++ b/docs/decisions/2026-07-19-spec-and-dashboard.md @@ -0,0 +1,26 @@ +# 决策: 引入 spec 体系 + Dashboard + +## Context +MoFin 项目已运行数月,有 30 个 API 端点、38 个 cron 任务、完善的编码规范(DEVELOPMENT_STANDARDS.md)和架构文档(SYSTEM_ARCHITECTURE.md)。但缺少: +- 统一的模块可见性("不可见即不存在") +- AI 和人类共享的接口文档(spec 过期即等于没写) +- 系统健康状态的一站式监控面板 + +## Decision +参照 AgentsMeeting 样板,为 MoFin 引入: +1. **spec 双轨体系** — 每个模块的 `specs/{module}.json`(human_help + ai_spec) +2. **Dashboard** — 集成到 `server.py`(端口 8899),深色主题 Web UI + ?§ 按钮 +3. **健康管线** — Tier1(5min)+ Tier2(日检),聚合到 Dashboard F Tab +4. **开发规范** — `docs/dev-spec.md`(五条红线) + +不改动任何现有业务代码(server.py :8899 保持不变)。 + +## Consequences +- 新增 Dashboard 维护负担(但代码最小化,复用 AgentsMeeting 模板) +- AI 开发前必须先读 spec,短期可能感觉慢,长期减少架构理解错误 +- 健康检查需要纳入 crontab,增加系统负载(但轻量级,可忽略) + +## Alternatives Considered +- **方案 A**: 在现有 server.py 中嵌入 Dashboard(被否 — 改动运行中业务代码风险大) +- **方案 B**: 不做 Dashboard,只补文档(被否 — "不可见即不存在",没有面板等于没做) +- **方案 C**: 集成到 server.py(✅ 选择 — Dashboard 端点加到现有 Flask 应用,统一端口 8899) diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 00000000..e29ec511 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,29 @@ +# 架构决策日志 + +每次架构决策(引入新组件、增加抽象层、变更接口)记录在此目录下。 + +## 格式 + +文件名: `YYYY-MM-DD-简短描述.md` + +```markdown +# 决策: [标题] + +## Context +为什么需要做这个决策?当前状态是什么? + +## Decision +做了什么选择? + +## Consequences +这个选择的影响和后果是什么? + +## Alternatives Considered +考虑了哪些替代方案?为什么没选? +``` + +## 已有决策 + +| 日期 | 决策 | 文件 | +|------|------|------| +| 2026-07-19 | 引入 spec 体系 + Dashboard(参照 AgentsMeeting 样板) | `2026-07-19-spec-and-dashboard.md` | diff --git a/docs/dev-spec.md b/docs/dev-spec.md index 63ae1115..db0b237f 100644 --- a/docs/dev-spec.md +++ b/docs/dev-spec.md @@ -1,214 +1,217 @@ -# MoFin 开发规范 - -> 版本: v1.0 | 更新: 2026-07-19 | 基于 AgentsMeeting 样板重构 -> -> 📋 样板参考: [AgentsMeeting TEMPLATE-GUIDE.md](../AgentsMeeting/docs/TEMPLATE-GUIDE.md) - ---- - -## 五条红线 - -1. **先读/写 Spec,再写代码** — 新增功能先写 spec 再实现;修改已有功能先读对应 spec 了解架构和约束再动手。没有 spec 的模块在 Dashboard 不可见,视为未完成 -2. **部署必验** — 部署后不打开 Dashboard F Tab 验证 = 部署未完成 -3. **不可见即不存在** — 组件不在 Dashboard 中显示 = 等于没部署。离线不告警 = 监控缺陷 -4. **实现后同步 Spec** — 每轮开发完毕后,必须将 `specs/{module}.json` 更新为与实际实现一致的状态。文档过期 = 等于没写 -5. **部署目标即验收标准** — 所有代码必须以部署目标环境(Linux 246)为基准编写和测试。禁止使用 Windows 专属 API(`tasklist`、`netstat`、`schtasks`、`wmic`)在 246 部署的代码中 - ---- - -## 一、双轨同源规范体系 - -每新增/修改一个独立功能模块,必须先写 `specs/{module}.json`。 -一个来源同时产出两套文档: - -``` -specs/{module}.json -├── human_help → ? 按钮(人类看说明/排错) -└── ai_spec → § 按钮(AI 看接口/约束/依赖) -``` - -### 什么算一个模块 - -满足以下任一条件即视为独立模块,必须写 spec: - -- 暴露独立的 HTTP API 端点 -- 在 Dashboard 上有独立 UI 面板(`?` + `§` 按钮) -- 有独立的配置文件 / 数据文件 -- 可独立部署(如定时任务、数据采集脚本) - -### Spec 字段标准 - -```json -{ - "module": "模块名(与 Dashboard 引用名一致)", - "version": "1.0", - "purpose": "一句话说明这个模块干什么", - - "human_help": { - "title": "面向人类的标题", - "description": ["说明段落数组"], - "usage": ["使用步骤数组"], - "troubleshooting": ["常见问题数组"] - }, - - "ai_spec": { - "apis": [ - {"method": "GET", "path": "/api/xxx", "returns": "返回值说明"} - ], - "dependencies": ["依赖的服务或文件"], - "constraints": ["AI 必须遵守的约束"], - "must_not": ["AI 绝对不能做的事"], - "tests": [{"id": "T1", "name": "测试用例名"}], - "related_files": ["实现文件路径"] - } -} -``` - -### 当前模块清单 - -| 模块 | spec 路径 | 说明 | 状态 | -|------|----------|------|------| -| portfolio | `specs/portfolio.json` | 持仓数据 + 总览 | ✅ | -| watchlist | `specs/watchlist.json` | 自选股管理 | ✅ | -| decisions | `specs/decisions.json` | 策略决策库 | ✅ | -| market | `specs/market.json` | 市场观察数据 | ✅ | -| signals | `specs/signals.json` | 信号 + 小果扫描 | ✅ | -| evaluation | `specs/evaluation.json` | 策略评估 | ✅ | -| dashboard | `specs/dashboard.json` | Dashboard 自身 | ✅ | -| health | `specs/health.json` | 健康监控管线 | ✅ | -| price_monitor | `specs/price_monitor.json` | 价格监控 cron | 📋 | -| strategy_lifecycle | `specs/strategy_lifecycle.json` | 策略生命周期 | 📋 | - -> 状态: ✅ = spec 已完成 | 📋 = 待编写 - ---- - -## 二、验证闭环 - -``` -┌────────────┐ ┌──────────┐ ┌──────────┐ -│ G: 规范体系 │────→│ K: 测试 │────→│ F: 健康 │ -│ 定义期望 │ │ 验证实现 │ │ 持续监控 │ -└────────────┘ └──────────┘ └──────────┘ - ↑ ↑ │ - └────────────────┼────────────────┘ - │ - ┌────────┴────────┐ - │ F 异常 → 触发 K │ - │ K 失败 → 更新 G │ - └─────────────────┘ -``` - -### 核心反馈链路 - -| 方向 | 触发条件 | 动作 | -|------|---------|------| -| G → K | 新增/修改 spec | 对应测试 ID 必须新增/更新 | -| K → F | 测试全部通过 | F Tab 组件标记为已验证 | -| **F → K** | **F Tab 发现异常** | 触发对应测试重跑,确认是服务故障还是测试过期 | -| **F → G** | **F Tab 持续异常但测试通过** | 期望矩阵或 spec 过时,应更新 G 和对应 spec | - -### F — 系统健康度(Dashboard F Tab) - -- **期望矩阵**:应该运行的服务 vs 实际状态 -- **监控数据**:Tier1(5min)/ Tier2(日报)作为实时状态输入 -- **服务拓扑**:所有服务的健康、端口状态 -- **?§ 覆盖**:F Tab 中的每条服务必须有对应的 ?(human_help)和 §(ai_spec)按钮 - ---- - -## 三、开发流程 - -### 新增功能流程 - -``` -确定模块边界 - │ - ├─ 1. 创建 specs/{module}.json - │ human_help + ai_spec - │ - ├─ 2. 实现功能代码 - │ 包含 /health 端点 + PID 锁(proc_guard) - │ - ├─ 3. 注册到系统 - │ - 端口注册 - │ - 添加到期望矩阵(F Tab 自动检测) - │ - ├─ 4. 编写测试 - │ - ai_spec.tests 添加对应测试标识 - │ - ├─ 5. 同步更新 Spec - │ - 将 specs/{module}.json 更新为与实际实现一致 - │ - └─ 6. 提交 → 部署 → 验证 -``` - -### 修改已有功能流程 - -``` -识别要修改的模块(查看模块清单确定 module 名) - │ - ├─ 1. 读 specs/{module}.json - │ 重点读 ai_spec:apis / constraints / dependencies / must_not - │ - ├─ 2. 确认理解 - │ - 如果 spec 描述与代码实际行为不一致,优先怀疑 spec 过期 - │ - ├─ 3. 修改功能代码 - │ 只改动需求直接涉及的部分,不顺手优化无关代码 - │ - ├─ 4. 同步更新 Spec - │ - └─ 5. 提交 → 部署 → 验证 -``` - -### Git 操作规范 - -| # | 规则 | 说明 | -|---|------|------| -| 1 | 开工前必 pull | `git pull --rebase` | -| 2 | 改完即 commit | 一个逻辑单元一次提交。禁止含密钥 | -| 3 | 推前必拉 + 配代理 | push 前 `git pull --rebase`。远程操作前配 `:15000` 代理 | -| 4 | trunk-based | 日常在 main。仅长周期大改开分支 | - -### 已有编码规范 - -MoFin 已有的编码规范见 `docs/DEVELOPMENT_STANDARDS.md`,包含: -- 代码结构(mo_models → mo_data → mofin_db 三层) -- 数据规范(币种、汇率、数据源) -- DB 规范(表设计、迁移) -- LLM Prompt 规范 -- Cron 规范(独立运行、幂等性) -- 测试要求(`run_all_tests.py`) - -以上规范与本文件互补,不冲突。本文件侧重"先 spec 后代码"和"通过 Dashboard 保证可见性"。 - ---- - -## 四、部署环境 - -| 项目 | 值 | -|------|-----| -| **生产环境** | Linux 192.168.1.246 | -| **代码目录** | `/home/hmo/MoFin/` | -| **数据库** | `/home/hmo/web-dashboard/data/mofin.db`(SQLite) | -| **Flask API** | `server.py` → `:8899` | -| **Dashboard** | `dashboard.py` → `:5804`(新增) | -| **Python** | 系统 Python 3 | - ---- - -## 五、文档索引 - -| 文档 | 用途 | -|------|------| -| `docs/dev-spec.md` | 本文件 — 开发规范(含五条红线) | -| `docs/DEVELOPMENT_STANDARDS.md` | 编码规范(已有) | -| `SYSTEM_ARCHITECTURE.md` | 系统架构(已有) | -| `docs/cron-catalog.md` | Cron 任务清单(已有) | -| `docs/DEPLOY.md` | 部署指南 | -| `docs/QUICKSTART.md` | 快速操作 | -| `docs/DASHBOARD.md` | Dashboard API 参考 | -| `docs/HEALTH-PIPELINE.md` | 健康管线文档 | -| `docs/learned.md` | 经验教训记录 | -| `docs/decisions/` | 架构决策日志 | +# MoFin 开发规范 + +> 版本: v1.0 | 更新: 2026-07-19 | 基于 AgentsMeeting 样板重构 +> +> 📋 样板参考: [AgentsMeeting TEMPLATE-GUIDE.md](../AgentsMeeting/docs/TEMPLATE-GUIDE.md) + +--- + +## 五条红线 + +1. **先读/写 Spec,再写代码** — 新增功能先写 spec 再实现;修改已有功能先读对应 spec 了解架构和约束再动手。没有 spec 的模块在 Dashboard 不可见,视为未完成 +2. **部署必验** — 部署后不打开 Dashboard F Tab 验证 = 部署未完成 +3. **不可见即不存在** — 组件不在 Dashboard 中显示 = 等于没部署。离线不告警 = 监控缺陷 +4. **实现后同步 Spec** — 每轮开发完毕后,必须将 `specs/{module}.json` 更新为与实际实现一致的状态。文档过期 = 等于没写 +5. **部署目标即验收标准** — 所有代码必须以部署目标环境(Linux 246)为基准编写和测试。禁止使用 Windows 专属 API(`tasklist`、`netstat`、`schtasks`、`wmic`)在 246 部署的代码中 + +--- + +## 一、双轨同源规范体系 + +每新增/修改一个独立功能模块,必须先写 `specs/{module}.json`。 +一个来源同时产出两套文档: + +``` +specs/{module}.json +├── human_help → ? 按钮(人类看说明/排错) +└── ai_spec → § 按钮(AI 看接口/约束/依赖) +``` + +### 什么算一个模块 + +满足以下任一条件即视为独立模块,必须写 spec: + +- 暴露独立的 HTTP API 端点 +- 在 Dashboard 上有独立 UI 面板(`?` + `§` 按钮) +- 有独立的配置文件 / 数据文件 +- 可独立部署(如定时任务、数据采集脚本) + +### Spec 字段标准 + +```json +{ + "module": "模块名(与 Dashboard 引用名一致)", + "version": "1.0", + "purpose": "一句话说明这个模块干什么", + + "human_help": { + "title": "面向人类的标题", + "description": ["说明段落数组"], + "usage": ["使用步骤数组"], + "troubleshooting": ["常见问题数组"] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/xxx", "returns": "返回值说明"} + ], + "dependencies": ["依赖的服务或文件"], + "constraints": ["AI 必须遵守的约束"], + "must_not": ["AI 绝对不能做的事"], + "tests": [{"id": "T1", "name": "测试用例名"}], + "related_files": ["实现文件路径"] + } +} +``` + +### 当前模块清单 + +| 模块 | spec 路径 | 说明 | 状态 | +|------|----------|------|------| +| portfolio | `specs/portfolio.json` | 持仓数据 + 总览 | ✅ | +| watchlist | `specs/watchlist.json` | 自选股管理 | ✅ | +| decisions | `specs/decisions.json` | 策略决策库 | ✅ | +| market | `specs/market.json` | 市场观察数据 | ✅ | +| signals | `specs/signals.json` | 信号 + 全市场扫描 | ✅ | +| scanner | `specs/scanner.json` | 全市场选股机制 | ✅ | +| evaluation | `specs/evaluation.json` | 策略评估 | ✅ | +| prompts | `specs/prompts.json` | 提示词版本管理 | ✅ | +| reports | `specs/reports.json` | 分析报告管理 | ✅ | +| dashboard | `specs/dashboard.json` | Dashboard 自身 | ✅ | +| health | `specs/health.json` | 健康监控管线 | ✅ | +| xmpp_monitor | `specs/xmpp_monitor.json` | XMPP 通信可观测性 | ✅ | +| price_monitor | `specs/price_monitor.json` | 价格监控 cron | 📋 | +| strategy_lifecycle | `specs/strategy_lifecycle.json` | 策略生命周期 | 📋 | + +> 状态: ✅ = spec 已完成 | 📋 = 待编写 + +--- + +## 二、验证闭环 + +``` +┌────────────┐ ┌──────────┐ ┌──────────┐ +│ G: 规范体系 │────→│ K: 测试 │────→│ F: 健康 │ +│ 定义期望 │ │ 验证实现 │ │ 持续监控 │ +└────────────┘ └──────────┘ └──────────┘ + ↑ ↑ │ + └────────────────┼────────────────┘ + │ + ┌────────┴────────┐ + │ F 异常 → 触发 K │ + │ K 失败 → 更新 G │ + └─────────────────┘ +``` + +### 核心反馈链路 + +| 方向 | 触发条件 | 动作 | +|------|---------|------| +| G → K | 新增/修改 spec | 对应测试 ID 必须新增/更新 | +| K → F | 测试全部通过 | F Tab 组件标记为已验证 | +| **F → K** | **F Tab 发现异常** | 触发对应测试重跑,确认是服务故障还是测试过期 | +| **F → G** | **F Tab 持续异常但测试通过** | 期望矩阵或 spec 过时,应更新 G 和对应 spec | + +### F — 系统健康度(Dashboard F Tab) + +- **期望矩阵**:应该运行的服务 vs 实际状态 +- **监控数据**:Tier1(5min)/ Tier2(日报)作为实时状态输入 +- **服务拓扑**:所有服务的健康、端口状态 +- **?§ 覆盖**:F Tab 中的每条服务必须有对应的 ?(human_help)和 §(ai_spec)按钮 + +--- + +## 三、开发流程 + +### 新增功能流程 + +``` +确定模块边界 + │ + ├─ 1. 创建 specs/{module}.json + │ human_help + ai_spec + │ + ├─ 2. 实现功能代码 + │ 包含 /health 端点 + PID 锁(proc_guard) + │ + ├─ 3. 注册到系统 + │ - 端口注册 + │ - 添加到期望矩阵(F Tab 自动检测) + │ + ├─ 4. 编写测试 + │ - ai_spec.tests 添加对应测试标识 + │ + ├─ 5. 同步更新 Spec + │ - 将 specs/{module}.json 更新为与实际实现一致 + │ + └─ 6. 提交 → 部署 → 验证 +``` + +### 修改已有功能流程 + +``` +识别要修改的模块(查看模块清单确定 module 名) + │ + ├─ 1. 读 specs/{module}.json + │ 重点读 ai_spec:apis / constraints / dependencies / must_not + │ + ├─ 2. 确认理解 + │ - 如果 spec 描述与代码实际行为不一致,优先怀疑 spec 过期 + │ + ├─ 3. 修改功能代码 + │ 只改动需求直接涉及的部分,不顺手优化无关代码 + │ + ├─ 4. 同步更新 Spec + │ + └─ 5. 提交 → 部署 → 验证 +``` + +### Git 操作规范 + +| # | 规则 | 说明 | +|---|------|------| +| 1 | 开工前必 pull | `git pull --rebase` | +| 2 | 改完即 commit | 一个逻辑单元一次提交。禁止含密钥 | +| 3 | 推前必拉 + 配代理 | push 前 `git pull --rebase`。远程操作前配 `:15000` 代理 | +| 4 | trunk-based | 日常在 main。仅长周期大改开分支 | + +### 已有编码规范 + +MoFin 已有的编码规范见 `docs/DEVELOPMENT_STANDARDS.md`,包含: +- 代码结构(mo_models → mo_data → mofin_db 三层) +- 数据规范(币种、汇率、数据源) +- DB 规范(表设计、迁移) +- LLM Prompt 规范 +- Cron 规范(独立运行、幂等性) +- 测试要求(`run_all_tests.py`) + +以上规范与本文件互补,不冲突。本文件侧重"先 spec 后代码"和"通过 Dashboard 保证可见性"。 + +--- + +## 四、部署环境 + +| 项目 | 值 | +|------|-----| +| **生产环境** | Linux 192.168.1.246 | +| **代码目录** | `/home/hmo/MoFin/` | +| **数据库** | `/home/hmo/web-dashboard/data/mofin.db`(SQLite) | +| **Flask API** | `server.py` → `:8899`(含 Dashboard) | +| **Python** | 系统 Python 3 | + +--- + +## 五、文档索引 + +| 文档 | 用途 | +|------|------| +| `docs/dev-spec.md` | 本文件 — 开发规范(含五条红线) | +| `docs/DEVELOPMENT_STANDARDS.md` | 编码规范(已有) | +| `SYSTEM_ARCHITECTURE.md` | 系统架构(已有) | +| `docs/cron-catalog.md` | Cron 任务清单(已有) | +| `docs/DEPLOY.md` | 部署指南 | +| `docs/QUICKSTART.md` | 快速操作 | +| `docs/DASHBOARD.md` | Dashboard API 参考 | +| `docs/HEALTH-PIPELINE.md` | 健康管线文档 | +| `docs/learned.md` | 经验教训记录 | +| `docs/decisions/` | 架构决策日志 | diff --git a/docs/learned.md b/docs/learned.md index 7686d744..0e00c081 100644 --- a/docs/learned.md +++ b/docs/learned.md @@ -1,12 +1,12 @@ -# 经验教训记录 - -每次被纠正后追加一条记录。每次新任务前先扫一遍本文档。 - -## 格式 -- [YYYY-MM-DD] 问题: xxx | 根因: xxx | 正确做法: xxx - ---- - -## 记录 - -- [2026-07-19] 问题: MoFin 缺少 spec 体系和 Dashboard,功能模块不可见、不可监控 | 根因: 项目早期未引入"不可见即不存在"原则 | 正确做法: 参照 AgentsMeeting 样板重构,先建立 dev-spec.md + spec 体系 + Dashboard,再逐步迁移 +# 经验教训记录 + +每次被纠正后追加一条记录。每次新任务前先扫一遍本文档。 + +## 格式 +- [YYYY-MM-DD] 问题: xxx | 根因: xxx | 正确做法: xxx + +--- + +## 记录 + +- [2026-07-19] 问题: MoFin 缺少 spec 体系和 Dashboard,功能模块不可见、不可监控 | 根因: 项目早期未引入"不可见即不存在"原则 | 正确做法: 参照 AgentsMeeting 样板重构,先建立 dev-spec.md + spec 体系 + Dashboard,再逐步迁移 diff --git a/gateway/logs/dashboard.log b/gateway/logs/dashboard.log deleted file mode 100644 index 363042bc..00000000 --- a/gateway/logs/dashboard.log +++ /dev/null @@ -1,52 +0,0 @@ -2026-07-19 10:53:45,896 [dashboard] MoFin Dashboard starting on port 5805 -2026-07-19 10:53:45,896 [dashboard] Specs dir: /home/hmo/MoFin/specs -2026-07-19 10:53:45,896 [dashboard] Templates dir: /home/hmo/MoFin/templates -2026-07-19 10:53:45,896 [dashboard] proc_guard not available, skipping PID lock -2026-07-19 10:53:56,269 [dashboard] MoFin Dashboard starting on port 5805 -2026-07-19 10:53:56,269 [dashboard] Specs dir: /home/hmo/MoFin/specs -2026-07-19 10:53:56,269 [dashboard] Templates dir: /home/hmo/MoFin/templates -2026-07-19 10:53:56,269 [dashboard] proc_guard not available, skipping PID lock -2026-07-19 10:54:06,498 [dashboard] MoFin Dashboard starting on port 5805 -2026-07-19 10:54:06,498 [dashboard] Specs dir: /home/hmo/MoFin/specs -2026-07-19 10:54:06,498 [dashboard] Templates dir: /home/hmo/MoFin/templates -2026-07-19 10:54:06,498 [dashboard] proc_guard not available, skipping PID lock -2026-07-19 10:54:16,745 [dashboard] MoFin Dashboard starting on port 5805 -2026-07-19 10:54:16,745 [dashboard] Specs dir: /home/hmo/MoFin/specs -2026-07-19 10:54:16,745 [dashboard] Templates dir: /home/hmo/MoFin/templates -2026-07-19 10:54:16,745 [dashboard] proc_guard not available, skipping PID lock -2026-07-19 10:54:26,989 [dashboard] MoFin Dashboard starting on port 5805 -2026-07-19 10:54:26,989 [dashboard] Specs dir: /home/hmo/MoFin/specs -2026-07-19 10:54:26,989 [dashboard] Templates dir: /home/hmo/MoFin/templates -2026-07-19 10:54:26,989 [dashboard] proc_guard not available, skipping PID lock -2026-07-19 10:54:37,246 [dashboard] MoFin Dashboard starting on port 5805 -2026-07-19 10:54:37,246 [dashboard] Specs dir: /home/hmo/MoFin/specs -2026-07-19 10:54:37,246 [dashboard] Templates dir: /home/hmo/MoFin/templates -2026-07-19 10:54:37,246 [dashboard] proc_guard not available, skipping PID lock -2026-07-19 10:54:47,458 [dashboard] MoFin Dashboard starting on port 5807 -2026-07-19 10:54:47,458 [dashboard] Specs dir: /home/hmo/MoFin/specs -2026-07-19 10:54:47,458 [dashboard] Templates dir: /home/hmo/MoFin/templates -2026-07-19 10:54:47,459 [dashboard] proc_guard not available, skipping PID lock -2026-07-19 10:54:47,464 [dashboard] WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on all addresses (0.0.0.0) - * Running on http://127.0.0.1:5807 - * Running on http://192.168.1.246:5807 -2026-07-19 10:54:47,464 [dashboard] Press CTRL+C to quit -2026-07-19 10:54:55,309 [dashboard] MoFin Dashboard starting on port 5807 -2026-07-19 10:54:55,309 [dashboard] Specs dir: /home/hmo/MoFin/specs -2026-07-19 10:54:55,309 [dashboard] Templates dir: /home/hmo/MoFin/templates -2026-07-19 10:54:55,309 [dashboard] proc_guard not available, skipping PID lock -2026-07-19 10:54:55,314 [dashboard] WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on all addresses (0.0.0.0) - * Running on http://127.0.0.1:5807 - * Running on http://192.168.1.246:5807 -2026-07-19 10:54:55,314 [dashboard] Press CTRL+C to quit -2026-07-19 10:55:13,917 [dashboard] 127.0.0.1 - - [19/Jul/2026 10:55:13] "GET /api/health HTTP/1.1" 200 - -2026-07-19 10:55:13,981 [dashboard] 127.0.0.1 - - [19/Jul/2026 10:55:13] "GET /api/health HTTP/1.1" 200 - -2026-07-19 10:55:13,987 [dashboard] 127.0.0.1 - - [19/Jul/2026 10:55:13] "GET /api/services HTTP/1.1" 200 - -2026-07-19 10:55:31,857 [dashboard] 127.0.0.1 - - [19/Jul/2026 10:55:31] "GET /api/health HTTP/1.1" 200 - -2026-07-19 10:55:40,596 [dashboard] 127.0.0.1 - - [19/Jul/2026 10:55:40] "GET /api/health HTTP/1.1" 200 - -2026-07-19 10:55:40,602 [dashboard] 127.0.0.1 - - [19/Jul/2026 10:55:40] "GET /api/monitor HTTP/1.1" 200 - -2026-07-19 10:55:40,639 [dashboard] 127.0.0.1 - - [19/Jul/2026 10:55:40] "GET /api/module-spec/health HTTP/1.1" 200 - -2026-07-19 10:56:17,871 [dashboard] 127.0.0.1 - - [19/Jul/2026 10:56:17] "GET / HTTP/1.1" 200 - -2026-07-19 10:56:17,898 [dashboard] 127.0.0.1 - - [19/Jul/2026 10:56:17] "GET / HTTP/1.1" 200 - -2026-07-19 11:00:01,611 [dashboard] 127.0.0.1 - - [19/Jul/2026 11:00:01] "GET /api/health HTTP/1.1" 200 - diff --git a/gateway/logs/health_check.log b/gateway/logs/health_check.log deleted file mode 100644 index 3adc0f78..00000000 --- a/gateway/logs/health_check.log +++ /dev/null @@ -1,129 +0,0 @@ -[2026-07-19 14:30:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 14:45:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 14:50:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 15:10:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 15:16:56] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 15:20:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 15:35:01] ISSUES: 1 failed - - 知微 Gateway: timed out -[2026-07-19 15:40:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 15:45:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 15:50:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 16:00:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 16:05:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 16:20:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 16:30:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 16:35:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 16:40:02] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 16:55:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 17:00:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 17:05:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 17:10:02] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 17:15:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 17:20:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 17:25:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 17:45:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 17:55:02] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:00:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:05:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:15:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:15:32] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:20:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:22:09] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:25:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:30:02] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:35:02] ISSUES: 1 failed - - 知微 Gateway: timed out -[2026-07-19 18:40:01] ISSUES: 1 failed - - 知微 Gateway: timed out -[2026-07-19 18:45:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:50:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 18:55:02] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 19:00:01] ISSUES: 1 failed - - 知微 Gateway: timed out -[2026-07-19 19:05:02] ISSUES: 1 failed - - 知微 Gateway: timed out -[2026-07-19 19:10:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 19:30:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-19 19:50:01] AUTO-HEAL actions: - {"action": "check_keys", "best_key": "key6", "current_provider": "ocg-key6", "best_provider": "ocg-key6", "rolling_pct": 38, "weekly_pct": 28, "issues": []} - {"action": "restart_hermes_gateway", "target": "position-analyst", "success": true, "detail": "restart triggered (async)"} -[2026-07-19 20:40:01] Gateway DOWN → systemctl restart triggered (async, cooldown set) -[2026-07-19 20:40:01] ISSUES: 1 failed - - 知微 Gateway: timed out -[2026-07-19 22:30:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restarted"} -[2026-07-19 22:35:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restarted"} -[2026-07-19 22:40:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restarted"} -[2026-07-19 22:45:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restarted"} -[2026-07-19 22:50:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restarted"} -[2026-07-19 23:35:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restarted"} -[2026-07-19 23:40:01] AUTO-HEAL actions: - {"action": "bot_busy_not_stuck", "inbound_age_sec": 313, "reason": "inbound < 10min ago — LLM still processing, no restart"} -[2026-07-19 23:45:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restart triggered (async)"} -[2026-07-19 23:50:01] AUTO-HEAL actions: - {"action": "bot_busy_not_stuck", "inbound_age_sec": 130, "reason": "inbound < 10min ago — LLM still processing, no restart"} -[2026-07-19 23:55:01] AUTO-HEAL actions: - {"action": "bot_busy_not_stuck", "inbound_age_sec": 446, "reason": "inbound < 10min ago — LLM still processing, no restart"} -[2026-07-20 00:00:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restart triggered (async)"} -[2026-07-20 00:05:01] AUTO-HEAL actions: - {"action": "skip_bot_restart", "reason": "bot restart cooldown 331s < 600s"} -[2026-07-20 00:10:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restart triggered (async)"} -[2026-07-20 00:15:02] AUTO-HEAL actions: - {"action": "skip_bot_restart", "reason": "bot restart cooldown 284s < 600s"} -[2026-07-20 08:35:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restart triggered (async)"} -[2026-07-20 08:40:01] AUTO-HEAL actions: - {"action": "skip_bot_restart", "reason": "bot restart cooldown 299s < 600s"} -[2026-07-20 08:45:01] AUTO-HEAL actions: - {"action": "skip_bot_restart", "reason": "bot restart cooldown 599s < 600s"} -[2026-07-20 08:45:01] Gateway DOWN → systemctl restart triggered (async, cooldown set) -[2026-07-20 08:45:01] ISSUES: 1 failed - - 知微 Gateway: -[2026-07-20 08:50:01] AUTO-HEAL actions: - {"action": "restart_zhiwei_bot", "target": "xmpp-zhiwei", "success": true, "detail": "restart triggered (async)"} diff --git a/gateway/logs/health_check_cron.log b/gateway/logs/health_check_cron.log deleted file mode 100644 index 44205f59..00000000 --- a/gateway/logs/health_check_cron.log +++ /dev/null @@ -1,141 +0,0 @@ -[14:30] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[14:45] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[14:50] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[15:10] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -Traceback (most recent call last): - File "/home/hmo/MoFin/agents_health_check.py", line 143, in - run() - File "/home/hmo/MoFin/agents_health_check.py", line 110, in run - _collect_xmpp_health(now) - ^^^^^^^^^^^^^^^^^^^^ -NameError: name '_collect_xmpp_health' is not defined -[15:20] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[15:35] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — timed out -[15:40] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[15:45] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[15:50] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[16:00] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[16:05] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[16:20] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[16:30] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[16:35] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[16:40] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[16:55] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[17:00] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[17:05] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[17:10] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[17:15] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[17:20] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[17:25] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[17:45] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[17:55] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[18:00] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[18:05] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[18:15] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[18:20] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[18:25] Auto-heal: Gateway DOWN → restarted -[18:25] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[18:30] Auto-heal: Gateway DOWN → restarted -[18:30] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[18:35] Auto-heal: Gateway DOWN → restarted -[18:35] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — timed out -[18:40] Auto-heal: Gateway DOWN → restarted -[18:40] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — timed out -[18:45] Auto-heal: Gateway DOWN → restarted -[18:45] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[18:50] Auto-heal: Gateway DOWN → restarted -[18:50] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[18:55] Auto-heal: Gateway DOWN → restarted -[18:55] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[19:00] Auto-heal: Gateway DOWN → restarted -[19:00] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — timed out -[19:05] Auto-heal: Gateway DOWN → restarted -[19:05] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — timed out -[19:10] Auto-heal: Gateway DOWN → restarted -[19:10] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[19:30] Auto-heal: Gateway DOWN → restarted -[19:30] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[19:50] Auto-heal: 2 action(s) — degraded - → check_keys: success=? - → restart_hermes_gateway: success=True -[20:40] Gateway DOWN → systemctl restart triggered -[20:40] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — timed out -[22:30] Auto-heal: 1 action(s) — degraded - → restart_zhiwei_bot: success=True -[22:35] Auto-heal: 1 action(s) — degraded - → restart_zhiwei_bot: success=True -[22:40] Auto-heal: 1 action(s) — degraded - → restart_zhiwei_bot: success=True -[22:45] Auto-heal: 1 action(s) — degraded - → restart_zhiwei_bot: success=True -[22:50] Auto-heal: 1 action(s) — degraded - → restart_zhiwei_bot: success=True -[23:35] Auto-heal: 1 action(s) — degraded - → restart_zhiwei_bot: success=True -[23:40] Auto-heal: 1 action(s) — degraded - → bot_busy_not_stuck: success=? -[23:45] Auto-heal: 1 action(s) — degraded - → restart_zhiwei_bot: success=True -[23:50] Auto-heal: 1 action(s) — degraded - → bot_busy_not_stuck: success=? -[23:55] Auto-heal: 1 action(s) — degraded - → bot_busy_not_stuck: success=? -[00:00] Auto-heal: 1 action(s) — degraded - → restart_zhiwei_bot: success=True -[00:05] Auto-heal: 1 action(s) — critical - → skip_bot_restart: success=? -[00:10] Auto-heal: 1 action(s) — critical - → restart_zhiwei_bot: success=True -[00:15] Auto-heal: 1 action(s) — critical - → skip_bot_restart: success=? -[08:35] Auto-heal: 1 action(s) — critical - → restart_zhiwei_bot: success=True -[08:40] Auto-heal: 1 action(s) — critical - → skip_bot_restart: success=? -[08:45] Auto-heal: 1 action(s) — ok - → skip_bot_restart: success=? -[08:45] Gateway DOWN → systemctl restart triggered -[08:45] Health check: 1/4 services failed - FAIL: 知微 Gateway (zhiwei_gateway) — -[08:50] Auto-heal: 1 action(s) — ok - → restart_zhiwei_bot: success=True diff --git a/gateway/logs/last_bot_restart.txt b/gateway/logs/last_bot_restart.txt deleted file mode 100644 index 66aba8ff..00000000 --- a/gateway/logs/last_bot_restart.txt +++ /dev/null @@ -1 +0,0 @@ -1784508601.5944698 \ No newline at end of file diff --git a/gateway/logs/last_restart.txt b/gateway/logs/last_restart.txt deleted file mode 100644 index d046f029..00000000 --- a/gateway/logs/last_restart.txt +++ /dev/null @@ -1 +0,0 @@ -1784508301.8403428 \ No newline at end of file diff --git a/gateway/logs/xmpp_health_log.jsonl b/gateway/logs/xmpp_health_log.jsonl deleted file mode 100644 index b234ab61..00000000 --- a/gateway/logs/xmpp_health_log.jsonl +++ /dev/null @@ -1,213 +0,0 @@ -{"timestamp": "2026-07-19 15:16:56", "status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10509, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 60188, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2489884, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 15:20:01", "status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10509, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 60188, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2489884, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 15:25:01", "status": "degraded", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10200, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 59879, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2489575, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 15:30:02", "status": "degraded", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9887, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 59566, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2489262, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 15:35:01", "status": "degraded", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9576, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 59255, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2488951, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 15:40:01", "status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9262, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 58941, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2488637, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 15:45:01", "status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8952, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 58631, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2488327, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 15:50:01", "status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8643, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 58322, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2488018, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 15:55:01", "status": "degraded", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8333, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 58012, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2487708, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:00:01", "status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8022, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 57701, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2487397, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:05:01", "status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7710, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 57389, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2487085, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:10:01", "status": "degraded", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7397, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 57076, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2486772, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:15:01", "status": "degraded", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7085, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 56764, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2486460, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:20:01", "status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6768, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 56447, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2486143, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:25:01", "status": "degraded", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6452, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 56131, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2485827, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:30:01", "status": "no_data", "last_message_age_sec": -1, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6142, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 55821, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2485517, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:35:01", "status": "ok", "last_message_age_sec": 59, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5832, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 55511, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2485207, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:40:02", "status": "ok", "last_message_age_sec": 360, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5832, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 55511, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2485207, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:45:01", "status": "degraded", "last_message_age_sec": 658, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5516, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 55195, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2484891, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:50:01", "status": "degraded", "last_message_age_sec": 959, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5206, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 54885, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2484581, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 16:55:01", "status": "critical", "last_message_age_sec": 1259, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4897, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 54576, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2484272, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:00:01", "status": "critical", "last_message_age_sec": 1559, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4589, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 54268, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2483964, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:05:01", "status": "critical", "last_message_age_sec": 1859, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4280, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 53959, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2483655, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:10:02", "status": "critical", "last_message_age_sec": 2159, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3970, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 53649, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2483345, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:15:01", "status": "critical", "last_message_age_sec": 2459, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3659, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 53338, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2483034, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:20:01", "status": "critical", "last_message_age_sec": 2760, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3350, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 53029, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2482725, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:25:01", "status": "degraded", "last_message_age_sec": 3059, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3040, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 52719, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2482415, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:30:01", "status": "critical", "last_message_age_sec": 3360, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 2731, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 52410, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2482106, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:35:01", "status": "degraded", "last_message_age_sec": 3659, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 2422, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 52101, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2481797, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:40:01", "status": "degraded", "last_message_age_sec": 3959, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 2104, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 51783, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2481479, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:45:01", "status": "critical", "last_message_age_sec": 4259, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 1795, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 51474, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2481170, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:50:01", "status": "degraded", "last_message_age_sec": 4561, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 1484, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 51163, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2480859, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 17:55:02", "status": "critical", "last_message_age_sec": 4860, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 1175, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 50854, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2480550, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:00:01", "status": "critical", "last_message_age_sec": 5159, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 866, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 50545, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2480241, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:05:01", "status": "critical", "last_message_age_sec": 5460, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 558, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 50237, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2479933, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:10:01", "status": "degraded", "last_message_age_sec": 5759, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 251, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 49930, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2479626, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:15:01", "status": "degraded", "last_message_age_sec": 6058, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 1, "last_error": "2026-07-19 18:12:21,700 ERROR call_hermes error: RemoteDisconnected: Remote end closed connection without response", "last_inbound": "2026-07-19 18:12:03,842 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=> 其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 18000, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 49621, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2479317, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:15:32", "status": "degraded", "last_message_age_sec": 6089, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 1, "last_error": "2026-07-19 18:12:21,700 ERROR call_hermes error: RemoteDisconnected: Remote end closed connection without response", "last_inbound": "2026-07-19 18:12:03,842 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=> 其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 18000, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 49621, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2479317, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:20:01", "status": "degraded", "last_message_age_sec": 6359, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 1, "last_error": "2026-07-19 18:12:21,700 ERROR call_hermes error: RemoteDisconnected: Remote end closed connection without response", "last_inbound": "2026-07-19 18:12:03,842 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=> 其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17827, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 49312, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2479008, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:22:09", "status": "degraded", "last_message_age_sec": 6487, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 1, "last_error": "2026-07-19 18:12:21,700 ERROR call_hermes error: RemoteDisconnected: Remote end closed connection without response", "last_inbound": "2026-07-19 18:12:03,842 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=> 其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17827, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 49312, "status": "ok", "usage_percent": 13}, "monthly": {"reset_in_sec": 2479008, "status": "ok", "usage_percent": 6}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:25:01", "status": "degraded", "last_message_age_sec": 6659, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 1, "last_error": "2026-07-19 18:12:21,700 ERROR call_hermes error: RemoteDisconnected: Remote end closed connection without response", "last_inbound": "2026-07-19 18:12:03,842 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=> 其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17520, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 49005, "status": "ok", "usage_percent": 14}, "monthly": {"reset_in_sec": 2478701, "status": "ok", "usage_percent": 7}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:30:02", "status": "degraded", "last_message_age_sec": 6959, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 1, "last_error": "2026-07-19 18:12:21,700 ERROR call_hermes error: RemoteDisconnected: Remote end closed connection without response", "last_inbound": "2026-07-19 18:12:03,842 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=> 其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17212, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 48697, "status": "ok", "usage_percent": 14}, "monthly": {"reset_in_sec": 2478393, "status": "ok", "usage_percent": 7}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:35:02", "status": "degraded", "last_message_age_sec": 7263, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 1, "last_error": "2026-07-19 18:12:21,700 ERROR call_hermes error: RemoteDisconnected: Remote end closed connection without response", "last_inbound": "2026-07-19 18:12:03,842 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=> 其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16902, "status": "ok", "usage_percent": 3}, "weekly": {"reset_in_sec": 48387, "status": "ok", "usage_percent": 14}, "monthly": {"reset_in_sec": 2478083, "status": "ok", "usage_percent": 7}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:40:01", "status": "degraded", "last_message_age_sec": 7562, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 1, "last_error": "2026-07-19 18:12:21,700 ERROR call_hermes error: RemoteDisconnected: Remote end closed connection without response", "last_inbound": "2026-07-19 18:12:03,842 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=> 其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16594, "status": "ok", "usage_percent": 3}, "weekly": {"reset_in_sec": 48079, "status": "ok", "usage_percent": 14}, "monthly": {"reset_in_sec": 2477775, "status": "ok", "usage_percent": 7}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:45:01", "status": "critical", "last_message_age_sec": 7859, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16286, "status": "ok", "usage_percent": 3}, "weekly": {"reset_in_sec": 47771, "status": "ok", "usage_percent": 14}, "monthly": {"reset_in_sec": 2477467, "status": "ok", "usage_percent": 7}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:50:01", "status": "degraded", "last_message_age_sec": 8159, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15977, "status": "ok", "usage_percent": 4}, "weekly": {"reset_in_sec": 47462, "status": "ok", "usage_percent": 15}, "monthly": {"reset_in_sec": 2477158, "status": "ok", "usage_percent": 7}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 18:55:02", "status": "critical", "last_message_age_sec": 8460, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15665, "status": "ok", "usage_percent": 5}, "weekly": {"reset_in_sec": 47150, "status": "ok", "usage_percent": 15}, "monthly": {"reset_in_sec": 2476846, "status": "ok", "usage_percent": 7}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 19:00:01", "status": "degraded", "last_message_age_sec": 8762, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": false}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "[Errno 104] Connection reset by peer"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15357, "status": "ok", "usage_percent": 6}, "weekly": {"reset_in_sec": 46842, "status": "ok", "usage_percent": 16}, "monthly": {"reset_in_sec": 2476538, "status": "ok", "usage_percent": 8}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 19:05:02", "status": "degraded", "last_message_age_sec": 9062, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "Remote end closed connection without response"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15040, "status": "ok", "usage_percent": 7}, "weekly": {"reset_in_sec": 46525, "status": "ok", "usage_percent": 16}, "monthly": {"reset_in_sec": 2476221, "status": "ok", "usage_percent": 8}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 19:10:01", "status": "critical", "last_message_age_sec": 9359, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14730, "status": "ok", "usage_percent": 8}, "weekly": {"reset_in_sec": 46215, "status": "ok", "usage_percent": 17}, "monthly": {"reset_in_sec": 2475911, "status": "ok", "usage_percent": 8}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 19:15:01", "status": "degraded", "last_message_age_sec": 9659, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14419, "status": "ok", "usage_percent": 10}, "weekly": {"reset_in_sec": 45904, "status": "ok", "usage_percent": 17}, "monthly": {"reset_in_sec": 2475600, "status": "ok", "usage_percent": 8}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 19:20:02", "status": "degraded", "last_message_age_sec": 9960, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14103, "status": "ok", "usage_percent": 10}, "weekly": {"reset_in_sec": 45588, "status": "ok", "usage_percent": 17}, "monthly": {"reset_in_sec": 2475284, "status": "ok", "usage_percent": 8}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 19:25:01", "status": "degraded", "last_message_age_sec": 10259, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14103, "status": "ok", "usage_percent": 10}, "weekly": {"reset_in_sec": 45588, "status": "ok", "usage_percent": 17}, "monthly": {"reset_in_sec": 2475284, "status": "ok", "usage_percent": 8}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 19:30:01", "status": "critical", "last_message_age_sec": 10560, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 13794, "status": "ok", "usage_percent": 18}, "weekly": {"reset_in_sec": 45279, "status": "ok", "usage_percent": 20}, "monthly": {"reset_in_sec": 2474975, "status": "ok", "usage_percent": 10}, "session_expired": false, "issues": [], "total_keys": 6}} -{"timestamp": "2026-07-19 19:35:01", "status": "degraded", "last_message_age_sec": 10859, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 13487, "status": "ok", "usage_percent": 22}, "weekly": {"reset_in_sec": 44972, "status": "ok", "usage_percent": 22}, "monthly": {"reset_in_sec": 2474668, "status": "ok", "usage_percent": 11}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 19:40:01", "status": "degraded", "last_message_age_sec": 11159, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 13178, "status": "ok", "usage_percent": 26}, "weekly": {"reset_in_sec": 44663, "status": "ok", "usage_percent": 24}, "monthly": {"reset_in_sec": 2474359, "status": "ok", "usage_percent": 12}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 19:45:02", "status": "degraded", "last_message_age_sec": 11459, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "error", "error": "timed out"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 12872, "status": "ok", "usage_percent": 32}, "weekly": {"reset_in_sec": 44357, "status": "ok", "usage_percent": 26}, "monthly": {"reset_in_sec": 2474053, "status": "ok", "usage_percent": 13}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 19:50:01", "status": "critical", "last_message_age_sec": 11759, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 12401, "status": "ok", "usage_percent": 38}, "weekly": {"reset_in_sec": 43886, "status": "ok", "usage_percent": 28}, "monthly": {"reset_in_sec": 2473582, "status": "ok", "usage_percent": 14}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:00:01", "status": "critical", "last_message_age_sec": 12359, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 11775, "status": "ok", "usage_percent": 49}, "weekly": {"reset_in_sec": 43260, "status": "ok", "usage_percent": 33}, "monthly": {"reset_in_sec": 2472956, "status": "ok", "usage_percent": 16}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:05:01", "status": "critical", "last_message_age_sec": 12659, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 11464, "status": "ok", "usage_percent": 60}, "weekly": {"reset_in_sec": 42949, "status": "ok", "usage_percent": 37}, "monthly": {"reset_in_sec": 2472645, "status": "ok", "usage_percent": 18}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:10:02", "status": "critical", "last_message_age_sec": 12960, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 11153, "status": "ok", "usage_percent": 70}, "weekly": {"reset_in_sec": 42638, "status": "ok", "usage_percent": 41}, "monthly": {"reset_in_sec": 2472334, "status": "ok", "usage_percent": 20}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:15:01", "status": "critical", "last_message_age_sec": 13259, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10842, "status": "ok", "usage_percent": 77}, "weekly": {"reset_in_sec": 42327, "status": "ok", "usage_percent": 44}, "monthly": {"reset_in_sec": 2472023, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:20:02", "status": "critical", "last_message_age_sec": 13559, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10532, "status": "ok", "usage_percent": 77}, "weekly": {"reset_in_sec": 42017, "status": "ok", "usage_percent": 44}, "monthly": {"reset_in_sec": 2471713, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:25:01", "status": "critical", "last_message_age_sec": 13859, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10301, "status": "ok", "usage_percent": 78}, "weekly": {"reset_in_sec": 41786, "status": "ok", "usage_percent": 44}, "monthly": {"reset_in_sec": 2471482, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:30:01", "status": "critical", "last_message_age_sec": 14159, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9991, "status": "ok", "usage_percent": 78}, "weekly": {"reset_in_sec": 41476, "status": "ok", "usage_percent": 44}, "monthly": {"reset_in_sec": 2471172, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:35:01", "status": "critical", "last_message_age_sec": 14459, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9681, "status": "ok", "usage_percent": 78}, "weekly": {"reset_in_sec": 41166, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2470862, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:40:01", "status": "critical", "last_message_age_sec": 14762, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9362, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 40847, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2470543, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:45:02", "status": "critical", "last_message_age_sec": 15059, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9047, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 40532, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2470228, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:50:01", "status": "critical", "last_message_age_sec": 15363, "error_rate_1h": 0, "bot_activity": {"error": "journalctl failed"}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8732, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 40217, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2469913, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 20:55:01", "status": "critical", "last_message_age_sec": 15661, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8423, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 39908, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2469604, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:00:01", "status": "critical", "last_message_age_sec": 15958, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8114, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 39599, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2469295, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:05:01", "status": "critical", "last_message_age_sec": 16259, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7806, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 39291, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2468987, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:10:01", "status": "critical", "last_message_age_sec": 16559, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7501, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 38986, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2468682, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:15:01", "status": "critical", "last_message_age_sec": 16858, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7501, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 38986, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2468682, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:20:02", "status": "critical", "last_message_age_sec": 17159, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7189, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 38674, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2468370, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:25:01", "status": "critical", "last_message_age_sec": 17459, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6878, "status": "ok", "usage_percent": 79}, "weekly": {"reset_in_sec": 38363, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2468059, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:30:01", "status": "critical", "last_message_age_sec": 17758, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:35:01", "status": "critical", "last_message_age_sec": 18059, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:40:01", "status": "critical", "last_message_age_sec": 18359, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:45:01", "status": "critical", "last_message_age_sec": 18658, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:50:01", "status": "critical", "last_message_age_sec": 18958, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 21:55:02", "status": "critical", "last_message_age_sec": 19259, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:00:01", "status": "critical", "last_message_age_sec": 19558, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:05:01", "status": "critical", "last_message_age_sec": 19859, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:10:01", "status": "critical", "last_message_age_sec": 20159, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:15:01", "status": "degraded", "last_message_age_sec": 20458, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-19 22:12:29,344 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 39543, "status": "ok", "usage_percent": 16.36}, "weekly": {"reset_in_sec": 626343, "status": "ok", "usage_percent": 3.27}, "monthly": {"reset_in_sec": 2543643, "status": "ok", "usage_percent": 0.69}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:20:01", "status": "degraded", "last_message_age_sec": 20758, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-19 22:12:29,344 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=其实我的现金和持仓都发生了变化。但是之前给你发截图,你好像并没有回应", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:25:01", "status": "degraded", "last_message_age_sec": 21059, "error_rate_1h": 0, "bot_activity": {"inbound": 3, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-19 22:24:46,921 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=上面截图是持仓。现金是241330cny", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:30:01", "status": "degraded", "last_message_age_sec": 21359, "error_rate_1h": 0, "bot_activity": {"inbound": 3, "outbound": 0, "errors": 2, "last_error": "2026-07-19 22:29:56,134 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 22:24:46,921 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=上面截图是持仓。现金是241330cny", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:35:01", "status": "degraded", "last_message_age_sec": 21658, "error_rate_1h": 0, "bot_activity": {"inbound": 3, "outbound": 0, "errors": 2, "last_error": "2026-07-19 22:29:56,134 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 22:24:46,921 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=上面截图是持仓。现金是241330cny", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:40:01", "status": "degraded", "last_message_age_sec": 21960, "error_rate_1h": 0, "bot_activity": {"inbound": 3, "outbound": 0, "errors": 2, "last_error": "2026-07-19 22:29:56,134 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 22:24:46,921 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=上面截图是持仓。现金是241330cny", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:45:01", "status": "degraded", "last_message_age_sec": 22259, "error_rate_1h": 0, "bot_activity": {"inbound": 2, "outbound": 0, "errors": 2, "last_error": "2026-07-19 22:29:56,134 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 22:24:46,921 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=上面截图是持仓。现金是241330cny", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:50:01", "status": "degraded", "last_message_age_sec": 22558, "error_rate_1h": 0, "bot_activity": {"inbound": 2, "outbound": 0, "errors": 2, "last_error": "2026-07-19 22:29:56,134 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 22:24:46,921 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=上面截图是持仓。现金是241330cny", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 22:55:01", "status": "degraded", "last_message_age_sec": 22859, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 2, "last_error": "2026-07-19 22:29:56,134 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:00:01", "status": "critical", "last_message_age_sec": 23159, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:05:01", "status": "critical", "last_message_age_sec": 23459, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 36349, "status": "ok", "usage_percent": 29.38}, "weekly": {"reset_in_sec": 623149, "status": "ok", "usage_percent": 5.96}, "monthly": {"reset_in_sec": 2540449, "status": "ok", "usage_percent": 1.23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:10:01", "status": "critical", "last_message_age_sec": 23759, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 33400, "status": "ok", "usage_percent": 36.79}, "weekly": {"reset_in_sec": 620200, "status": "ok", "usage_percent": 7.36}, "monthly": {"reset_in_sec": 2537500, "status": "ok", "usage_percent": 1.54}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:15:01", "status": "critical", "last_message_age_sec": 24059, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 33280, "status": "ok", "usage_percent": 37.36}, "weekly": {"reset_in_sec": 620080, "status": "ok", "usage_percent": 7.49}, "monthly": {"reset_in_sec": 2537380, "status": "ok", "usage_percent": 1.57}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:20:01", "status": "critical", "last_message_age_sec": 24359, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17997, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 31477, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2461173, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:25:01", "status": "critical", "last_message_age_sec": 24658, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17682, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 31162, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2460858, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:30:02", "status": "degraded", "last_message_age_sec": 24959, "error_rate_1h": 0, "bot_activity": {"inbound": 5, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-19 23:26:24,985 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=以上两张图是持仓和现金", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17367, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 30847, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2460543, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:35:01", "status": "degraded", "last_message_age_sec": 25259, "error_rate_1h": 0, "bot_activity": {"inbound": 7, "outbound": 0, "errors": 1, "last_error": "2026-07-19 23:34:07,470 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 23:34:59,820 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=上面的地址是windows的,246上实际的文件路径你应该知道", "last_outbound": null}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17045, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 30525, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2460221, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:40:01", "status": "degraded", "last_message_age_sec": 25559, "error_rate_1h": 0, "bot_activity": {"inbound": 7, "outbound": 0, "errors": 1, "last_error": "2026-07-19 23:34:07,470 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 23:34:59,820 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=上面的地址是windows的,246上实际的文件路径你应该知道", "last_outbound": null, "last_inbound_age_sec": 302, "last_outbound_age_sec": -1}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16736, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 30216, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2459912, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:45:01", "status": "degraded", "last_message_age_sec": 25858, "error_rate_1h": 0, "bot_activity": {"inbound": 7, "outbound": 0, "errors": 1, "last_error": "2026-07-19 23:34:07,470 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 23:34:59,820 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=上面的地址是windows的,246上实际的文件路径你应该知道", "last_outbound": null, "last_inbound_age_sec": 602, "last_outbound_age_sec": -1}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16426, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 29906, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2459602, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:50:01", "status": "degraded", "last_message_age_sec": 26159, "error_rate_1h": 0, "bot_activity": {"inbound": 9, "outbound": 0, "errors": 1, "last_error": "2026-07-19 23:34:07,470 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 23:48:13,262 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=没有变化?那如果你总市值是对的,加上我的现金,你怎么算出来的总资产才783k?", "last_outbound": null, "last_inbound_age_sec": 108, "last_outbound_age_sec": -1}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16116, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 29596, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2459292, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-19 23:55:01", "status": "degraded", "last_message_age_sec": 26459, "error_rate_1h": 0, "bot_activity": {"inbound": 9, "outbound": 0, "errors": 1, "last_error": "2026-07-19 23:34:07,470 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 23:48:13,262 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=没有变化?那如果你总市值是对的,加上我的现金,你怎么算出来的总资产才783k?", "last_outbound": null, "last_inbound_age_sec": 409, "last_outbound_age_sec": -1}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15803, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 29283, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2458979, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:00:01", "status": "degraded", "last_message_age_sec": 26759, "error_rate_1h": 0, "bot_activity": {"inbound": 4, "outbound": 0, "errors": 1, "last_error": "2026-07-19 23:34:07,470 ERROR call_hermes error: TimeoutError: timed out", "last_inbound": "2026-07-19 23:48:13,262 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=没有变化?那如果你总市值是对的,加上我的现金,你怎么算出来的总资产才783k?", "last_outbound": null, "last_inbound_age_sec": 708, "last_outbound_age_sec": -1}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15490, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 28970, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2458666, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:05:01", "status": "critical", "last_message_age_sec": 27059, "error_rate_1h": 0, "bot_activity": {"inbound": 2, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-19 23:48:13,262 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=没有变化?那如果你总市值是对的,加上我的现金,你怎么算出来的总资产才783k?", "last_outbound": null, "last_inbound_age_sec": 1008, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15179, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 28659, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2458355, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:10:01", "status": "critical", "last_message_age_sec": 27359, "error_rate_1h": 0, "bot_activity": {"inbound": 2, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-19 23:48:13,262 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=没有变化?那如果你总市值是对的,加上我的现金,你怎么算出来的总资产才783k?", "last_outbound": null, "last_inbound_age_sec": 1308, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14867, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 28347, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2458043, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:15:02", "status": "critical", "last_message_age_sec": 27662, "error_rate_1h": 0, "bot_activity": {"inbound": 2, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-19 23:48:13,262 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=没有变化?那如果你总市值是对的,加上我的现金,你怎么算出来的总资产才783k?", "last_outbound": null, "last_inbound_age_sec": 1612, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "fast"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14552, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 28032, "status": "ok", "usage_percent": 45}, "monthly": {"reset_in_sec": 2457728, "status": "ok", "usage_percent": 22}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:20:01", "status": "critical", "last_message_age_sec": 27959, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.3s", "age_sec": 1, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14239, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 27719, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2457415, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:25:01", "status": "critical", "last_message_age_sec": 28258, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "11.0s", "age_sec": 114, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 13930, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 27410, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2457106, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:30:01", "status": "critical", "last_message_age_sec": 28559, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "11.0s", "age_sec": 415, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 13620, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 27100, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2456796, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:35:01", "status": "critical", "last_message_age_sec": 28858, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "11.0s", "age_sec": 714, "source": "agent.log"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 46491, "status": "ok", "usage_percent": 2.38}, "weekly": {"reset_in_sec": 615291, "status": "ok", "usage_percent": 12.75}, "monthly": {"reset_in_sec": 2532591, "status": "ok", "usage_percent": 2.67}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:40:01", "status": "critical", "last_message_age_sec": 29159, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "11.0s", "age_sec": 1015, "source": "agent.log"}, "best_key": {"key_id": "key7", "label": "key7 (Kimi)", "masked": "sk-kimi-...Rt2T", "rolling": {"reset_in_sec": 46491, "status": "ok", "usage_percent": 2.38}, "weekly": {"reset_in_sec": 615291, "status": "ok", "usage_percent": 12.75}, "monthly": {"reset_in_sec": 2532591, "status": "ok", "usage_percent": 2.67}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:45:01", "status": "critical", "last_message_age_sec": 29459, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "11.0s", "age_sec": 1315, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 12689, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 26169, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2455865, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:50:01", "status": "critical", "last_message_age_sec": 29759, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "11.0s", "age_sec": 1614, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 12381, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 25861, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2455557, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 00:55:01", "status": "critical", "last_message_age_sec": 30059, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "unknown", "error": "no LLM calls in log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 12068, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 25548, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2455244, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:00:01", "status": "critical", "last_message_age_sec": 30359, "error_rate_1h": 0, "bot_activity": {"error": "journalctl failed"}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": false}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.0s", "age_sec": 46, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 11753, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 25233, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2454929, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:05:01", "status": "critical", "last_message_age_sec": 30659, "error_rate_1h": 0, "bot_activity": {"error": "journalctl failed"}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.0s", "age_sec": 347, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 11443, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 24923, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2454619, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:10:01", "status": "critical", "last_message_age_sec": 30959, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.0s", "age_sec": 647, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 11132, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 24612, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2454308, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:15:01", "status": "critical", "last_message_age_sec": 31259, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "7.1s", "age_sec": 272, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "3.1s", "age_sec": 252, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10822, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 24302, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2453998, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:20:01", "status": "critical", "last_message_age_sec": 31559, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "7.1s", "age_sec": 571, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "3.1s", "age_sec": 551, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10822, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 24302, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2453998, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:25:01", "status": "critical", "last_message_age_sec": 31859, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.5s", "age_sec": 221, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.4s", "age_sec": 242, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10512, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 23992, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2453688, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:30:01", "status": "critical", "last_message_age_sec": 32159, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.5s", "age_sec": 521, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.4s", "age_sec": 542, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10202, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 23682, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2453378, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:35:01", "status": "critical", "last_message_age_sec": 32459, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "6.1s", "age_sec": 139, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "5.4s", "age_sec": 14, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9894, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 23374, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2453070, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:40:01", "status": "critical", "last_message_age_sec": 32759, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "6.1s", "age_sec": 439, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "12.5s", "age_sec": 9, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9584, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 23064, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2452760, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:45:01", "status": "critical", "last_message_age_sec": 33059, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 58, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 203, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9276, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 22756, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2452452, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:50:01", "status": "critical", "last_message_age_sec": 33359, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 358, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 503, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8967, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 22447, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2452143, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 01:55:01", "status": "critical", "last_message_age_sec": 33659, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 658, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "3.0s", "age_sec": 275, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8659, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 22139, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2451835, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:00:01", "status": "critical", "last_message_age_sec": 33960, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.6s", "age_sec": 270, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "3.0s", "age_sec": 576, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8351, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 21831, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2451527, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:05:01", "status": "ok", "last_message_age_sec": 58, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.6s", "age_sec": 570, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "49.1s", "age_sec": 76, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8042, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 21522, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2451218, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:10:01", "status": "ok", "last_message_age_sec": 358, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.0s", "age_sec": 208, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "49.1s", "age_sec": 376, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7735, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 21215, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2450911, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:15:01", "status": "critical", "last_message_age_sec": 657, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.0s", "age_sec": 507, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "3.0s", "age_sec": 239, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7426, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 20906, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2450602, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:20:01", "status": "critical", "last_message_age_sec": 957, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "2.8s", "age_sec": 154, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "3.0s", "age_sec": 540, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7116, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 20596, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2450292, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:25:01", "status": "critical", "last_message_age_sec": 1258, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "2.8s", "age_sec": 454, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.7s", "age_sec": 244, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6809, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 20289, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2449985, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:30:01", "status": "critical", "last_message_age_sec": 1557, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.6s", "age_sec": 81, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.7s", "age_sec": 544, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6502, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 19982, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2449678, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:35:01", "status": "critical", "last_message_age_sec": 1856, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.6s", "age_sec": 380, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "50.5s", "age_sec": 129, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6195, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 19675, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2449371, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:40:01", "status": "critical", "last_message_age_sec": 2157, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.5s", "age_sec": 25, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "50.5s", "age_sec": 429, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5889, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 19369, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2449065, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:45:01", "status": "critical", "last_message_age_sec": 2456, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.5s", "age_sec": 325, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 239, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5582, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 19062, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2448758, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:50:02", "status": "critical", "last_message_age_sec": 2757, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.5s", "age_sec": 626, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 540, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5275, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 18755, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2448451, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 02:55:01", "status": "critical", "last_message_age_sec": 3057, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.2s", "age_sec": 254, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.7s", "age_sec": 210, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4968, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 18448, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2448144, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:00:01", "status": "critical", "last_message_age_sec": 3356, "error_rate_1h": 0.0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.2s", "age_sec": 554, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.7s", "age_sec": 510, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4661, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 18141, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2447837, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:05:01", "status": "critical", "last_message_age_sec": 3657, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 200, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "39.6s", "age_sec": 4, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4354, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 17834, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2447530, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:10:02", "status": "critical", "last_message_age_sec": 3957, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 501, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "20.8s", "age_sec": 241, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4047, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 17527, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2447223, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:15:01", "status": "critical", "last_message_age_sec": 4257, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.0s", "age_sec": 137, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.5s", "age_sec": 284, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3740, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 17220, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2446916, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:20:01", "status": "critical", "last_message_age_sec": 4557, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.0s", "age_sec": 437, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.5s", "age_sec": 584, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3433, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 16913, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2446609, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:25:01", "status": "critical", "last_message_age_sec": 4856, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.4s", "age_sec": 76, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.5s", "age_sec": 282, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3127, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 16607, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2446303, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:30:01", "status": "critical", "last_message_age_sec": 5157, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.4s", "age_sec": 376, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.5s", "age_sec": 582, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 2820, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 16300, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2445996, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:35:01", "status": "critical", "last_message_age_sec": 5457, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.6s", "age_sec": 13, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "17.0s", "age_sec": 241, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 2514, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 15994, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2445690, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:40:01", "status": "critical", "last_message_age_sec": 5757, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.6s", "age_sec": 314, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "17.0s", "age_sec": 542, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 2207, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 15687, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2445383, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:45:02", "status": "critical", "last_message_age_sec": 6057, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.6s", "age_sec": 614, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "156.5s", "age_sec": 22, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 1900, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 15380, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2445076, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:50:01", "status": "critical", "last_message_age_sec": 6357, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.4s", "age_sec": 245, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "156.5s", "age_sec": 321, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 1594, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 15074, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2444770, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 03:55:01", "status": "critical", "last_message_age_sec": 6657, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.4s", "age_sec": 545, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "156.5s", "age_sec": 621, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 1287, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 14767, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2444463, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:00:02", "status": "critical", "last_message_age_sec": 6957, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.1s", "age_sec": 193, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "159.8s", "age_sec": 94, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 979, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 14459, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2444155, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:05:01", "status": "critical", "last_message_age_sec": 7257, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.1s", "age_sec": 492, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "159.8s", "age_sec": 393, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 672, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 14152, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2443848, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:10:01", "status": "critical", "last_message_age_sec": 7557, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 132, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "130.5s", "age_sec": 27, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 366, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 13846, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2443542, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:15:02", "status": "critical", "last_message_age_sec": 7857, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 433, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "29.3s", "age_sec": 145, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 59, "status": "ok", "usage_percent": 1}, "weekly": {"reset_in_sec": 13539, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2443235, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:20:01", "status": "critical", "last_message_age_sec": 8157, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.1s", "age_sec": 73, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "29.3s", "age_sec": 445, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17955, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 13232, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2442928, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:25:01", "status": "critical", "last_message_age_sec": 8457, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.1s", "age_sec": 372, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 265, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17649, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 12926, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2442622, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:30:01", "status": "critical", "last_message_age_sec": 8757, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 9, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 566, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17342, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 12619, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2442315, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:35:01", "status": "critical", "last_message_age_sec": 9057, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 308, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "7.5s", "age_sec": 249, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 17035, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 12312, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2442008, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:40:02", "status": "critical", "last_message_age_sec": 9357, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 609, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "7.5s", "age_sec": 550, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16728, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 12005, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2441701, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:45:01", "status": "critical", "last_message_age_sec": 9657, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.6s", "age_sec": 238, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.9s", "age_sec": 260, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16728, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 12005, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2441701, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:50:01", "status": "critical", "last_message_age_sec": 9956, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.6s", "age_sec": 538, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.9s", "age_sec": 560, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16422, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 11699, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2441395, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 04:55:01", "status": "critical", "last_message_age_sec": 10257, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.1s", "age_sec": 187, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.9s", "age_sec": 255, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 16115, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 11392, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2441088, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:00:01", "status": "critical", "last_message_age_sec": 10556, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.1s", "age_sec": 487, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.9s", "age_sec": 555, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15808, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 11085, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2440781, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:05:01", "status": "critical", "last_message_age_sec": 10857, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.8s", "age_sec": 123, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "189.9s", "age_sec": 72, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15502, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 10779, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2440475, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:10:01", "status": "critical", "last_message_age_sec": 11157, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.8s", "age_sec": 422, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "82.6s", "age_sec": 186, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 15195, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 10472, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2440168, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:15:01", "status": "critical", "last_message_age_sec": 11456, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.1s", "age_sec": 65, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.7s", "age_sec": 252, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14888, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 10165, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2439861, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:20:01", "status": "critical", "last_message_age_sec": 11757, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.1s", "age_sec": 365, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.7s", "age_sec": 552, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14581, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 9858, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2439554, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:25:01", "status": "critical", "last_message_age_sec": 12056, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.9s", "age_sec": 1, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.9s", "age_sec": 246, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 14274, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 9551, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2439247, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:30:01", "status": "critical", "last_message_age_sec": 12358, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.9s", "age_sec": 303, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.9s", "age_sec": 548, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 13967, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 9244, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2438940, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:35:01", "status": "critical", "last_message_age_sec": 12657, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.9s", "age_sec": 601, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "9.5s", "age_sec": 225, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 13660, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 8937, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2438633, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:40:01", "status": "critical", "last_message_age_sec": 12956, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.5s", "age_sec": 234, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "9.5s", "age_sec": 525, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 13354, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 8631, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2438327, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:45:02", "status": "critical", "last_message_age_sec": 13257, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.5s", "age_sec": 535, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.5s", "age_sec": 243, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 13047, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 8324, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2438020, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:50:01", "status": "critical", "last_message_age_sec": 13557, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.5s", "age_sec": 175, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.5s", "age_sec": 542, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 12740, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 8017, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2437713, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 05:55:01", "status": "critical", "last_message_age_sec": 13856, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.5s", "age_sec": 475, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 238, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 12434, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 7711, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2437407, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:00:01", "status": "critical", "last_message_age_sec": 14157, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 120, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 538, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 12127, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 7404, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2437100, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:05:01", "status": "critical", "last_message_age_sec": 14456, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.2s", "age_sec": 420, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "92.9s", "age_sec": 5, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 11820, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 7097, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2436793, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:10:01", "status": "critical", "last_message_age_sec": 14757, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "8.1s", "age_sec": 52, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "60.4s", "age_sec": 243, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 11513, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 6790, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2436486, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:15:01", "status": "critical", "last_message_age_sec": 15056, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "8.1s", "age_sec": 352, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "6.4s", "age_sec": 223, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 11206, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 6483, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2436179, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:20:02", "status": "critical", "last_message_age_sec": 15357, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.0s", "age_sec": 4, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "6.4s", "age_sec": 524, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10899, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 6176, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2435872, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:25:01", "status": "critical", "last_message_age_sec": 15657, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.4s", "age_sec": 296, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.6s", "age_sec": 231, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10592, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 5869, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2435565, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:30:01", "status": "critical", "last_message_age_sec": 15957, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.4s", "age_sec": 596, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.6s", "age_sec": 531, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 10286, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 5563, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2435259, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:35:01", "status": "critical", "last_message_age_sec": 16256, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "2.9s", "age_sec": 231, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "32.0s", "age_sec": 218, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9979, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 5256, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2434952, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:40:01", "status": "critical", "last_message_age_sec": 16557, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "2.9s", "age_sec": 531, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "32.0s", "age_sec": 518, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9672, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 4949, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2434645, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:45:01", "status": "critical", "last_message_age_sec": 16856, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.3s", "age_sec": 173, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 282, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9366, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 4643, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2434339, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:50:01", "status": "critical", "last_message_age_sec": 17157, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.3s", "age_sec": 473, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.8s", "age_sec": 582, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 9059, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 4336, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2434032, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 06:55:01", "status": "critical", "last_message_age_sec": 17457, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.0s", "age_sec": 111, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.7s", "age_sec": 286, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8752, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 4029, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2433725, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:00:01", "status": "critical", "last_message_age_sec": 17757, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.0s", "age_sec": 411, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.7s", "age_sec": 586, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8446, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 3723, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2433419, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:05:02", "status": "critical", "last_message_age_sec": 18057, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.1s", "age_sec": 54, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "138.3s", "age_sec": 35, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 8139, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 3416, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2433112, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:10:01", "status": "critical", "last_message_age_sec": 18357, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.1s", "age_sec": 353, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "92.0s", "age_sec": 242, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7833, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 3110, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2432806, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:15:01", "status": "critical", "last_message_age_sec": 18656, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "4.1s", "age_sec": 653, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "17.1s", "age_sec": 249, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7526, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 2803, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2432499, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:20:01", "status": "critical", "last_message_age_sec": 18957, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.1s", "age_sec": 292, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "17.1s", "age_sec": 550, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 7219, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 2496, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2432192, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:25:01", "status": "critical", "last_message_age_sec": 19257, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.1s", "age_sec": 591, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.9s", "age_sec": 279, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6913, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 2190, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2431886, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:30:02", "status": "critical", "last_message_age_sec": 19557, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.5s", "age_sec": 231, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.9s", "age_sec": 580, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6606, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 1883, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2431579, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:35:01", "status": "critical", "last_message_age_sec": 19857, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.5s", "age_sec": 530, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "64.9s", "age_sec": 95, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 6300, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 1577, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2431273, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:40:01", "status": "critical", "last_message_age_sec": 20157, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.0s", "age_sec": 168, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "64.9s", "age_sec": 396, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5993, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 1270, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2430966, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:45:01", "status": "critical", "last_message_age_sec": 20457, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.0s", "age_sec": 467, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.6s", "age_sec": 266, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5686, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 963, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2430659, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:50:01", "status": "critical", "last_message_age_sec": 20757, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "2.9s", "age_sec": 105, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "2.6s", "age_sec": 566, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5379, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 656, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2430352, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 07:55:01", "status": "critical", "last_message_age_sec": 21056, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "2.9s", "age_sec": 405, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "6.2s", "age_sec": 261, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 5073, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 350, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2430046, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:00:01", "status": "critical", "last_message_age_sec": 21357, "error_rate_1h": 0, "bot_activity": {"inbound": 0, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": null, "last_outbound": null, "last_inbound_age_sec": -1, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "3.0s", "age_sec": 44, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "6.2s", "age_sec": 562, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4766, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 43, "status": "ok", "usage_percent": 46}, "monthly": {"reset_in_sec": 2429739, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:05:01", "status": "critical", "last_message_age_sec": 21657, "error_rate_1h": 0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:01:40,718 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=一会就要开盘了。你检查一下系统是不是正常。", "last_outbound": null, "last_inbound_age_sec": 201, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "6.8s", "age_sec": 12, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "214.2s", "age_sec": 60, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4460, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 604537, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 2429433, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:10:01", "status": "critical", "last_message_age_sec": 21957, "error_rate_1h": 0, "bot_activity": {"inbound": 3, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:07:38,478 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=我说的是持仓和自选的股,每个交易日开盘前都应重评一次", "last_outbound": null, "last_inbound_age_sec": 143, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.9s", "age_sec": 61, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "211.9s", "age_sec": 151, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 4153, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 604230, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 2429126, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:15:01", "status": "critical", "last_message_age_sec": 22257, "error_rate_1h": 0, "bot_activity": {"inbound": 5, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:12:32,867 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=> [08:10] [08:08] 盘前全量重评已完成,39/39 全部成功。**结果是:持仓14只全部持有状态,无买入", "last_outbound": null, "last_inbound_age_sec": 149, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.3s", "age_sec": 2, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "121.2s", "age_sec": 132, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3846, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 603923, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 2428819, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:20:01", "status": "critical", "last_message_age_sec": 22557, "error_rate_1h": 0, "bot_activity": {"inbound": 5, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:12:32,867 INFO 📩 收到: from=hmo@yoin.fun/Conversations.gJgBWWNBNE type=chat body=> [08:10] [08:08] 盘前全量重评已完成,39/39 全部成功。**结果是:持仓14只全部持有状态,无买入", "last_outbound": null, "last_inbound_age_sec": 449, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "11.3s", "age_sec": 204, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "121.2s", "age_sec": 432, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3540, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 603617, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 2428513, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:25:02", "status": "critical", "last_message_age_sec": 22857, "error_rate_1h": 0, "bot_activity": {"inbound": 6, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:22:14,972 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=我要的不是你手动的缝缝补补,我要的是系统能全功能就位。这样,我让笑笑来做这个改动。你这里别动", "last_outbound": null, "last_inbound_age_sec": 168, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.7s", "age_sec": 161, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "34.0s", "age_sec": 189, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 3233, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 603310, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 2428206, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:30:02", "status": "critical", "last_message_age_sec": 23157, "error_rate_1h": 0, "bot_activity": {"inbound": 6, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:22:14,972 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=我要的不是你手动的缝缝补补,我要的是系统能全功能就位。这样,我让笑笑来做这个改动。你这里别动", "last_outbound": null, "last_inbound_age_sec": 468, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "5.7s", "age_sec": 461, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "34.0s", "age_sec": 489, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 2926, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 603003, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 2427899, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:35:01", "status": "critical", "last_message_age_sec": 23457, "error_rate_1h": 0, "bot_activity": {"inbound": 5, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:22:14,972 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=我要的不是你手动的缝缝补补,我要的是系统能全功能就位。这样,我让笑笑来做这个改动。你这里别动", "last_outbound": null, "last_inbound_age_sec": 768, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "109.6s", "age_sec": 58, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "92.5s", "age_sec": 65, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 2926, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 603003, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 2427899, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:40:01", "status": "critical", "last_message_age_sec": 23757, "error_rate_1h": 0, "bot_activity": {"inbound": 3, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:22:14,972 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=我要的不是你手动的缝缝补补,我要的是系统能全功能就位。这样,我让笑笑来做这个改动。你这里别动", "last_outbound": null, "last_inbound_age_sec": 1067, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "40.0s", "age_sec": 6, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "49.8s", "age_sec": 13, "source": "agent.log"}, "best_key": {"key_id": "key6", "label": "key6 (ycdennismo@163.com)", "masked": "sk-oF1WG...hCS3", "rolling": {"reset_in_sec": 2620, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 602697, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 2427593, "status": "ok", "usage_percent": 23}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:45:01", "status": "ok", "last_message_age_sec": 58, "error_rate_1h": 0.0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:22:14,972 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=我要的不是你手动的缝缝补补,我要的是系统能全功能就位。这样,我让笑笑来做这个改动。你这里别动", "last_outbound": null, "last_inbound_age_sec": 1367, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": false}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "11.4s", "age_sec": 4, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "19.1s", "age_sec": 142, "source": "agent.log"}, "best_key": {"key_id": "key2", "label": "key2 (Google, hua65111@gmail.com)", "masked": "sk-5miR8...tGTB", "rolling": {"reset_in_sec": 18000, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 602394, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 1765184, "status": "ok", "usage_percent": 50}, "session_expired": false, "issues": [], "total_keys": 7}} -{"timestamp": "2026-07-20 08:50:01", "status": "ok", "last_message_age_sec": 357, "error_rate_1h": 0.0, "bot_activity": {"inbound": 1, "outbound": 0, "errors": 0, "last_error": null, "last_inbound": "2026-07-20 08:22:14,972 INFO 📩 收到: from=hmo@yoin.fun/gajim.02ZMOMIM type=chat body=我要的不是你手动的缝缝补补,我要的是系统能全功能就位。这样,我让笑笑来做这个改动。你这里别动", "last_outbound": null, "last_inbound_age_sec": 1667, "last_outbound_age_sec": -1, "last_error_age_sec": -1, "last_error_resolved": false}, "gateways": {"zhiwei": {"port": 8643, "alive": true}, "mohe": {"port": 8642, "alive": true}, "xiaoguo": {"port": 8645, "alive": true}}, "llm_provider": {"status": "ok", "latency": "20.8s", "age_sec": 13, "source": "agent.log"}, "llm_provider_default": {"status": "ok", "latency": "19.1s", "age_sec": 442, "source": "agent.log"}, "best_key": {"key_id": "key2", "label": "key2 (Google, hua65111@gmail.com)", "masked": "sk-5miR8...tGTB", "rolling": {"reset_in_sec": 18000, "status": "ok", "usage_percent": 0}, "weekly": {"reset_in_sec": 602088, "status": "ok", "usage_percent": 0}, "monthly": {"reset_in_sec": 1764878, "status": "ok", "usage_percent": 50}, "session_expired": false, "issues": [], "total_keys": 7}} diff --git a/gateway/logs/xmpp_messages.jsonl b/gateway/logs/xmpp_messages.jsonl deleted file mode 100644 index 164144c8..00000000 --- a/gateway/logs/xmpp_messages.jsonl +++ /dev/null @@ -1,6 +0,0 @@ -{"timestamp": "2026-07-19 16:30:03", "direction": "out", "from": "zhiwei@yoin.fun", "to": "hmo@yoin.fun", "body_preview": "【每日汇总】今日以下cron报告未送达(已拦截): • 脚本输出 99次 无操作信号的报告正常静默,有操作信号的都已送达。", "status": "ok", "error": null, "latency_ms": 1436, "epoch": 1784449803.6249352} -{"timestamp": "2026-07-19 16:32:02", "direction": "out", "from": "zhiwei@yoin.fun", "to": "hmo@yoin.fun", "body_preview": "【每日汇总】今日以下cron报告未送达(已拦截): • 脚本输出 99次 无操作信号的报告正常静默,有操作信号的都已送达。", "status": "ok", "error": null, "latency_ms": 401, "epoch": 1784449922.3636103} -{"timestamp": "2026-07-19 16:34:02", "direction": "out", "from": "zhiwei@yoin.fun", "to": "hmo@yoin.fun", "body_preview": "【每日汇总】今日以下cron报告未送达(已拦截): • 脚本输出 99次 无操作信号的报告正常静默,有操作信号的都已送达。", "status": "ok", "error": null, "latency_ms": 430, "epoch": 1784450042.5418942} -{"timestamp": "2026-07-20 02:04:04", "direction": "out", "from": "zhiwei@yoin.fun", "to": "hmo@yoin.fun", "body_preview": "没有 accepted 提案,正常工作。", "status": "ok", "error": null, "latency_ms": 2424, "epoch": 1784484244.5065012} -{"timestamp": "2026-07-20 08:44:02", "direction": "out", "from": "zhiwei@yoin.fun", "to": "hmo@yoin.fun", "body_preview": "Now I have everything. Here's the report: --- 【⚡ 重点推荐操作】 ① 中芯国际(688981) 现价139.9 |仓位6.4%|止损131.6 技术面:弱,阻力155.5/支撑139(周五低) 操作:开盘若跳空破135减半仓,科创50续跌-7%大概率触发止损 ② 腾讯控股(00700) 现价461.6 |仓位6.1%|entry区间450-4", "status": "ok", "error": null, "latency_ms": 1185, "epoch": 1784508242.9271429} -{"timestamp": "2026-07-20 08:44:03", "direction": "out", "from": "zhiwei@yoin.fun", "to": "hmo@yoin.fun", "body_preview": "【⚠️宏观高风险】2026-07-20 08:32 — 全面系统性大跌进行中 整体风险:HIGH(90%置信度)— 确认全面系统性大跌 核心判断: 周一A股延续上周五暴跌,所有8个指数全数下跌超过2%。催化剂是美伊冲突进入第9晚且升级至打击核电站+能源供给中断风险。但存在有序吸收信号(黄金反跌/美股期货上涨/国家队增持),这不是纯恐慌场景。 --- 催化剂: 美军连续第九晚袭击伊朗,已打击", "status": "ok", "error": null, "latency_ms": 409, "epoch": 1784508243.488295} diff --git a/gateway/temp/health_todos.jsonl b/gateway/temp/health_todos.jsonl deleted file mode 100644 index 46a3b78d..00000000 --- a/gateway/temp/health_todos.jsonl +++ /dev/null @@ -1,44 +0,0 @@ -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T14:30:01.660636"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T14:45:01.509824"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T14:50:01.299686"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T15:10:01.664172"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T15:16:56.095701"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T15:20:01.499188"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "timed out", "timestamp": "2026-07-19T15:35:01.728475"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T15:40:01.641145"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T15:45:01.566000"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T15:50:01.481496"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T16:00:01.977074"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T16:05:01.820740"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T16:20:01.556094"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T16:30:01.714587"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T16:35:01.890435"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T16:40:02.012734"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T16:55:01.801842"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T17:00:01.449735"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T17:05:01.397273"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T17:10:02.184096"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T17:15:01.441236"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T17:20:01.917698"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T17:25:01.507624"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T17:45:01.480343"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T17:55:02.087320"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:00:01.432964"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:05:01.877289"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:15:01.180290"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:15:32.047339"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:20:01.729488"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:22:09.444652"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:25:01.641678"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:30:02.180070"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "timed out", "timestamp": "2026-07-19T18:35:02.017521"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "timed out", "timestamp": "2026-07-19T18:40:01.770567"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:45:01.936046"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:50:01.336498"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T18:55:02.111939"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "timed out", "timestamp": "2026-07-19T19:00:01.584617"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "timed out", "timestamp": "2026-07-19T19:05:02.034721"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T19:10:01.565114"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-19T19:30:01.210250"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "timed out", "timestamp": "2026-07-19T20:40:01.598513"} -{"service": "zhiwei_gateway", "label": "知微 Gateway", "reason": "", "timestamp": "2026-07-20T08:45:01.667438"} diff --git a/gateway/temp/last_health_check.json b/gateway/temp/last_health_check.json deleted file mode 100644 index 6a924f29..00000000 --- a/gateway/temp/last_health_check.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "services": [ - { - "name": "mofin_api", - "label": "MoFin API", - "type": "http", - "port": 8899, - "health": { - "ok": true - }, - "detail": "HTTP 200" - }, - { - "name": "zhiwei_gateway", - "label": "知微 Gateway", - "type": "http", - "port": 8643, - "health": { - "ok": true - }, - "detail": "HTTP 200" - }, - { - "name": "ejabberd", - "label": "ejabberd XMPP", - "type": "tcp", - "port": 5222, - "health": { - "ok": true - }, - "detail": "ok" - }, - { - "name": "mofin_db", - "label": "MoFin 数据库", - "type": "db", - "port": 0, - "health": { - "ok": true - }, - "detail": "ok" - } - ], - "summary": { - "ok": 4, - "total": 4 - }, - "generated_at": "2026-07-20 08:50:01" -} \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 3b5131b4..00000000 --- a/index.html +++ /dev/null @@ -1,1695 +0,0 @@ - - - - - -MoFin · 莫荷情报 - - - - - - -
- -
-
- 📊 -

MoFin

- 知微 -
-
- - -
-
- - -
- - - - - - - - - - - - 📸 上传 -
- - -
- - - - - - - - - - -
- - - - - - - - - - - - - \ No newline at end of file diff --git a/scripts/analyze_health.py b/scripts/analyze_health.py new file mode 100644 index 00000000..f23c7859 --- /dev/null +++ b/scripts/analyze_health.py @@ -0,0 +1,45 @@ +import json + +d = json.load(open('/tmp/mofin_health.json')) + +print("generated_at:", d.get('generated_at')) +print() + +# 1. Pipelines with error/warn status +print("=== PIPELINES (error/warn) ===") +for p in d.get('pipelines', []): + if p.get('status') in ('error', 'fail', 'warn'): + print(f"[{p['status']}] {p.get('name') or p.get('script')} | profile={p.get('profile')} | type={p.get('type')} | schedule={p.get('schedule')} | last_run={p.get('last_run')}") + +print() +print("=== FEATURE TREE (non-ok nodes) ===") +def walk(n, path=''): + label = n.get('label', '?') + p = f"{path}/{label}" + if n.get('status') not in ('ok', None): + print(f"[{n.get('status')}] {p}") + for pipe in n.get('pipes', []): + if pipe.get('status') != 'ok': + print(f" pipe: [{pipe.get('status')}] {pipe.get('name') or pipe.get('script')} | last_run={pipe.get('last_run')}") + for c in n.get('children', []): + walk(c, p) +walk(d.get('feature_tree', {})) + +print() +print("=== DATA ENTITIES (orphan/write_only) ===") +for e in d.get('entities', []): + if e.get('flow_status') in ('orphan', 'write_only'): + print(f"[{e['flow_status']}] {e['name']} | rows={e.get('rows')} | writers={e.get('writers')} | readers={e.get('readers')} | {e.get('desc','')[:60]}") + +print() +print("=== ARCHITECTURE violations ===") +arch = d.get('architecture', {}) +print("violation_count:", arch.get('violation_count')) +for v in (arch.get('price_api_violations') or [])[:10]: + print(f" {v.get('script')} L{v.get('line')}") + +print() +print("=== JSON files with warn ===") +for j in d.get('json_files', []): + if j.get('warn'): + print(f"[warn] {j['name']} | {j.get('desc','')[:60]} | readers={j.get('readers')}") \ No newline at end of file diff --git a/scripts/check_backfill_job.py b/scripts/check_backfill_job.py new file mode 100644 index 00000000..b217a471 --- /dev/null +++ b/scripts/check_backfill_job.py @@ -0,0 +1,7 @@ +import json +d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json')) +jobs = d if isinstance(d, list) else d.get('jobs', []) +for j in jobs: + if '批量补全' in j.get('name', '') or '九维' in j.get('name', ''): + print(json.dumps({k: j.get(k) for k in ('name', 'script', 'no_agent', 'prompt', 'schedule', 'enabled')}, + ensure_ascii=False, indent=2)) \ No newline at end of file diff --git a/scripts/check_current_errors.py b/scripts/check_current_errors.py new file mode 100644 index 00000000..24f68c94 --- /dev/null +++ b/scripts/check_current_errors.py @@ -0,0 +1,19 @@ +import json + +targets = ['evolution-pulse', '大脑任务执行', '元自成长-每日', '跨市场背离检测', '宏观新闻采集', + '记忆守卫-每日', '开盘前钉对钉验证', '盘前热点扫描', '集合竞价观察', + '候选股过滤管道-每30分', '候选股自动提拔-每30分', '价格监控-高频', 'Cron监护-高频', + '自选买入区提醒-盘前午间尾盘', '市场精选推荐-每日', '持仓情报-盘后'] + +for jf, label in [('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json', 'pa'), + ('/home/hmo/.hermes/cron/jobs.json', 'default')]: + d = json.load(open(jf)) + jobs = d if isinstance(d, list) else d.get('jobs', []) + for j in jobs: + if j.get('name') in targets: + err = str(j.get('last_error') or '')[:180] + no_agent = j.get('no_agent', False) + script = j.get('script', '') + print(f"[{label}] {j['name']} | run={str(j.get('last_run_at'))[:19]} | {'script:'+script if no_agent else 'LLM'}") + print(f" err: {err}") + print() \ No newline at end of file diff --git a/scripts/check_dashboard.py b/scripts/check_dashboard.py new file mode 100644 index 00000000..9bdf9052 --- /dev/null +++ b/scripts/check_dashboard.py @@ -0,0 +1,9 @@ +import urllib.request, json +req = urllib.request.Request('http://127.0.0.1:8899/api/xmpp/health') +data = json.loads(urllib.request.urlopen(req, timeout=120).read()) +print('status:', data.get('status')) +print('gateways:', list(k + '=' + str(v.get('alive')) for k, v in data.get('gateways', {}).items())) +print('llm:', data.get('llm_provider')) +print('best_key:', data.get('best_key', {}).get('key_id'), + '| weekly:', data.get('best_key', {}).get('weekly', {}).get('usage_percent'), '%', + '| rolling:', data.get('best_key', {}).get('rolling', {}).get('usage_percent'), '%') \ No newline at end of file diff --git a/scripts/check_db_paths.py b/scripts/check_db_paths.py new file mode 100644 index 00000000..29a00f40 --- /dev/null +++ b/scripts/check_db_paths.py @@ -0,0 +1,22 @@ +import sqlite3 + +# Script connects to THIS db +proj_db = '/home/hmo/projects/MoFin/data/mofin.db' +# Real data lives in THIS db +real_db = '/home/hmo/web-dashboard/data/mofin.db' + +for label, path in [("project", proj_db), ("real", real_db)]: + db = sqlite3.connect(path) + tables = [r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table'")] + print(f"{label} db ({path}): {len(tables)} tables") + for t in tables: + if t == 'todos': + sql = db.execute(f"SELECT sql FROM sqlite_master WHERE name='{t}'").fetchone() + print(f" {t}: {sql[0][:100] if sql else 'no sql'}") + elif t in ('holdings', 'holding_strategies', 'watchlist_stocks', 'portfolio_summary'): + cnt = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0] + print(f" {t}: {cnt} rows") + else: + cnt = db.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0] + print(f" {t}: {cnt} rows") + db.close() diff --git a/scripts/check_db_state.py b/scripts/check_db_state.py new file mode 100644 index 00000000..7e1695af --- /dev/null +++ b/scripts/check_db_state.py @@ -0,0 +1,25 @@ +import sqlite3 +conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') +print('=== portfolio_summary schema ===') +for r in conn.execute("SELECT sql FROM sqlite_master WHERE name='portfolio_summary'"): + print(r[0]) +print() +print('=== latest row ===') +cur = conn.execute('SELECT * FROM portfolio_summary ORDER BY id DESC LIMIT 1') +cols = [d[0] for d in cur.description] +row = cur.fetchone() +for c, v in zip(cols, row): + print(f' {c} = {v}') +print() +print('=== decision_type distribution ===') +for r in conn.execute("SELECT decision_type, COUNT(*) FROM holding_strategies WHERE status='active' GROUP BY decision_type"): + print(f' {r[0]}: {r[1]}') +print() +print('=== full_analysis staleness ===') +for r in conn.execute(""" + SELECT decision_type, + SUM(CASE WHEN full_analysis IS NULL OR LENGTH(full_analysis)<500 THEN 1 ELSE 0 END) as missing, + SUM(CASE WHEN LENGTH(full_analysis)>=500 THEN 1 ELSE 0 END) as has_fa, + COUNT(*) as total + FROM holding_strategies WHERE status='active' GROUP BY decision_type"""): + print(f' {r[0]}: missing={r[1]} has={r[2]} total={r[3]}') \ No newline at end of file diff --git a/scripts/check_missing_scripts.py b/scripts/check_missing_scripts.py new file mode 100644 index 00000000..f181a989 --- /dev/null +++ b/scripts/check_missing_scripts.py @@ -0,0 +1,31 @@ +import os + +files = ['meta_growth.py', 'macro_context_collector.py', 'divergence_detector.py', + 'memory_guardian.py', 'fix_gateway_port.py'] +d = '/home/hmo/.hermes/profiles/position-analyst/scripts/' +for f in files: + p = os.path.join(d, f) + if os.path.islink(p): + target = os.readlink(p) + ok = os.path.exists(p) + print(f"{f} -> symlink -> {target} [{'OK' if ok else 'BROKEN'}]") + elif os.path.exists(p): + print(f"{f} -> real file") + else: + print(f"{f} -> MISSING") + +print() +print('=== deploy/profile-scripts candidates ===') +deploy = '/home/hmo/MoFin/deploy/profile-scripts/' +for f in os.listdir(deploy): + if any(k in f for k in ['meta_growth', 'macro_context', 'divergence', 'memory_guardian', 'fix_gateway']): + print(' ', f) + +print() +print('=== search whole MoFin for the 5 scripts ===') +import subprocess +for f in files: + r = subprocess.run(['find', '/home/hmo/MoFin', '/home/hmo/projects', '-name', f, + '-not', '-path', '*/venv/*'], capture_output=True, text=True, timeout=30) + found = [l for l in r.stdout.splitlines() if l.strip()] + print(f"{f}: {found if found else 'NOT FOUND'}") \ No newline at end of file diff --git a/scripts/check_new_imports.py b/scripts/check_new_imports.py new file mode 100644 index 00000000..e18498ed --- /dev/null +++ b/scripts/check_new_imports.py @@ -0,0 +1,2 @@ +from mofin_db import read_capital_flow_cache, write_live_prices, write_mtf_cache, write_capital_flow_cache +print("imports OK") diff --git a/scripts/check_srv.py b/scripts/check_srv.py new file mode 100644 index 00000000..7a14726b --- /dev/null +++ b/scripts/check_srv.py @@ -0,0 +1,6 @@ +import urllib.request, json +r = urllib.request.urlopen("http://localhost:8899/api/portfolio") +d = json.loads(r.read()) +for h in d.get('holdings', []): + if h['code'] in ('01888', '00700', '000657'): + print(f"{h['code']} {h['name']}: price={h['price']} curr={h.get('currency')}") diff --git a/scripts/check_todos_schema.py b/scripts/check_todos_schema.py new file mode 100644 index 00000000..e3ddca01 --- /dev/null +++ b/scripts/check_todos_schema.py @@ -0,0 +1,8 @@ +import sqlite3 + +for label, path in [("project", '/home/hmo/projects/MoFin/data/mofin.db'), ("real", '/home/hmo/web-dashboard/data/mofin.db')]: + db = sqlite3.connect(path) + sql = db.execute("SELECT sql FROM sqlite_master WHERE name='todos'").fetchone() + print(f"=== {label}: {path} ===") + print(sql[0] if sql else "NOT FOUND") + db.close() diff --git a/scripts/cron_status.py b/scripts/cron_status.py new file mode 100644 index 00000000..1b5de1d1 --- /dev/null +++ b/scripts/cron_status.py @@ -0,0 +1,31 @@ +import json +from datetime import datetime, timezone, timedelta + +d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json')) +jobs = d if isinstance(d, list) else d.get('jobs', []) +now = datetime.now(timezone.utc) +print(f"{'name':32} {'last_run':20} {'status':8} {'next_run':20} {'en'}") +print('-' * 95) +for j in sorted(jobs, key=lambda x: x.get('last_run_at') or ''): + if not j.get('enabled', True): + continue + lr = (j.get('last_run_at') or '?')[:19] + nr = (j.get('next_run_at') or '?')[:19] + st = str(j.get('last_status', '?')) + print(f"{j.get('name','?')[:32]:32} {lr:20} {st:8} {nr:20} {j.get('enabled')}") + +# also check gateway-side cron (default profile) +print() +print('=== default profile cron ===') +try: + d2 = json.load(open('/home/hmo/.hermes/cron/jobs.json')) + jobs2 = d2 if isinstance(d2, list) else d2.get('jobs', []) + for j in sorted(jobs2, key=lambda x: x.get('last_run_at') or ''): + if not j.get('enabled', True): + continue + lr = (j.get('last_run_at') or '?')[:19] + nr = (j.get('next_run_at') or '?')[:19] + st = str(j.get('last_status', '?')) + print(f"{j.get('name','?')[:32]:32} {lr:20} {st:8} {nr:20} {j.get('enabled')}") +except Exception as e: + print('err:', e) \ No newline at end of file diff --git a/scripts/dump_health.py b/scripts/dump_health.py new file mode 100644 index 00000000..2b9a6610 --- /dev/null +++ b/scripts/dump_health.py @@ -0,0 +1,5 @@ +import json, sys +line = sys.stdin.read().strip() +d = json.loads(line) +llm = d.get('llm_provider', {}) +print(d['timestamp'], '| status:', d['status'], '| llm:', llm.get('status', '?'), '| best:', d.get('best_key', {}).get('key_id', '?')) \ No newline at end of file diff --git a/scripts/find_cron_errors.py b/scripts/find_cron_errors.py new file mode 100644 index 00000000..eda4f02e --- /dev/null +++ b/scripts/find_cron_errors.py @@ -0,0 +1,15 @@ +import json, os, glob + +# find job ids for the failing jobs +targets = { + 'pa': ('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json', + ['大脑任务执行', 'Gateway看门狗-知微', '元自成长-每日', '记忆守卫-每日', '宏观新闻采集', '跨市场背离检测']), + 'default': ('/home/hmo/.hermes/cron/jobs.json', + ['大脑任务执行', 'evolution-pulse', 'wiki-self-growth', '知识研究-日常', '梦境循环-知识库归并']), +} +for label, (jf, names) in targets.items(): + d = json.load(open(jf)) + jobs = d if isinstance(d, list) else d.get('jobs', []) + for j in jobs: + if j.get('name') in names: + print(f"{label} | {j['name']} | id={j.get('id')} | status={j.get('last_status')} | error={str(j.get('last_error'))[:200]}") \ No newline at end of file diff --git a/scripts/fix_self_todo_path.py b/scripts/fix_self_todo_path.py new file mode 100644 index 00000000..9b0bec8a --- /dev/null +++ b/scripts/fix_self_todo_path.py @@ -0,0 +1,6 @@ +"""Fix DB_PATH in self_todo_executor.py""" +path = '/home/hmo/.hermes/profiles/position-analyst/scripts/self_todo_executor.py' +content = open(path).read() +content = content.replace('projects/MoFin/data', 'web-dashboard/data') +open(path, 'w').write(content) +print("DB_PATH fixed to web-dashboard/data/mofin.db") diff --git a/scripts/fix_symlinks.py b/scripts/fix_symlinks.py new file mode 100644 index 00000000..69c10a49 --- /dev/null +++ b/scripts/fix_symlinks.py @@ -0,0 +1,33 @@ +import os, sys + +scripts_dir = '/home/hmo/.hermes/profiles/position-analyst/scripts' +fixed, skipped, errors = 0, 0, [] +for name in os.listdir(scripts_dir): + p = os.path.join(scripts_dir, name) + if not os.path.islink(p): + skipped += 1 + continue + target = os.path.realpath(p) + if not os.path.exists(target): + errors.append(f"{name}: broken symlink -> {target}") + continue + try: + os.unlink(p) + os.link(target, p) # hardlink: same inode, .resolve() stays in scripts_dir + fixed += 1 + except Exception as e: + errors.append(f"{name}: {e}") + +print(f"fixed: {fixed}, skipped (non-symlink): {skipped}, errors: {len(errors)}") +for e in errors: + print(' ', e) + +# verify with the scheduler's own check +from pathlib import Path +scripts_resolved = Path(scripts_dir).resolve() +test = (Path(scripts_dir) / 'meta_growth.py').resolve() +try: + test.relative_to(scripts_resolved) + print('VERIFY OK: meta_growth.py now passes relative_to check') +except ValueError: + print('VERIFY FAIL: still resolves outside') \ No newline at end of file diff --git a/scripts/fix_todos_db.py b/scripts/fix_todos_db.py new file mode 100644 index 00000000..a75ece55 --- /dev/null +++ b/scripts/fix_todos_db.py @@ -0,0 +1,47 @@ +"""Fix: unify todos table schema across project and real DB""" +import sqlite3 + +project_db = '/home/hmo/projects/MoFin/data/mofin.db' +real_db = '/home/hmo/web-dashboard/data/mofin.db' + +# Zhiwei's canonical schema (from project db) +target_schema = """ + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + status TEXT DEFAULT 'pending', + priority TEXT DEFAULT 'medium', + source TEXT DEFAULT 'manual', + fix_action TEXT, + retry_count INTEGER DEFAULT 0, + note TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +""" + +def ensure_todos(db_path, label): + db = sqlite3.connect(db_path) + existing = db.execute("SELECT name FROM sqlite_master WHERE name='todos'").fetchone() + if not existing: + db.execute(f"CREATE TABLE todos ({target_schema})") + print(f"{label}: created todos table") + else: + # Ensure all columns exist + existing_cols = {r[1] for r in db.execute("PRAGMA table_info(todos)")} + needed = {'title', 'description', 'status', 'priority', 'source', 'fix_action', + 'retry_count', 'note', 'created_at', 'updated_at'} + missing = needed - existing_cols + for col in missing: + if col in ('retry_count',): + db.execute(f"ALTER TABLE todos ADD COLUMN {col} INTEGER DEFAULT 0") + elif col in ('created_at', 'updated_at'): + db.execute(f"ALTER TABLE todos ADD COLUMN {col} TIMESTAMP DEFAULT CURRENT_TIMESTAMP") + else: + db.execute(f"ALTER TABLE todos ADD COLUMN {col} TEXT") + print(f"{label}: checked, {len(missing)} missing columns added" if missing else f"{label}: schema OK") + db.commit() + db.close() + +ensure_todos(project_db, "project db") +ensure_todos(real_db, "real db") +print("\nDone. Both DBs now have matching todos schema.") diff --git a/scripts/get_full_errors.py b/scripts/get_full_errors.py new file mode 100644 index 00000000..22390662 --- /dev/null +++ b/scripts/get_full_errors.py @@ -0,0 +1,10 @@ +import json + +d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json')) +jobs = d if isinstance(d, list) else d.get('jobs', []) +for j in jobs: + if j.get('name') in ('候选股自动提拔-每30分', '候选股过滤管道-每30分', '开盘前钉对钉验证'): + print('='*70) + print(j['name'], '| last:', str(j.get('last_run_at'))[:19]) + print(str(j.get('last_error'))[:900]) + print() \ No newline at end of file diff --git a/scripts/key_status.py b/scripts/key_status.py new file mode 100644 index 00000000..11a0b932 --- /dev/null +++ b/scripts/key_status.py @@ -0,0 +1,20 @@ +import urllib.request, json +req = urllib.request.Request('http://127.0.0.1:5803/api/keys') +data = json.loads(urllib.request.urlopen(req, timeout=10).read()) +print(f"{'key':6} {'weekly':15} {'monthly':15} {'rolling':15} {'workspace':40}") +print('-'*95) +for k in data['keys']: + print(f"{k['key_id']:6} " + f"{k['weekly']['status']:15} " + f"{k['monthly']['status']:15} " + f"{k['rolling']['status']:15} " + f"{k['workspace_id'][:40]:40}") + +best = None +for k in data['keys']: + if (k['weekly']['status'] == 'ok' and + k['monthly']['status'] == 'ok' and + k['rolling']['status'] == 'ok'): + if best is None or k['weekly']['usage_percent'] < best['weekly']['usage_percent']: + best = k +print('\nBEST:', best['key_id'] if best else 'NONE', '- all weekly rate-limited' if not best else '') \ No newline at end of file diff --git a/scripts/list_tables.py b/scripts/list_tables.py new file mode 100644 index 00000000..0fbc83c0 --- /dev/null +++ b/scripts/list_tables.py @@ -0,0 +1,7 @@ +import sqlite3 +db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') +tables = [r[0] for r in db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")] +for t in tables: + cols = [r[1] for r in db.execute(f"PRAGMA table_info({t})")] + print(f"{t}: {', '.join(cols)}") +db.close() diff --git a/scripts/market_scanner.py b/scripts/market_scanner.py index 4d06b7ee..2bdb6b9d 100644 --- a/scripts/market_scanner.py +++ b/scripts/market_scanner.py @@ -1,205 +1,205 @@ -#!/usr/bin/env python3 -"""market_scanner.py — 全市场异动扫描(替代小果扫描线) - -每15分钟扫描: - 1. 板块轮动检测(从已采集的 sector_snapshots 读) - 2. 热门板块领涨股扫描(从腾讯API批量拉) - 3. 资金流向异常检测(从已有 capital_flow_cache 读) - 4. 输出候选股到 candidates 表 - -不依赖小果LLM,纯数据驱动。 -""" -import sys, json, sqlite3, urllib.request, re, time -from pathlib import Path -from datetime import datetime - -# XMPP 日志 hook -try: - sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) - from xmpp_logger import log_xmpp -except ImportError: - def log_xmpp(*a, **kw): pass - -DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") -CANDIDATES_FILE = Path("/home/hmo/web-dashboard/data/candidate_pool.json") - -UA = "Mozilla/5.0" - -def get_conn(): - return sqlite3.connect(str(DB_PATH)) - -def fetch_qq_batch(symbols): - """腾讯批量行情""" - if not symbols: - return {} - url = f"http://qt.gtimg.cn/q={','.join(symbols)}" - try: - req = urllib.request.Request(url, headers={"User-Agent": UA}) - proxy = urllib.request.ProxyHandler({}) - opener = urllib.request.build_opener(proxy) - with opener.open(req, timeout=15) as r: - text = r.read().decode("gbk") - results = {} - for line in text.strip().split("\n"): - if "~" not in line: - continue - parts = line.split("~") - if len(parts) < 40: - continue - m = re.search(r'_(\w+)=', parts[0]) - market = m.group(1) if m else "" - code = parts[2] - name = parts[1] - price = float(parts[3]) if parts[3] else 0 - chg_pct = float(parts[32]) if parts[32] else 0 - high = float(parts[33]) if parts[33] else 0 - low = float(parts[34]) if parts[34] else 0 - volume = int(parts[6]) if parts[6] else 0 - amount = float(parts[37]) if len(parts) > 37 and parts[37] else 0 - if price > 0: - results[code] = {"code": code, "name": name, "price": price, - "change_pct": chg_pct, "high": high, "low": low, - "volume": volume, "amount": amount, - "market": "SH" if market == "sh" else "SZ" if market == "sz" else "HK"} - return results - except Exception as e: - print(f"[SCANNER] 腾讯API错误: {e}", file=sys.stderr) - return {} - -def scan_hot_sectors(): - """从DB读热门板块,返回板块名+领涨股""" - conn = get_conn() - latest = conn.execute("SELECT MAX(id) FROM market_snapshots").fetchone()[0] - if not latest: - conn.close() - return [] - sectors = conn.execute(""" - SELECT name, change_pct, lead_stock, lead_stock_code, up_count, down_count - FROM sector_snapshots WHERE snapshot_id=? - ORDER BY change_pct DESC LIMIT 15 - """, (latest,)).fetchall() - conn.close() - return [{"name": s[0], "change": s[1], "lead_stock": s[2], - "lead_code": s[3], "up": s[4], "down": s[5]} for s in sectors if s[1] > 2.0] - -def scan_candidates(): - """主扫描流程""" - print(f"[SCANNER] {datetime.now().strftime('%H:%M')} 开始扫描", flush=True) - - # 1. 热门板块领涨股 - hot = scan_hot_sectors() - print(f" 热门板块(涨幅>2%): {len(hot)}个", flush=True) - - candidates = {} - - # 从热门板块拉领涨股 - lead_codes = [] - for s in hot: - if s.get("lead_code"): - lc = str(s["lead_code"]).strip() - if lc and lc not in candidates: - lead_codes.append(lc) - candidates[lc] = {"source": f"板块:{s['name']}(+{s['change']:.1f}%)", "sector": s["name"]} - - print(f" 领涨股待查: {len(lead_codes)}只", flush=True) - - # 2. 腾讯API批量查行情 - symbols = [] - for c in lead_codes: - if len(c) == 6: - if c.startswith(("5", "6", "9")): - symbols.append(f"sh{c}") - else: - symbols.append(f"sz{c}") - else: - symbols.append(f"hk{c}") - - prices = fetch_qq_batch(symbols) - print(f" 行情返回: {len(prices)}只", flush=True) - - # 3. 评估候选 - new_candidates = [] - for code, info in prices.items(): - if code not in candidates: - continue - src = candidates[code] - price = info["price"] - chg = info["change_pct"] - name = info["name"] - vol = info["volume"] - amt = info["amount"] - - # 条件:涨幅>3%,有量 - if chg < 3.0: - continue - if vol <= 0: - continue - - score = min(10, round(3 + chg * 0.5 + (amt / 1e8 if amt > 0 else 0) * 0.1, 1)) - - entry_low = round(price * 0.95, 2) - entry_high = round(price, 2) - stop_loss = round(price * 0.92, 2) - take_profit = round(price * 1.15, 2) - - candidate = { - "code": code, - "name": name, - "price": price, - "change_pct": chg, - "score": score, - "entry_low": entry_low, - "entry_high": entry_high, - "stop_loss": stop_loss, - "take_profit": take_profit, - "source": src["source"], - "sector": src.get("sector", ""), - "reason": f"热门板块{src['sector']}领涨+{chg:.1f}%" - } - new_candidates.append(candidate) - print(f" ✅ {code} {name} 价{price} (+{chg:.1f}%) 评分{score}", flush=True) - - # 4. 写入candidates表 - if new_candidates: - conn = get_conn() - for c in new_candidates: - conn.execute( - "INSERT OR REPLACE INTO candidates (code, name, price, change_pct, score, " - "entry_low, entry_high, stop_loss, take_profit, source, sector, reason, created_at) " - "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now','localtime'))", - (c["code"], c["name"], c["price"], c["change_pct"], c["score"], - c["entry_low"], c["entry_high"], c["stop_loss"], c["take_profit"], - c["source"], c["sector"], c["reason"]) - ) - conn.commit() - conn.close() - print(f" ✅ 写入{len(new_candidates)}只候选", flush=True) - else: - print(f" ⚪ 无新候选", flush=True) - - # 5. 推送到Dad(只推高分) - high_score = [c for c in new_candidates if c["score"] >= 6] - if high_score: - lines = ["🔍 市场扫描发现潜在机会:"] - for c in high_score[:3]: - lines.append( - f" {c['name']}({c['code']}) 价{c['price']:.2f}(+{c['change_pct']:.1f}%) " - f"评分{c['score']}/10 | {c['reason']}" - ) - msg = "\n".join(lines) - # 推XMPP(通过知微 Bot HTTP 桥 :5805) - import time as _time - t0 = _time.time() - try: - payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode() - req = urllib.request.Request("http://127.0.0.1:5805/", data=payload, - headers={"Content-Type": "application/json"}) - urllib.request.urlopen(req, timeout=5) - print(f" 📨 已推送{len(high_score)}只高评分候选", flush=True) - log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "ok", None, int((_time.time()-t0)*1000)) - except Exception as e: - print(f" ⚠️ 推送失败: {e}", file=sys.stderr) - log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "error", str(e)[:200], int((_time.time()-t0)*1000)) - -if __name__ == "__main__": - scan_candidates() +#!/usr/bin/env python3 +"""market_scanner.py — 全市场异动扫描(替代小果扫描线) + +每15分钟扫描: + 1. 板块轮动检测(从已采集的 sector_snapshots 读) + 2. 热门板块领涨股扫描(从腾讯API批量拉) + 3. 资金流向异常检测(从已有 capital_flow_cache 读) + 4. 输出候选股到 candidates 表 + +不依赖小果LLM,纯数据驱动。 +""" +import sys, json, sqlite3, urllib.request, re, time +from pathlib import Path +from datetime import datetime + +# XMPP 日志 hook +try: + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + from xmpp_logger import log_xmpp +except ImportError: + def log_xmpp(*a, **kw): pass + +DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") +CANDIDATES_FILE = Path("/home/hmo/web-dashboard/data/candidate_pool.json") + +UA = "Mozilla/5.0" + +def get_conn(): + return sqlite3.connect(str(DB_PATH)) + +def fetch_qq_batch(symbols): + """腾讯批量行情""" + if not symbols: + return {} + url = f"http://qt.gtimg.cn/q={','.join(symbols)}" + try: + req = urllib.request.Request(url, headers={"User-Agent": UA}) + proxy = urllib.request.ProxyHandler({}) + opener = urllib.request.build_opener(proxy) + with opener.open(req, timeout=15) as r: + text = r.read().decode("gbk") + results = {} + for line in text.strip().split("\n"): + if "~" not in line: + continue + parts = line.split("~") + if len(parts) < 40: + continue + m = re.search(r'_(\w+)=', parts[0]) + market = m.group(1) if m else "" + code = parts[2] + name = parts[1] + price = float(parts[3]) if parts[3] else 0 + chg_pct = float(parts[32]) if parts[32] else 0 + high = float(parts[33]) if parts[33] else 0 + low = float(parts[34]) if parts[34] else 0 + volume = int(parts[6]) if parts[6] else 0 + amount = float(parts[37]) if len(parts) > 37 and parts[37] else 0 + if price > 0: + results[code] = {"code": code, "name": name, "price": price, + "change_pct": chg_pct, "high": high, "low": low, + "volume": volume, "amount": amount, + "market": "SH" if market == "sh" else "SZ" if market == "sz" else "HK"} + return results + except Exception as e: + print(f"[SCANNER] 腾讯API错误: {e}", file=sys.stderr) + return {} + +def scan_hot_sectors(): + """从DB读热门板块,返回板块名+领涨股""" + conn = get_conn() + latest = conn.execute("SELECT MAX(id) FROM market_snapshots").fetchone()[0] + if not latest: + conn.close() + return [] + sectors = conn.execute(""" + SELECT name, change_pct, lead_stock, lead_stock_code, up_count, down_count + FROM sector_snapshots WHERE snapshot_id=? + ORDER BY change_pct DESC LIMIT 15 + """, (latest,)).fetchall() + conn.close() + return [{"name": s[0], "change": s[1], "lead_stock": s[2], + "lead_code": s[3], "up": s[4], "down": s[5]} for s in sectors if s[1] > 2.0] + +def scan_candidates(): + """主扫描流程""" + print(f"[SCANNER] {datetime.now().strftime('%H:%M')} 开始扫描", flush=True) + + # 1. 热门板块领涨股 + hot = scan_hot_sectors() + print(f" 热门板块(涨幅>2%): {len(hot)}个", flush=True) + + candidates = {} + + # 从热门板块拉领涨股 + lead_codes = [] + for s in hot: + if s.get("lead_code"): + lc = str(s["lead_code"]).strip() + if lc and lc not in candidates: + lead_codes.append(lc) + candidates[lc] = {"source": f"板块:{s['name']}(+{s['change']:.1f}%)", "sector": s["name"]} + + print(f" 领涨股待查: {len(lead_codes)}只", flush=True) + + # 2. 腾讯API批量查行情 + symbols = [] + for c in lead_codes: + if len(c) == 6: + if c.startswith(("5", "6", "9")): + symbols.append(f"sh{c}") + else: + symbols.append(f"sz{c}") + else: + symbols.append(f"hk{c}") + + prices = fetch_qq_batch(symbols) + print(f" 行情返回: {len(prices)}只", flush=True) + + # 3. 评估候选 + new_candidates = [] + for code, info in prices.items(): + if code not in candidates: + continue + src = candidates[code] + price = info["price"] + chg = info["change_pct"] + name = info["name"] + vol = info["volume"] + amt = info["amount"] + + # 条件:涨幅>3%,有量 + if chg < 3.0: + continue + if vol <= 0: + continue + + score = min(10, round(3 + chg * 0.5 + (amt / 1e8 if amt > 0 else 0) * 0.1, 1)) + + entry_low = round(price * 0.95, 2) + entry_high = round(price, 2) + stop_loss = round(price * 0.92, 2) + take_profit = round(price * 1.15, 2) + + candidate = { + "code": code, + "name": name, + "price": price, + "change_pct": chg, + "score": score, + "entry_low": entry_low, + "entry_high": entry_high, + "stop_loss": stop_loss, + "take_profit": take_profit, + "source": src["source"], + "sector": src.get("sector", ""), + "reason": f"热门板块{src['sector']}领涨+{chg:.1f}%" + } + new_candidates.append(candidate) + print(f" ✅ {code} {name} 价{price} (+{chg:.1f}%) 评分{score}", flush=True) + + # 4. 写入candidates表 + if new_candidates: + conn = get_conn() + for c in new_candidates: + conn.execute( + "INSERT OR REPLACE INTO candidates (code, name, price, change_pct, score, " + "entry_low, entry_high, stop_loss, take_profit, source, sector, reason, created_at) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,datetime('now','localtime'))", + (c["code"], c["name"], c["price"], c["change_pct"], c["score"], + c["entry_low"], c["entry_high"], c["stop_loss"], c["take_profit"], + c["source"], c["sector"], c["reason"]) + ) + conn.commit() + conn.close() + print(f" ✅ 写入{len(new_candidates)}只候选", flush=True) + else: + print(f" ⚪ 无新候选", flush=True) + + # 5. 推送到Dad(只推高分) + high_score = [c for c in new_candidates if c["score"] >= 6] + if high_score: + lines = ["🔍 市场扫描发现潜在机会:"] + for c in high_score[:3]: + lines.append( + f" {c['name']}({c['code']}) 价{c['price']:.2f}(+{c['change_pct']:.1f}%) " + f"评分{c['score']}/10 | {c['reason']}" + ) + msg = "\n".join(lines) + # 推XMPP(通过知微 Bot HTTP 桥 :5805) + import time as _time + t0 = _time.time() + try: + payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode() + req = urllib.request.Request("http://127.0.0.1:5805/", data=payload, + headers={"Content-Type": "application/json"}) + urllib.request.urlopen(req, timeout=5) + print(f" 📨 已推送{len(high_score)}只高评分候选", flush=True) + log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "ok", None, int((_time.time()-t0)*1000)) + except Exception as e: + print(f" ⚠️ 推送失败: {e}", file=sys.stderr) + log_xmpp("out", "zhiwei@yoin.fun", "hmo@yoin.fun", msg, "error", str(e)[:200], int((_time.time()-t0)*1000)) + +if __name__ == "__main__": + scan_candidates() diff --git a/scripts/pp.py b/scripts/pp.py new file mode 100644 index 00000000..d0968003 --- /dev/null +++ b/scripts/pp.py @@ -0,0 +1,3 @@ +import sys, json +d = json.loads(sys.stdin.read()) +print(d['timestamp'], d['status'], 'llm:', d['llm_provider']['status'], '| best_key:', d['best_key']['key_id']) \ No newline at end of file diff --git a/scripts/sc2.py b/scripts/sc2.py new file mode 100644 index 00000000..fec840ab --- /dev/null +++ b/scripts/sc2.py @@ -0,0 +1,8 @@ +import ast, sys +p = '/home/hmo/MoFin/server.py' +try: + ast.parse(open(p).read()) + print('SYNTAX OK:', p) +except SyntaxError as e: + print('SYNTAX ERROR:', e) + sys.exit(1) \ No newline at end of file diff --git a/scripts/sc3.py b/scripts/sc3.py new file mode 100644 index 00000000..bc386a1d --- /dev/null +++ b/scripts/sc3.py @@ -0,0 +1,9 @@ +import ast, sys +for p in ['/home/hmo/MoFin/deploy/profile-scripts/batch_reassess.py', + '/home/hmo/MoFin/deploy/profile-scripts/premarket_full_review.py']: + try: + ast.parse(open(p).read()) + print('OK:', p) + except SyntaxError as e: + print('SYNTAX ERROR:', p, e) + sys.exit(1) \ No newline at end of file diff --git a/scripts/strategy-staleness-check.py b/scripts/strategy-staleness-check.py index db5c9ea3..a77bf060 100644 --- a/scripts/strategy-staleness-check.py +++ b/scripts/strategy-staleness-check.py @@ -13,7 +13,7 @@ import json, sys, os, re, urllib.request, sqlite3 sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from datetime import datetime -from mo_data import read_decisions, read_portfolio, get_price +from mo_data import read_decisions, read_portfolio DB_PATH = '/home/hmo/web-dashboard/data/mofin.db' OUTPUT_PATH = "/home/hmo/web-dashboard/data/strategy_staleness_report.json" @@ -23,7 +23,25 @@ CRITICAL_DAYS = 21 # 超过21天→严重警告 DIVERGENCE_WARN = 30 # 偏离买入区>30%→警告 DIVERGENCE_CRIT = 50 # 偏离>50%→严重 -# ── 使用 mo_data.get_price 统一获取价格 ── +def get_price(code): + """从腾讯API获取当前价""" + try: + market = "sh" if code.startswith("6") else "sz" if code.startswith("0") or code.startswith("3") else "" + if code.startswith(("00", "30")) or code.startswith("68"): + market = "sh" if code.startswith("6") else "sz" + elif code.startswith(("01", "02", "03")): + market = "sz" + url = f"http://qt.gtimg.cn/q={market}{code}" + req = urllib.request.Request(url, headers={"User-Agent": "curl/7.81"}) + with urllib.request.urlopen(req, timeout=5) as resp: + raw = resp.read().decode("gbk") + parts = raw.split("~") + if len(parts) > 3: + price = float(parts[3]) if parts[3] else 0 + chg = float(parts[32]) if parts[32] else 0 + return price, chg if price > 0 else (None, None) + except: pass + return None, None def parse_buy_zone(current): """从策略current字段提取买入区间最低和最高""" diff --git a/scripts/syntax_check.py b/scripts/syntax_check.py new file mode 100644 index 00000000..6fc8ff9f --- /dev/null +++ b/scripts/syntax_check.py @@ -0,0 +1,8 @@ +import ast, sys +src = open('/home/hmo/MoFin/deploy/bot/xmpp_agent_core.py').read() +try: + ast.parse(src) + print('SYNTAX OK, lines:', len(src.splitlines())) +except SyntaxError as e: + print('SYNTAX ERROR:', e) + sys.exit(1) \ No newline at end of file diff --git a/scripts/test_auto_heal.py b/scripts/test_auto_heal.py new file mode 100644 index 00000000..ce4f8c0d --- /dev/null +++ b/scripts/test_auto_heal.py @@ -0,0 +1,7 @@ +import sys +sys.path.insert(0, '/home/hmo/projects/MoFin') +import xmpp_logger as x +import json +print("=== AUTO HEAL RESULT ===") +result = x.auto_heal() +print(json.dumps(result, ensure_ascii=False, indent=2)) \ No newline at end of file diff --git a/scripts/test_image_pipeline.py b/scripts/test_image_pipeline.py new file mode 100644 index 00000000..15bce7c1 --- /dev/null +++ b/scripts/test_image_pipeline.py @@ -0,0 +1,23 @@ +import sys +sys.argv = ['test', '--agent', 'zhiwei'] +# Prevent bot from actually connecting — just test the image functions +src = open('/home/hmo/MoFin/deploy/bot/xmpp_agent_core.py').read() +# Cut off at main entry to avoid starting the bot +cut = src.find("if __name__ ==") +if cut > 0: + src = src[:cut] +ns = {} +exec(compile(src, 'xmpp_agent_core.py', 'exec'), ns) + +url = "https://upload.yoin.fun/upload/d22cef590582300cc5580c722a19280054bf0d6a/brEbU8RxXwipU9VuhZqhhFOIAmQI4epG5dp5so6U/LBbTaPJdRfmzxjBg0uh21A.jpg" +print("== _is_image_url ==", ns['_is_image_url'](url)) +print("== download ==") +img = ns['_download_image'](url) +print("bytes:", len(img) if img else None) +if img: + print("== OCR ==") + ok, text = ns['_ocr_image'](img) + print("ok:", ok) + print("text:", text[:600]) +print("== full pipeline ==") +print(ns['_process_image_message'](url)[:700]) \ No newline at end of file diff --git a/scripts/test_key7_guard.py b/scripts/test_key7_guard.py new file mode 100644 index 00000000..20bdc43f --- /dev/null +++ b/scripts/test_key7_guard.py @@ -0,0 +1,12 @@ +import sys, json +sys.path.insert(0, '/home/hmo/MoFin') +import importlib, xmpp_logger +importlib.reload(xmpp_logger) +x = xmpp_logger +print("current provider:", x.current_provider()) +print("KEY_TO_PROVIDER keys:", list(x.KEY_TO_PROVIDER.keys())) +bk = x.best_key() +print("best_key:", bk["key_id"] if bk else None, "| total_keys:", bk.get("total_keys") if bk else "-") +# Simulate: if best key were key7 (kimi), auto_heal must skip switch +result = x.auto_heal() +print("auto_heal actions:", json.dumps(result["actions"], ensure_ascii=False)) \ No newline at end of file diff --git a/scripts/test_llm.py b/scripts/test_llm.py new file mode 100644 index 00000000..52a3042b --- /dev/null +++ b/scripts/test_llm.py @@ -0,0 +1,9 @@ +import urllib.request, json +data = json.dumps({'model':'deepseek-v4-flash','messages':[{'role':'user','content':'say hi'}],'max_tokens':10}).encode() +req = urllib.request.Request('http://127.0.0.1:8643/v1/chat/completions', data=data, + headers={'Content-Type':'application/json','Authorization':'Bearer hermes123'}) +try: + resp = urllib.request.urlopen(req, timeout=90) + print('OK:', resp.read().decode()[:300]) +except Exception as e: + print('FAIL:', e) \ No newline at end of file diff --git a/scripts/test_multi_profile.py b/scripts/test_multi_profile.py new file mode 100644 index 00000000..01d40aaf --- /dev/null +++ b/scripts/test_multi_profile.py @@ -0,0 +1,22 @@ +import ast, sys, json +p = '/home/hmo/MoFin/xmpp_logger.py' +try: + ast.parse(open(p).read()) + print('SYNTAX OK') +except SyntaxError as e: + print('SYNTAX ERROR:', e) + sys.exit(1) + +sys.path.insert(0, '/home/hmo/MoFin') +import importlib, xmpp_logger +importlib.reload(xmpp_logger) +x = xmpp_logger +import time + +print('== current_provider(zhiwei):', x.current_provider('zhiwei')) +print('== current_provider(default):', x.current_provider('default')) +print('== scan zhiwei:', json.dumps(x._scan_agent_log(time.time(), 'zhiwei'), ensure_ascii=False)) +print('== scan default:', json.dumps(x._scan_agent_log(time.time(), 'default'), ensure_ascii=False)) +print() +print('== auto_heal ==') +print(json.dumps(x.auto_heal(), ensure_ascii=False, indent=2)) \ No newline at end of file diff --git a/scripts/test_noping.py b/scripts/test_noping.py new file mode 100644 index 00000000..6a232329 --- /dev/null +++ b/scripts/test_noping.py @@ -0,0 +1,20 @@ +import ast, sys +for p in ['/home/hmo/MoFin/xmpp_logger.py']: + try: + ast.parse(open(p).read()) + print('SYNTAX OK:', p) + except SyntaxError as e: + print('SYNTAX ERROR:', p, e) + sys.exit(1) +sys.path.insert(0, '/home/hmo/MoFin') +import xmpp_logger as x +import time, json +print('== _scan_agent_log ==') +print(json.dumps(x._scan_agent_log(time.time()), ensure_ascii=False)) +print('== health (no LLM ping) ==') +import time as t +t0 = t.time() +h = x.health() +print(f'took {t.time()-t0:.1f}s') +print('llm_provider:', h.get('llm_provider')) +print('status:', h.get('status')) \ No newline at end of file diff --git a/scripts/test_production.py b/scripts/test_production.py new file mode 100644 index 00000000..679e96db --- /dev/null +++ b/scripts/test_production.py @@ -0,0 +1,8 @@ +import sys, json +sys.path.insert(0, '/home/hmo/MoFin') +import xmpp_logger as x +print("current provider:", x.current_provider()) +print("health status:", x.health().get("status")) +print("verify_llm:", x._verify_llm()) +print("=== auto_heal ===") +print(json.dumps(x.auto_heal(), ensure_ascii=False, indent=2)) \ No newline at end of file diff --git a/scripts/test_single.py b/scripts/test_single.py new file mode 100644 index 00000000..46e3054e --- /dev/null +++ b/scripts/test_single.py @@ -0,0 +1,7 @@ +import urllib.request,json,time +r = urllib.request.Request( + "https://push2.eastmoney.com/api/qt/stock/get?secid=116.00700&fields=f43,f170&fltt=2", + headers={"User-Agent": "Mozilla/5.0"}) +start = time.time() +resp = json.loads(urllib.request.urlopen(r, timeout=5).read()) +print(f"OK {time.time()-start:.1f}s price={resp.get('data',{}).get('f43','?')}") diff --git a/scripts/test_sn_ocr2.py b/scripts/test_sn_ocr2.py new file mode 100644 index 00000000..8c6df72c --- /dev/null +++ b/scripts/test_sn_ocr2.py @@ -0,0 +1,29 @@ +import json, urllib.request, base64 + +KEY = "sk-aRNj3UwKSLPsDfh15QNTPwbHxahblfaO" +BASE = "https://token.sensenova.cn/v1" +MODEL = "sensenova-6.7-flash-lite" + +img_b64 = base64.b64encode(open('/tmp/test_shot.jpg', 'rb').read()).decode() +payload = json.dumps({ + "model": MODEL, + "messages": [{ + "role": "user", + "content": [ + {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}, + {"type": "text", "text": "请识别这张图片中的所有文字内容,包括数字、股票名称、金额、日期。用中文回复。"} + ] + }], + "max_tokens": 1500, +}).encode() +req = urllib.request.Request(f"{BASE}/chat/completions", data=payload, + headers={"Content-Type": "application/json", "Authorization": f"Bearer {KEY}"}) +try: + resp = urllib.request.urlopen(req, timeout=90) + data = json.loads(resp.read().decode()) + msg = data.get('choices', [{}])[0].get('message', {}) + text = msg.get('content', '') or msg.get('reasoning', '') + print('OCR OK:') + print(text[:1500]) +except Exception as e: + print('OCR FAIL:', e) \ No newline at end of file diff --git a/scripts/test_xmpp_logger.py b/scripts/test_xmpp_logger.py new file mode 100644 index 00000000..fd1dd598 --- /dev/null +++ b/scripts/test_xmpp_logger.py @@ -0,0 +1,11 @@ +import sys +sys.path.insert(0, '/home/hmo/projects/MoFin') +import xmpp_logger as x +print("current provider:", x.current_provider()) +h = x.health() +print("health status:", h.get("status")) +print("llm_provider:", h.get("llm_provider")) +print("=== best_key ===") +print(x.best_key()) +print("=== verify_llm ===") +print(x._verify_llm()) \ No newline at end of file diff --git a/scripts/update_backfill_cron.py b/scripts/update_backfill_cron.py new file mode 100644 index 00000000..bfef2ff4 --- /dev/null +++ b/scripts/update_backfill_cron.py @@ -0,0 +1,27 @@ +import json, shutil +from datetime import datetime + +jf = '/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json' +shutil.copy(jf, jf + '.bak-20260720') + +d = json.load(open(jf)) +jobs = d if isinstance(d, list) else d.get('jobs', []) +for j in jobs: + if j.get('name') == '批量补全九维分析-一次性': + j['name'] = '自选12维分析补全-每日午间' + j['script'] = 'watchlist_12d_backfill.py' + j['schedule'] = {"kind": "cron", "expr": "30 12 * * 1-5", "display": "30 12 * * 1-5"} + j['schedule_display'] = "30 12 * * 1-5" + j['next_run_at'] = "2026-07-20T12:30:00+08:00" + j['state'] = 'scheduled' + print('updated job:', j['name'], '| script:', j['script'], '| schedule:', j['schedule_display']) + break +else: + print('job not found!') + +if isinstance(d, list): + json.dump(jobs, open(jf, 'w'), ensure_ascii=False, indent=2) +else: + d['jobs'] = jobs + json.dump(d, open(jf, 'w'), ensure_ascii=False, indent=2) +print('saved') \ No newline at end of file diff --git a/scripts/validate_fixes.py b/scripts/validate_fixes.py new file mode 100644 index 00000000..442bc2be --- /dev/null +++ b/scripts/validate_fixes.py @@ -0,0 +1,37 @@ +import json, urllib.request +from pathlib import Path + +print("=== 验证1: 5个被 Blocked 的脚本现在是否通过调度器路径检查 ===") +scripts_dir = Path('/home/hmo/.hermes/profiles/position-analyst/scripts') +resolved = scripts_dir.resolve() +files = ['meta_growth.py', 'macro_context_collector.py', 'divergence_detector.py', + 'memory_guardian.py', 'fix_gateway_port.py'] +all_ok = True +for f in files: + p = (scripts_dir / f).resolve() + try: + p.relative_to(resolved) + exists = p.exists() + print(f" PASS {f} (exists={exists})") + if not exists: + all_ok = False + except ValueError: + all_ok = False + print(f" FAIL {f} still blocked") +print(' =>', 'ALL PASS' if all_ok else 'STILL FAILING') + +print() +print("=== 验证2: default gateway (8642) LLM 调用(原 key1 429,现 key6)===") +payload = json.dumps({'model': 'deepseek-v4-flash', + 'messages': [{'role': 'user', 'content': 'reply with one word: ok'}], + 'max_tokens': 10}).encode() +req = urllib.request.Request('http://127.0.0.1:8642/v1/chat/completions', data=payload, + headers={'Content-Type': 'application/json', + 'Authorization': 'Bearer hermes123'}) +try: + resp = urllib.request.urlopen(req, timeout=90) + d = json.loads(resp.read().decode()) + content = d.get('choices', [{}])[0].get('message', {}).get('content', '') + print(' LLM OK:', content[:100]) +except Exception as e: + print(' LLM FAIL:', e) \ No newline at end of file diff --git a/scripts/verify_300308.py b/scripts/verify_300308.py new file mode 100644 index 00000000..b85a8c43 --- /dev/null +++ b/scripts/verify_300308.py @@ -0,0 +1,7 @@ +import sqlite3 +conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db') +r = conn.execute("SELECT code, LENGTH(full_analysis), reassessed_at, timing_signal, stop_loss, take_profit FROM holding_strategies WHERE code='300308'").fetchone() +print(r) +# also show first 300 chars of the analysis +r2 = conn.execute("SELECT substr(full_analysis, 1, 400) FROM holding_strategies WHERE code='300308'").fetchone() +print(r2[0] if r2 else 'none') \ No newline at end of file diff --git a/scripts/verify_health_fix.py b/scripts/verify_health_fix.py new file mode 100644 index 00000000..2f99043b --- /dev/null +++ b/scripts/verify_health_fix.py @@ -0,0 +1,14 @@ +import urllib.request, json +d = json.loads(urllib.request.urlopen('http://127.0.0.1:8899/api/xmpp/health', timeout=120).read()) +ba = d.get('bot_activity', {}) +print('status:', d.get('status')) +print('last_error:', (ba.get('last_error') or '')[:80]) +print('last_error_age_sec:', ba.get('last_error_age_sec')) +print('last_error_resolved:', ba.get('last_error_resolved')) +print('last_outbound_age_sec:', ba.get('last_outbound_age_sec')) +print('---') +m = json.loads(urllib.request.urlopen('http://127.0.0.1:8899/api/xmpp/messages', timeout=30).read()) +msgs = m.get('messages', []) +print('messages count:', len(msgs)) +for x in msgs[:5]: + print(' ', x.get('timestamp'), x.get('direction'), (x.get('body_preview') or '')[:50]) \ No newline at end of file diff --git a/scripts/verify_self_todo.py b/scripts/verify_self_todo.py new file mode 100644 index 00000000..62700ca8 --- /dev/null +++ b/scripts/verify_self_todo.py @@ -0,0 +1,32 @@ +"""Verify self_todo_executor works with real DB""" +import subprocess +script = '/home/hmo/.hermes/profiles/position-analyst/scripts/self_todo_executor.py' + +# Test 1: DB_PATH +content = open(script).read() +if 'web-dashboard/data/mofin.db' in content: + print("DB_PATH: OK") +else: + print("DB_PATH: WRONG") + exit(1) + +# Test 2: script can import and run +try: + import importlib.util + spec = importlib.util.spec_from_file_location("executor", script) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + print("Import: OK") +except Exception as e: + print(f"Import: FAIL -> {e}") + exit(1) + +# Test 3: get_pending works +try: + rows = mod.get_pending() + print(f"get_pending: OK ({len(rows)} pending)") +except Exception as e: + print(f"get_pending: FAIL -> {e}") + exit(1) + +print("\nAll checks passed.") diff --git a/specs/dashboard.json b/specs/dashboard.json index ca98a671..2a91e48e 100644 --- a/specs/dashboard.json +++ b/specs/dashboard.json @@ -1,60 +1,60 @@ -{ - "module": "dashboard", - "version": "1.0", - "purpose": "MoFin 管理门户。独立 Flask 应用(端口 5804),统一展示系统健康状态、模块 spec 帮助和监控数据。", - - "human_help": { - "title": "Dashboard — 管理门户", - "description": [ - "MoFin 统一管理面板,提供系统健康状态总览和各模块的帮助文档。", - "采用深色主题 Web UI,与 AgentsMeeting Dashboard 一致的视觉风格。", - "访问地址: http://192.168.1.246:5804" - ], - "usage": [ - "打开浏览器访问 http://192.168.1.246:5804", - "F 健康 Tab — 查看所有服务运行状态和健康管线数据", - "G 规范 Tab — 查看开发规范文档", - "点击 ? 按钮 — 查看面向人类的模块帮助", - "点击 § 按钮 — 查看面向 AI 的接口约束文档" - ], - "troubleshooting": [ - "Dashboard 不响应 → ssh 246 'sudo systemctl restart mofin-dashboard'", - "F Tab 无数据 → 检查 crontab 中健康检查任务是否运行", - "Spec 加载失败 → 检查 specs/ 目录权限和 JSON 格式" - ] - }, - - "ai_spec": { - "apis": [ - {"method": "GET", "path": "/api/health", "returns": "{status:'ok', uptime:N}"}, - {"method": "GET", "path": "/api/services", "returns": "{services[{name, type, port, status}]} — 所有注册服务的运行状态"}, - {"method": "GET", "path": "/api/expected", "returns": "{expected[{name, port, expected, critical}], actual{name:status}} — 期望状态矩阵"}, - {"method": "GET", "path": "/api/monitor", "returns": "{tier1{summary{ok,total}}, tier2{summary{ok,total}}, tasks[{name,status}]} — 聚合监控数据"}, - {"method": "GET", "path": "/api/module-spec/", "returns": "specs/{module}.json 内容"} - ], - "dependencies": [ - "specs/ 目录 — 所有模块的 spec JSON 文件", - "agents_health_check.py — Tier1 健康检查(生成 last_health_check.json)", - "agents_daily_health.py — Tier2 日检(生成 last_daily_health.json)", - "server.py :8899 — 业务 API(Dashboard 通过 HTTP 检测其可达性)" - ], - "constraints": [ - "Dashboard 部署在 Linux 246 上,端口 5804", - "通过 systemd 守护(mofin-dashboard.service)", - "不依赖 server.py :8899(可独立运行)", - "前端 5 秒轮询 /api/services + /api/expected", - "?§ 按钮通过 /api/module-spec/ 读取 spec JSON" - ], - "must_not": [ - "不要修改 server.py :8899 来集成 Dashboard(保持独立)", - "不要在 Dashboard 中硬编码业务数据(只做监控和文档展示)", - "不要移除 ?§ 按钮系统(这是 spec 可视化的核心)" - ], - "related_files": [ - "dashboard.py — Flask 后端", - "templates/dashboard.html — 前端", - "specs/ — Spec 文件目录", - "gateway/logs/ — 运行时日志" - ] - } -} +{ + "module": "dashboard", + "version": "1.0", + "purpose": "MoFin 管理门户。独立 Flask 应用(端口 8899),统一展示系统健康状态、模块 spec 帮助和监控数据。", + + "human_help": { + "title": "Dashboard — 管理门户", + "description": [ + "MoFin 统一管理面板,提供系统健康状态总览和各模块的帮助文档。", + "采用深色主题 Web UI,与 AgentsMeeting Dashboard 一致的视觉风格。", + "访问地址: http://192.168.1.246:8899" + ], + "usage": [ + "打开浏览器访问 http://192.168.1.246:8899", + "F 健康 Tab — 查看所有服务运行状态和健康管线数据", + "G 规范 Tab — 查看开发规范文档", + "点击 ? 按钮 — 查看面向人类的模块帮助", + "点击 § 按钮 — 查看面向 AI 的接口约束文档" + ], + "troubleshooting": [ + "Dashboard 不响应 → ssh 246 'sudo systemctl restart mofin-dashboard'", + "F Tab 无数据 → 检查 crontab 中健康检查任务是否运行", + "Spec 加载失败 → 检查 specs/ 目录权限和 JSON 格式" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/health", "returns": "{status:'ok', uptime:N}"}, + {"method": "GET", "path": "/api/services", "returns": "{services[{name, type, port, status}]} — 所有注册服务的运行状态"}, + {"method": "GET", "path": "/api/expected", "returns": "{expected[{name, port, expected, critical}], actual{name:status}} — 期望状态矩阵"}, + {"method": "GET", "path": "/api/monitor", "returns": "{tier1{summary{ok,total}}, tier2{summary{ok,total}}, tasks[{name,status}]} — 聚合监控数据"}, + {"method": "GET", "path": "/api/module-spec/", "returns": "specs/{module}.json 内容"} + ], + "dependencies": [ + "specs/ 目录 — 所有模块的 spec JSON 文件", + "agents_health_check.py — Tier1 健康检查(生成 last_health_check.json)", + "agents_daily_health.py — Tier2 日检(生成 last_daily_health.json)", + "server.py :8899 — 业务 API(Dashboard 通过 HTTP 检测其可达性)" + ], + "constraints": [ + "Dashboard 部署在 Linux 246 上,端口 8899", + "通过 systemd 守护(mofin-dashboard.service)", + "不依赖 server.py :8899(可独立运行)", + "前端 5 秒轮询 /api/services + /api/expected", + "?§ 按钮通过 /api/module-spec/ 读取 spec JSON" + ], + "must_not": [ + "不要修改 server.py :8899 来集成 Dashboard(保持独立)", + "不要在 Dashboard 中硬编码业务数据(只做监控和文档展示)", + "不要移除 ?§ 按钮系统(这是 spec 可视化的核心)" + ], + "related_files": [ + "dashboard.py — Flask 后端", + "templates/dashboard.html — 前端", + "specs/ — Spec 文件目录", + "gateway/logs/ — 运行时日志" + ] + } +} diff --git a/specs/decisions.json b/specs/decisions.json index f961e607..625bd197 100644 --- a/specs/decisions.json +++ b/specs/decisions.json @@ -1,53 +1,53 @@ -{ - "module": "decisions", - "version": "1.0", - "purpose": "策略决策库。管理持仓和自选股的策略决策(止损/止盈/买入区/操作建议),支持新旧格式兼容。", - - "human_help": { - "title": "策略决策库", - "description": [ - "存储每只持仓/自选股的策略决策数据,包括止损价、止盈价、买入区间、操作建议等。", - "数据来自知微 LLM 分析,通过 /api/analysis/batch 写入。", - "支持新旧两种数据格式的自动兼容(新格式:stop_loss/take_profit 顶层字段;旧格式:trigger 对象)。" - ], - "usage": [ - "GET /api/decisions — 获取全部决策(按标签+执行状态排序)", - "POST /api/decisions/add — 新增/更新一条决策(同股票旧决策自动标记 superseded)", - "POST /api/decisions/tag — 设置/清除推荐标签(current_recommend / active_manual)", - "GET /api/decisions/pending — 获取有未确认建议的条目" - ], - "troubleshooting": [ - "决策列表为空 → 确认 regenerate_all 已执行或知微已写入", - "旧格式不显示 → 检查归一化逻辑(/api/decisions GET handler)" - ] - }, - - "ai_spec": { - "apis": [ - {"method": "GET", "path": "/api/decisions", "returns": "{decisions[{code, name, type, status, tag, action, trigger{stop_loss, take_profit, entry_zone}, current, zone_breach, updated_reason, advice_timeline, changelog, execution, analysis}], total, regenerated_at}"}, - {"method": "POST", "path": "/api/decisions/add", "returns": "{status:'ok', entry:{...}}"}, - {"method": "POST", "path": "/api/decisions/tag", "returns": "{status:'ok', code, tag}"}, - {"method": "GET", "path": "/api/decisions/pending", "returns": "[{code, name, current, pending_advice[{date, direction, price, summary, status}]}]"} - ], - "dependencies": [ - "mo_data.py — read_decisions()", - "mofin_db.py — write_holding_strategy()", - "strategy_lifecycle.py — regenerate_all 触发全量重评" - ], - "constraints": [ - "支持新旧两种格式的读取兼容(normalized 逻辑)", - "同股票新决策会自动将旧决策标记为 superseded", - "排序规则:current_recommend 标签 > 执行状态(partial_exit > executing > observing) > code", - "advice_timeline 去重:同日期+同方向+摘要前40字相同 → skip" - ], - "must_not": [ - "不要在决策中硬编码价格阈值(应由 LLM 分析生成)", - "不要删除 superseded 的旧决策(保留历史记录)" - ], - "related_files": [ - "server.py — /api/decisions* 路由", - "mo_data.py — read_decisions()", - "mofin_db.py — holding_strategies 表" - ] - } -} +{ + "module": "decisions", + "version": "1.0", + "purpose": "策略决策库。管理持仓和自选股的策略决策(止损/止盈/买入区/操作建议),支持新旧格式兼容。", + + "human_help": { + "title": "策略决策库", + "description": [ + "存储每只持仓/自选股的策略决策数据,包括止损价、止盈价、买入区间、操作建议等。", + "数据来自知微 LLM 分析,通过 /api/analysis/batch 写入。", + "支持新旧两种数据格式的自动兼容(新格式:stop_loss/take_profit 顶层字段;旧格式:trigger 对象)。" + ], + "usage": [ + "GET /api/decisions — 获取全部决策(按标签+执行状态排序)", + "POST /api/decisions/add — 新增/更新一条决策(同股票旧决策自动标记 superseded)", + "POST /api/decisions/tag — 设置/清除推荐标签(current_recommend / active_manual)", + "GET /api/decisions/pending — 获取有未确认建议的条目" + ], + "troubleshooting": [ + "决策列表为空 → 确认 regenerate_all 已执行或知微已写入", + "旧格式不显示 → 检查归一化逻辑(/api/decisions GET handler)" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/decisions", "returns": "{decisions[{code, name, type, status, tag, action, trigger{stop_loss, take_profit, entry_zone}, current, zone_breach, updated_reason, advice_timeline, changelog, execution, analysis}], total, regenerated_at}"}, + {"method": "POST", "path": "/api/decisions/add", "returns": "{status:'ok', entry:{...}}"}, + {"method": "POST", "path": "/api/decisions/tag", "returns": "{status:'ok', code, tag}"}, + {"method": "GET", "path": "/api/decisions/pending", "returns": "[{code, name, current, pending_advice[{date, direction, price, summary, status}]}]"} + ], + "dependencies": [ + "mo_data.py — read_decisions()", + "mofin_db.py — write_holding_strategy()", + "strategy_lifecycle.py — regenerate_all 触发全量重评" + ], + "constraints": [ + "支持新旧两种格式的读取兼容(normalized 逻辑)", + "同股票新决策会自动将旧决策标记为 superseded", + "排序规则:current_recommend 标签 > 执行状态(partial_exit > executing > observing) > code", + "advice_timeline 去重:同日期+同方向+摘要前40字相同 → skip" + ], + "must_not": [ + "不要在决策中硬编码价格阈值(应由 LLM 分析生成)", + "不要删除 superseded 的旧决策(保留历史记录)" + ], + "related_files": [ + "server.py — /api/decisions* 路由", + "mo_data.py — read_decisions()", + "mofin_db.py — holding_strategies 表" + ] + } +} diff --git a/specs/evaluation.json b/specs/evaluation.json index 772f685c..4e6042d1 100644 --- a/specs/evaluation.json +++ b/specs/evaluation.json @@ -1,48 +1,48 @@ -{ - "module": "evaluation", - "version": "1.0", - "purpose": "策略评估系统。提供策略双维度评估结果查询、手动触发评估和准确率统计。", - - "human_help": { - "title": "策略评估", - "description": [ - "评估每只股票策略的有效性,来自 strategy_evaluator.py(每周六 21:00 自动运行)。", - "支持手动触发评估(POST /api/evaluation/trigger)。", - "评估数据主源为 evaluation.json,备选为 decisions.json 中的 evaluation 字段。" - ], - "usage": [ - "GET /api/evaluation — 获取全部策略评估结果", - "POST /api/evaluation/trigger — 手动触发策略评估(执行 strategy_evaluator.py)", - "GET /api/stats/accuracy — 获取准确率统计数据", - "GET /api/feedback — 获取策略反馈数据" - ], - "troubleshooting": [ - "评估数据为空 → 确认 strategy_evaluator.py 已运行过", - "触发失败 → 检查 strategy_evaluator.py 路径是否正确" - ] - }, - - "ai_spec": { - "apis": [ - {"method": "GET", "path": "/api/evaluation", "returns": "[{code, name, type, current, evaluations[{date, dimension, score, reason}]}]"}, - {"method": "POST", "path": "/api/evaluation/trigger", "returns": "{status:'ok', output, error} — 执行 strategy_evaluator.py,timeout 60s"}, - {"method": "GET", "path": "/api/stats/accuracy", "returns": "accuracy_stats.json 内容"}, - {"method": "GET", "path": "/api/feedback", "returns": "strategy_feedback.json 内容"} - ], - "dependencies": [ - "strategy_evaluator.py — 双维度评估脚本(cron: 周六 21:00)", - "evaluation.json — 评估结果主数据源", - "accuracy_stats.json — 准确率统计", - "strategy_feedback.json — 反馈数据" - ], - "constraints": [ - "POST /api/evaluation/trigger 会阻塞最多 60 秒", - "评估数据优先读 evaluation.json,fallback 读 decisions.json 的 evaluation 字段" - ], - "related_files": [ - "server.py — /api/evaluation, /api/evaluation/trigger, /api/stats/accuracy, /api/feedback", - "strategy_evaluator.py", - "strategy_feedback.py" - ] - } -} +{ + "module": "evaluation", + "version": "1.0", + "purpose": "策略评估系统。提供策略双维度评估结果查询、手动触发评估和准确率统计。", + + "human_help": { + "title": "策略评估", + "description": [ + "评估每只股票策略的有效性,来自 strategy_evaluator.py(每周六 21:00 自动运行)。", + "支持手动触发评估(POST /api/evaluation/trigger)。", + "评估数据主源为 evaluation.json,备选为 decisions.json 中的 evaluation 字段。" + ], + "usage": [ + "GET /api/evaluation — 获取全部策略评估结果", + "POST /api/evaluation/trigger — 手动触发策略评估(执行 strategy_evaluator.py)", + "GET /api/stats/accuracy — 获取准确率统计数据", + "GET /api/feedback — 获取策略反馈数据" + ], + "troubleshooting": [ + "评估数据为空 → 确认 strategy_evaluator.py 已运行过", + "触发失败 → 检查 strategy_evaluator.py 路径是否正确" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/evaluation", "returns": "[{code, name, type, current, evaluations[{date, dimension, score, reason}]}]"}, + {"method": "POST", "path": "/api/evaluation/trigger", "returns": "{status:'ok', output, error} — 执行 strategy_evaluator.py,timeout 60s"}, + {"method": "GET", "path": "/api/stats/accuracy", "returns": "accuracy_stats.json 内容"}, + {"method": "GET", "path": "/api/feedback", "returns": "strategy_feedback.json 内容"} + ], + "dependencies": [ + "strategy_evaluator.py — 双维度评估脚本(cron: 周六 21:00)", + "evaluation.json — 评估结果主数据源", + "accuracy_stats.json — 准确率统计", + "strategy_feedback.json — 反馈数据" + ], + "constraints": [ + "POST /api/evaluation/trigger 会阻塞最多 60 秒", + "评估数据优先读 evaluation.json,fallback 读 decisions.json 的 evaluation 字段" + ], + "related_files": [ + "server.py — /api/evaluation, /api/evaluation/trigger, /api/stats/accuracy, /api/feedback", + "strategy_evaluator.py", + "strategy_feedback.py" + ] + } +} diff --git a/specs/health.json b/specs/health.json index caf08660..34c7e63b 100644 --- a/specs/health.json +++ b/specs/health.json @@ -1,73 +1,73 @@ -{ - "module": "health", - "version": "1.0", - "purpose": "MoFin 系统健康监控管线。三层监控(Tier1 快速检查 + Tier2 日检),聚合到 Dashboard F Tab 展示。", - - "human_help": { - "title": "F 健康 — 系统健康", - "description": [ - "实时监控 MoFin 所有关键服务(Flask API、数据库、cron 任务)的运行状态。", - "两层监控:", - " Tier1 — 每 5 分钟快速端口/进程检查", - " Tier2 — 每日 8:00 开盘前全面体检(进程+端口+DB+磁盘+cron)", - "检查结果聚合到 Dashboard F Tab,异常自动告警。" - ], - "usage": [ - "1. 打开 Dashboard → F 健康 Tab 查看概览", - "2. 绿色 = 正常,黄色 = 部分异常,红色 = 严重异常", - "3. 异常服务列表直接显示影响描述", - "4. 定时任务区域检查所有 cron 是否正常运行" - ], - "troubleshooting": [ - "F Tab 显示无数据 → 检查 crontab 中健康检查是否部署", - "服务显示 down 但实际在运行 → 检查端口或 health 端点是否正确", - "TODO 堆积 → 检查 self_todo_executor 是否在 crontab 中" - ] - }, - - "ai_spec": { - "apis": [ - {"method": "GET", "path": "/api/services", "returns": "{services[{name, type, port, health{ok}, status}]}"}, - {"method": "GET", "path": "/api/expected", "returns": "{expected[{name, port, expected, critical}], actual{name:status}}"}, - {"method": "GET", "path": "/api/monitor", "returns": "{tier1{summary{ok,total}}, tier2{summary{ok,total}}, tasks[{name,status}]}"} - ], - "dependencies": [ - "agents_health_check.py — Tier1(每 5 分钟,socket 端口 + HTTP /health)", - "agents_daily_health.py — Tier2(每日 8:00,端口+进程+DB+磁盘+cron)", - "mofin.db — SQLite 数据库(检查可读写)" - ], - "architecture": { - "monitored_services": [ - {"name": "mofin_api", "port": 8899, "type": "http", "check": "GET /api/portfolio"}, - {"name": "mofin_dashboard", "port": 5804, "type": "http", "check": "GET /api/health"}, - {"name": "zhiwei_gateway", "port": 8643, "type": "http", "check": "GET /v1/health"}, - {"name": "ejabberd", "port": 5222, "type": "tcp", "check": "socket connect"}, - {"name": "mofin_db", "port": 0, "type": "file", "check": "sqlite3 connect + SELECT"} - ] - }, - "constraints": [ - "Tier1 全正常时静默(不输出日志)", - "Tier1 异常写入 gateway/temp/health_todos.jsonl", - "Tier1 报告写入 gateway/temp/last_health_check.json", - "Tier2 报告写入 gateway/temp/last_daily_health.json", - "Dashboard /api/monitor 聚合读取以上 JSON 文件" - ], - "must_not": [ - "不要在健康检查中修改业务数据", - "不要硬编码 Windows 路径或命令", - "不要检查已停用的服务(wechat_agent 等)" - ], - "tests": [ - {"id": "H01", "name": "/api/services 返回服务列表", "endpoint": "GET /api/services"}, - {"id": "H02", "name": "/api/expected 返回期望矩阵", "endpoint": "GET /api/expected"}, - {"id": "H03", "name": "/api/monitor 返回监控数据", "endpoint": "GET /api/monitor"} - ], - "related_files": [ - "agents_health_check.py — Tier1 健康检查", - "agents_daily_health.py — Tier2 日检", - "dashboard.py — /api/services, /api/expected, /api/monitor", - "templates/dashboard.html — F Tab 渲染", - "specs/health.json — 本 spec 文件" - ] - } -} +{ + "module": "health", + "version": "1.0", + "purpose": "MoFin 系统健康监控管线。三层监控(Tier1 快速检查 + Tier2 日检),聚合到 Dashboard F Tab 展示。", + + "human_help": { + "title": "F 健康 — 系统健康", + "description": [ + "实时监控 MoFin 所有关键服务(Flask API、数据库、cron 任务)的运行状态。", + "两层监控:", + " Tier1 — 每 5 分钟快速端口/进程检查", + " Tier2 — 每日 8:00 开盘前全面体检(进程+端口+DB+磁盘+cron)", + "检查结果聚合到 Dashboard F Tab,异常自动告警。" + ], + "usage": [ + "1. 打开 Dashboard → F 健康 Tab 查看概览", + "2. 绿色 = 正常,黄色 = 部分异常,红色 = 严重异常", + "3. 异常服务列表直接显示影响描述", + "4. 定时任务区域检查所有 cron 是否正常运行" + ], + "troubleshooting": [ + "F Tab 显示无数据 → 检查 crontab 中健康检查是否部署", + "服务显示 down 但实际在运行 → 检查端口或 health 端点是否正确", + "TODO 堆积 → 检查 self_todo_executor 是否在 crontab 中" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/services", "returns": "{services[{name, type, port, health{ok}, status}]}"}, + {"method": "GET", "path": "/api/expected", "returns": "{expected[{name, port, expected, critical}], actual{name:status}}"}, + {"method": "GET", "path": "/api/monitor", "returns": "{tier1{summary{ok,total}}, tier2{summary{ok,total}}, tasks[{name,status}]}"} + ], + "dependencies": [ + "agents_health_check.py — Tier1(每 5 分钟,socket 端口 + HTTP /health)", + "agents_daily_health.py — Tier2(每日 8:00,端口+进程+DB+磁盘+cron)", + "mofin.db — SQLite 数据库(检查可读写)" + ], + "architecture": { + "monitored_services": [ + {"name": "mofin_api", "port": 8899, "type": "http", "check": "GET /api/portfolio"}, + {"name": "mofin_dashboard", "port": 5804, "type": "http", "check": "GET /api/health"}, + {"name": "zhiwei_gateway", "port": 8643, "type": "http", "check": "GET /v1/health"}, + {"name": "ejabberd", "port": 5222, "type": "tcp", "check": "socket connect"}, + {"name": "mofin_db", "port": 0, "type": "file", "check": "sqlite3 connect + SELECT"} + ] + }, + "constraints": [ + "Tier1 全正常时静默(不输出日志)", + "Tier1 异常写入 gateway/temp/health_todos.jsonl", + "Tier1 报告写入 gateway/temp/last_health_check.json", + "Tier2 报告写入 gateway/temp/last_daily_health.json", + "Dashboard /api/monitor 聚合读取以上 JSON 文件" + ], + "must_not": [ + "不要在健康检查中修改业务数据", + "不要硬编码 Windows 路径或命令", + "不要检查已停用的服务(wechat_agent 等)" + ], + "tests": [ + {"id": "H01", "name": "/api/services 返回服务列表", "endpoint": "GET /api/services"}, + {"id": "H02", "name": "/api/expected 返回期望矩阵", "endpoint": "GET /api/expected"}, + {"id": "H03", "name": "/api/monitor 返回监控数据", "endpoint": "GET /api/monitor"} + ], + "related_files": [ + "agents_health_check.py — Tier1 健康检查", + "agents_daily_health.py — Tier2 日检", + "dashboard.py — /api/services, /api/expected, /api/monitor", + "templates/dashboard.html — F Tab 渲染", + "specs/health.json — 本 spec 文件" + ] + } +} diff --git a/specs/market.json b/specs/market.json index 656d60ce..96a32168 100644 --- a/specs/market.json +++ b/specs/market.json @@ -1,42 +1,42 @@ -{ - "module": "market", - "version": "1.0", - "purpose": "市场观察数据。提供大盘指数和板块数据的查询和更新。", - - "human_help": { - "title": "市场观察", - "description": [ - "展示大盘指数(上证、深证、恒生等)和板块热度数据。", - "数据由 market_watch.py cron(每 30 分钟)自动采集。", - "优先从 DB 读取(market_snapshots / sector_snapshots 表),DB 无数据时 fallback 到 market.json。" - ], - "usage": [ - "GET /api/market — 获取最新市场数据(指数 + 板块)", - "POST /api/update/market — 更新市场数据(由 market_watch 调用)" - ], - "troubleshooting": [ - "市场数据为空 → 检查 market_watch cron 是否正常运行", - "数据显示旧 → 手动运行 python3 market_watch.py 更新" - ] - }, - - "ai_spec": { - "apis": [ - {"method": "GET", "path": "/api/market", "returns": "{indices[{name, code, price, change_pct}], sectors[{name, change_pct, leader}]}"}, - {"method": "POST", "path": "/api/update/market", "returns": "{status:'ok'}"} - ], - "dependencies": [ - "mofin_db.py — market_snapshots / sector_snapshots 表", - "market_watch.py — 大盘采集 cron(*/30 9-15)", - "market_screener.py — 全市场筛选 cron" - ], - "constraints": [ - "DB 优先读取,JSON 仅做 fallback" - ], - "related_files": [ - "server.py — /api/market, /api/update/market", - "market_watch.py — 大盘数据采集", - "market_screener.py — 全市场筛选" - ] - } -} +{ + "module": "market", + "version": "1.0", + "purpose": "市场观察数据。提供大盘指数和板块数据的查询和更新。", + + "human_help": { + "title": "市场观察", + "description": [ + "展示大盘指数(上证、深证、恒生等)和板块热度数据。", + "数据由 market_watch.py cron(每 30 分钟)自动采集。", + "优先从 DB 读取(market_snapshots / sector_snapshots 表),DB 无数据时 fallback 到 market.json。" + ], + "usage": [ + "GET /api/market — 获取最新市场数据(指数 + 板块)", + "POST /api/update/market — 更新市场数据(由 market_watch 调用)" + ], + "troubleshooting": [ + "市场数据为空 → 检查 market_watch cron 是否正常运行", + "数据显示旧 → 手动运行 python3 market_watch.py 更新" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/market", "returns": "{indices[{name, code, price, change_pct}], sectors[{name, change_pct, leader}]}"}, + {"method": "POST", "path": "/api/update/market", "returns": "{status:'ok'}"} + ], + "dependencies": [ + "mofin_db.py — market_snapshots / sector_snapshots 表", + "market_watch.py — 大盘采集 cron(*/30 9-15)", + "market_screener.py — 全市场筛选 cron" + ], + "constraints": [ + "DB 优先读取,JSON 仅做 fallback" + ], + "related_files": [ + "server.py — /api/market, /api/update/market", + "market_watch.py — 大盘数据采集", + "market_screener.py — 全市场筛选" + ] + } +} diff --git a/specs/portfolio.json b/specs/portfolio.json index 24806dd8..450b8b69 100644 --- a/specs/portfolio.json +++ b/specs/portfolio.json @@ -1,60 +1,60 @@ -{ - "module": "portfolio", - "version": "1.0", - "purpose": "持仓数据查询与管理。提供持仓列表、资产概览、实时价格更新。", - - "human_help": { - "title": "持仓管理", - "description": [ - "本模块管理老爸的股票持仓数据,包括个股持仓明细和总资产概览。", - "数据存储在 SQLite (mofin.db),由 price_monitor cron 每 2 分钟更新价格。", - "港股以 HKD 存储,汇总时自动转换为 CNY。" - ], - "usage": [ - "GET /api/portfolio — 获取完整持仓列表(含价格、涨跌幅、盈亏)", - "GET /api/overview — 获取总资产概览(总资产、股票市值、现金、仓位、top movers)", - "POST /api/update/portfolio — 批量更新持仓数据(由 cron 调用)", - "POST /api/update/realtime — 实时价格更新(由 price_monitor 调用)" - ], - "troubleshooting": [ - "数据库查询失败 → 检查 mofin.db 是否存在且可读写", - "港股价格异常 → 确认 hk_rate.py 汇率 API 可达", - "数据不更新 → 检查 crontab 中 price_monitor 是否正常运行" - ] - }, - - "ai_spec": { - "apis": [ - {"method": "GET", "path": "/api/portfolio", "returns": "{total_assets, stock_value, cash, position_pct, total_pnl, holdings[{code, name, price, cost, shares, change_pct, currency, ...}]}"}, - {"method": "GET", "path": "/api/overview", "returns": "{total_assets, stock_value, cash, position_pct, total_pnl, top_movers, market, alerts, updated_at}"}, - {"method": "POST", "path": "/api/update/portfolio", "returns": "{status:'ok'}"}, - {"method": "POST", "path": "/api/update/realtime", "returns": "{status:'ok'}"} - ], - "dependencies": [ - "mo_data.py — read_portfolio() 统一读取层", - "mofin_db.py — get_conn(), query_holdings(), query_portfolio_summary()", - "price_monitor.py — 唯一价格写入者,cron: */2 9-16 1-5" - ], - "constraints": [ - "港股个股价格/成本以 HKD 存储,currency='HKD'", - "A股个股价格/成本以 CNY 存储,currency='CNY'", - "总资产/总市值以 CNY 汇总(calc_total_assets 自动转换)", - "禁止跨币种直接比较或加减", - "price_monitor 是唯一的价格写入源,其他脚本禁止直接写价格" - ], - "must_not": [ - "不要在各业务脚本中直接写 SQL(必须通过 mofin_db.py)", - "不要硬编码汇率(必须通过 hk_rate.py 的 get_hk_rate())", - "不要直接 json.load 读数据(必须通过 mo_data.py)", - "不要自己实现 calc_total_assets / is_hk_stock(必须用 mo_models.py)" - ], - "related_files": [ - "server.py — API 路由定义", - "mo_models.py — 数据模型(calc_total_assets, is_hk_stock, to_cny)", - "mo_data.py — 统一读取层", - "mofin_db.py — DB 层", - "price_monitor.py — 价格更新 cron", - "hk_rate.py — 港币汇率" - ] - } -} +{ + "module": "portfolio", + "version": "1.0", + "purpose": "持仓数据查询与管理。提供持仓列表、资产概览、实时价格更新。", + + "human_help": { + "title": "持仓管理", + "description": [ + "本模块管理老爸的股票持仓数据,包括个股持仓明细和总资产概览。", + "数据存储在 SQLite (mofin.db),由 price_monitor cron 每 2 分钟更新价格。", + "港股以 HKD 存储,汇总时自动转换为 CNY。" + ], + "usage": [ + "GET /api/portfolio — 获取完整持仓列表(含价格、涨跌幅、盈亏)", + "GET /api/overview — 获取总资产概览(总资产、股票市值、现金、仓位、top movers)", + "POST /api/update/portfolio — 批量更新持仓数据(由 cron 调用)", + "POST /api/update/realtime — 实时价格更新(由 price_monitor 调用)" + ], + "troubleshooting": [ + "数据库查询失败 → 检查 mofin.db 是否存在且可读写", + "港股价格异常 → 确认 hk_rate.py 汇率 API 可达", + "数据不更新 → 检查 crontab 中 price_monitor 是否正常运行" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/portfolio", "returns": "{total_assets, stock_value, cash, position_pct, total_pnl, holdings[{code, name, price, cost, shares, change_pct, currency, ...}]}"}, + {"method": "GET", "path": "/api/overview", "returns": "{total_assets, stock_value, cash, position_pct, total_pnl, top_movers, market, alerts, updated_at}"}, + {"method": "POST", "path": "/api/update/portfolio", "returns": "{status:'ok'}"}, + {"method": "POST", "path": "/api/update/realtime", "returns": "{status:'ok'}"} + ], + "dependencies": [ + "mo_data.py — read_portfolio() 统一读取层", + "mofin_db.py — get_conn(), query_holdings(), query_portfolio_summary()", + "price_monitor.py — 唯一价格写入者,cron: */2 9-16 1-5" + ], + "constraints": [ + "港股个股价格/成本以 HKD 存储,currency='HKD'", + "A股个股价格/成本以 CNY 存储,currency='CNY'", + "总资产/总市值以 CNY 汇总(calc_total_assets 自动转换)", + "禁止跨币种直接比较或加减", + "price_monitor 是唯一的价格写入源,其他脚本禁止直接写价格" + ], + "must_not": [ + "不要在各业务脚本中直接写 SQL(必须通过 mofin_db.py)", + "不要硬编码汇率(必须通过 hk_rate.py 的 get_hk_rate())", + "不要直接 json.load 读数据(必须通过 mo_data.py)", + "不要自己实现 calc_total_assets / is_hk_stock(必须用 mo_models.py)" + ], + "related_files": [ + "server.py — API 路由定义", + "mo_models.py — 数据模型(calc_total_assets, is_hk_stock, to_cny)", + "mo_data.py — 统一读取层", + "mofin_db.py — DB 层", + "price_monitor.py — 价格更新 cron", + "hk_rate.py — 港币汇率" + ] + } +} diff --git a/specs/signals.json b/specs/signals.json index 2accd5b2..5119603e 100644 --- a/specs/signals.json +++ b/specs/signals.json @@ -1,50 +1,50 @@ -{ - "module": "signals", - "version": "1.0", - "purpose": "信号数据。提供市场信号查询,数据来自趋势检测(macro_context_collector/divergence_detector)和全市场扫描(market_scanner)。", - - "human_help": { - "title": "信号与扫描", - "description": [ - "展示系统产生的交易信号,包括趋势检测信号和全市场扫描候选股。", - "趋势信号来自 macro_context_collector(宏观)和 divergence_detector(背离检测)。", - "全市场扫描由 market_scanner.py 纯数据驱动(不依赖 LLM),每 15 分钟从热门板块筛选候选股。", - "所有信号存储在 signal_news 表中,候选股存储在 candidates 表中。" - ], - "usage": [ - "GET /api/signals — 获取最近 20 条信号(含趋势和扫描信号)", - "GET /api/candidates — 获取候选股池(最近 50 只,市场扫描产出)" - ], - "troubleshooting": [ - "信号为空 → 检查 macro_context_collector 和 market_scanner cron 状态", - "候选池为空 → 确认 sector_snapshots 有数据,market_scanner cron 在运行" - ] - }, - - "ai_spec": { - "apis": [ - {"method": "GET", "path": "/api/signals", "returns": "[{id, sector, overall_sentiment, summary, source, created_at, signal_type, severity}] — 最近 20 条信号"}, - {"method": "GET", "path": "/api/candidates", "returns": "[{code, name, reason, score_2nd..score_5th, score_final, pass_s2..pass_s5, promoted, log, created_at}] — 候选股池(由 market_scanner 产出)"} - ], - "dependencies": [ - "mofin_db.py — signal_news / candidates 表", - "macro_context_collector.py — 宏观信号采集 cron", - "divergence_detector.py — 背离检测 cron", - "scripts/market_scanner.py — 全市场扫描 cron(*/15 9-15 1-5,纯数据驱动)" - ], - "constraints": [ - "signal_news 和 sector_signals 通过 LEFT JOIN 关联", - "candidates 表由 market_scanner 写入,纯数据驱动不调 LLM", - "旧 xiaoguo_scanner 已废弃,不再使用" - ], - "must_not": [ - "不要调小果 LLM API", - "不要依赖 xiaoguo_scan_tracker 表(已废弃)" - ], - "related_files": [ - "server.py — /api/signals, /api/candidates", - "scripts/market_scanner.py — 全市场扫描", - "macro_context_collector.py" - ] - } -} +{ + "module": "signals", + "version": "1.0", + "purpose": "信号数据。提供市场信号查询,数据来自趋势检测(macro_context_collector/divergence_detector)和全市场扫描(market_scanner)。", + + "human_help": { + "title": "信号与扫描", + "description": [ + "展示系统产生的交易信号,包括趋势检测信号和全市场扫描候选股。", + "趋势信号来自 macro_context_collector(宏观)和 divergence_detector(背离检测)。", + "全市场扫描由 market_scanner.py 纯数据驱动(不依赖 LLM),每 15 分钟从热门板块筛选候选股。", + "所有信号存储在 signal_news 表中,候选股存储在 candidates 表中。" + ], + "usage": [ + "GET /api/signals — 获取最近 20 条信号(含趋势和扫描信号)", + "GET /api/candidates — 获取候选股池(最近 50 只,市场扫描产出)" + ], + "troubleshooting": [ + "信号为空 → 检查 macro_context_collector 和 market_scanner cron 状态", + "候选池为空 → 确认 sector_snapshots 有数据,market_scanner cron 在运行" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/signals", "returns": "[{id, sector, overall_sentiment, summary, source, created_at, signal_type, severity}] — 最近 20 条信号"}, + {"method": "GET", "path": "/api/candidates", "returns": "[{code, name, reason, score_2nd..score_5th, score_final, pass_s2..pass_s5, promoted, log, created_at}] — 候选股池(由 market_scanner 产出)"} + ], + "dependencies": [ + "mofin_db.py — signal_news / candidates 表", + "macro_context_collector.py — 宏观信号采集 cron", + "divergence_detector.py — 背离检测 cron", + "scripts/market_scanner.py — 全市场扫描 cron(*/15 9-15 1-5,纯数据驱动)" + ], + "constraints": [ + "signal_news 和 sector_signals 通过 LEFT JOIN 关联", + "candidates 表由 market_scanner 写入,纯数据驱动不调 LLM", + "旧 xiaoguo_scanner 已废弃,不再使用" + ], + "must_not": [ + "不要调小果 LLM API", + "不要依赖 xiaoguo_scan_tracker 表(已废弃)" + ], + "related_files": [ + "server.py — /api/signals, /api/candidates", + "scripts/market_scanner.py — 全市场扫描", + "macro_context_collector.py" + ] + } +} diff --git a/specs/watchlist.json b/specs/watchlist.json index b69bef5f..9c975101 100644 --- a/specs/watchlist.json +++ b/specs/watchlist.json @@ -1,42 +1,42 @@ -{ - "module": "watchlist", - "version": "1.0", - "purpose": "自选股列表管理。提供自选股查询和批量更新。", - - "human_help": { - "title": "自选股管理", - "description": [ - "管理老爸的自选股列表,与持仓分开存储。", - "数据存储在 SQLite (mofin.db) 的 watchlist_stocks 表。", - "小果扫描器会消费自选股信号并自动添加到自选列表。" - ], - "usage": [ - "GET /api/watchlist — 获取完整自选股列表", - "POST /api/update/watchlist — 批量更新自选股(由 cron 调用)" - ], - "troubleshooting": [ - "数据库查询失败 → 检查 mofin.db 是否可读写", - "自选股列表为空 → 确认 regenerate_all 或手动添加过自选股" - ] - }, - - "ai_spec": { - "apis": [ - {"method": "GET", "path": "/api/watchlist", "returns": "{stocks[{code, name, price, change_pct, currency, analysis{...}}]}"}, - {"method": "POST", "path": "/api/update/watchlist", "returns": "{status:'ok'}"} - ], - "dependencies": [ - "mo_data.py — read_watchlist()", - "mofin_db.py — query_watchlist(), write_watchlist_stock()", - "xiaoguo_signal_consumer.py — 消费小果信号,自动加自选" - ], - "constraints": [ - "自选股也必须标注 currency 字段(HKD/CNY)" - ], - "related_files": [ - "server.py — API 路由定义", - "mo_data.py — read_watchlist()", - "mofin_db.py — watchlist_stocks 表" - ] - } -} +{ + "module": "watchlist", + "version": "1.0", + "purpose": "自选股列表管理。提供自选股查询和批量更新。", + + "human_help": { + "title": "自选股管理", + "description": [ + "管理老爸的自选股列表,与持仓分开存储。", + "数据存储在 SQLite (mofin.db) 的 watchlist_stocks 表。", + "小果扫描器会消费自选股信号并自动添加到自选列表。" + ], + "usage": [ + "GET /api/watchlist — 获取完整自选股列表", + "POST /api/update/watchlist — 批量更新自选股(由 cron 调用)" + ], + "troubleshooting": [ + "数据库查询失败 → 检查 mofin.db 是否可读写", + "自选股列表为空 → 确认 regenerate_all 或手动添加过自选股" + ] + }, + + "ai_spec": { + "apis": [ + {"method": "GET", "path": "/api/watchlist", "returns": "{stocks[{code, name, price, change_pct, currency, analysis{...}}]}"}, + {"method": "POST", "path": "/api/update/watchlist", "returns": "{status:'ok'}"} + ], + "dependencies": [ + "mo_data.py — read_watchlist()", + "mofin_db.py — query_watchlist(), write_watchlist_stock()", + "xiaoguo_signal_consumer.py — 消费小果信号,自动加自选" + ], + "constraints": [ + "自选股也必须标注 currency 字段(HKD/CNY)" + ], + "related_files": [ + "server.py — API 路由定义", + "mo_data.py — read_watchlist()", + "mofin_db.py — watchlist_stocks 表" + ] + } +} diff --git a/static/index.html b/static/index.html index bb9fb0f8..fba53f7c 100644 --- a/static/index.html +++ b/static/index.html @@ -1,1929 +1,1929 @@ - - - - - -MoFin · 莫荷情报 - - - - - - -
- -
-
- 📊 -

MoFin

- 知微 -
-
- - -
-
- - -
- - - - - - - - - - - - - 📸 上传 -
- - -
- - - - - - - - - - - -
- - - - - - - - - - - - + + + + + +MoFin · 莫荷情报 + + + + + + +
+ +
+
+ 📊 +

MoFin

+ 知微 +
+
+ + +
+
+ + +
+ + + + + + + + + + + + + 📸 上传 +
+ + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index fa5ae8b0..de651ae1 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,277 +1,277 @@ - -MoFin Dashboard - -

MoFin

持仓情报系统 · Dashboard
-
-
-
- + +MoFin Dashboard + +

MoFin

持仓情报系统 · Dashboard
+
+
+
+