2026-05-23 15:27:40 +08:00
|
|
|
|
# 脚本批量执行平台 — 技术说明(SSOT)
|
|
|
|
|
|
|
|
|
|
|
|
**最后更新**: 2026-05-22
|
2026-06-04 23:02:21 +08:00
|
|
|
|
**状态**: 已实现,待本地 / 外网 E2E 验证
|
2026-05-23 15:27:40 +08:00
|
|
|
|
|
|
|
|
|
|
## 1. 目标与约束
|
|
|
|
|
|
|
|
|
|
|
|
| 目标 | 实现方式 |
|
|
|
|
|
|
|------|----------|
|
|
|
|
|
|
| 2000+ 台批量执行 | 内置分批(默认 50 台/批),批内并发 10 |
|
|
|
|
|
|
| 长任务不阻塞 HTTP | `nohup` + 子机结束 `curl` 主站回调 |
|
|
|
|
|
|
| 短任务执行中可停止 | Agent `wait=false` → `pid` → `/exec/kill` |
|
|
|
|
|
|
| 减轻 MySQL 压力 | 执行态**只写 Redis**;MySQL **60s 分批刷盘** |
|
|
|
|
|
|
| 全链路可审计 | `audit_logs` 即时写入;`results._meta.events` 时间线 |
|
|
|
|
|
|
| 不用 WS 换票 / 异步 exec API | 同步 `POST /api/scripts/exec` + 页顶状态条轮询 |
|
|
|
|
|
|
|
|
|
|
|
|
**不可改 / 环境项**(`.env`):
|
|
|
|
|
|
|
|
|
|
|
|
- `NEXUS_API_BASE_URL` — 长任务子机回调主站地址(必填,否则长任务 400)
|
|
|
|
|
|
- `NEXUS_SCRIPT_EXEC_BATCH_SIZE`(默认 50)、`NEXUS_SCRIPT_EXEC_CONCURRENCY`(默认 10)
|
|
|
|
|
|
|
|
|
|
|
|
**代码内常量**(`server/infrastructure/redis/script_execution_store.py`,不写 `.env`):
|
|
|
|
|
|
|
|
|
|
|
|
| 常量 | 值 | 含义 |
|
|
|
|
|
|
|------|-----|------|
|
|
|
|
|
|
| `REDIS_TTL_SECONDS` | 7200 | 热数据最长 2h |
|
|
|
|
|
|
| `FLUSH_INTERVAL_SECONDS` | 60 | Redis→MySQL 刷盘周期 |
|
|
|
|
|
|
| `FLUSH_BATCH_LIMIT` | 50 | 每轮最多刷 50 条执行记录 |
|
|
|
|
|
|
| `STUCK_NO_PROGRESS_SECONDS` | 3600 | 卡住检测阈值(1h) |
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 架构与数据流
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart LR
|
|
|
|
|
|
UI[scripts.html / script-executions.html]
|
|
|
|
|
|
API[POST /api/scripts/exec]
|
|
|
|
|
|
SVC[ScriptService]
|
|
|
|
|
|
Redis[(Redis script_exec:id)]
|
|
|
|
|
|
Flush[script_execution_flush 60s]
|
|
|
|
|
|
MySQL[(script_executions)]
|
|
|
|
|
|
Agent[子机 Agent :8800]
|
|
|
|
|
|
CB[POST /api/agent/script-callback]
|
|
|
|
|
|
|
|
|
|
|
|
UI --> API --> SVC
|
|
|
|
|
|
SVC --> Redis
|
|
|
|
|
|
SVC --> Agent
|
|
|
|
|
|
Agent -->|长任务结束 curl| CB --> Redis
|
|
|
|
|
|
Flush --> MySQL
|
|
|
|
|
|
UI -->|GET executions/id| Redis
|
|
|
|
|
|
UI -.->|历史列表可能滞后| MySQL
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 Redis 热数据
|
|
|
|
|
|
|
|
|
|
|
|
- Key: `script_exec:{execution_id}` — JSON 全量快照(status、results、events、operator…)
|
|
|
|
|
|
- 队列: `script_exec:flush_queue` — 待刷 MySQL 的 id 集合
|
|
|
|
|
|
- 长任务回调: `script_job:{job_id}` — 一次性 secret(TTL 7 天,见 `script_jobs.py`)
|
|
|
|
|
|
|
|
|
|
|
|
### 2.2 MySQL 冷数据
|
|
|
|
|
|
|
|
|
|
|
|
- 表: `script_executions`
|
|
|
|
|
|
- `results` JSON:`{ "<server_id>": { ... }, "_meta": { "events": [...] } }`
|
|
|
|
|
|
- 刷盘后从 flush 队列移除;读接口**优先 Redis**,无 Redis 再读 MySQL
|
|
|
|
|
|
|
|
|
|
|
|
### 2.3 进度字段
|
|
|
|
|
|
|
|
|
|
|
|
- API 字段 `progress`: `"done/total"`(`pending` 计为未完成)
|
|
|
|
|
|
- 实现: `execution_progress_summary()` in `script_execution_store.py`
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 执行模式
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 短任务(默认)
|
|
|
|
|
|
|
|
|
|
|
|
1. 主站 `POST` 子机 Agent `/exec` + `wait: false` → 得 `pid`,**立即** `patch_live` 写入 Redis(`phase: pending`)
|
|
|
|
|
|
2. 主站 `POST` `/exec/wait` 等待结束
|
|
|
|
|
|
3. 用户点「停止」→ 主站 `POST` `/exec/kill`(需**新版 Agent**)
|
|
|
|
|
|
4. 未升级 Agent:回退同步 `/exec` `wait=true`,**无法**中途停止
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 长任务(`long_task: true`)
|
|
|
|
|
|
|
|
|
|
|
|
1. 命令存库前缀 `[long_task] `
|
|
|
|
|
|
2. `script_jobs.build_long_task_command()`:`nohup` + 日志 `/var/log/nexus-job/{job_id}.log` + 结束 curl 主站
|
|
|
|
|
|
3. 点火成功 → `phase: pending` + `job_id` + `started_at`
|
|
|
|
|
|
4. 子机 `POST /api/agent/script-callback` → 更新 results + WebSocket `script_job_done`
|
|
|
|
|
|
5. 停止:kill `${LOG}.pid` + 撤销 Redis job secret
|
|
|
|
|
|
|
|
|
|
|
|
## 4. HTTP API(JWT,除回调)
|
|
|
|
|
|
|
|
|
|
|
|
| 方法 | 路径 | 说明 |
|
|
|
|
|
|
|------|------|------|
|
|
|
|
|
|
| GET | `/api/scripts/exec-config` | `batch_size`, `concurrency` |
|
|
|
|
|
|
| POST | `/api/scripts/exec` | `server_ids`, `command`, `timeout`, `long_task`, `credential_id?` |
|
|
|
|
|
|
| GET | `/api/scripts/executions` | 分页历史 `limit`, `offset`, `status` |
|
|
|
|
|
|
| GET | `/api/scripts/executions/{id}` | `?fetch_logs=true` 拉子机 tail 日志 |
|
|
|
|
|
|
| POST | `/api/scripts/executions/{id}/stop` | 停止 pending |
|
|
|
|
|
|
| POST | `/api/scripts/executions/{id}/retry` | 失败/未完成重试;**新建** `execution_id`,响应含 `parent_execution_id` |
|
|
|
|
|
|
| POST | `/api/scripts/executions/{id}/mark-stuck` | 人工标记卡住 |
|
|
|
|
|
|
| POST | `/api/agent/script-callback` | `job_id`, `secret`, `server_id`, `exit_code`(无 JWT;Redis 限速) |
|
|
|
|
|
|
|
|
|
|
|
|
## 4.1 安全与执行面
|
|
|
|
|
|
|
|
|
|
|
|
| 风险 | 现状 | 缓解 |
|
|
|
|
|
|
|------|------|------|
|
|
|
|
|
|
| 子机 `subprocess_shell` / 长任务 `bash -c` | 运维必需能力 | 仅 API Key 可调 `/exec`;主站 `check_dangerous_command` **拒绝**危险模式;Agent 每次 exec 打 WARNING 审计日志 |
|
|
|
|
|
|
| 回调 HTTP 明文 | 生产配置错误 | `master_callback_url()` 非 localhost 强制 `https://` |
|
|
|
|
|
|
| 回调刷接口 | 无 JWT | `script_callback_rate`:每 job 10/min、每 IP 60/min |
|
|
|
|
|
|
| 重试语义 | 每次 retry 新建记录 | API 返回 `parent_execution_id` 关联原单;历史页按 id 追踪 |
|
|
|
|
|
|
|
|
|
|
|
|
子机应网络隔离:仅中心机 IP 可访问 Agent `:8800`。
|
|
|
|
|
|
|
|
|
|
|
|
## 5. 子机 Agent 接口
|
|
|
|
|
|
|
|
|
|
|
|
文件: `web/agent/agent.py`
|
|
|
|
|
|
|
|
|
|
|
|
| 方法 | 路径 | 说明 |
|
|
|
|
|
|
|------|------|------|
|
|
|
|
|
|
| POST | `/exec` | `wait` 默认 true;false 返回 `pid` |
|
|
|
|
|
|
| POST | `/exec/wait` | 等待 `pid` |
|
|
|
|
|
|
| POST | `/exec/kill` | 终止 `pid` |
|
|
|
|
|
|
|
|
|
|
|
|
**部署**:各子机更新 Agent 后重启;否则短任务 stop 不可用。
|
|
|
|
|
|
|
|
|
|
|
|
## 6. 后台任务
|
|
|
|
|
|
|
|
|
|
|
|
- 模块: `server/background/script_execution_flush.py`
|
|
|
|
|
|
- 注册: `server/main.py` → `script_execution_flush_loop()`(primary worker)
|
|
|
|
|
|
- 每 60s:
|
|
|
|
|
|
1. `detect_stuck_executions()` — 单台 pending 超 1h → `server_auto_stuck`;整单无更新 1h → `auto_stuck`
|
|
|
|
|
|
2. `flush_pending_batch()` — 最多 50 条写入 MySQL
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 审计(`audit_logs.action`)
|
|
|
|
|
|
|
|
|
|
|
|
| action | 触发场景 |
|
|
|
|
|
|
|--------|----------|
|
|
|
|
|
|
| `execute_started` | 创建执行记录 |
|
|
|
|
|
|
| `execute_command` | 记录实际下发命令摘要 |
|
|
|
|
|
|
| `stop_execution` | 用户停止 |
|
|
|
|
|
|
| `retry_execution` | 用户重试 |
|
|
|
|
|
|
| `mark_stuck` | 用户标记卡住 |
|
|
|
|
|
|
| `script_job_callback` | 长任务子机回调 |
|
|
|
|
|
|
| `auto_stuck` | 整单 1h 无 Redis 更新 |
|
|
|
|
|
|
| `server_auto_stuck` | 单台 pending 超 1h |
|
|
|
|
|
|
|
|
|
|
|
|
前端筛选: `web/app/audit.html` 已增加上述选项。
|
|
|
|
|
|
|
|
|
|
|
|
## 8. 前端页面
|
|
|
|
|
|
|
|
|
|
|
|
| 页面 | 路径 | 功能 |
|
|
|
|
|
|
|------|------|------|
|
|
|
|
|
|
| 脚本库 | `web/app/scripts.html` | 执行弹窗、分批、长任务勾选、页顶「批量执行状态」 |
|
|
|
|
|
|
| 执行历史 | `web/app/script-executions.html` | 列表、进度、停止/重试/卡住、详情+日志、10s 轮询 |
|
|
|
|
|
|
| 侧栏 | `web/app/layout.js` | 菜单项「执行历史」 |
|
|
|
|
|
|
|
|
|
|
|
|
页顶状态条按钮:刷新状态、状态+日志、停止、重试、标记卡住。
|
|
|
|
|
|
|
|
|
|
|
|
## 9. 涉及文件清单
|
|
|
|
|
|
|
|
|
|
|
|
### 后端
|
|
|
|
|
|
|
|
|
|
|
|
| 文件 | 职责 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| `server/application/services/script_service.py` | 分批执行、Agent 调用、stop/retry/list |
|
|
|
|
|
|
| `server/application/services/script_jobs.py` | 长任务 nohup + job Redis |
|
|
|
|
|
|
| `server/application/services/script_job_callback.py` | 回调合并结果 |
|
|
|
|
|
|
| `server/infrastructure/redis/script_execution_store.py` | Redis 热数据、刷盘队列、卡住检测、progress |
|
|
|
|
|
|
| `server/background/script_execution_flush.py` | 60s 循环 |
|
|
|
|
|
|
| `server/infrastructure/database/script_repo.py` | `list_recent`, `update_status` |
|
|
|
|
|
|
| `server/api/scripts.py` | REST 路由 |
|
|
|
|
|
|
| `server/api/agent.py` | `script-callback` |
|
|
|
|
|
|
| `server/config.py` | `SCRIPT_EXEC_BATCH_SIZE`, `SCRIPT_EXEC_CONCURRENCY` |
|
|
|
|
|
|
| `server/main.py` | 注册 flush 后台任务 |
|
|
|
|
|
|
|
|
|
|
|
|
### 前端 / Agent
|
|
|
|
|
|
|
|
|
|
|
|
| 文件 | 职责 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| `web/app/scripts.html` | 执行 UI + 状态条 |
|
|
|
|
|
|
| `web/app/script-executions.html` | 历史页 |
|
|
|
|
|
|
| `web/app/layout.js` | 侧栏 |
|
|
|
|
|
|
| `web/app/audit.html` | 审计筛选 |
|
|
|
|
|
|
| `web/agent/agent.py` | exec / wait / kill |
|
|
|
|
|
|
|
|
|
|
|
|
## 10. 已知限制
|
|
|
|
|
|
|
|
|
|
|
|
1. MySQL 视图比 Redis **最多晚 60s**(直接查库时);页面轮询读 Redis 无影响。
|
|
|
|
|
|
2. 短任务 stop 依赖子机 Agent 新版本。
|
|
|
|
|
|
3. 自动卡住:单台按 `started_at`;整单按 `updated_at`(非单台日志活跃度)。
|
|
|
|
|
|
4. 危险命令仍由 `check_dangerous_command` 拦截。
|
|
|
|
|
|
5. 长任务依赖 `NEXUS_API_BASE_URL` 对子机可达,且生产须为 HTTPS(本地可用 `http://localhost`)。
|
|
|
|
|
|
6. **重试**会创建新的 `script_executions.id`;原单结果保留,通过 `parent_execution_id` 关联。
|
|
|
|
|
|
|
|
|
|
|
|
## 11. 测试清单(待执行)
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] T-SE1: 选 3 台短任务,页顶进度 `3/3`,审计有 `execute_started`
|
|
|
|
|
|
- [ ] T-SE2: 选 60 台,确认分 2 批、合并一条状态卡
|
|
|
|
|
|
- [ ] T-SE3: 长任务 + 配置 `NEXUS_API_BASE_URL`,回调后状态变完成、WS 通知
|
|
|
|
|
|
- [ ] T-SE4: 长任务执行中「停止」,子机进程结束
|
|
|
|
|
|
- [ ] T-SE5: 新版 Agent 短任务执行中「停止」
|
|
|
|
|
|
- [ ] T-SE6: 执行历史页停止/重试/状态+日志
|
|
|
|
|
|
- [ ] T-SE7: 等待或调低 `STUCK_NO_PROGRESS_SECONDS` 验证 `server_auto_stuck` / `auto_stuck`
|
|
|
|
|
|
- [ ] T-SE8: Redis 有 key 时 MySQL 在 60s 内出现对应行
|
|
|
|
|
|
|
|
|
|
|
|
## 12. 相关 Changelog
|
|
|
|
|
|
|
|
|
|
|
|
| 文档 | 内容 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| [2026-05-22-script-execution-platform.md](../changelog/2026-05-22-script-execution-platform.md) | 本次交付总览与文件变更索引 |
|
|
|
|
|
|
| [2026-05-22-script-exec-batching.md](../changelog/2026-05-22-script-exec-batching.md) | 内置分批与 exec-config |
|
|
|
|
|
|
| [2026-05-22-script-job-callback.md](../changelog/2026-05-22-script-job-callback.md) | 长任务回调与 Redis 刷盘摘要 |
|