# 系统服务控制 本文定义后台控制面动作与现有 `planet.sh` 服务管理命令之间的固定映射。 目标是在复用当前运维脚本语义的同时,不向前端或 API 调用方暴露任意 shell 执行能力。 ## 范围 - 这套映射只用于管理端运维控制。 - 控制面必须提交固定 action 名称,而不是原始 shell 命令。 - 后端负责把允许的 action 翻译成固定的 `planet.sh` 调用。 ## 设计规则 - 只允许执行白名单 action。 - 前端绝不能发送任意 shell 字符串。 - 后端必须从固定映射表构造命令参数。 - 高风险 action 应限制为 `super_admin`。 - 在 UI 连续性重要时,优先局部重启,而不是全栈重启。 ## Action 映射 | Action 名称 | 用途 | `planet.sh` 命令 | 备注 | | --- | --- | --- | --- | | `restart-backend` | 只重启后端 API | `./planet.sh restart -b` | 页面通常短暂失联后由 `/health` 轮询恢复。 | | `restart-frontend` | 只重启前端开发服务器 | `./planet.sh restart -f` | 页面入口会短暂不可用;UI 通过前端入口探测恢复后刷新。 | | `restart-database` | 重启 PostgreSQL 和 Redis 容器 | `./planet.sh restart -d` | 适合数据库/缓存需要受控重启但不希望重启 UI 的场景。 | | `restart-system` | 重启整个应用栈 | `./planet.sh restart` | 前端会短暂中断;UI 应进入引导恢复模式。 | | `restart-backend-port` | 在指定端口重启后端 | `./planet.sh restart -b ` | 执行前必须由后端校验端口。 | | `restart-frontend-port` | 在指定端口重启前端 | `./planet.sh restart -f ` | 执行前必须由后端校验端口。 | | `health-check` | 读取当前服务健康状态 | `./planet.sh health` | 安全的只读运维动作。 | | `show-logs-backend` | 查看后端日志 | `./planet.sh log -b` | 更适合 CLI/运维工具,不建议作为普通 Web UI 日志流。 | | `show-logs-frontend` | 查看前端日志 | `./planet.sh log -f` | 更适合 CLI/运维工具,不建议作为普通 Web UI 日志流。 | ## 默认不暴露到 UI 的能力 除非有明确产品需求并经过额外安全评审,否则以下脚本能力不应直接暴露到 Web UI: - `./planet.sh restart` - `./planet.sh start` - `./planet.sh stop` - `./planet.sh createuser` - 任何未来的原始 shell 透传能力 原因: - 全量重启可能打断当前控制会话; - stop/start 影响面更大; - 用户创建不是服务控制操作; - 原始 shell 透传会引入不必要的权限风险。 ## 第一阶段推荐 UI 契约 ### 前端 action payload ```json { "action": "restart-backend" } ``` ### 后端命令解析 ```text restart-backend -> ["./planet.sh", "restart", "-b"] restart-database -> ["./planet.sh", "restart", "-d"] restart-system -> ["./planet.sh", "restart"] restart-frontend -> ["./planet.sh", "restart", "-f"] health-check -> ["./planet.sh", "health"] ``` ## API 草案 ### 主接口 - `POST /api/v1/system/restart-tasks` 用途: - 创建受控重启任务; - 将白名单 action 解析成固定 `planet.sh` 命令; - 把执行交给外部 runner 或 detached subprocess。 ### 请求体 ```json { "action": "restart-backend" } ``` 未来可选形态: ```json { "action": "restart-backend-port", "port": 8000 } ``` ### 响应 ```json { "task_id": "restart_20260331_153000_ab12cd", "action": "restart-backend", "status": "queued", "stage": "accepted", "message": "Restart task accepted" } ``` ### 任务查询接口 - `GET /api/v1/system/restart-tasks/{task_id}` 响应结构: ```json { "task_id": "restart_20260331_153000_ab12cd", "action": "restart-backend", "status": "queued", "stage": "accepted", "message": "Waiting for execution", "requested_by": { "id": 1, "username": "admin" }, "created_at": "2026-03-31T15:30:00+08:00", "updated_at": "2026-03-31T15:30:02+08:00" } ``` ### 可选日志接口 - `GET /api/v1/system/restart-tasks/{task_id}/logs` 建议响应: ```json { "task_id": "restart_20260331_153000_ab12cd", "lines": [ "accepted restart-backend request", "spawning restart command", "waiting for backend shutdown", "waiting for backend health recovery" ] } ``` 日志接口在第一阶段不是必需项。首版可以只依赖任务状态加 `/health` 轮询。 ## 任务状态模型 ### Status - `queued` - `running` - `succeeded` - `failed` - `timeout` ### Stage - `accepted` - `spawning` - `stopping` - `starting` - `waiting_for_health` - `healthy` - `failed` ### 含义 - `status` 是高层终态/非终态状态。 - `stage` 是面向运维人员和 UI 的执行阶段。 - `message` 是 modal 或全屏遮罩中展示的短文本。 ## 权限模型 - `restart-backend` 应要求 `super_admin`。 - 权限检查应沿用 [users.py](/home/ray/dev/linkong/planet/backend/app/api/v1/users.py) 中已有的角色模式。 - 前端可以对非 `super_admin` 隐藏控件,但后端必须继续强制鉴权。 ## 存储模型 推荐第一阶段实现: - 将重启任务状态存入 Redis; - 任务生命周期保持较短; - 最近日志用有界列表保存。 建议 key: - `system:restart_task:{task_id}` - `system:restart_task:{task_id}:logs` 建议字段: - `task_id` - `action` - `status` - `stage` - `message` - `requested_by_id` - `requested_by_username` - `created_at` - `updated_at` ## 执行模型 处理请求的 API 进程不应依赖自身持续存活来流式输出完整重启日志。 推荐执行流程: 1. 校验调用方和 action 2. 在 Redis 中创建任务状态 3. 将 action 解析为固定 `planet.sh` argv 4. 启动 detached executor 5. 返回 `task_id` 6. executor 在重启过程中更新任务状态 7. 前端轮询健康状态和/或任务状态,直到服务恢复 推荐命令解析示例: ```text restart-backend -> ["./planet.sh", "restart", "-b"] restart-frontend -> ["./planet.sh", "restart", "-f"] restart-backend-port -> ["./planet.sh", "restart", "-b", ""] health-check -> ["./planet.sh", "health"] ``` ## 前端轮询流程 推荐第一阶段 UX: 1. 用户点击 `重启后端` 2. 确认 modal 说明服务会短暂不可用 3. 前端调用 `POST /api/v1/system/restart-tasks` 4. UI 进入阻塞式重启状态 5. 前端每 `1-2s` 轮询 `/health` 6. 临时请求失败视为预期现象 7. 连续 `2-3` 次健康检查成功后,前端刷新页面 可选增强轮询: 1. 后端仍可达时轮询任务状态接口 2. 断连开始后切换为 `/health` 恢复轮询 3. 健康恢复后刷新页面 ## 前端状态机 - `idle` - `confirming` - `submitting` - `waiting_for_shutdown` - `waiting_for_recovery` - `recovered` - `failed` - `timeout` 建议 UI 文案: - `已发送重启指令` - `正在停止后端服务` - `正在等待服务恢复` - `服务已恢复,正在刷新页面` - `恢复超时,请手动检查服务状态` ## 当前 Dashboard 实现 Dashboard 当前已实现: - `restart-backend` - `restart-frontend` - `restart-ai-provider` - `restart-database` - `restart-system` - `super_admin` 权限门禁 - 任务创建接口 - Redis 任务状态 - 前端确认 modal - 后端 `/health` 轮询 - 前端入口轮询 - 恢复后自动刷新页面 暂不实现: - 原始 shell 命令透传 - 任意服务控制 - 完整终端 stdout 流式输出 - 多 action 并发重启队列 ## 实现清单 ### 后端 1. 在 `backend/app/api/v1/` 下新增专用系统控制 API 模块 2. 增加基于白名单的 `planet.sh` action 解析器 3. 将重启任务状态存入 Redis 4. 增加 detached restart-runner 脚本执行 5. 暴露: - `POST /api/v1/system/restart-tasks` - `GET /api/v1/system/restart-tasks/{task_id}` - 可选任务日志接口 6. 对所有 restart-task 接口强制 `super_admin` 权限 ### 前端 1. 在 dashboard 为 `super_admin` 增加 `重启服务` 控件 2. 发送前展示确认 modal 3. 提交后将 modal 切换为阻塞式重启状态 4. 后端重启使用 `/health` 轮询确认恢复 5. 前端重启和完全重启使用前端入口探测确认恢复 6. 连续健康检查成功后自动刷新页面 7. 展示简短阶段日志,而不是原始终端流 ### 运维说明 1. 优先使用局部重启,只有确实需要时才执行完全重启 2. 前端重启会打断当前页面入口,必须进入恢复等待状态 3. 命令执行必须始终从仓库根目录发起 4. API 边界只能传递固定 action 名称 ## 校验要求 - 拒绝任何不在白名单中的 action。 - 如果增加带端口 action,端口必须校验为 `1..65535` 的整数。 - 从仓库根目录解析命令,确保 `planet.sh` 的工作目录稳定。 - detached runner 使用 `zsh -ic` 执行白名单命令,确保 `~/.zshrc` 中的本地环境变量进入重启流程。 - 记录请求 action、操作者身份、执行开始时间和结果。