restructure: 开发原则 Tab 精简为 G+K+F+H + 历史版本 + F 健康视图
This commit is contained in:
+51
-136
@@ -1,163 +1,78 @@
|
||||
# AgentsMeeting — 产品需求文档 (PRD)
|
||||
|
||||
> 版本: v0.1 | 状态: 初稿 | 客户: hmo (老莫) | PM: mohe (莫荷) | 研发: xxm (小小莫)
|
||||
> 版本: v0.2 | 精简: 2026-07-16 | 客户: hmo (老莫)
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目背景 <!-- ✅ 背景描述准确,所列问题在本次开发中已部分解决 -->
|
||||
## 一、项目背景
|
||||
|
||||
老莫运营一套多 Agent 协作系统,涵盖 XMPP 消息通道、AI Gateway、会话管理、记忆系统、Provider 调用链、多 profile 隔离等模块。系统经过数月迭代已具备完整功能,但存在以下问题:
|
||||
老莫运营一套多 Agent 协作系统,涵盖 XMPP 消息通道、AI Gateway、会话管理、记忆系统、Provider 调用链、Web Dashboard 等模块。
|
||||
|
||||
- 频繁断连/失联 — Gateway 进程意外退出、XMPP bot 掉线后重连失败、provider key 过期/配额超限
|
||||
- 架构文档缺失 — 系统仅以代码和配置形式存在,缺乏完整架构图、数据流文档和模块边界定义
|
||||
- 模块耦合不清晰 — Gateway、Session、Memory、Provider 之间的依赖关系未显式定义,排障困难
|
||||
- 部署标准化不足 — 各 profile 的启动/重启/监控流程不统一
|
||||
- 排查效率低 — 断联事件发生后需人工翻日志逐一排查,缺乏监控告警体系
|
||||
|
||||
**立项目标:** 将现有系统按软件工程规范进行文档化、模块化整理,查漏补缺,不推倒重来。最终产出标准化源码包、部署脚本和运维文档,可稳定部署到服务器并支持客户端对接。
|
||||
**立项目标**:将现有系统按软件工程规范进行文档化、模块化整理,查漏补缺,不推倒重来。最终产出标准化的源码包、部署脚本和运维文档,可稳定部署到服务器并支持客户端对接。
|
||||
|
||||
---
|
||||
|
||||
## 2. 系统范围 <!-- ✅ 模块划分清晰,基本覆盖系统全貌 -->
|
||||
## 二、系统概览
|
||||
|
||||
### 2.1 消息通道层 (Channels) <!-- ✅ XMPP+微信已实现, QQ ⏸️ 暂缓 -->
|
||||
|
||||
- **XMPP 通道** — 通过 ejabberd 服务器 + XMPP bot (slixmpp) 实现群聊/私聊消息收发。bot 进程:xmpp_bot.py (mohe)、xmpp_xiaoguo_bot.py、xmpp_zhiwei_bot.py。支持 __SILENT__/__REPLY__ 前缀机制控制 bot 输出行为。
|
||||
- **微信通道** — Windows 端 wechat_agent 桥接程序,抓取微信消息后通过 HTTP POST 到 Gateway,回复经 5801 端口推回微信窗口,长消息拆分为 2000 字/段。
|
||||
- **QQ 通道** — 规划中,未实现。 <!-- ⏸️ 暂缓 — 当前无迫切需求 -->
|
||||
|
||||
### 2.2 Gateway 层 <!-- ✅ 8642/8643 运行中,健康检查已配置 -->
|
||||
|
||||
- 基于 hermes-agent 框架,每个 profile 独立 Gateway 进程。
|
||||
- 职责:消息路由、会话管理、Provider 调用链、工具执行、回复生成。
|
||||
- 端口映射:8642(默认)、8643(知微)、8645(小果)、8646(mohe旧/废弃)
|
||||
- 超时配置:1800s 总超时,900s 告警,180s 通知间隔。
|
||||
- 健康检查:/v1/health 端点返回 pong。
|
||||
|
||||
### 2.3 会话管理层 (Session) <!-- ✅ SQLite state.db, 硬截断200条已实现 -->
|
||||
|
||||
- 存储:SQLite (state.db),各 profile 独立文件。
|
||||
- 核心机制:硬截断最近 200 条消息 (hermes_state.py LIMIT 200)。
|
||||
- session_rewrite:可设置重写规则(如默认 → sisyphus)。
|
||||
- 已知问题:Gateway 内存缓存 session,移走 state.db 后需重启进程才生效。
|
||||
|
||||
### 2.4 记忆系统 (Memory) <!-- ✅ SOUL/MEMORY/USER.md 已配置 -->
|
||||
|
||||
- **SOUL.md** — 系统提示,行为最高准则(身份锚定、行为规则、沉默机制)。
|
||||
- **MEMORY.md** — 长期记忆,被 LLM 检索并注入上下文。
|
||||
- **USER.md** — 用户画像,记录沟通风格和偏好。
|
||||
- 已知问题:SOUL 与 MEMORY 规则冲突时,LLM 优先执行 MEMORY 中的简略规则。
|
||||
|
||||
### 2.5 Provider 调用链 <!-- ✅ volcengine/ocg/omlx 已配置, fallback 仍需改进 -->
|
||||
|
||||
- 每个 profile 独立配置 provider 和 fallback 链。
|
||||
- 支持:volcengine (火山引擎)、ocg-old/ocg-new、omlx (本地 MLX)。
|
||||
- credential_pool_strategies:多 key 轮询/fallback 策略。
|
||||
- 已知问题:key 过期/配额超限后 fallback 可能因配置格式问题不触发。
|
||||
|
||||
### 2.6 辅助系统 <!-- ✅ 全部已运行 -->
|
||||
|
||||
- **cron 调度系统** — 14 个 job(价格监控、数据同步、持仓分析等)。
|
||||
- **MoFin 股票系统** — price_monitor、technical_analysis、strategy 模块。
|
||||
- **MCP 服务** — agentmemory (记忆检索)、websearch (网络搜索)。
|
||||
- **Skills 系统** — 34 个技能分类。
|
||||
- **Web Dashboard** — 状态监控页面。
|
||||
|
||||
### 2.7 基础设施 <!-- ✅ 服务器/进程管理已就绪 -->
|
||||
|
||||
- **服务器** — Linux 192.168.1.246 (主阵地,所有 Gateway/cron/XMPP bot/ejabberd)
|
||||
- **客户端** — Windows 192.168.1.16 (跑 wechat_agent 桥 + xxm chat_bridge)
|
||||
- **阿里云中转** — 47.115.32.206 (备用/中转)
|
||||
- **XMPP 服务器** — ejabberd (Docker)
|
||||
- **进程管理** — systemd service(每个 Gateway + 每个 Bot 独立 service)
|
||||
| 层 | 组件 | 说明 |
|
||||
|----|------|------|
|
||||
| **消息通道** | XMPP (ejabberd + bot)、微信 (wechat_agent)、QQ (规划中) | 多渠道统一接入 |
|
||||
| **Gateway** | Hermes API Server (:8642~8646),各 profile 独立 | 消息路由、会话、Provider 调用 |
|
||||
| **会话管理** | SQLite state.db,硬截断 200 条 | 各 profile 独立文件 |
|
||||
| **记忆系统** | SOUL.md + MEMORY.md + USER.md | 系统提示 + 长期记忆 + 用户画像 |
|
||||
| **Provider** | volcengine / ocg / omlx,多 key 轮询/fallback | 多供应商 LLM 调用 |
|
||||
| **辅助系统** | cron 调度 (14 jobs)、MoFin 股票、MCP 服务、Skills 系统 | 扩展功能 |
|
||||
| **基础设施** | Linux 246 (主阵地)、Windows 16 (桥接)、阿里云中转 | |
|
||||
| **管理门户** | Dashboard (:5803) | 监控 + 测试 + 规范 + 健康 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 数据流 <!-- ✅ XMPP 消息流+微信消息流均通畅 -->
|
||||
## 三、总体性需求(双轨体系未覆盖部分)
|
||||
|
||||
### 3.1 XMPP 消息流
|
||||
以下需求不适合放入模块级 `specs/*.json`,属于跨模块/架构层级的约束:
|
||||
|
||||
用户 → ejabberd → XMPP Bot → POST /v1/chat/completions → Gateway(localhost:{port}) → Session → Memory → Provider → 回复 → ejabberdctl send_stanza → ejabberd → 用户
|
||||
### 3.1 架构约束
|
||||
|
||||
### 3.2 微信消息流
|
||||
1. **一条消息只经过一个 Session** — 通道级隔离,不跨 profile 共享上下文
|
||||
2. **数据流路径可追溯** — 消息来源 → Gateway → Session → LLM → 回复 → 目标通道,每一步可日志追踪
|
||||
3. **SOUL.md 是行为最高准则** — MEMORY 不应包含与 SOUL 冲突的规则
|
||||
4. **配置即代码** — 所有配置纳入版本管理,禁止手动改线上文件
|
||||
5. **不推倒重来** — 以现有代码为基础,只做文档化和查漏补缺
|
||||
|
||||
微信 → wechat_agent(Windows) → POST 192.168.1.246:8642/v1/chat/completions → Gateway 8642 → Session + Memory + Provider → 回复 → POST 192.168.1.246:5801 → wechat_agent → 微信
|
||||
### 3.2 可用性要求
|
||||
|
||||
1. **所有进程必须支持健康检查 + 自动重启** — systemd / 看门狗守护
|
||||
2. **Gateway 异常退出后 30s 内自动拉起** — 系统级恢复,不依赖人工
|
||||
3. **XMPP bot 断线自动重连** — 应用层心跳 + systemd 兜底重启
|
||||
4. **微信通道崩了不影响其他通道** — 通道级故障隔离
|
||||
|
||||
### 3.3 安全约束
|
||||
|
||||
1. **敏感信息不得入 git** — API Key、Cookie、密码仅存环境变量或 `.gitignore` 文件
|
||||
2. **Dashboard 管理接口需鉴权** — 涉部署操作(启停服务)须 Bearer token
|
||||
3. **记忆系统不持久化敏感对话** — LLM 产出的敏感内容不应写入长期记忆
|
||||
|
||||
### 3.4 性能基准
|
||||
|
||||
1. **消息端到端延迟 < 30s**(含 LLM 推理)— 超过 60s 视为异常
|
||||
2. **Session 上下文上限 200 条 / 12k tokens** — 硬截断防膨胀
|
||||
3. **Dashboard 页面加载 < 3s** — 所有 API 聚合到 `/api/health-overview`
|
||||
|
||||
---
|
||||
|
||||
## 4. 已知问题与风险 <!-- 🔄 R01-R09 部分已修复(R02 heartbeat, R08 监控体系) -->
|
||||
## 四、需求分布说明
|
||||
|
||||
| 编号 | 问题 | 影响 | 当前措施 | 优先级 | 当前状态 |
|
||||
|------|------|------|----------|--------|----------|
|
||||
| R01 | Gateway 进程意外退出未自动拉起 | 服务中断 | 无自动恢复 | P0 | 🔄 Gateway 健康已纳入监控(I/J Tab显示), 自动拉起待 systemd 配置 |
|
||||
| R02 | XMPP Bot 断线后重连失败 | 消息丢失 | slixmpp reconnect 不可靠 | P0 | ✅ frp heartbeat + 应用层保活(60s检测×3→exit→systemd重启) |
|
||||
| R03 | Provider key 过期/配额超限无告警 | LLM 调用失败 | fallback 不总能触发 | P0 | 🔄 fallback链待验证 |
|
||||
| R04 | Session 膨胀导致响应退化 | 回复质量下降 | 硬截断 200 条 | P1 | ✅ 硬截断200条已实现 |
|
||||
| R05 | SOUL vs MEMORY 规则冲突 | 行为异常 | 需手动清理 memory | P1 | 🔄 需持续维护 |
|
||||
| R06 | Gateway 非热加载 session | 更新需重启进程 | 手动 kill -9 | P1 | ❌ 非热加载问题未改 |
|
||||
| R07 | 群聊消息路由混淆 | LLM 分不清职责 | 来源标记 + 身份锚定 | P2 | 🔄 已缓解 |
|
||||
| R08 | 缺乏统一监控告警 | 排障依赖人工 | 无 | P1 | ✅ 监控体系已建立(Phase2) |
|
||||
| R09 | QQ 通道未实现 | 缺少渠道 | 规划中 | P3 | ⏸️ 暂缓 — 当前无迫切需求 |
|
||||
绝大多数具体需求(功能细节、API 定义、UI 交互、模块边界)**不在本文档描述**,而是通过双轨同源体系记录:
|
||||
|
||||
### 4.1 根因详解
|
||||
```
|
||||
每个功能模块 → specs/{module}.json
|
||||
├── human_help(? 按钮 → 人类看说明/排错)
|
||||
└── ai_spec(§ 按钮 → AI 看接口/约束/测试)
|
||||
```
|
||||
|
||||
1. **消息路由混淆** — 群聊消息进同一 session 后 LLM 分不清哪条是自己该处理的。已通过 [群聊] 时间戳来源标记 + 独立 session 隔离缓解。
|
||||
|
||||
2. **粘性 session** — Gateway 进程即使移走 session 文件,内存中缓存的旧 session 仍存在,必须 kill 重启。
|
||||
|
||||
3. **SOUL vs MEMORY 冲突** — SOUL 是系统提示,MEMORY 是上下文,LLM 检索 MEMORY 命中时会覆盖 SOUL 规则。这是小果“闭不了嘴”的根因。
|
||||
|
||||
4. **会话膨胀 → 响应退化** — 知微曾出现 13.8 万 token 上下文,223 条消息中仅 26 条是用户消息。已用硬截断 200 条解决。
|
||||
|
||||
5. **通道断连** — XMPP bot systemd 显示 active 但实际未连上 ejabberd。ejabberd 长期不活跃连接会被自动踢掉。
|
||||
|
||||
6. **网络拓扑混乱** — 192.168.1.246 (Linux 服务器) 与 47.115.32.206 (阿里云) 混淆,曾导致 SSH 排查走错路。
|
||||
当前已用双轨覆盖的模块:`usage_monitor`、`easytier`、`rdp`。
|
||||
新增模块必须走此流程。
|
||||
|
||||
---
|
||||
|
||||
## 5. 设计原则 <!-- ✅ 6条原则均已在本次开发中遵循 -->
|
||||
|
||||
1. **一条消息只经过一个 Session** — 通道级隔离,不跨 profile 共享上下文。
|
||||
2. **SOUL.md 是行为最高准则** — MEMORY 不应包含与 SOUL 冲突的规则。
|
||||
3. **所有进程支持健康检查和自动重启** — systemd + 心跳检测。
|
||||
4. **数据流路径可追踪** — 消息来源 → Gateway → Session → LLM → 回复 → 目标通道,每一步可日志追踪。
|
||||
5. **配置即代码** — 所有配置纳入版本管理。
|
||||
6. **不推倒重来** — 以现有代码为基础,只做文档化和查漏补缺。
|
||||
---
|
||||
|
||||
## 6. 交付物清单 <!-- 🔄 PRD完成, 架构设计+模块接口进行中, 其他待启动 -->
|
||||
|
||||
| 交付物 | 负责人 | 状态 | 实现情况 |
|
||||
|--------|--------|------|----------|
|
||||
| PRD (本文档) | xxm | 初稿 | ✅ 已完成, dashboard H Tab展示 |
|
||||
| 系统架构设计文档 | xxm + mohe | 待起草 | 🔄 ARCHITECTURE.md+dev-spec.md已完成 |
|
||||
| 模块接口规范 | xxm | 待起草 | ❌ 待起草 |
|
||||
| 代码工程化 (src/ 迁移) | xxm | 待启动 | ❌ 待启动 |
|
||||
| 部署与运维文档 | xxm + mohe | 待起草 | 🔄 架构图已完成(ARCHITECTURE.md) |
|
||||
| 测试套件 | xxm | ✅ 已完成 | ✅ tests/test_health_checks.py (15/15 通过) |
|
||||
| 断连根因修复清单 | mohe | 待整理 | 🔄 R02已修复, 其他待排查 |
|
||||
| 部署脚本 | xxm + mohe | 待启动 | ✅ deploy/windows/ 已就绪 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 项目时间线 <!-- 🔄 阶段0-1完成(PRD+架构), 阶段2-4待推进 -->
|
||||
|
||||
| 阶段 | 内容 | 时间 |
|
||||
|------|------|------|
|
||||
| 阶段0 | 资产盘点 | Day 1 ✅ |
|
||||
| 阶段1 | PRD + 架构设计 | Day 2-3 |
|
||||
| 阶段2 | 代码工程化 | Day 4-7 |
|
||||
| 阶段3 | 断连根因修复 + 测试 | Day 8-10 |
|
||||
| 阶段4 | 部署上线 + 验收 | Day 11-12 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 角色与职责 <!-- ✅ 角色已确认 -->
|
||||
|
||||
- **客户 (老莫 hmo)** — 需求提出方,审阅并确认所有交付物
|
||||
- **PM (莫荷 mohe)** — 资源协调、架构审核、进度跟踪、质量验收
|
||||
- **研发 (小小莫 xxm)** — 文档起草、代码工程化、部署实施
|
||||
|
||||
---
|
||||
|
||||
*PRD v0.1 — 文档结束*
|
||||
*PRD v0.2 — 精简版,仅保留双轨体系覆盖不到的架构级需求*
|
||||
|
||||
@@ -1179,6 +1179,39 @@ def api_spec():
|
||||
return jsonify({"ok": False, "error": str(e), "content": ""})
|
||||
|
||||
|
||||
@app.route("/api/spec/history")
|
||||
def api_spec_history():
|
||||
"""返回 dev-spec.md 的 git 历史版本"""
|
||||
return _git_history_for(".memory/dev-spec.md")
|
||||
|
||||
|
||||
@app.route("/api/prd/history")
|
||||
def api_prd_history():
|
||||
"""返回 PRD.md 的 git 历史版本"""
|
||||
return _git_history_for("docs/PRD.md")
|
||||
|
||||
|
||||
def _git_history_for(filepath):
|
||||
"""Helper: run git log for a file and return formatted history"""
|
||||
project_root = _PROJECT_DIR
|
||||
try:
|
||||
cmd = ["git", "log", "--oneline", "--date=short", "--format=%h %ai %s", "--", filepath]
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
lines = result.stdout.strip().split("\n")
|
||||
return jsonify({"ok": True, "log": lines, "count": len(lines)})
|
||||
else:
|
||||
return jsonify({"ok": True, "log": [], "count": 0})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": True, "log": [], "count": 0, "error": str(e)})
|
||||
|
||||
|
||||
@app.route("/api/prd")
|
||||
def api_prd():
|
||||
"""返回产品需求文档 (PRD) 内容"""
|
||||
|
||||
@@ -83,8 +83,8 @@ th{color:var(--dim);font-weight:600;font-size:19px}
|
||||
<div class="tabs" id="tabs"></div><div id="panes"></div><div id="toast" class="toast"></div>
|
||||
<div id="spec-modal" class="spec-modal"><div class="modal-box" id="spec-modal-box"></div></div>
|
||||
<script>
|
||||
var T=[{"id": "agents", "label": "Agents"}, {"id": "kanban", "label": "Kanban"}, {"id": "infra", "label": "Infrastructure"}, {"id": "principles", "label": "开发原则", "children": [{"id": "git", "label": "A 源码"}, {"id": "svc", "label": "B 服务"}, {"id": "mon", "label": "C 监控"}, {"id": "todos", "label": "D 修复"}, {"id": "meta", "label": "E 成长"}, {"id": "exp", "label": "F 期望"},{"id":"spec","label":"G 规范"},{"id":"prd","label":"H 需求"},{"id":"tree","label":"I 功能树"},{"id":"topo","label":"J 拓扑"},{"id":"tests","label":"K 测试"}]}];
|
||||
var at=localStorage.getItem('t')||'agents',as=localStorage.getItem('s')||'git';
|
||||
var T=[{"id":"agents","label":"Agents"},{"id":"kanban","label":"Kanban"},{"id":"infra","label":"Infrastructure"},{"id":"principles","label":"开发原则","children":[{"id":"spec","label":"G 规范"},{"id":"tests","label":"K 测试"},{"id":"health","label":"F 健康"},{"id":"prd","label":"H 需求"}]}];
|
||||
var at=localStorage.getItem('t')||'agents',as=localStorage.getItem('s')||'spec';
|
||||
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
||||
function qs(id){return document.getElementById(id)}
|
||||
function toast(m,t){var e=qs('toast');e.textContent=m;e.className='toast show '+(t||'ok');setTimeout(function(){e.className='toast'},2500)}
|
||||
@@ -132,7 +132,7 @@ p+='</div>';
|
||||
for(var j=0;j<t.children.length;j++){var c=t.children[j];p+='<div id="sp-'+c.id+'"'+(c.id===as?'':' style="display:none"')+'><div class="section"><div id="ct-'+c.id+'"><span class="dim">Loading...</span></div></div></div>';}
|
||||
p+='</div>';}else{p+='<div class="tab-pane'+(a?' active':'')+'" id="pn-'+t.id+'"><div id="ct-'+t.id+'"><span class="dim">Loading...</span></div></div>';}}
|
||||
qs('tabs').innerHTML=b;qs('panes').innerHTML=p;}
|
||||
function sw(id){if(id==='principles'){at='principles';as=localStorage.getItem('s')||'git'}else{at=id;localStorage.setItem('t',id)}
|
||||
function sw(id){if(id==='principles'){at='principles';as=localStorage.getItem('s')||'spec'}else{at=id;localStorage.setItem('t',id)}
|
||||
document.querySelectorAll('.tab-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T.find(function(t){return t.id===at||(t.children&&t.children.some(function(c){return c.id===at}))}).label)});
|
||||
document.querySelectorAll('.tab-pane').forEach(function(p){p.classList.toggle('active',p.id==='pn-'+id)});
|
||||
if(id==='principles'){document.querySelectorAll('.sub-btn').forEach(function(b){b.classList.toggle('active',b.textContent.trim()===T[3].children.find(function(c){return c.id===as}).label)});document.querySelectorAll('[id^="sp-"]').forEach(function(p){p.style.display=p.id==='sp-'+as?'':'none'});(FETCHES[as]||function(){})()}}
|
||||
@@ -149,12 +149,22 @@ h+='</div>';qs('ct-agents').innerHTML=h;}catch(e){}}
|
||||
async function doAct(id,act){try{var r=await fetch('/api/agents/'+id+'/'+act,{method:'POST'}),d=await r.json();toast(act+(d.ok?' OK':' FAILED'),d.ok?'ok':'err');setTimeout(fa,1500)}catch(e){toast('Error: '+e.message,'err')}}
|
||||
function tgL(id){if(L.has(id))L.delete(id);else L.add(id);var lp=qs('lp-'+id);if(lp)lp.className='lp'+(L.has(id)?' open':'');if(L.has(id))fL(id)}
|
||||
async function fL(id){var el=qs('lc-'+id);if(!el||!L.has(id))return;try{var r=await fetch('/api/agents/'+id+'/logs?lines=50'),d=await r.json();el.innerHTML=(d.lines||[]).map(function(l){return'<div>'+esc(l)+'</div>'}).join('')||'<span class="dim">no log data</span>';el.scrollTop=el.scrollHeight}catch(e){el.innerHTML='<span class="red">Failed</span>'}}
|
||||
async function fG(){try{var r=await fetch('/api/git'),d=await r.json();var h='';for(var i=0;i<d.log.length;i++){h+='<div class="log-line">'+esc(d.log[i])+'</div>';}if(!h)h='<span class="dim">No commits</span>';fill('git',h)}catch(e){}}
|
||||
async function fS(){try{var r=await fetch('/api/services'),d=await r.json(),ok=0;var h='<table><tr><th>Service</th><th>Port</th><th>Health</th><th>WD</th><th>Type</th></tr>';for(var i=0;i<d.services.length;i++){var s=d.services[i],a=s.health&&s.health.ok;if(a)ok++;h+='<tr><td>'+esc(s.name)+'</td><td>'+(s.port||'-')+'</td><td>'+(a?'OK':'DOWN')+'</td><td>'+(s.watchdog?'Y':'N')+'</td><td>'+esc(s.type)+'</td></tr>';}h+='</table>';fill('svc',h)}catch(e){}}
|
||||
async function fM(){try{var r=await fetch('/api/monitor'),d=await r.json();var h='';if(d.tier1&&d.tier1.summary)h+='<div class="section" style=margin-bottom:12px><div class=stat-c><div class=n>'+d.tier1.summary.ok+'/'+d.tier1.summary.total+'</div><div class=l>T1 Health</div></div>'+(d.tier1.status==='no_data'?'<div style=margin-top:8px;color:var(--yellow)>⏸️ 监控报告未部署到本机 (246)</div>':'')+'</div>';if(d.tier2&&d.tier2.summary)h+='<div class="section"><div class=stat-c><div class=n>'+d.tier2.summary.ok+'/'+d.tier2.summary.total+'</div><div class=l>T2 Daily Health</div></div>'+(d.tier2.status==='no_data'?'<div style=margin-top:8px;color:var(--yellow)>⏸️ 日报监控未部署到本机 (246)</div>':'')+'</div>';if(d.tasks&&d.tasks.length){h+='<div style=margin-top:16px;font-size:17px;font-weight:600>定时任务</div>';for(var i=0;i<d.tasks.length;i++){var t=d.tasks[i];h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:10px 14px;margin-top:8px"><strong>'+esc(t.name)+'</strong> <span class="tag '+(t.status==='ok'||t.status==='timer_ok'?'g':t.status==='not_deployed'?'y':'r')+'">'+esc(t.status)+'</span></div>';}}if(!h)h='<span class=dim>No monitoring data</span>';fill('mon',h)}catch(e){fill('mon','<span class=tag r>Error: '+esc(e.message)+'</span>')}}
|
||||
async function fT(){try{var r=await fetch('/api/todos'),d=await r.json();fill('todos','<span>'+(d.todos.length)+' records</span>')}catch(e){}}
|
||||
async function fE(){try{var r=await fetch('/api/metagrowth'),d=await r.json();fill('meta','fixes: '+(d.total_fixes||0)+' commits: '+(d.commit_count||0))}catch(e){}}
|
||||
async function fF(){try{var r=await fetch('/api/expected'),d=await r.json();var ok=0;for(var i=0;i<d.expected.length;i++){var e=d.expected[i],a=d.actual[e.name]||'',cmp=(e.expected.indexOf('running')===0&&a==='running')||(e.expected.indexOf('scheduled')===0&&(a==='scheduled'||a==='running'));if(cmp)ok++}fill('exp',ok+'/'+d.expected.length+' compliant')}catch(e){}}
|
||||
async function fHealth(){try{
|
||||
var h='';
|
||||
try{var r=await fetch('/api/expected'),d=await r.json();var ok=0;for(var i=0;i<d.expected.length;i++){var e=d.expected[i],a=d.actual[e.name]||'',cmp=(e.expected.indexOf('running')===0&&a==='running')||(e.expected.indexOf('scheduled')===0&&(a==='scheduled'||a==='running'));if(cmp)ok++}
|
||||
h+='<div class=section><h2>期望矩阵 <span class=tag '+(ok===d.expected.length?'g':'y')+'>'+ok+'/'+d.expected.length+'</span></h2><table><tr><th>服务</th><th>期望</th><th>实际</th><th>状态</th></tr>';
|
||||
for(var i=0;i<d.expected.length;i++){var e=d.expected[i],a=d.actual[e.name]||'',cmp=(e.expected.indexOf('running')===0&&a==='running')||(e.expected.indexOf('scheduled')===0&&(a==='scheduled'||a==='running'));h+='<tr><td>'+esc(e.name)+'</td><td>'+esc(e.expected)+'</td><td>'+esc(a)+'</td><td>'+(cmp?'<span class=tag g>OK</span>':'<span class=tag r>MISMATCH</span>')+'</td></tr>';}
|
||||
h+='</table></div>';}catch(e){}
|
||||
try{var r2=await fetch('/api/services'),d2=await r2.json();var ok2=0;h+='<div class=section><h2>服务健康 <span class=tag g>'+d2.services.length+' services</span></h2><table><tr><th>Service</th><th>Type</th><th>Port</th><th>Health</th><th>WD</th></tr>';
|
||||
for(var i=0;i<d2.services.length;i++){var s=d2.services[i],a=s.health&&s.health.ok;if(a)ok2++;h+='<tr><td>'+esc(s.name)+'</td><td>'+esc(s.type||'-')+'</td><td>'+(s.port||'-')+'</td><td>'+(a?'<span class=tag g>OK</span>':'<span class=tag r>DOWN</span>')+'</td><td>'+(s.watchdog?'<span class=tag g>Y</span>':'<span class=tag r>N</span>')+'</td></tr>';}
|
||||
h+='</table><div style=margin-top:8px;color:var(--dim)>健康: '+ok2+'/'+d2.services.length+' | 看门狗: '+d2.services.filter(function(s){return s.watchdog}).length+'/'+d2.services.length+'</div></div>';}catch(e){}
|
||||
try{var r3=await fetch('/api/monitor'),d3=await r3.json();h+='<div class=section><h2>监控数据</h2>';
|
||||
if(d3.tier1&&d3.tier1.summary)h+='<div class=stat-c><div class=n>'+d3.tier1.summary.ok+'/'+d3.tier1.summary.total+'</div><div class=l>T1 健康</div></div>';
|
||||
if(d3.tier2&&d3.tier2.summary)h+='<div class=stat-c style=margin-top:8px><div class=n>'+d3.tier2.summary.ok+'/'+d3.tier2.summary.total+'</div><div class=l>T2 日报</div></div>';
|
||||
if(d3.tasks&&d3.tasks.length){h+='<div style=margin-top:12px><h2>定时任务</h2>';for(var i=0;i<d3.tasks.length;i++){var t=d3.tasks[i];h+='<div style=background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px 12px;margin:4px 0><strong>'+esc(t.name)+'</strong> <span class=tag '+(t.status==='ok'||t.status==='timer_ok'?'g':t.status==='not_deployed'?'y':'r')+'>'+esc(t.status)+'</span></div>';}h+='</div>';}
|
||||
h+='</div>';}catch(e){}
|
||||
if(!h)h='<span class=dim>No data</span>';
|
||||
fill('health',h)}catch(e){fill('health','<span class=tag r>Error</span>')}}
|
||||
async function fK(){try{var r=await fetch('/api/kanban'),d=await r.json();var tasks=d.tasks||[];var s=document.getElementById('kf-status');var a=document.getElementById('kf-assignee');var fs=s?s.value:'';var fa=a?a.value:'';
|
||||
if(tasks.length==0){fill('kanban','<div style=text-align:center;padding:40px;color:var(--dim)>No tasks</div>');return}
|
||||
var html='<div style=display:flex;gap:8px;margin-bottom:16px>';html+='<select id=kf-status style=background:var(--card);border:1px solid var(--border);border-radius:5px;padding:6px 10px;color:var(--text);font-size:18px><option value="">All Status</option><option value=ready>Ready</option><option value=progress>In Progress</option><option value=blocked>Blocked</option><option value=done>Done</option></select>';
|
||||
@@ -330,57 +340,45 @@ function _renderUsageMetric(label,m,barColor){
|
||||
+'<div style="font-size:10px;color:var(--dim);margin-top:1px">'+esc(txt)+'</div>'
|
||||
+'</div>';
|
||||
}
|
||||
async function fH(){try{var r=await fetch('/api/prd'),d=await r.json();if(!d.ok||!d.content){fill('prd','Failed');return}var el=document.getElementById('ct-prd');var sp=el?el.querySelector('div')?el.querySelector('div').scrollTop:0:0;fill('prd','<div style=padding:8px 0>'+mdRender(d.content)+'</div>');var el2=document.getElementById('ct-prd');if(el2&&sp){setTimeout(function(){(el2.querySelector('div')||el2).scrollTop=sp},0)}}catch(e){fill('prd','Error')}}async function fTree(){try{var r=await fetch('/api/services'),d=await r.json();if(!d.services||!d.services.length){fill('tree','No data');return}
|
||||
var tree={name:'System',children:[]};var map={};d.services.forEach(function(s){map[s.name]=s;if(!s.depends_on||!s.depends_on.length){tree.children.push(s)}else{s._parents=s.depends_on}});
|
||||
var h='<div style=font-size:19px;line-height:1.8>';
|
||||
function renderNode(svc,depth){if(!svc)return;var indent='padding-left:'+(depth*20)+'px';var dot=svc.health&&svc.health.ok?'<span class=sd g></span>':'<span class=sd r></span>';var wd=svc.watchdog?'<span class=tag g style=font-size:18px>WD</span>':'<span class=tag r style=font-size:18px>noWD</span>';h+='<div style="'+indent+';display:flex;align-items:center;gap:8px;padding:3px 0">'+dot+'<span style=font-weight:'+(depth===0?'700':'400')+'>'+esc(svc.name)+'</span><span class=tag d style=font-size:18px>'+esc(svc.type||'')+'</span>'+wd+'<span style=color:var(--dim);font-size:19px>'+(svc.port?':'+svc.port:'')+'</span></div>';
|
||||
if(svc.children){for(var i=0;i<svc.children.length;i++){renderNode(svc.children[i],depth+1)}}}
|
||||
// Build tree by connecting children to parents
|
||||
d.services.forEach(function(s){if(s._parents){s._parents.forEach(function(p){var parent=d.services.find(function(x){return x.name===p});if(parent){if(!parent.children)parent.children=[];parent.children.push(s)}})}});
|
||||
renderNode(tree,0);
|
||||
h+='</div>';var allOk=d.services.filter(function(s){return s.health&&s.health.ok}).length;var allWatched=d.services.filter(function(s){return s.watchdog}).length;h+='<div style=margin-top:10px;padding:8px;background:var(--bg);border-radius:6px;color:var(--dim);font-size:19px>Health: '+allOk+'/'+d.services.length+' | Watchdog: '+allWatched+'/'+d.services.length+'</div>';fill('tree',h)}catch(e){fill('tree','<span class=tag r>Error</span>')}}async function fTopo(){try{var r=await fetch('/api/services'),d=await r.json();if(!d.services||!d.services.length){fill('topo','No data');return}
|
||||
var h='<div style=font-size:19px;line-height:2>';
|
||||
var types={'core':'核心服务','bridge':'桥接服务','mgmt':'管理服务','infra':'基础设施'};
|
||||
d.services.forEach(function(s){
|
||||
var ok=s.health&&s.health.ok;
|
||||
var dot=ok?'<span class=sd g></span>':'<span class=sd r></span>';
|
||||
var wd=s.watchdog?'<span class=tag g style=font-size:18px>WD</span>':'<span class=tag r style=font-size:18px>noWD</span>';
|
||||
h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:10px 14px;margin-bottom:8px">';
|
||||
h+='<div style=display:flex;align-items:center;gap:8px>'+dot+'<strong>'+esc(s.name)+'</strong><span class=tag d style=font-size:18px>'+esc(types[s.type]||s.type)+'</span>'+wd+'<span style=color:var(--dim);font-size:19px>'+(s.port?':'+s.port:'')+'</span></div>';
|
||||
// Dependencies
|
||||
if(s.depends_on&&s.depends_on.length){h+='<div style=margin-top:4px;padding-left:20px;font-size:18px;color:var(--dim)>依赖: ';s.depends_on.forEach(function(dp,i){if(i>0)h+=' → ';h+='<span style=color:var(--accent)>'+esc(dp)+'</span>'});h+='</div>'}
|
||||
// Connections from other services to this one
|
||||
var deps=d.services.filter(function(x){return x.depends_on&&x.depends_on.indexOf(s.name)>=0});
|
||||
if(deps.length){h+='<div style=margin-top:2px;padding-left:20px;font-size:18px;color:var(--dim)>被依赖: ';deps.forEach(function(dp,i){if(i>0)h+=' ← ';h+='<span style=color:var(--yellow)>'+esc(dp.name)+'</span>'});h+='</div>'}
|
||||
h+='</div>';});
|
||||
h+='<div style=margin-top:10px;padding:8px;background:var(--bg);border-radius:6px;color:var(--dim);font-size:19px>箭头方向: 依赖 → 被依赖 | 健康: '+d.services.filter(function(s){return s.health&&s.health.ok}).length+'/'+d.services.length+'</div>';
|
||||
fill('topo',h)}catch(e){fill('topo','<span class=tag r>Error</span>')}}async function fTests(){try{var r=await fetch('/api/tests'),d=await r.json();var h='';if(!d.ok||!d.tests){fill('tests','<span class=tag r>Error</span>');return}
|
||||
async function fH(){try{
|
||||
var r=await fetch('/api/prd'),d=await r.json();
|
||||
if(!d.ok||!d.content){fill('prd','Failed');return}
|
||||
var h='<div style=padding:8px 0>'+mdRender(d.content)+'</div>';
|
||||
try{var rh=await fetch('/api/prd/history'),dh=await rh.json();
|
||||
if(dh.ok&&dh.log&&dh.log.length){
|
||||
h+='<div class=section style=margin-top:16px><h2 style=font-size:17px;cursor:pointer onclick="var e=document.getElementById(\'prd-history\');e.style.display=e.style.display===\'none\'?\'block\':\'none\'">历史版本 <span style=color:var(--dim);font-size:18px>('+dh.count+' commits)</span></h2><div id=prd-history style=display:none;margin-top:8px>';
|
||||
for(var i=0;i<dh.log.length;i++){h+='<div class=log-line>'+esc(dh.log[i])+'</div>';}
|
||||
h+='</div></div>';}
|
||||
}catch(e){}
|
||||
fill('prd',h)}catch(e){fill('prd','Error')}}
|
||||
|
||||
async function fTests(){try{var r=await fetch('/api/tests'),d=await r.json();var h='';if(!d.ok||!d.tests){fill('tests','<span class=tag r>Error</span>');return}
|
||||
var s=d.summary;h+='<div style=display:flex;gap:12px;margin-bottom:16px><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--accent)>'+s.total+'</div><div style=color:var(--dim);font-size:19px>Total</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--green)>'+s.passed+'</div><div style=color:var(--dim);font-size:19px>Passed</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--red)>'+s.failed+'</div><div style=color:var(--dim);font-size:19px>Failed</div></div></div>';
|
||||
h+='<table><tr><th>Test</th><th>Result</th><th>Detail</th></tr>';
|
||||
d.tests.forEach(function(t){h+='<tr><td>'+esc(t.name)+'</td><td>'+(t.ok?'<span class=tag g>PASS</span>':'<span class=tag r>FAIL</span>')+'</td><td class=mono style=font-size:19px;color:var(--dim)>'+esc(t.detail||'')+'</td></tr>';});
|
||||
h+='</table>';h+='<div style=margin-top:8px;color:var(--dim);font-size:19px>Last run: '+esc(d.time||'')+'</div>';
|
||||
fill('tests',h)}catch(e){fill('tests','<span class=tag r>Error: '+esc(e.message)+'</span>')}}async function fTests(){try{var r=await fetch('/api/tests'),d=await r.json();var h='';if(!d.ok||!d.tests){fill('tests','<span class=tag r>Error</span>');return}
|
||||
var s=d.summary;h+='<div style=display:flex;gap:12px;margin-bottom:16px><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--accent)>'+s.total+'</div><div style=color:var(--dim);font-size:19px>Total</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--green)>'+s.passed+'</div><div style=color:var(--dim);font-size:19px>Passed</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--red)>'+s.failed+'</div><div style=color:var(--dim);font-size:19px>Failed</div></div></div>';
|
||||
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--red)>❌ Failed ('+s.failed+')</div>';
|
||||
d.tests.filter(function(t){return!t.ok}).forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--red);border-radius:6px;padding:8px 12px;margin-bottom:6px"><div style=display:flex;align-items:center;gap:8px><span style="background:var(--red);color:#fff;padding:2px 8px;border-radius:4px;font-weight:700;font-size:19px">FAIL</span><strong>'+esc(t.name)+'</strong></div><div style=margin-top:4px;font-size:18px;color:var(--dim)>'+esc(t.detail||'')+'</div></div>';});
|
||||
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--green)>✅ Passed ('+s.passed+')</div>';
|
||||
d.tests.filter(function(t){return t.ok}).forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:6px 12px;margin-bottom:4px"><span style="background:var(--green);color:#fff;padding:1px 6px;border-radius:3px;font-weight:700;font-size:18px">PASS</span> <span style=font-size:19px>'+esc(t.name)+'</span> <span style=font-size:19px;color:var(--dim)>'+esc(t.detail||'')+'</span></div>';});
|
||||
h+='<div style=margin-top:8px;color:var(--dim);font-size:19px>Last run: '+esc(d.time||'')+'</div>';
|
||||
fill('tests',h)}catch(e){fill('tests','<span class=tag r>Error: '+esc(e.message)+'</span>')}}async function fTests(){try{var r=await fetch('/api/tests'),d=await r.json();var h='';if(!d.ok||!d.tests){fill('tests','<span class=tag r>Error</span>');return}
|
||||
var s=d.summary;h+='<div style=display:flex;gap:12px;margin-bottom:16px><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--accent)>'+s.total+'</div><div style=color:var(--dim);font-size:19px>Total</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--green)>'+s.passed+'</div><div style=color:var(--dim);font-size:19px>Passed</div></div><div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;text-align:center"><div style=font-size:32px;font-weight:700;color:var(--red)>'+s.failed+'</div><div style=color:var(--dim);font-size:19px>Failed</div></div></div>';
|
||||
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--yellow)>⏸️ Expected Fail ('+s.failed+')</div>';
|
||||
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--yellow)>Expected Fail ('+s.failed+')</div>';
|
||||
d.tests.filter(function(t){return!t.ok&&t.expected}).forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--yellow);border-radius:6px;padding:6px 12px;margin-bottom:4px"><span style="background:var(--yellow);color:#000;padding:1px 6px;border-radius:3px;font-weight:700;font-size:18px">EXPECTED</span> <span style=font-size:19px>'+esc(t.name)+'</span> <span style=font-size:19px;color:var(--dim)>'+esc(t.detail||'')+'</span></div>';});
|
||||
var realFails=d.tests.filter(function(t){return!t.ok&&!t.expected});
|
||||
if(realFails.length){h+='<div style=margin-bottom:8px;font-weight:600;color:var(--red)>❌ Real Fail ('+realFails.length+')</div>';realFails.forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--red);border-radius:6px;padding:8px 12px;margin-bottom:6px"><div style=display:flex;align-items:center;gap:8px><span style="background:var(--red);color:#fff;padding:2px 8px;border-radius:4px;font-weight:700;font-size:19px">FAIL</span><strong>'+esc(t.name)+'</strong></div><div style=margin-top:4px;font-size:18px;color:var(--dim)>'+esc(t.detail||'')+'</div></div>';});}
|
||||
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--green)>✅ Passed ('+s.passed+')</div>';
|
||||
if(realFails.length){h+='<div style=margin-bottom:8px;font-weight:600;color:var(--red)>Real Fail ('+realFails.length+')</div>';realFails.forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--red);border-radius:6px;padding:8px 12px;margin-bottom:6px"><div style=display:flex;align-items:center;gap:8px><span style="background:var(--red);color:#fff;padding:2px 8px;border-radius:4px;font-weight:700;font-size:19px">FAIL</span><strong>'+esc(t.name)+'</strong></div><div style=margin-top:4px;font-size:18px;color:var(--dim)>'+esc(t.detail||'')+'</div></div>';});}
|
||||
h+='<div style=margin-bottom:8px;font-weight:600;color:var(--green)>Passed ('+s.passed+')</div>';
|
||||
d.tests.filter(function(t){return t.ok}).forEach(function(t){h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:6px;padding:6px 12px;margin-bottom:4px"><span style="background:var(--green);color:#fff;padding:1px 6px;border-radius:3px;font-weight:700;font-size:18px">PASS</span> <span style=font-size:19px>'+esc(t.name)+'</span> <span style=font-size:19px;color:var(--dim)>'+esc(t.detail||'')+'</span></div>';});
|
||||
h+='<div style=margin-top:8px;color:var(--dim);font-size:19px>Last run: '+esc(d.time||'')+'</div>';
|
||||
fill('tests',h)}catch(e){fill('tests','<span class=tag r>Error: '+esc(e.message)+'</span>')}}var FETCHES={agents:fa,git:fG,svc:fS,mon:fM,todos:fT,meta:fE,exp:fF,spec:fS2,prd:fH,tree:fTree,topo:fTopo,tests:fTests,kanban:fK,infra:fI,easytier:fE2,rdp:fR,principles:function(){return(FETCHES[as]||function(){})()}};
|
||||
async function loadAll(){var d=new Date();qs('subtitle').textContent=d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes();var skip=['spec','prd','tree','topo'];await Promise.all(Object.keys(FETCHES).filter(function(k){return skip.indexOf(k)<0}).map(function(k){return FETCHES[k]()}));}
|
||||
fill('tests',h)}catch(e){fill('tests','<span class=tag r>Error: '+esc(e.message)+'</span>')}}
|
||||
|
||||
var FETCHES={agents:fa,spec:fS2,tests:fTests,health:fHealth,prd:fH,kanban:fK,infra:fI,easytier:fE2,rdp:fR,principles:function(){return(FETCHES[as]||function(){})()}};
|
||||
async function loadAll(){var d=new Date();qs('subtitle').textContent=d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate()+' '+d.getHours()+':'+d.getMinutes();var skip=['spec','prd','health'];await Promise.all(Object.keys(FETCHES).filter(function(k){return skip.indexOf(k)<0}).map(function(k){return FETCHES[k]()}));}
|
||||
|
||||
async function fE2(){try{var r=await fetch('/api/easytier'),d=await r.json(),s=d.status||{};var e1=document.getElementById('et-w');if(e1){e1.className='d '+(s.windows==='running'?'ok':'stopped')}var e2=document.getElementById('et-246');if(e2){e2.className='d '+(s['246']==='running'?'ok':'stopped')}if(d.virtual_ips){var w=document.getElementById('et-w-ip');if(w)w.textContent=d.virtual_ips.windows||'';var t=document.getElementById('et-246-ip');if(t)t.textContent=d.virtual_ips['246']||''}}catch(e){}}
|
||||
async function fR(){try{var r=await fetch('/api/rdp'),d=await r.json();var e1=document.getElementById('rdp-st');if(e1){e1.className='d '+(d.rdp_enabled?'ok':'stopped')}var e2=document.getElementById('rdp-tun');if(e2){e2.className='d '+(d.tunnel_running?'ok':'stopped')}var info=document.getElementById('rdp-info');if(info)info.textContent=d.public_endpoint||''}catch(e){}}
|
||||
var fS2_c=null;var fS2_last='';async function fS2(){try{var r=await fetch('/api/spec'),d=await r.json();if(!d.ok||!d.content){fill('spec','Failed');return}var el=document.getElementById('ct-spec');var sp=el?el.querySelector('div')?el.querySelector('div').scrollTop:0:0;fill('spec','<div style=padding:8px 0>'+mdRender(d.content)+'</div>');var el2=document.getElementById('ct-spec');if(el2&&sp){setTimeout(function(){(el2.querySelector('div')||el2).scrollTop=sp},0)}}catch(e){fill('spec','Error')}}
|
||||
var fS2_c=null;var fS2_last='';async function fS2(){try{
|
||||
var r=await fetch('/api/spec'),d=await r.json();
|
||||
if(!d.ok||!d.content){fill('spec','Failed');return}
|
||||
var h='<div style=padding:8px 0>'+mdRender(d.content)+'</div>';
|
||||
try{var rh=await fetch('/api/spec/history'),dh=await rh.json();
|
||||
if(dh.ok&&dh.log&&dh.log.length){
|
||||
h+='<div class=section style=margin-top:16px><h2 style=font-size:17px;cursor:pointer onclick="var e=document.getElementById(\'spec-history\');e.style.display=e.style.display===\'none\'?\'block\':\'none\'">历史版本 <span style=color:var(--dim);font-size:18px>('+dh.count+' commits)</span></h2><div id=spec-history style=display:none;margin-top:8px>';
|
||||
for(var i=0;i<dh.log.length;i++){h+='<div class=log-line>'+esc(dh.log[i])+'</div>';}
|
||||
h+='</div></div>';}
|
||||
}catch(e){}
|
||||
fill('spec',h)}catch(e){fill('spec','Error')}}
|
||||
|
||||
function mdRender(md){if(!md)return'';var h='';var blocks=md.split('\n\n');for(var b=0;b<blocks.length;b++){var block=blocks[b].trim();if(!block)continue;
|
||||
if(block.startsWith('```')){var code=block.replace(/^```[\s\S]*?\n/,'').replace(/```$/,'').trim();h+='<pre class=mono style=background:var(--bg);padding:12px;border-radius:6px;overflow-x:auto>'+esc(code)+'</pre>';continue;}
|
||||
@@ -396,7 +394,4 @@ h+='<p style="line-height:1.7;margin:6px 0">'+inMd(block)+'</p>';}
|
||||
return h;
|
||||
function inMd(t){return t.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>').replace(/\*([^*]+)\*/g,'<em>$1</em>').replace(/`([^`]+)`/g,'<code style="background:var(--bg);padding:1px 4px;border-radius:3px;font-size:0.9em">$1</code>').replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" style="color:var(--accent)" target=_blank>$1</a>')}}
|
||||
init();loadAll();setInterval(loadAll,10000);setInterval(fK,15000);
|
||||
|
||||
|
||||
setInterval(fE,10000);setInterval(fR,10000);
|
||||
</script></body></html>
|
||||
Reference in New Issue
Block a user