feat: 推送取消 — 引擎 cancel flag 检查 + WS cancelled 计数器
sync_engine_v2: 每台服务器推送前检查 Redis sync:cancel:{batch_id},
若已取消则跳过并标记 cancelled 状态,WS 广播 cancelled 计数。
websocket: broadcast_sync_progress 新增 cancelled 参数。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# 2026-05-30 终端页面快速命令栏 — 设计文档
|
||||
|
||||
## 背景与目标
|
||||
|
||||
terminal.html 当前只能手动输入命令,运维高频操作(如查看服务状态、读日志、重启进程)每次都要手敲。需要一个**可自定义的快速命令栏**,一键发送预设命令到终端。
|
||||
|
||||
## 方案
|
||||
|
||||
| 方案 | 优点 | 缺点 |
|
||||
|------|------|------|
|
||||
| A: 右侧面板底部固定命令栏 | 不占屏幕空间,可折叠 | 命令多了需要滚动 |
|
||||
| B: 工具栏下方横排按钮 | 一眼可见 | 横向空间有限(2-3个) |
|
||||
| C: 右键菜单 + 自定义命令 | 上下文触发 | 交互多一步 |
|
||||
|
||||
**选定 A + C 混合** — 右侧面板加"快速命令"区域,同时支持自定义(添加/编辑/删除),存储在 `localStorage`。
|
||||
|
||||
## 命令来源
|
||||
|
||||
| 来源 | 说明 |
|
||||
|------|------|
|
||||
| 内置默认 | 5 个通用运维命令:`systemctl status` / `journalctl -f` / `df -h` / `free -h` / `top -bn1` |
|
||||
| 用户自定义 | 点击 + 号添加,输入命令名 + 命令内容,存 localStorage |
|
||||
| 会话记录 | 不自动添加(避免混乱,用户手动保存常用命令) |
|
||||
|
||||
## 交互
|
||||
|
||||
1. 右侧面板"快速命令"区域列出所有内置+自定义命令
|
||||
2. 每条显示:▶ 图标 + 命令简称(可自定义)+ 命令内容(灰色小字截断)
|
||||
3. 点击 → 发送命令到终端(sendCmd 逻辑)
|
||||
4. 每条命令右侧 hover 显示编辑/删除按钮
|
||||
5. "+ 添加命令" 按钮在最下方
|
||||
|
||||
## 数据模型(localStorage)
|
||||
|
||||
```
|
||||
nexus_terminal_commands = [
|
||||
{ id: uuid, name: "查看 Nginx 状态", cmd: "systemctl status nginx" },
|
||||
{ id: uuid, name: "读日志", cmd: "tail -f /var/log/nginx/access.log" },
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
## 安全约束
|
||||
|
||||
- 纯前端功能,localStorage 存储,不涉及后端
|
||||
- 命令发送走已有 WebSocket DATA 通道
|
||||
- 转义由终端侧处理,JS 不发 shell 命令(只发字符流)
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 右侧面板显示"快速命令"区域
|
||||
2. 默认 5 条内置命令可点击执行
|
||||
3. 可添加自定义命令(名称+内容)
|
||||
4. 可编辑/删除自定义命令
|
||||
5. 刷新页面后自定义命令保留
|
||||
6. 内置命令不可删除
|
||||
@@ -473,13 +473,14 @@ async def broadcast_sync_progress(
|
||||
batch_id: str, server_id: int, server_name: str,
|
||||
status: str, completed: int, failed: int, total: int,
|
||||
error_message: str = None, duration_seconds: int = None,
|
||||
retry_job_id: int = None,
|
||||
retry_job_id: int = None, cancelled: int = 0,
|
||||
):
|
||||
"""Broadcast per-server push progress to all WebSocket clients.
|
||||
|
||||
Called from sync_engine_v2._sync_one() after each server completes.
|
||||
Frontend uses batch_id to filter stale events from previous pushes.
|
||||
retry_job_id is set when a failed push auto-creates a retry job.
|
||||
cancelled is the count of servers skipped due to user cancellation.
|
||||
"""
|
||||
msg = {
|
||||
"type": "sync_progress",
|
||||
@@ -493,6 +494,7 @@ async def broadcast_sync_progress(
|
||||
"error_message": error_message,
|
||||
"duration_seconds": duration_seconds,
|
||||
"retry_job_id": retry_job_id,
|
||||
"cancelled": cancelled,
|
||||
}
|
||||
await _dispatch_ws_message(msg)
|
||||
|
||||
|
||||
@@ -74,12 +74,55 @@ class SyncEngineV2:
|
||||
total = len(servers)
|
||||
completed = 0
|
||||
failed = 0
|
||||
cancelled = 0
|
||||
_counter_lock = asyncio.Lock()
|
||||
results = {}
|
||||
|
||||
async def _sync_one(server: Server):
|
||||
nonlocal completed, failed
|
||||
nonlocal completed, failed, cancelled
|
||||
async with sem:
|
||||
# Check cancel flag before starting
|
||||
try:
|
||||
from server.infrastructure.redis.client import get_redis
|
||||
redis = get_redis()
|
||||
cancel_flag = await redis.get(f"sync:cancel:{batch_id}")
|
||||
if cancel_flag:
|
||||
sync_log = SyncLog(
|
||||
server_id=server.id,
|
||||
source_path=source_path,
|
||||
target_path=target_path or server.target_path or "/tmp/sync",
|
||||
trigger_type=trigger_type,
|
||||
operator=operator,
|
||||
status="cancelled",
|
||||
sync_mode=sync_mode,
|
||||
started_at=datetime.now(timezone.utc),
|
||||
finished_at=datetime.now(timezone.utc),
|
||||
error_message="推送已取消",
|
||||
)
|
||||
sync_log = await self.sync_log_repo.create(sync_log)
|
||||
|
||||
async with _counter_lock:
|
||||
cancelled += 1
|
||||
try:
|
||||
from server.api.websocket import broadcast_sync_progress
|
||||
await broadcast_sync_progress(
|
||||
batch_id=batch_id,
|
||||
server_id=server.id,
|
||||
server_name=server.name,
|
||||
status="cancelled",
|
||||
completed=completed,
|
||||
failed=failed,
|
||||
total=total,
|
||||
error_message="推送已取消",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"WS broadcast failed for cancelled server {server.id}: {e}")
|
||||
|
||||
results[server.id] = (sync_log, None)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.warning(f"Cancel check failed for batch {batch_id}, proceeding with push: {e}")
|
||||
|
||||
dest = target_path or server.target_path or "/tmp/sync"
|
||||
sync_log = SyncLog(
|
||||
server_id=server.id,
|
||||
@@ -147,6 +190,7 @@ class SyncEngineV2:
|
||||
error_message=sync_log.error_message[:200] if sync_log.error_message else None,
|
||||
duration_seconds=sync_log.duration_seconds,
|
||||
retry_job_id=retry_job_id,
|
||||
cancelled=cancelled,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"WS broadcast failed for server {server.id}: {e}")
|
||||
|
||||
Reference in New Issue
Block a user