fix: spec audit — align specs with reality (xmpp_bot 5807, dashboard missing endpoints, article_processor X-Lines, api_proxy capability, health_check JSON escape); fix 5802->5807 across dashboard/health_check/watchdog consumers

This commit is contained in:
hmo
2026-08-06 16:48:16 +08:00
parent 1dd3f9aad5
commit a8b46178e3
15 changed files with 131 additions and 89 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ SERVICES = [
{"name": "hermes_gateway_mohe", "host": "127.0.0.1", "port": 8642, "health_url": "http://127.0.0.1:8642/v1/health", "depends_on": [], "desc": "莫荷 AI Gateway"}, {"name": "hermes_gateway_mohe", "host": "127.0.0.1", "port": 8642, "health_url": "http://127.0.0.1:8642/v1/health", "depends_on": [], "desc": "莫荷 AI Gateway"},
{"name": "hermes_gateway_zhiwei","host": "127.0.0.1", "port": 8643, "health_url": "http://127.0.0.1:8643/v1/health", "depends_on": [], "desc": "知微 AI Gateway"}, {"name": "hermes_gateway_zhiwei","host": "127.0.0.1", "port": 8643, "health_url": "http://127.0.0.1:8643/v1/health", "depends_on": [], "desc": "知微 AI Gateway"},
{"name": "wechat_bridge", "host": "127.0.0.1", "port": 3001, "health_url": None, "depends_on": [], "desc": "微信桥接 Docker"}, {"name": "wechat_bridge", "host": "127.0.0.1", "port": 3001, "health_url": None, "depends_on": [], "desc": "微信桥接 Docker"},
{"name": "xmpp_bot_xxm", "host": "192.168.1.16", "port": 5802, "health_url": "http://192.168.1.16:5802/health", "depends_on": ["ejabberd"],"desc": "小小莫 XMPP Bot"}, {"name": "xmpp_bot_xxm", "host": "192.168.1.16", "port": 5807, "health_url": "http://192.168.1.16:5807/health", "depends_on": ["ejabberd"],"desc": "小小莫 XMPP Bot"},
{"name": "article_processor", "host": "192.168.1.16", "port": 5810, "health_url": "http://192.168.1.16:5810/health", "depends_on": [], "desc": "文章抓取服务"}, {"name": "article_processor", "host": "192.168.1.16", "port": 5810, "health_url": "http://192.168.1.16:5810/health", "depends_on": [], "desc": "文章抓取服务"},
] ]
+2 -2
View File
@@ -80,8 +80,8 @@ SERVICES = [
{ {
"name": "xmpp_bot_xxm", "name": "xmpp_bot_xxm",
"host": "192.168.1.16", "host": "192.168.1.16",
"port": 5802, "port": 5807,
"health_url": "http://192.168.1.16:5802/health", "health_url": "http://192.168.1.16:5807/health",
"fix_cmd": None, # 远程服务无法自动修复 "fix_cmd": None, # 远程服务无法自动修复
"remote": True, "remote": True,
"critical": True, "critical": True,
+4 -4
View File
@@ -4,7 +4,7 @@ dashboard.py - AgentsMeeting management dashboard backend
========================================================= =========================================================
Flask app on :5803. Monitors agents across platforms via: Flask app on :5803. Monitors agents across platforms via:
- SSH + ejabberdctl connected_users (cross-platform, authoritative) - SSH + ejabberdctl connected_users (cross-platform, authoritative)
- xmpp_bot HTTP API :5802 (/health, /muc - fallback) - xmpp_bot HTTP API :5807 (/health, /muc - fallback)
- Local process/port checks (Windows only) - Local process/port checks (Windows only)
Auto-recovery: restarts local Windows agents after 3 consecutive offline checks. Auto-recovery: restarts local Windows agents after 3 consecutive offline checks.
@@ -22,7 +22,7 @@ TEMPLATES_DIR = _SCRIPT_DIR / "templates"
CONFIG_DIR = _PROJECT_DIR / "config" CONFIG_DIR = _PROJECT_DIR / "config"
LOGS_DIR = _GATEWAY_DIR / "logs" LOGS_DIR = _GATEWAY_DIR / "logs"
TEMP_DIR = _GATEWAY_DIR / "temp" TEMP_DIR = _GATEWAY_DIR / "temp"
XMPP_BRIDGE_URL = os.environ.get("XMPP_BRIDGE_URL", "http://192.168.1.16:5802") XMPP_BRIDGE_URL = os.environ.get("XMPP_BRIDGE_URL", "http://192.168.1.16:5807")
EJABBERD_HOST = os.environ.get("EJABBERD_HOST", "192.168.1.246") EJABBERD_HOST = os.environ.get("EJABBERD_HOST", "192.168.1.246")
# On Linux (mohe), dashboard runs from systemd with different paths. Support explicit override. # On Linux (mohe), dashboard runs from systemd with different paths. Support explicit override.
@@ -104,7 +104,7 @@ def _default_agents():
"id": "agent-001", "name": "R&D Assistant", "display_name": "xxm", "id": "agent-001", "name": "R&D Assistant", "display_name": "xxm",
"jid": "xxm@yoin.fun", "platform": "windows", "host": "192.168.1.16", "jid": "xxm@yoin.fun", "platform": "windows", "host": "192.168.1.16",
"bot_type": "xmpp", "provider": "volcengine", "bot_type": "xmpp", "provider": "volcengine",
"services": [{"type": "xmpp_bot", "port": 5802}], "services": [{"type": "xmpp_bot", "port": 5807}],
}, },
{ {
"id": "agent-002", "name": "Automation Manager", "display_name": "mohe", "id": "agent-002", "name": "Automation Manager", "display_name": "mohe",
@@ -900,7 +900,7 @@ def api_services():
services = [] services = []
# xmpp_bot (跨平台:通过 XMPP_BRIDGE_URL 检测 Windows .16) # xmpp_bot (跨平台:通过 XMPP_BRIDGE_URL 检测 Windows .16)
xmpp_url = f"{XMPP_BRIDGE_URL}/health" xmpp_url = f"{XMPP_BRIDGE_URL}/health"
svc1 = {"name": "xmpp_bot", "port": 5802, "health": _health_status(xmpp_url), svc1 = {"name": "xmpp_bot", "port": 5807, "health": _health_status(xmpp_url),
"watchdog": True, "pid_lock": True, "type": "core", "depends_on": ["ejabberd"]} "watchdog": True, "pid_lock": True, "type": "core", "depends_on": ["ejabberd"]}
services.append(svc1) services.append(svc1)
# article_processor (跨平台:通过 PLATFORM_SERVICES 中的 health_url) # article_processor (跨平台:通过 PLATFORM_SERVICES 中的 health_url)
+2 -2
View File
@@ -1,4 +1,4 @@
""" """
xxm health check. Runs every 5 min via Task Scheduler. xxm health check. Runs every 5 min via Task Scheduler.
Checks: Checks:
1. Is xmpp_bot process alive? If not → restart 1. Is xmpp_bot process alive? If not → restart
@@ -117,7 +117,7 @@ def main():
# This catches "process alive but XMPP dead" — the most common failure mode. # This catches "process alive but XMPP dead" — the most common failure mode.
try: try:
import urllib.request, json import urllib.request, json
req = urllib.request.Request("http://127.0.0.1:5802/health") req = urllib.request.Request("http://127.0.0.1:5807/health")
resp = json.loads(urllib.request.urlopen(req, timeout=5).read()) resp = json.loads(urllib.request.urlopen(req, timeout=5).read())
if not resp.get("xmpp_connected", True): if not resp.get("xmpp_connected", True):
wlog("CRIT: xmpp_connected=false via /health — forcing restart") wlog("CRIT: xmpp_connected=false via /health — forcing restart")
+4 -4
View File
@@ -1,10 +1,10 @@
""" """
Windows Service — xxm Bot Health Monitor Windows Service — xxm Bot Health Monitor
========================================= =========================================
Replaces Task Scheduler-based health_check_xxm.py. Replaces Task Scheduler-based health_check_xxm.py.
Design: Design:
- HTTP health check (GET :5802/health), NO subprocess polling - HTTP health check (GET :5807/health), NO subprocess polling
- Zero console windows (WMIC/tasklist-free during normal operation) - Zero console windows (WMIC/tasklist-free during normal operation)
- Manageable via services.msc or `net start/stop xxm-health` - Manageable via services.msc or `net start/stop xxm-health`
@@ -42,11 +42,11 @@ _STARTUP_WAIT = 5 # seconds to wait after start before first check
class HealthService(win32serviceutil.ServiceFramework): class HealthService(win32serviceutil.ServiceFramework):
"""Windows Service: polls :5802/health, restarts bot on failure.""" """Windows Service: polls :5807/health, restarts bot on failure."""
_svc_name_ = "xxm-health" _svc_name_ = "xxm-health"
_svc_display_name_ = "AgentsMeeting xxm Health Service" _svc_display_name_ = "AgentsMeeting xxm Health Service"
_svc_description_ = "Monitors xxm bot via HTTP :5802/health, auto-restarts on failure. No console windows." _svc_description_ = "Monitors xxm bot via HTTP :5807/health, auto-restarts on failure. No console windows."
def __init__(self, args): def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args) win32serviceutil.ServiceFramework.__init__(self, args)
+2 -2
View File
@@ -1,4 +1,4 @@
""" """
Mohe reply watcher — polls HTTP bridge every 30s for new mohe messages. Mohe reply watcher — polls HTTP bridge every 30s for new mohe messages.
Logs new replies to logs/mohe_inbox.log. Runs as persistent background process. Logs new replies to logs/mohe_inbox.log. Runs as persistent background process.
@@ -21,7 +21,7 @@ log("mohe_watcher started")
while True: while True:
try: try:
url = "http://127.0.0.1:5802/messages?from=mohe" url = "http://127.0.0.1:5807/messages?from=mohe"
resp = urllib.request.urlopen(url, timeout=5) resp = urllib.request.urlopen(url, timeout=5)
data = json.loads(resp.read()) data = json.loads(resp.read())
msgs = data.get("messages", []) msgs = data.get("messages", [])
+14 -5
View File
@@ -1,20 +1,29 @@
{ {
"module": "api_proxy", "module": "api_proxy",
"version": "1.0", "version": "1.1",
"status": "deprecated", "status": "deprecated",
"purpose": "API Proxy:8787原用于代理 VolcEngine / opencode-go 等外部 API。已不再使用:火山模型已停用,OpenCode 连接 bug 已修复。代码保留但不再运行。", "purpose": "API Proxy:8787多 upstream API 反向代理,支持错误码吞掉、重试降级、fake 200 兜底。当前已停用:火山模型已停用,OpenCode 连接 bug 已修复。代码保留但不再运行246/16 均无此进程监听 8787。",
"ui_location": "已从 Infrastructure → Platform 中移除", "ui_location": "已从 Infrastructure → Platform 中移除",
"human_help": { "human_help": {
"title": "API Proxy — ⛔ 已停用", "title": "API Proxy — ⛔ 已停用",
"description": [ "description": [
"此服务已不再使用。原功能:", "此服务已不再使用。原功能:",
"• 代理 VolcEngine DeepSeek API → 火山已停用", "• 多 upstream API 反向代理(volcengine / opencode-go 等)",
"• 代理 opencode-go-new/old → 原 bug 已修复", "• 错误吞掉:upstream 失败重试 N 次后返回提示消息或 fake 200,避免调用方崩溃",
"• 代理 volcengine 原生模型名(如 deepseek-v4-flash)与 proxy 安全名(如 deepseek-v4-flash-go-safe",
"代码保留在 gateway/scripts/api_proxy.py 但已不运行。" "代码保留在 gateway/scripts/api_proxy.py 但已不运行。"
] ]
}, },
"ai_spec": { "ai_spec": {
"status": "deprecated", "status": "deprecated",
"note": "不再使用,dashboard 中已移除监控。如需恢复:重新加入 PLATFORM_SERVICES 并启动 api_proxy.py。" "service_ports": [
{"port": 8787, "service": "api_proxy.py", "host": "0.0.0.0", "purpose": "多 upstream API 反向代理(已停用)"}
],
"apis": [
{"method": "GET", "path": "/*", "port": 8787, "returns": "upstream 响应或降级响应", "desc": "任意路径反向代理到配置的 upstream base_url + path"},
{"method": "POST", "path": "/*", "port": 8787, "returns": "upstream 响应或降级响应", "desc": "同上 — 支持 /v1/chat/completions 等 OpenAI 兼容端点"},
{"method": "DELETE", "path": "/*", "port": 8787, "returns": "upstream 响应或降级响应", "desc": "同上"}
],
"note": "不再使用,dashboard 中已移除监控。如需恢复:重新加入 PLATFORM_SERVICES 并启动 api_proxy.py。核心能力:多 upstream 路由(配置化 base_url 映射)、错误吞掉(MAX_RETRIES 重试)、fake 200 兜底(模型不可用时返回 mock 响应避免调用方崩溃)。"
} }
} }
+5 -4
View File
@@ -1,6 +1,6 @@
{ {
"module": "article_processor", "module": "article_processor",
"version": "1.1", "version": "1.2",
"purpose": "文章抓取服务运行在 Windows (192.168.1.16:5810),负责抓取微信公众号全文链接并转换为纯文本,支持 OCR 图片识别。", "purpose": "文章抓取服务运行在 Windows (192.168.1.16:5810),负责抓取微信公众号全文链接并转换为纯文本,支持 OCR 图片识别。",
"ui_location": "Infrastructure tab → Platform → 文章抓取服务 (5810)", "ui_location": "Infrastructure tab → Platform → 文章抓取服务 (5810)",
@@ -30,9 +30,10 @@
"ai_spec": { "ai_spec": {
"apis": [ "apis": [
{"method": "GET", "path": "/health", "returns": "{ok, service, port, ocr_model, status}", "desc": "服务状态 + OCR 模型信息"}, {"method": "GET", "path": "/health", "returns": "{ok, service, port, ocr_model, status, last_fetch}", "desc": "服务状态 + OCR 模型信息 + 上次抓取结果"},
{"method": "GET", "path": "/logs?lines=N", "returns": "{ok, lines[]}", "desc": "最近 N 行日志"}, {"method": "GET", "path": "/logs", "returns": "{ok, lines[]}", "desc": "最近 N 行日志 — N 通过 header 'X-Lines' 指定(默认 200),不是 query 参数!示例: curl -H 'X-Lines: 50' :5810/logs"},
{"method": "POST", "path": "/process", "body": "{\"url\":\"...\"}", "returns": "{ok, title, content, images[]}", "desc": "抓取并处理文章(主端点)"} {"method": "POST", "path": "/process", "body": "{\"url\":\"...\"}", "returns": "{ok, title, content, images[]}", "desc": "抓取并处理文章(主端点)"},
{"method": "POST", "path": "/fetch-and-ocr", "body": "{\"url\":\"...\"}", "returns": "同 /process", "desc": "抓取 + OCR 一体化端点(/process 的变体,spec 1.0 未声明)"}
], ],
"dependencies": [ "dependencies": [
"依赖本地 Chrome + 持久 profileDrissionPage 驱动)", "依赖本地 Chrome + 持久 profileDrissionPage 驱动)",
+9 -6
View File
@@ -1,6 +1,6 @@
{ {
"module": "dashboard", "module": "dashboard",
"version": "2.0", "version": "2.1",
"purpose": "AgentsMeeting 管理门户 — Flask app on :5803。监控所有 Agent 的在线状态(跨平台 SSH ejabberdctl + xmpp_bot HTTP API + 本地进程检测)、提供 Web UItabs: Agents/Kanban/Infrastructure/开发原则)、管理 Agent 启停(systemctl on Linux, subprocess on Windows)、EasyTier/RDP 远程控制代理、OpenCode Go 用量监控、Kanban 看板、自修复流水线、git 提交历史等。", "purpose": "AgentsMeeting 管理门户 — Flask app on :5803。监控所有 Agent 的在线状态(跨平台 SSH ejabberdctl + xmpp_bot HTTP API + 本地进程检测)、提供 Web UItabs: Agents/Kanban/Infrastructure/开发原则)、管理 Agent 启停(systemctl on Linux, subprocess on Windows)、EasyTier/RDP 远程控制代理、OpenCode Go 用量监控、Kanban 看板、自修复流水线、git 提交历史等。",
"ui_location": "主 Dashboard UI (http://127.0.0.1:5803)", "ui_location": "主 Dashboard UI (http://127.0.0.1:5803)",
@@ -16,7 +16,7 @@
], ],
"participants": [ "participants": [
{"name": "莫荷", "device": "Linux 192.168.1.246", "role": "生产环境 dashboard 部署机(systemd 管理) + 公网入口(ags.yoin.fun → nginx → :5803", "note": "AGENTSMEETING_ROOT 环境变量覆盖项目路径(/home/hmo/projects/AgentsMeeting"}, {"name": "莫荷", "device": "Linux 192.168.1.246", "role": "生产环境 dashboard 部署机(systemd 管理) + 公网入口(ags.yoin.fun → nginx → :5803", "note": "AGENTSMEETING_ROOT 环境变量覆盖项目路径(/home/hmo/projects/AgentsMeeting"},
{"name": "小小莫", "device": "Windows 192.168.1.16", "role": "开发环境 dashboard + xmpp_bot HTTP bridge (:5802) + EasyTier/RDP 控制代理", "note": "dashboard 本地运行用于开发和测试"}, {"name": "小小莫", "device": "Windows 192.168.1.16", "role": "开发环境 dashboard + xmpp bot HTTP bridge (:5807) + EasyTier/RDP 控制代理", "note": "dashboard 本地运行用于开发和测试"},
{"name": "ejabberd", "device": "246 Docker", "role": "XMPP 服务器 — 权威在线状态源", "note": "通过 SSH docker exec ejabberdctl connected_users 查询(Windows)或直接 docker execLinux"}, {"name": "ejabberd", "device": "246 Docker", "role": "XMPP 服务器 — 权威在线状态源", "note": "通过 SSH docker exec ejabberdctl connected_users 查询(Windows)或直接 docker execLinux"},
{"name": "老莫", "device": "任意浏览器", "role": "Dashboard 使用者", "note": "访问 http://192.168.1.246:5803 或 公网 https://ags.yoin.fun"} {"name": "老莫", "device": "任意浏览器", "role": "Dashboard 使用者", "note": "访问 http://192.168.1.246:5803 或 公网 https://ags.yoin.fun"}
], ],
@@ -34,7 +34,7 @@
"troubleshooting": [ "troubleshooting": [
"如果 Dashboard 打不开:检查 5803 端口是否监听 — netstat -ano | findstr 5803Linux 检查 systemctl status dashboard", "如果 Dashboard 打不开:检查 5803 端口是否监听 — netstat -ano | findstr 5803Linux 检查 systemctl status dashboard",
"如果 Agent 全部显示离线:检查 ejabberd SSH 连接 — ssh hmo@192.168.1.246 'docker exec ejabberd ejabberdctl connected_users'", "如果 Agent 全部显示离线:检查 ejabberd SSH 连接 — ssh hmo@192.168.1.246 'docker exec ejabberd ejabberdctl connected_users'",
"如果 EasyTier/RDP 按钮无反应:检查 xmpp_bot HTTP bridge (:5802) 是否在线 + _BRIDGE_KEY 是否匹配", "如果 EasyTier/RDP 按钮无反应:检查 xmpp_bot HTTP bridge (:5807) 是否在线 + _BRIDGE_KEY 是否匹配 + XMPP_BRIDGE_URL 是否指向 5807",
"如果 OpenCode Go Usage 卡片无数据:检查 usage_collector.py 脚本是否存在 + temp/usage_stats.json 是否生成", "如果 OpenCode Go Usage 卡片无数据:检查 usage_collector.py 脚本是否存在 + temp/usage_stats.json 是否生成",
"如果 Kanban 无任务:检查 ~/.hermes/kanban.db 是否存在且可读", "如果 Kanban 无任务:检查 ~/.hermes/kanban.db 是否存在且可读",
"如果 spec 弹窗空白:检查 specs/{module}.json 文件是否存在,格式是否正确", "如果 spec 弹窗空白:检查 specs/{module}.json 文件是否存在,格式是否正确",
@@ -74,11 +74,14 @@
{"method": "GET", "path": "/api/prd", "returns": "{content, path} — PRD.md 全文", "note": "H 需求 tab 内容"}, {"method": "GET", "path": "/api/prd", "returns": "{content, path} — PRD.md 全文", "note": "H 需求 tab 内容"},
{"method": "GET", "path": "/api/prd/history", "returns": "{log:[...], count} — git log", "note": "PRD.md 的 git 提交历史"}, {"method": "GET", "path": "/api/prd/history", "returns": "{log:[...], count} — git log", "note": "PRD.md 的 git 提交历史"},
{"method": "GET", "path": "/api/tests", "returns": "tests_api.run_tests() 返回的测试结果", "note": "K 测试 tab — 通过 from tests_api import run_tests 调用"}, {"method": "GET", "path": "/api/tests", "returns": "tests_api.run_tests() 返回的测试结果", "note": "K 测试 tab — 通过 from tests_api import run_tests 调用"},
{"method": "GET", "path": "/api/autoheal", "returns": "{time, dry_run, total_expected, anomalies_found, actions_taken[]}", "note": "读取 temp/last_auto_heal.jsonauto_heal.py 每次运行写入的摘要)"},
{"method": "GET", "path": "/api/keys", "returns": "{ok, keys:[{key_id, workspace_id, label, masked_key, ...}], usage:{...}}", "note": "Key Registry — 列出所有 API keymasked+ 用量数据(合并 accounts.json 与 usage_stats.json"},
{"method": "GET", "path": "/api/wechat/status", "returns": "{online, message, port_ok, qr_url, qr_timestamp, session_age_hours, last_ok_at, last_err_at}", "note": "WeChat Bridge 登录状态 + QR 码(TCP check port 3001"},
{"method": "GET", "path": "/api/health", "returns": "{ok, time, xmpp_bot_alive, ejabberd_alive}", "note": "健康检查端点 — xmpp_bot + ejabberd 双重验证"} {"method": "GET", "path": "/api/health", "returns": "{ok, time, xmpp_bot_alive, ejabberd_alive}", "note": "健康检查端点 — xmpp_bot + ejabberd 双重验证"}
], ],
"dependencies": [ "dependencies": [
"ejabberd on 192.168.1.246:5222 — 权威在线状态源(SSH docker exec ejabberdctl connected_users", "ejabberd on 192.168.1.246:5222 — 权威在线状态源(SSH docker exec ejabberdctl connected_users",
"xmpp_bot HTTP bridge on Windows 192.168.1.16:5802 — EasyTier/RDP 代理 + /health + /muc", "xmpp_bot HTTP bridge on Windows 192.168.1.16:5807 — EasyTier/RDP 代理 + /health + /muc",
"_bridge_post() + _BRIDGE_KEY = 'xxm_bridge_8f3a2c' — 与 xmpp_bot HTTP bridge 的认证机制", "_bridge_post() + _BRIDGE_KEY = 'xxm_bridge_8f3a2c' — 与 xmpp_bot HTTP bridge 的认证机制",
"config/agents.yaml — Agent 实例注册配置(JID/platform/host/services", "config/agents.yaml — Agent 实例注册配置(JID/platform/host/services",
"proc_guard.py — PID 锁(防重复启动 dashboard 自身)", "proc_guard.py — PID 锁(防重复启动 dashboard 自身)",
@@ -99,7 +102,7 @@
"nginx on 246 — 公网反向代理(ags.yoin.fun → :5803" "nginx on 246 — 公网反向代理(ags.yoin.fun → :5803"
], ],
"architecture": { "architecture": {
"flow": "Dashboard(Flask :5803) → ① ejabberd SSH docker exec → ② xmpp_bot HTTP API :5802 → ③ 本地进程扫描 → 汇总 agent 状态 → JSON API → dashboard.html 前端渲染", "flow": "Dashboard(Flask :5803) → ① ejabberd SSH docker exec → ② xmpp_bot HTTP API :5807 → ③ 本地进程扫描 → 汇总 agent 状态 → JSON API → dashboard.html 前端渲染",
"detection_layers": [ "detection_layers": [
"Layer 1 (权威): SSH docker exec ejabberdctl connected_users — 跨平台可信, 查到谁在 XMPP 登录", "Layer 1 (权威): SSH docker exec ejabberdctl connected_users — 跨平台可信, 查到谁在 XMPP 登录",
"Layer 2 (桥接): xmpp_bot /health API — 本地 bot 连接状态(Windows专用)", "Layer 2 (桥接): xmpp_bot /health API — 本地 bot 连接状态(Windows专用)",
@@ -126,7 +129,7 @@
"PID 锁机制 — 启动前调用 guard('dashboard'),退出时自动清理锁文件", "PID 锁机制 — 启动前调用 guard('dashboard'),退出时自动清理锁文件",
"跨平台兼容 — sys.platform 检测 win32/linuxSSH vs 直接 docker exec 分支处理", "跨平台兼容 — sys.platform 检测 win32/linuxSSH vs 直接 docker exec 分支处理",
"AGENTSMEETING_ROOT 环境变量 — 在 Linux systemd 部署中覆盖项目路径(/home/hmo/projects/AgentsMeeting", "AGENTSMEETING_ROOT 环境变量 — 在 Linux systemd 部署中覆盖项目路径(/home/hmo/projects/AgentsMeeting",
"XMPP_BRIDGE_URL 默认 http://192.168.1.16:5802 — 可通过环境变量覆盖", "XMPP_BRIDGE_URL 默认 http://192.168.1.16:5807 — 可通过环境变量覆盖246 systemd 已设 5807",
"EJABBERD_HOST 默认 192.168.1.246 — 可通过环境变量覆盖", "EJABBERD_HOST 默认 192.168.1.246 — 可通过环境变量覆盖",
"不要硬编码 workspace ID 或 API keys — 都从 config/accounts.json 或环境变量读取", "不要硬编码 workspace ID 或 API keys — 都从 config/accounts.json 或环境变量读取",
"SSH 免密登录是 ejabberdctl 查询的前提 — BatchMode=yes, 不提示密码", "SSH 免密登录是 ejabberdctl 查询的前提 — BatchMode=yes, 不提示密码",
+1 -1
View File
@@ -61,7 +61,7 @@
], ],
"known_issues": [ "known_issues": [
"scheduled tasks 在 Linux 246 上无法检测 Windows 计划任务状态,统一显示 not_deployed", "scheduled tasks 在 Linux 246 上无法检测 Windows 计划任务状态,统一显示 not_deployed",
"部分服务的 HTTP health check 端点可能返回 Connection refused(如 xmpp_bot 在 5802 仅 Windows 端监听)" "部分服务的 HTTP health check 端点可能返回 Connection refused(如 xmpp_bot 在 5807 仅 Windows 端监听)"
], ],
"related_files": [ "related_files": [
"gateway/scripts/templates/dashboard.html — fHealth() 渲染函数", "gateway/scripts/templates/dashboard.html — fHealth() 渲染函数",
+1 -1
View File
@@ -21,7 +21,7 @@
], ],
"troubleshooting": [ "troubleshooting": [
"如果定时任务未运行:检查 Windows Task Scheduler → agents-health-check", "如果定时任务未运行:检查 Windows Task Scheduler → agents-health-check",
"如果报告未更新:检查 C:\Users\hmo\AppData\Local\Programs\Python\Python310\python.exe 是否可用", "如果报告未更新:检查 C:\\Users\\hmo\\AppData\\Local\\Programs\\Python\\Python310\\python.exe 是否可用",
"如果 TODO 堆积:检查 self_todo_executor 是否正常消费" "如果 TODO 堆积:检查 self_todo_executor 是否正常消费"
], ],
"related": "依赖 service_registry.py(服务定义); 输出给 self_todo_executor.py(修复消费); 报告被 dashboard.py 读取" "related": "依赖 service_registry.py(服务定义); 输出给 self_todo_executor.py(修复消费); 报告被 dashboard.py 读取"
+33 -12
View File
@@ -1,8 +1,8 @@
{ {
"module": "kanban", "module": "kanban",
"version": "1.0", "version": "1.1",
"purpose": "Dashboard 'Kanban' tab 展示看板任务列表,支持按状态(Ready/In Progress/Blocked/Done)和负责人筛选,实时刷新。", "purpose": "看板任务系统。两个接口层:(1) kanban_api.py (:9580) 提供跨机器 Agent 完整读写 REST API(2) dashboard.py (:5803) 'Kanban' tab 只读展示看板任务列表,支持按状态和负责人筛选,实时刷新。",
"ui_location": "主菜单 → Kanban tab", "ui_location": "主菜单 → Kanban tab (dashboard :5803)",
"human_help": { "human_help": {
"title": "Kanban 看板", "title": "Kanban 看板",
@@ -18,32 +18,53 @@
"4. 页面每 15s 自动刷新任务列表" "4. 页面每 15s 自动刷新任务列表"
], ],
"troubleshooting": [ "troubleshooting": [
"如果任务列表为空:检查 Kanban API 是否正常运行", "如果任务列表为空:检查 Hermes kanban.db 是否存在(~/.hermes/kanban.db",
"如果筛选器无反应:页面自动刷新后会重置筛选状态 — 请确保在刷新间隔内操作" "如果筛选器无反应:页面自动刷新后会重置筛选状态 — 请确保在刷新间隔内操作"
], ],
"related": "G 规范定义开发流程和任务管理规范" "related": "G 规范定义开发流程和任务管理规范Agent 处理卡片流程见 docs/KANBAN-HANDLER-PROTOCOL.md"
}, },
"ai_spec": { "ai_spec": {
"service_ports": [
{"port": 9580, "service": "kanban_api.py", "host": "0.0.0.0", "purpose": "跨机器 Agent 完整读写 API(直接操作 ~/.hermes/kanban.db"},
{"port": 5803, "service": "dashboard.py", "host": "246", "purpose": "Dashboard Kanban tab 只读展示"}
],
"apis": [ "apis": [
{"method": "GET", "path": "/api/kanban", "returns": "{ok, tasks[{id,title,body,status,assignee,created_at}]}"} {"method": "GET", "path": "/api/kanban", "port": 5803, "returns": "{ok, tasks[{id,title,body,status,assignee,created_at}]}", "description": "Dashboard 只读:返回全部任务列表"},
{"method": "POST", "path": "/kanban/create", "port": 9580, "returns": "{ok, task_id, raw}", "description": "创建卡片 (title 必填, assignee/body 可选)"},
{"method": "POST", "path": "/kanban/create_xmpp", "port": 9580, "returns": "{ok, task_id, notified}", "description": "创建卡片并 XMPP 通知核心群 (coregroup@conference.yoin.fun)"},
{"method": "POST", "path": "/kanban/list", "port": 9580, "returns": "{tasks[]}", "description": "列表 (可选 status/assignee 筛选)"},
{"method": "POST", "path": "/kanban/show", "port": 9580, "returns": "{id,title,body,status,assignee,created_by,created_at}", "description": "单卡详情"},
{"method": "POST", "path": "/kanban/update", "port": 9580, "returns": "{ok}", "description": "更新 status/assignee (id 必填)"},
{"method": "POST", "path": "/kanban/comment", "port": 9580, "returns": "{ok}", "description": "评论卡片 (id/body 必填, author 可选) + XMPP 通知"},
{"method": "POST", "path": "/kanban/comments", "port": 9580, "returns": "{comments[]}", "description": "读某卡片评论 (id 必填)"}
], ],
"dependencies": [ "dependencies": [
"看板数据由 dashboard.py 从后端 Kanban 服务/数据库获取" "看板数据存于 ~/.hermes/kanban.db (SQLite, 表: tasks / task_comments)",
"dashboard.py 直接读同一 kanban.db 做只读展示",
"kanban_api.py 的 create/comment 通过 hermes CLI 或 XMPP 桥 (127.0.0.1:5804) 通知核心群"
], ],
"constraints": [ "constraints": [
"页面每 15s 轮询刷新 (setInterval(fK,15000))", "页面每 15s 轮询刷新 (setInterval(fK,15000))",
"筛选器状态通过 DOM 直接读取 (document.getElementById)", "筛选器状态通过 DOM 直接读取 (document.getElementById)",
"任务卡片展开使用 onclick 切换 expanded class — max-height 从 60px ↔ none 切换" "任务卡片展开使用 onclick 切换 expanded class — max-height 从 60px ↔ none 切换",
"Agent 处理卡片:拉详情用 POST /kanban/show(不是 GET /api/kanban/t_xxx,该端点不存在)",
"kanban_api.py 全部端点均为 POST(含 list/show/comments 这类读操作)"
], ],
"tests": [ "tests": [
{"id": "KB01", "name": "/api/kanban 返回任务列表", "endpoint": "GET /api/kanban"}, {"id": "KB01", "name": "/api/kanban 返回任务列表", "endpoint": "GET /api/kanban (:5803)"},
{"id": "KB02", "name": "筛选器过滤正确", "endpoint": "n/a (frontend)"} {"id": "KB02", "name": "筛选器过滤正确", "endpoint": "n/a (frontend)"},
{"id": "KB03", "name": "kanban_api 创建任务", "endpoint": "POST /kanban/create (:9580)"},
{"id": "KB04", "name": "kanban_api 更新状态", "endpoint": "POST /kanban/update (:9580)"},
{"id": "KB05", "name": "kanban_api 评论", "endpoint": "POST /kanban/comment (:9580)"}
], ],
"related_files": [ "related_files": [
"scripts/kanban_api.py — :9580 完整读写 API",
"gateway/scripts/templates/dashboard.html — fK() 渲染函数", "gateway/scripts/templates/dashboard.html — fK() 渲染函数",
"gateway/scripts/dashboard.py — /api/kanban 端点", "gateway/scripts/dashboard.py — /api/kanban 只读端点",
"gateway/scripts/specs/kanban.json — 本 spec 文件" "gateway/scripts/specs/kanban.json — 本 spec 文件",
"docs/KANBAN-HANDLER-PROTOCOL.md — Agent 处理卡片协议",
"skills/kanban-handler/SKILL.md — handler session 工作流"
] ]
} }
} }
+4 -4
View File
@@ -10,18 +10,18 @@
"每 10 分钟由 Windows Task Scheduler 触发,自动修复健康检查发现的异常。", "每 10 分钟由 Windows Task Scheduler 触发,自动修复健康检查发现的异常。",
"读取 health_todos.jsonl 中的 pending 条目,执行对应的 fix_script。", "读取 health_todos.jsonl 中的 pending 条目,执行对应的 fix_script。",
"修复成功 → 标记 completed。", "修复成功 → 标记 completed。",
"修复失败 → 通过 XMPP HTTP 桥(:5802/send)发送告警到群聊。", "修复失败 → 通过 XMPP HTTP 桥(:5807/send)发送告警到群聊。",
"特殊情况:如果错误信息为 'another instance is already running',说明服务已在正常运行,标记 completed 跳过。" "特殊情况:如果错误信息为 'another instance is already running',说明服务已在正常运行,标记 completed 跳过。"
], ],
"usage": [ "usage": [
"TODO 文件:gateway/temp/health_todos.jsonl(输入+输出)", "TODO 文件:gateway/temp/health_todos.jsonl(输入+输出)",
"日志:gateway/logs/todo_executor.log", "日志:gateway/logs/todo_executor.log",
"告警通道:HTTP POST → 127.0.0.1:5802/send → XMPP 群聊" "告警通道:HTTP POST → 127.0.0.1:5807/send → XMPP 群聊"
], ],
"troubleshooting": [ "troubleshooting": [
"如果 TODO 堆积不减少:检查 agents-todo-executor 定时任务是否运行", "如果 TODO 堆积不减少:检查 agents-todo-executor 定时任务是否运行",
"如果修复失败:检查 fix_script 路径是否存在(远程服务的 fix_script 为 null,无法自动修复)", "如果修复失败:检查 fix_script 路径是否存在(远程服务的 fix_script 为 null,无法自动修复)",
"如果告警未发出:检查 xmpp_bot HTTP 桥 :5802/send 是否可达" "如果告警未发出:检查 xmpp_bot HTTP 桥 :5807/send 是否可达"
], ],
"related": "消费 agents_health_check.py 的 TODO 输出; 通过 xmpp_bot HTTP 桥发送告警" "related": "消费 agents_health_check.py 的 TODO 输出; 通过 xmpp_bot HTTP 桥发送告警"
}, },
@@ -29,7 +29,7 @@
"ai_spec": { "ai_spec": {
"dependencies": [ "dependencies": [
"health_todos.jsonl — TODO 输入(由 agents_health_check.py 写入)", "health_todos.jsonl — TODO 输入(由 agents_health_check.py 写入)",
"xmpp_bot HTTP 桥 (:5802/send) — 告警输出通道", "xmpp_bot HTTP 桥 (:5807/send) — 告警输出通道",
"Python 3.10", "Python 3.10",
"Windows Task Scheduler — 每10分钟触发" "Windows Task Scheduler — 每10分钟触发"
], ],
+41 -33
View File
@@ -1,37 +1,37 @@
{ {
"module": "xmpp_bot", "module": "xmpp_bot",
"version": "2.0", "version": "3.0",
"purpose": "XMPP Bot — 笑笑(xxm@yoin.fun)。连接 ejabberd via slixmpp,桥接 XMPP 消息到 OpenCode serve session,提供 HTTP bridge (:5802) 给 dashboard 代理 EasyTier/RDP/health 查询。支持 MUC 群聊、私有聊天、消息去重、协调者协议、MAM 恢复守护。", "purpose": "XMPP Bot — 笑笑(xxm@yoin.fun)。统一 Bot 核心 xmpp_agent_core.py--agent xxm连接 ejabberd via slixmpp,桥接 XMPP 消息到 OpenCode serve session,提供 HTTP bridge (:5807) 给 dashboard 代理 EasyTier/RDP/health 查询。支持 MUC 群聊、私有聊天、消息去重、协调者协议、MAM 恢复守护。",
"ui_location": "Agents tab → xxm Agent 卡片 + Services 标签", "ui_location": "Agents tab → xxm Agent 卡片 + Services 标签",
"human_help": { "human_help": {
"title": "XMPP Bot — xxm 消息收发", "title": "XMPP Bot — xxm 消息收发",
"description": [ "description": [
"xmpp_bot.py 是 Windows 开发机上运行的 XMPP 消息机器人JID: xxm@yoin.fun。它连接 ejabberd XMPP 服务器,把群聊和私聊消息桥接到 OpenCode serve session,让 AI 能参与 XMPP 群聊对话。", "xxm 消息机器人JID: xxm@yoin.fun)运行在 Windows 开发机,由统一 Bot 核心 xmpp_agent_core.py 以 --agent xxm 启动(兼容入口 xxm_bot.py。它连接 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 消息收发(MUC 群聊 + 私聊)、HTTP bridge 服务(:5807,提供 /health /muc /easytier /rdp /usage /send 端点)、消息去重(100条缓存)、协调者协议(coordinator/GRANT/REVOKE in-band 信令)、5分钟沉默 cooldown。",
"去重机制:通过 XMPP stanza message ID 去重,100 条缓存用完自动清空。防止 MAM 恢复和实时消息重复处理。", "去重机制:通过 XMPP stanza message ID 去重,100 条缓存用完自动清空。防止 MAM 恢复和实时消息重复处理。",
"协调者协议:mohe 是默认协调者。hmo 可切换 coordinator={name}。支持 GRANT(授权一次发言)和 REVOKE(禁言5分钟)。全部通过 XMPP in-band 消息信令,无需外部 DB。", "协调者协议:mohe 是默认协调者。hmo 可切换 coordinator={name}。支持 GRANT(授权一次发言)和 REVOKE(禁言5分钟)。全部通过 XMPP in-band 消息信令,无需外部 DB。",
"HTTP bridge 安全:/easytier /rdp /usage 等端点需要 X-Api-Key 认证(与 dashboard 的 _BRIDGE_KEY 匹配)。" "HTTP bridge 安全:/easytier /rdp /usage 等端点需要 X-Api-Key 认证(与 dashboard 的 _BRIDGE_KEY 匹配)。/health 免认证供监控系统使用。"
], ],
"participants": [ "participants": [
{"name": "小小莫", "device": "Windows 192.168.1.16", "role": "xmpp_bot 运行者 + OpenCode serve session 所有者", "note": "bot 跑在本地 Python 进程中"}, {"name": "小小莫", "device": "Windows 192.168.1.16", "role": "xmpp bot 运行者 + OpenCode serve session 所有者", "note": "bot 跑在本地 Python 进程中xmpp_agent_core.py --agent xxm"},
{"name": "ejabberd", "device": "246 Docker", "role": "XMPP 服务器 — 消息路由", "note": "192.168.1.246:5222"}, {"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"} {"name": "莫荷", "device": "Linux 192.168.1.246", "role": "默认协调者 + dashboard 使用者", "note": "dashboard 通过 _bridge_post 代理到 xmpp bot HTTP bridge (:5807)"}
], ],
"usage": [ "usage": [
"1. 启动 bot: python xmpp_bot.py(自动获取 PID 锁,防重复)", "1. 启动 bot: python xmpp_agent_core.py --agent xxm(自动获取 PID 锁,防重复)",
"2. bot 自动连接 ejabberd 并加入 MUC 群聊: coregroup + jujidina", "2. bot 自动连接 ejabberd 并加入 MUC 群聊: coregroup + jujidina",
"3. 私聊消息: 直接发送到 OpenCode session → AI 处理 → 通过 XMPP 回复", "3. 私聊消息: 直接发送到 OpenCode session → AI 处理 → 通过 XMPP 回复",
"4. 群聊消息: 通过 MUC 接收 → 去重 + 协调者检查 → AI 处理 → MUC 回复", "4. 群聊消息: 通过 MUC 接收 → 去重 + 协调者检查 → AI 处理 → MUC 回复",
"5. 协调者控制: hmo 在群里发 '[GRANT:xxm]' 授权/取消禁言,'[REVOKE:xxm]' 禁言5分钟", "5. 协调者控制: hmo 在群里发 '[GRANT:xxm]' 授权/取消禁言,'[REVOKE:xxm]' 禁言5分钟",
"6. 沉默命令: 对 bot 说 '闭嘴'/'别说话'/'安静' → 5分钟静默", "6. 沉默命令: 对 bot 说 '闭嘴'/'别说话'/'安静' → 5分钟静默",
"7. HTTP bridge: GET :5802/health 查连接状态,POST :5802/easytier 控制 VPN" "7. HTTP bridge: GET :5807/health 查连接状态,POST :5807/easytier 控制 VPN"
], ],
"troubleshooting": [ "troubleshooting": [
"如果 bot 不回复消息: 检查 OpenCode serve session 是否在线 (localhost:4096) — session_attach 超时5min", "如果 bot 不回复消息: 检查 OpenCode serve session 是否在线 (localhost:4096) — session_attach 超时5min",
"如果群聊收不到: 检查 MUC join 是否成功 — 看日志是否有 'joined MUC' 或 MAM recovery 是否卡住", "如果群聊收不到: 检查 MUC join 是否成功 — 看日志是否有 'joined MUC' 或 MAM recovery 是否卡住",
"如果 HTTP bridge 不可达: 检查 5802 端口 — netstat -ano | findstr 5802", "如果 HTTP bridge 不可达: 检查 5807 端口 — netstat -ano | findstr 5807",
"如果 dashboard EasyTier/RDP 无响应: 检查 _BRIDGE_KEY 是否匹配 — dashboard 和 xmpp_bot 必须一致", "如果 dashboard EasyTier/RDP 无响应: 检查 _BRIDGE_KEY 是否匹配 — dashboard 和 bot 必须一致;再确认 dashboard 的 XMPP_BRIDGE_URL 指向 :5807",
"如果消息重复: 去重缓存100条上限后自动清空 — 极少数情况下可能漏过去,通常无害", "如果消息重复: 去重缓存100条上限后自动清空 — 极少数情况下可能漏过去,通常无害",
"如果 bot 启动失败: 检查 proc_guard PID 锁 — 删除 temp/.xmpp_bot.pid 后重试" "如果 bot 启动失败: 检查 proc_guard PID 锁 — 删除 temp/.xmpp_bot.pid 后重试"
], ],
@@ -40,12 +40,12 @@
"ai_spec": { "ai_spec": {
"apis": [ "apis": [
{"method": "GET", "path": "/health", "returns": "{ok, xmpp_connected, ejabberd_alive, bot_jid, uptime_seconds}", "note": "XMPP 连接状态 + bot 自检"}, {"method": "GET", "path": "/health", "port": 5807, "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": "GET", "path": "/muc", "port": 5807, "returns": "{rooms: {room_name: {participants: [{jid, nick}]}}}", "note": "MUC 群聊参与者列表(R01 已知不稳定)— 需 X-Api-Key"},
{"method": "POST", "path": "/easytier", "body": "{\"action\":\"status|start|stop\"}", "returns": "{ok, running, ...}", "note": "EasyTier VPN 控制 — 需 X-Api-Key"}, {"method": "POST", "path": "/easytier", "port": 5807, "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": "/rdp", "port": 5807, "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": "/usage", "port": 5807, "body": "{\"action\":\"status|collect_now\"}", "returns": "{ok, accounts, ...}", "note": "OpenCode Go 用量查询/采集 — 需 X-Api-Key"},
{"method": "POST", "path": "/send", "body": "{\"message\":\"text\"}", "returns": "{ok}", "note": "发送群聊消息"} {"method": "POST", "path": "/send", "port": 5807, "body": "{\"message\":\"text\"}", "returns": "{ok}", "note": "发送群聊消息 — 需 X-Api-Key"}
], ],
"dependencies": [ "dependencies": [
"slixmpp — XMPP 客户端库 (slixmpp.ClientXMPP)", "slixmpp — XMPP 客户端库 (slixmpp.ClientXMPP)",
@@ -54,11 +54,14 @@
"proc_guard.py — PID 锁 (guard('xmpp_bot'))", "proc_guard.py — PID 锁 (guard('xmpp_bot'))",
"ejabberd 192.168.1.246:5222 — XMPP 服务器", "ejabberd 192.168.1.246:5222 — XMPP 服务器",
"OpenCode serve session (localhost:4096) — AI 处理后端", "OpenCode serve session (localhost:4096) — AI 处理后端",
"MUC rooms: coregroup@conference.yoin.fun + jujidina@conference.yoin.fun" "MUC rooms: coregroup@conference.yoin.fun + jujidina@conference.yoin.fun",
"HTTP bridge 由 xmpp_agent_core.py 内嵌 (_BridgeHandler),非独立进程"
], ],
"architecture": { "architecture": {
"entrypoint": "xmpp_agent_core.py --agent xxm(兼容入口 xxm_bot.py → xmpp_agent_core.py --agent xxm",
"flow": "XMPP消息 → slixmpp → 去重 (_is_duplicate) → MAM恢复守护 → 协调者信令解析 → SessionRouter → SessionBridge → OpenCode serve → AI回复 → XMPP发送", "flow": "XMPP消息 → slixmpp → 去重 (_is_duplicate) → MAM恢复守护 → 协调者信令解析 → SessionRouter → SessionBridge → OpenCode serve → AI回复 → XMPP发送",
"http_bridge": "HTTP server on :5802 (独立线程) — /health /muc /easytier /rdp /usage /send", "http_bridge": "HTTP server on :5807_BridgeHandler 线程)— /health /muc /easytier /rdp /usage /send。历史端口 5802 已被 wechat-hermes-gateway 占用(见 xmpp_agent_core.py L79-82 注释)",
"bridge_auth": "_bridge_auth() 校验 X-Api-Key header/health 免认证,L957-958",
"dedup": "threading.Lock 保护的 set — 100条缓存,满了清空", "dedup": "threading.Lock 保护的 set — 100条缓存,满了清空",
"mam_recovery": "启动后30s内收到的群消息被丢弃 (MAM历史回放保护) — 超时后强制禁用", "mam_recovery": "启动后30s内收到的群消息被丢弃 (MAM历史回放保护) — 超时后强制禁用",
"coordinator": "in-band XMPP 信令 — hmo切换coordinator / GRANT临时授权 / REVOKE禁言5min", "coordinator": "in-band XMPP 信令 — hmo切换coordinator / GRANT临时授权 / REVOKE禁言5min",
@@ -66,39 +69,44 @@
}, },
"constraints": [ "constraints": [
"PID 锁必须获取成功才能启动 — guard('xmpp_bot')", "PID 锁必须获取成功才能启动 — guard('xmpp_bot')",
"HTTP bridge 的 /easytier /rdp /usage 需要 X-Api-Key (与 dashboard _BRIDGE_KEY 一致)", "HTTP bridge 的 /muc /easytier /rdp /usage /send 需要 X-Api-Key (与 dashboard _BRIDGE_KEY 一致)/health 免认证",
"端口 5807 — 5802 已被 wechat-hermes-gateway 的 xmpp_bot.py 占用,不要改回 5802(曾导致 ~50% /easytier /rdp 请求 400",
"dashboard 侧必须设 XMPP_BRIDGE_URL=http://192.168.1.16:5807246 systemd 已配置)",
"MUC join 时需处理 DNS 解析超时 (conference.yoin.fun) — 用 try/except 包裹", "MUC join 时需处理 DNS 解析超时 (conference.yoin.fun) — 用 try/except 包裹",
"去重缓存上限 100 — 足够处理正常流量,极端情况下清空后可能漏1-2条", "去重缓存上限 100 — 足够处理正常流量,极端情况下清空后可能漏1-2条",
"MAM 恢复超时 30s — 如果 ejabberd 没发 MAM history,不会永远卡住", "MAM 恢复超时 30s — 如果 ejabberd 没发 MAM history,不会永远卡住",
"不要用 threading 处理消息 — slixmpp 是 asyncio 驱动的" "不要用 threading 处理消息 — slixmpp 是 asyncio 驱动的"
], ],
"must_not": [ "must_not": [
"不要把 _BRIDGE_KEY 硬编码到 dashboard.py 和 xmpp_bot.py 之外的任何文件", "不要把 _BRIDGE_KEY 硬编码到 dashboard.py 和 bot 之外的任何文件",
"不要在 MAM 恢复期间处理群消息 — 会导致 AI 收到重复/过时上下文", "不要在 MAM 恢复期间处理群消息 — 会导致 AI 收到重复/过时上下文",
"不要修改去重逻辑的锁结构 — threading.Lock 必须保持一致" "不要修改去重逻辑的锁结构 — threading.Lock 必须保持一致",
"不要把端口改回 5802 — 与 wechat-hermes-gateway 冲突"
], ],
"related_modules": [ "related_modules": [
{"module": "dashboard", "relation": "dashboard 通过 _bridge_post 代理 EasyTier/RDP/usage 到 xmpp_bot HTTP bridge"}, {"module": "dashboard", "relation": "dashboard 通过 _bridge_post 代理 EasyTier/RDP/usage 到 bot HTTP bridge (:5807)"},
{"module": "easytier", "relation": "xmpp_bot 提供 /easytier 端点给 dashboard 代理 — bot 是 EasyTier 控制的实际执行者"}, {"module": "easytier", "relation": "bot 提供 /easytier 端点给 dashboard 代理 — bot 是 EasyTier 控制的实际执行者"},
{"module": "rdp", "relation": "xmpp_bot 提供 /rdp 端点 — bot 是 RDP 控制的实际执行者"}, {"module": "rdp", "relation": "bot 提供 /rdp 端点 — bot 是 RDP 控制的实际执行者"},
{"module": "usage_monitor", "relation": "xmpp_bot 提供 /usage 端点 — 旧版用法,新版 dashboard 直接读本地 usage_stats.json"}, {"module": "usage_monitor", "relation": "bot 提供 /usage 端点 — 旧版用法,新版 dashboard 直接读本地 usage_stats.json"},
{"module": "agents", "relation": "xxm agent 的核心通信层 — dashboard 通过 ejabberdctl 查询 xmpp_bot 的 JID 在线状态"} {"module": "agents", "relation": "xxm agent 的核心通信层 — dashboard 通过 ejabberdctl 查询 bot 的 JID 在线状态"}
], ],
"tests": [ "tests": [
{"id": "XB01", "name": "GET /health 返回 xmpp_connected + ejabberd_alive", "endpoint": "GET :5802/health"}, {"id": "XB01", "name": "GET /health 返回 xmpp_connected + ejabberd_alive", "endpoint": "GET :5807/health"},
{"id": "XB02", "name": "GET /muc 返回群聊参与者列表", "endpoint": "GET :5802/muc"}, {"id": "XB02", "name": "GET /muc 返回群聊参与者列表", "endpoint": "GET :5807/muc(带 X-Api-Key"},
{"id": "XB03", "name": "POST /easytier status 需认证", "endpoint": "POST :5802/easytier → 无 key 应 401"}, {"id": "XB03", "name": "POST /easytier status 需认证", "endpoint": "POST :5807/easytier → 无 key 应 401"},
{"id": "XB04", "name": "POST /send 发送消息成功", "endpoint": "POST :5802/send → ok=true"}, {"id": "XB04", "name": "POST /send 发送消息成功", "endpoint": "POST :5807/send(带 X-Api-Key→ ok=true"},
{"id": "XB05", "name": "去重: 相同 message ID 第二次被跳过", "endpoint": "注入重复 msg_id → _is_duplicate 返回 True"} {"id": "XB05", "name": "去重: 相同 message ID 第二次被跳过", "endpoint": "注入重复 msg_id → _is_duplicate 返回 True"}
], ],
"known_issues": [ "known_issues": [
"R01: MUC join 超时 — conference.yoin.fun DNS 偶尔不可达,需 raw presence 双保险", "R01: MUC join 超时 — conference.yoin.fun DNS 偶尔不可达,需 raw presence 双保险",
"slixmpp 依赖 Python asyncio — 与 Flask (同步 threading) 混跑时偶尔死锁", "slixmpp 依赖 Python asyncio — 与 Flask (同步 threading) 混跑时偶尔死锁",
"HTTP bridge 无 HTTPS — 仅局域网内使用,不暴露公网", "HTTP bridge 无 HTTPS — 仅局域网内使用,不暴露公网",
"去重清空时极少数重复消息可能漏过 — 100条缓存未命中时当做新消息处理" "去重清空时极少数重复消息可能漏过 — 100条缓存未命中时当做新消息处理",
"历史端口 5802 与 wechat-hermes-gateway 冲突导致 400(已迁移 5807,勿回退)"
], ],
"related_files": [ "related_files": [
"gateway/scripts/xmpp_bot.py — 本文件 (943行)", "xmpp_agent_core.py — 统一 Bot 核心(HTTP bridge 实现在此,xxm http_port=5807 L82",
"xxm_bot.py — 兼容入口 → xmpp_agent_core.py --agent xxm",
"gateway/scripts/chat_bridge.py — SessionBridge", "gateway/scripts/chat_bridge.py — SessionBridge",
"gateway/scripts/session_router.py — SessionRouter + 协调者逻辑", "gateway/scripts/session_router.py — SessionRouter + 协调者逻辑",
"gateway/scripts/proc_guard.py — PID 锁", "gateway/scripts/proc_guard.py — PID 锁",
+8 -8
View File
@@ -1,4 +1,4 @@
""" """
tests_api.py — 自动测试接口 tests_api.py — 自动测试接口
从 dev-spec.md + PRD.md 提取测试用例,对系统进行实时检测。 从 dev-spec.md + PRD.md 提取测试用例,对系统进行实时检测。
通过 service_registry 的 BASE 定位文档。 通过 service_registry 的 BASE 定位文档。
@@ -93,15 +93,15 @@ def run_tests():
# ─── B: 服务架构(直接检查各服务,不走 dashboard 路由避免死锁)─── # ─── B: 服务架构(直接检查各服务,不走 dashboard 路由避免死锁)───
# xmpp_bot: 只要端口开放且返回响应即认为存活(新旧版 API 不同) # xmpp_bot: 只要端口开放且返回响应即认为存活(新旧版 API 不同)
try: try:
s = urllib.request.urlopen("http://127.0.0.1:5802/health", timeout=3) s = urllib.request.urlopen("http://127.0.0.1:5807/health", timeout=3)
add("B1: xmpp_bot :5802/health", True, f"HTTP {s.getcode()}") add("B1: xmpp_bot :5807/health", True, f"HTTP {s.getcode()}")
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
# HTTP error means bot responded (alive), just doesn't support GET # HTTP error means bot responded (alive), just doesn't support GET
add("B1: xmpp_bot :5802/health", True, f"HTTP {e.code}(存活,旧版API)") add("B1: xmpp_bot :5807/health", True, f"HTTP {e.code}(存活,旧版API)")
except Exception as e: except Exception as e:
# Connection refused → bot is DOWN. Linux 上 bot 跑在 Windows, expected # Connection refused → bot is DOWN. Linux 上 bot 跑在 Windows, expected
is_linux = sys.platform != "win32" is_linux = sys.platform != "win32"
add("B1: xmpp_bot :5802/health", is_linux, str(e)[:50] + ("(仅Windows,生产环境正常)" if is_linux else ""), expected=is_linux) add("B1: xmpp_bot :5807/health", is_linux, str(e)[:50] + ("(仅Windows,生产环境正常)" if is_linux else ""), expected=is_linux)
try: try:
s = urllib.request.urlopen("http://127.0.0.1:5810/health", timeout=3) s = urllib.request.urlopen("http://127.0.0.1:5810/health", timeout=3)
@@ -157,7 +157,7 @@ def run_tests():
try: try:
payload = json.dumps({"message": "test from tests_api"}).encode() payload = json.dumps({"message": "test from tests_api"}).encode()
req = urllib.request.Request("http://127.0.0.1:5802/send", req = urllib.request.Request("http://127.0.0.1:5807/send",
data=payload, headers={"Content-Type": "application/json", "X-Api-Key": "xxm_bridge_8f3a2c"}, data=payload, headers={"Content-Type": "application/json", "X-Api-Key": "xxm_bridge_8f3a2c"},
method="POST") method="POST")
resp = urllib.request.urlopen(req, timeout=5) resp = urllib.request.urlopen(req, timeout=5)
@@ -165,7 +165,7 @@ def run_tests():
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
add("D2: /send 端点可达", e.code in (200, 401, 400), f"HTTP {e.code}" + ("(需认证)" if e.code == 401 else "(生产旧版bot无此端点)"), expected=e.code != 200) add("D2: /send 端点可达", e.code in (200, 401, 400), f"HTTP {e.code}" + ("(需认证)" if e.code == 401 else "(生产旧版bot无此端点)"), expected=e.code != 200)
except Exception as e: except Exception as e:
# D2 依赖 xmpp_bot (:5802), Linux 上 expected # D2 依赖 xmpp_bot (:5807), Linux 上 expected
add("D2: /send 端点可达", sys.platform != "win32", str(e)[:60], expected=sys.platform != "win32") add("D2: /send 端点可达", sys.platform != "win32", str(e)[:60], expected=sys.platform != "win32")
# ─── F: 可视化(直接检查文件和服务)─── # ─── F: 可视化(直接检查文件和服务)───
@@ -207,7 +207,7 @@ def run_tests():
add("E: 元成长 — reflog", False, "reflog不存在", expected=True) add("E: 元成长 — reflog", False, "reflog不存在", expected=True)
# F: 端口可达(由 B1-B3 覆盖,此处用 socket 交叉验证) # F: 端口可达(由 B1-B3 覆盖,此处用 socket 交叉验证)
fp = 0 fp = 0
for pn, pp in [("xmpp_bot",5802),("dashboard",5803),("article_processor",5810)]: for pn, pp in [("xmpp_bot",5807),("dashboard",5803),("article_processor",5810)]:
try: try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1) s.settimeout(1)