# 脚本批量执行平台 — 技术说明(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}` — 一次性 secret(TTL 7 天,见 `script_jobs.py`) ### 2.2 MySQL 冷数据 - 表: `script_executions` - `results` JSON:`{ "": { ... }, "_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 刷盘摘要 |