Files
Nexus/docs/project/script-execution.md
2026-07-08 22:31:31 +08:00

210 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 脚本批量执行平台 — 技术说明(SSOT)
**最后更新**: 2026-05-22
**状态**: 已实现,待本地 / 外网 E2E 验证
## 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}` — 一次性 secretTTL 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 APIJWT,除回调)
| 方法 | 路径 | 说明 |
|------|------|------|
| 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`(无 JWTRedis 限速) |
## 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` 默认 truefalse 返回 `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 刷盘摘要 |