334 lines
8.7 KiB
Markdown
334 lines
8.7 KiB
Markdown
# 系统服务控制
|
||
|
||
本文定义后台控制面动作与现有 `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 <port>` | 执行前必须由后端校验端口。 |
|
||
| `restart-frontend-port` | 在指定端口重启前端 | `./planet.sh restart -f <port>` | 执行前必须由后端校验端口。 |
|
||
| `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", "<port>"]
|
||
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、操作者身份、执行开始时间和结果。
|