{
  "module": "xmpp_bot",
  "version": "2.0",
  "purpose": "XMPP Bot — 笑笑（xxm@yoin.fun）。连接 ejabberd via slixmpp，桥接 XMPP 消息到 OpenCode serve session，提供 HTTP bridge (:5802) 给 dashboard 代理 EasyTier/RDP/health 查询。支持 MUC 群聊、私有聊天、消息去重、协调者协议、MAM 恢复守护。",
  "ui_location": "Agents tab → xxm Agent 卡片 + Services 标签",

  "human_help": {
    "title": "XMPP Bot — xxm 消息收发",
    "description": [
      "xmpp_bot.py 是 Windows 开发机上运行的 XMPP 消息机器人，JID: xxm@yoin.fun。它连接 ejabberd XMPP 服务器，把群聊和私聊消息桥接到 OpenCode serve session，让 AI 能参与 XMPP 群聊对话。",
      "核心功能：XMPP 消息收发（MUC 群聊 + 私聊）、HTTP bridge 服务（:5802，提供 /health /muc /easytier /rdp /usage 端点）、消息去重（100条缓存）、协调者协议（coordinator/GRANT/REVOKE in-band 信令）、5分钟沉默 cooldown。",
      "去重机制：通过 XMPP stanza message ID 去重，100 条缓存用完自动清空。防止 MAM 恢复和实时消息重复处理。",
      "协调者协议：mohe 是默认协调者。hmo 可切换 coordinator={name}。支持 GRANT（授权一次发言）和 REVOKE（禁言5分钟）。全部通过 XMPP in-band 消息信令，无需外部 DB。",
      "HTTP bridge 安全：/easytier /rdp /usage 等端点需要 X-Api-Key 认证（与 dashboard 的 _BRIDGE_KEY 匹配）。"
    ],
    "participants": [
      {"name": "小小莫", "device": "Windows 192.168.1.16", "role": "xmpp_bot 运行者 + OpenCode serve session 所有者", "note": "bot 跑在本地 Python 进程中"},
      {"name": "ejabberd", "device": "246 Docker", "role": "XMPP 服务器 — 消息路由", "note": "192.168.1.246:5222"},
      {"name": "莫荷", "device": "Linux 192.168.1.246", "role": "默认协调者 + dashboard 使用者", "note": "dashboard 通过 _bridge_post 代理到 xmpp_bot HTTP bridge"}
    ],
    "usage": [
      "1. 启动 bot: python xmpp_bot.py（自动获取 PID 锁，防重复）",
      "2. bot 自动连接 ejabberd 并加入 MUC 群聊: coregroup + jujidina",
      "3. 私聊消息: 直接发送到 OpenCode session → AI 处理 → 通过 XMPP 回复",
      "4. 群聊消息: 通过 MUC 接收 → 去重 + 协调者检查 → AI 处理 → MUC 回复",
      "5. 协调者控制: hmo 在群里发 '[GRANT:xxm]' 授权/取消禁言，'[REVOKE:xxm]' 禁言5分钟",
      "6. 沉默命令: 对 bot 说 '闭嘴'/'别说话'/'安静' → 5分钟静默",
      "7. HTTP bridge: GET :5802/health 查连接状态，POST :5802/easytier 控制 VPN"
    ],
    "troubleshooting": [
      "如果 bot 不回复消息: 检查 OpenCode serve session 是否在线 (localhost:4096) — session_attach 超时5min",
      "如果群聊收不到: 检查 MUC join 是否成功 — 看日志是否有 'joined MUC' 或 MAM recovery 是否卡住",
      "如果 HTTP bridge 不可达: 检查 5802 端口 — netstat -ano | findstr 5802",
      "如果 dashboard EasyTier/RDP 无响应: 检查 _BRIDGE_KEY 是否匹配 — dashboard 和 xmpp_bot 必须一致",
      "如果消息重复: 去重缓存100条上限后自动清空 — 极少数情况下可能漏过去，通常无害",
      "如果 bot 启动失败: 检查 proc_guard PID 锁 — 删除 temp/.xmpp_bot.pid 后重试"
    ],
    "related": "依赖 chat_bridge.py (SessionBridge) + session_router.py (SessionRouter) + proc_guard.py (PID 锁)"
  },

  "ai_spec": {
    "apis": [
      {"method": "GET", "path": "/health", "returns": "{ok, xmpp_connected, ejabberd_alive, bot_jid, uptime_seconds}", "note": "XMPP 连接状态 + bot 自检"},
      {"method": "GET", "path": "/muc", "returns": "{rooms: {room_name: {participants: [{jid, nick}]}}}", "note": "MUC 群聊参与者列表（R01 已知不稳定）"},
      {"method": "POST", "path": "/easytier", "body": "{\"action\":\"status|start|stop\"}", "returns": "{ok, running, ...}", "note": "EasyTier VPN 控制 — 需 X-Api-Key"},
      {"method": "POST", "path": "/rdp", "body": "{\"action\":\"status|start|stop\"}", "returns": "{ok, rdp_enabled, tunnel_running}", "note": "RDP 远程桌面控制 — 需 X-Api-Key"},
      {"method": "POST", "path": "/usage", "body": "{\"action\":\"status|collect_now\"}", "returns": "{ok, accounts, ...}", "note": "OpenCode Go 用量查询/采集 — 需 X-Api-Key"},
      {"method": "POST", "path": "/send", "body": "{\"message\":\"text\"}", "returns": "{ok}", "note": "发送群聊消息"}
    ],
    "dependencies": [
      "slixmpp — XMPP 客户端库 (slixmpp.ClientXMPP)",
      "chat_bridge.py — SessionBridge: 连接 OpenCode serve session (ses_xxm_xmpp)",
      "session_router.py — SessionRouter: 消息路由 + 命令分发 + 协调者逻辑",
      "proc_guard.py — PID 锁 (guard('xmpp_bot'))",
      "ejabberd 192.168.1.246:5222 — XMPP 服务器",
      "OpenCode serve session (localhost:4096) — AI 处理后端",
      "MUC rooms: coregroup@conference.yoin.fun + jujidina@conference.yoin.fun"
    ],
    "architecture": {
      "flow": "XMPP消息 → slixmpp → 去重 (_is_duplicate) → MAM恢复守护 → 协调者信令解析 → SessionRouter → SessionBridge → OpenCode serve → AI回复 → XMPP发送",
      "http_bridge": "HTTP server on :5802 (独立线程) — /health /muc /easytier /rdp /usage /send",
      "dedup": "threading.Lock 保护的 set — 100条缓存，满了清空",
      "mam_recovery": "启动后30s内收到的群消息被丢弃 (MAM历史回放保护) — 超时后强制禁用",
      "coordinator": "in-band XMPP 信令 — hmo切换coordinator / GRANT临时授权 / REVOKE禁言5min",
      "shutup": "关键词匹配 '闭嘴'/'别说话'/'安静' → 5分钟不回复"
    },
    "constraints": [
      "PID 锁必须获取成功才能启动 — guard('xmpp_bot')",
      "HTTP bridge 的 /easytier /rdp /usage 需要 X-Api-Key (与 dashboard _BRIDGE_KEY 一致)",
      "MUC join 时需处理 DNS 解析超时 (conference.yoin.fun) — 用 try/except 包裹",
      "去重缓存上限 100 — 足够处理正常流量，极端情况下清空后可能漏1-2条",
      "MAM 恢复超时 30s — 如果 ejabberd 没发 MAM history，不会永远卡住",
      "不要用 threading 处理消息 — slixmpp 是 asyncio 驱动的"
    ],
    "must_not": [
      "不要把 _BRIDGE_KEY 硬编码到 dashboard.py 和 xmpp_bot.py 之外的任何文件",
      "不要在 MAM 恢复期间处理群消息 — 会导致 AI 收到重复/过时上下文",
      "不要修改去重逻辑的锁结构 — threading.Lock 必须保持一致"
    ],
    "related_modules": [
      {"module": "dashboard", "relation": "dashboard 通过 _bridge_post 代理 EasyTier/RDP/usage 到 xmpp_bot HTTP bridge"},
      {"module": "easytier", "relation": "xmpp_bot 提供 /easytier 端点给 dashboard 代理 — bot 是 EasyTier 控制的实际执行者"},
      {"module": "rdp", "relation": "xmpp_bot 提供 /rdp 端点 — bot 是 RDP 控制的实际执行者"},
      {"module": "usage_monitor", "relation": "xmpp_bot 提供 /usage 端点 — 旧版用法，新版 dashboard 直接读本地 usage_stats.json"},
      {"module": "agents", "relation": "xxm agent 的核心通信层 — dashboard 通过 ejabberdctl 查询 xmpp_bot 的 JID 在线状态"}
    ],
    "tests": [
      {"id": "XB01", "name": "GET /health 返回 xmpp_connected + ejabberd_alive", "endpoint": "GET :5802/health"},
      {"id": "XB02", "name": "GET /muc 返回群聊参与者列表", "endpoint": "GET :5802/muc"},
      {"id": "XB03", "name": "POST /easytier status 需认证", "endpoint": "POST :5802/easytier → 无 key 应 401"},
      {"id": "XB04", "name": "POST /send 发送消息成功", "endpoint": "POST :5802/send → ok=true"},
      {"id": "XB05", "name": "去重: 相同 message ID 第二次被跳过", "endpoint": "注入重复 msg_id → _is_duplicate 返回 True"}
    ],
    "known_issues": [
      "R01: MUC join 超时 — conference.yoin.fun DNS 偶尔不可达，需 raw presence 双保险",
      "slixmpp 依赖 Python asyncio — 与 Flask (同步 threading) 混跑时偶尔死锁",
      "HTTP bridge 无 HTTPS — 仅局域网内使用，不暴露公网",
      "去重清空时极少数重复消息可能漏过 — 100条缓存未命中时当做新消息处理"
    ],
    "related_files": [
      "gateway/scripts/xmpp_bot.py — 本文件 (943行)",
      "gateway/scripts/chat_bridge.py — SessionBridge",
      "gateway/scripts/session_router.py — SessionRouter + 协调者逻辑",
      "gateway/scripts/proc_guard.py — PID 锁",
      "gateway/scripts/specs/xmpp_bot.json — 本 spec",
      "gateway/logs/xmpp_bot.log — 运行日志",
      "gateway/temp/.xmpp_bot.pid — PID 锁文件"
    ]
  }
}
