diff --git a/AGENTS.md b/AGENTS.md index d780d9f..52bd7ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ React 不直接访问 SSH、文件系统或加密实现。新增原生能力时 - 复制转发规则不得复制运行态;运行中的规则修改前必须先停止 Listener。 - SFTP read/write 不得作为 upload/download 的别名,本地路径与远程路径不得使用位置含糊的调用。 - 大模型新配置必须先用临时凭据完成真实测试,测试期间不得写 settings 或 secrets;连接字段变化后保存必须重新锁定。 -- AI 远程执行只能复用当前活跃 SSH connection,每次执行前显式确认;不建立独立 podexec/Kubernetes 凭据边界,也不保存命令或工具输出。 +- AI 远程执行只能复用当前活跃 SSH connection;按 strict/balanced/permissive 模式决定确认阈值,critical 始终硬拒绝;不建立独立 podexec/Kubernetes 凭据边界,也不保存命令或工具输出。 - `ssh://` 禁止携带密码或静默直连;冷启动与运行中唤起都必须通过原生有界队列交付给快速连接确认。 ## 命令 diff --git a/README.md b/README.md index 0d65607..c88b321 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - OpenSSH 配置:导入 `~/.ssh/config` 常用字段;界面支持单跳选择,Rust 后端支持最多 8 跳 - 系统链接:`ssh://user@host:port` 唤起已有或新启动的 OpsXShell,并进入预填充快速连接确认 - 本地终端与串口:系统 Shell、串口设备与波特率配置 -- SSH Agent CLI:OpenAI/Responses/Anthropic 兼容 Provider、new-api 与 Ollama;自然语言驱动最多 8 步的执行→观察循环,每步确认、五级风险和破坏性命令硬拒绝 +- SSH Agent CLI:OpenAI/Responses/Anthropic 兼容 Provider、new-api 与 Ollama;自然语言驱动最多 8 步的执行→观察循环,三级安全模式、五级风险和破坏性命令硬拒绝 - 本地安全:AES-256-GCM 加密会话密码、密钥口令和模型 API Key,不保存终端命令或 AI 对话历史 ## 产品边界 @@ -58,10 +58,11 @@ npm run dev npm run acceptance:quick npm run acceptance:full ./build.sh v2.1.1 mac-arm64 +build.bat v2.1.1 win-x64 ./release.py v2.1.1 --dry-run ``` -`build.sh` 会校验版本、运行门禁、构建并验证安装包,最终产物统一复制到 `release/v<版本>/<平台>/` 并生成 `SHA256SUMS.txt`。显式传入新版本会同步所有版本文件,但不会自动创建或推送 Git tag。`release.py` 在 tag 推送后创建 Gitee Release、上传安装包和校验文件,并核对远程附件;先使用 `--dry-run` 检查待发布文件。平台打包必须在对应系统执行:macOS 在 macOS 构建,Windows x86_64 在 Windows 构建。 +`build.sh`(macOS/Git Bash)和 `build.bat`(Windows CMD)会校验版本、运行门禁、构建并验证安装包,最终产物统一复制到 `release/v<版本>/<平台>/` 并生成 `SHA256SUMS.txt`。显式传入新版本会同步所有版本文件,但不会自动创建或推送 Git tag。`release.py` 在 tag 推送后创建 Gitee Release、上传安装包和校验文件,并核对远程附件;先使用 `--dry-run` 检查待发布文件。平台打包必须在对应系统执行:macOS 在 macOS 构建,Windows x86_64 在 Windows 构建。 ## 目录 @@ -95,6 +96,7 @@ scripts/ 构建和验收脚本 - [使用指南](docs/user-guide.md) - [架构说明](docs/architecture.md) - [SSH Agent CLI 设计](docs/ai-agent.md) +- [产品体验与基础能力评估](docs/ux-foundation-audit.md) - [测试与验收](docs/testing-acceptance.md) - [发布指南](docs/release.md) diff --git a/ROADMAP.md b/ROADMAP.md index aa91e2a..d85d5df 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,7 +11,8 @@ - `ssh.rs` 已拆出认证、known_hosts、代理/ProxyJump、路径和 SFTP 模块 - `tunnel.rs` 已拆出运行模型、SOCKS5 协议与 Relay 模块 - 注册 `ssh://` 系统链接,支持冷启动队列和运行中单实例唤起 -- SSH Agent CLI 单任务入口、8 步执行观察循环、五级风险、critical 硬拒绝、输出分析和重复命令拦截 +- SSH Agent CLI 单任务入口、8 步执行观察循环、三级安全模式、五级风险、critical 硬拒绝、输出分析和重复命令拦截 +- 全终端广播、批量主机选择与执行进度、终端标准右键菜单和远端单 Shell 关闭 - 远程 Shell EOF/TMOUT 与 SSH 连接生命周期分离,支持单分屏一键重开 ## 当前重点 diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..7f65bfb --- /dev/null +++ b/build.bat @@ -0,0 +1,215 @@ +@echo off +setlocal EnableExtensions EnableDelayedExpansion +chcp 65001 >nul + +rem OpsXShell v2.1.x native Windows release builder. +rem Run this script from Windows x64 with the MSVC Rust toolchain installed. + +cd /d "%~dp0" +set "VERSION_ARG=" +set "CHECK_MODE=full" +set "RUST_TARGET=x86_64-pc-windows-msvc" +set "OUTPUT_PLATFORM=windows-x64" + +:parse_args +if "%~1"=="" goto args_done +set "ARG=%~1" + +if /I "!ARG!"=="-h" goto usage_ok +if /I "!ARG!"=="--help" goto usage_ok +if /I "!ARG!"=="--quick" ( + set "CHECK_MODE=quick" + shift + goto parse_args +) +if /I "!ARG!"=="--skip-checks" ( + set "CHECK_MODE=none" + shift + goto parse_args +) +if /I "!ARG!"=="win-x64" ( + shift + goto parse_args +) +if /I "!ARG:~0,4!"=="v2.1" goto set_version_arg +if /I "!ARG:~0,4!"=="2.1." goto set_version_arg +call :fail "未知参数:!ARG!(使用 build.bat --help 查看用法)" +exit /b 1 + +:set_version_arg +if defined VERSION_ARG ( + call :fail "只能指定一个版本号" + exit /b 1 +) +set "VERSION_ARG=!ARG!" +if /I "!VERSION_ARG:~0,1!"=="v" set "VERSION_ARG=!VERSION_ARG:~1!" +shift +goto parse_args + +:args_done +if /I not "%OS%"=="Windows_NT" ( + call :fail "build.bat 必须在 Windows 环境运行" + exit /b 1 +) + +for %%C in (node npm npx cargo rustc powershell) do ( + where %%C >nul 2>nul + if errorlevel 1 ( + call :fail "缺少构建依赖:%%C" + exit /b 1 + ) +) + +for /f "usebackq delims=" %%V in (`node -p "require('./package.json').version"`) do set "CURRENT_VERSION=%%V" +if not defined CURRENT_VERSION ( + call :fail "无法读取 package.json 版本" + exit /b 1 +) + +if defined VERSION_ARG ( + set "VERSION=%VERSION_ARG%" +) else ( + set "VERSION=%CURRENT_VERSION%" +) + +node -e "if (!/^2\.1\.\d+(?:[.-][0-9A-Za-z.-]+)?$/.test(process.argv[1])) process.exit(1)" "%VERSION%" +if errorlevel 1 ( + call :fail "版本必须是 v2.1.x 语义版本,当前输入:%VERSION%" + exit /b 1 +) + +where rustup >nul 2>nul +if not errorlevel 1 ( + rustup target list --installed | findstr /X /C:"%RUST_TARGET%" >nul + if errorlevel 1 ( + call :fail "Rust target 未安装:%RUST_TARGET%;请先运行 rustup target add %RUST_TARGET%" + exit /b 1 + ) +) + +if defined VERSION_ARG if /I not "%VERSION%"=="%CURRENT_VERSION%" ( + call :log "同步版本号到 v%VERSION%" + node scripts\set-version.mjs "%VERSION%" + if errorlevel 1 exit /b 1 +) + +node scripts\set-version.mjs --check "%VERSION%" +if errorlevel 1 exit /b 1 + +if not exist node_modules\ ( + call :log "安装锁定依赖" + call npm ci + if errorlevel 1 exit /b 1 +) + +set "GIT_SHA=unknown" +where git >nul 2>nul +if not errorlevel 1 ( + for /f "delims=" %%G in ('git rev-parse --short HEAD 2^>nul') do set "GIT_SHA=%%G" +) +call :log "OpsXShell v%VERSION% · win-x64 · !GIT_SHA!" + +if /I "%CHECK_MODE%"=="full" ( + call :log "运行完整发布门禁" + call npm run acceptance:full + if errorlevel 1 exit /b 1 +) else if /I "%CHECK_MODE%"=="quick" ( + call :log "运行快速发布门禁" + call npm run acceptance:quick + if errorlevel 1 exit /b 1 +) else ( + call :log "已按参数跳过构建前门禁" +) + +node scripts\release-preflight.mjs windows +if errorlevel 1 exit /b 1 + +call :log "构建 Windows x86_64 原生安装包" +call npx tauri build --ci --target "%RUST_TARGET%" +if errorlevel 1 exit /b 1 + +set "BUNDLE_ROOT=src-tauri\target\%RUST_TARGET%\release\bundle" +node scripts\verify-release.mjs windows "%BUNDLE_ROOT%" +if errorlevel 1 exit /b 1 + +set "OUTPUT_DIR=%CD%\release\v%VERSION%\%OUTPUT_PLATFORM%" +if exist "%OUTPUT_DIR%" rmdir /S /Q "%OUTPUT_DIR%" +mkdir "%OUTPUT_DIR%" +if errorlevel 1 ( + call :fail "无法创建输出目录:%OUTPUT_DIR%" + exit /b 1 +) + +set /A ARTIFACT_COUNT=0 +for %%F in ("%BUNDLE_ROOT%\nsis\*.exe" "%BUNDLE_ROOT%\msi\*.msi" "%BUNDLE_ROOT%\wix\*.msi") do ( + if exist "%%~fF" ( + copy /Y "%%~fF" "%OUTPUT_DIR%\%%~nxF" >nul + if errorlevel 1 ( + call :fail "复制安装包失败:%%~fF" + exit /b 1 + ) + set /A ARTIFACT_COUNT+=1 + ) +) + +if "%ARTIFACT_COUNT%"=="0" ( + call :fail "构建完成但未找到 NSIS/MSI 安装包:%BUNDLE_ROOT%" + exit /b 1 +) + +powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "$files = Get-ChildItem -LiteralPath $env:OUTPUT_DIR -File ^| Where-Object { $_.Extension -in '.exe', '.msi' };" ^ + "$lines = @($files ^| ForEach-Object { (Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName).Hash.ToLowerInvariant() + ' ' + $_.Name });" ^ + "[System.IO.File]::WriteAllLines((Join-Path $env:OUTPUT_DIR 'SHA256SUMS.txt'), [string[]]$lines, (New-Object System.Text.UTF8Encoding($false)))" +if errorlevel 1 ( + call :fail "生成 SHA256SUMS.txt 失败" + exit /b 1 +) + +call :log "构建完成:%OUTPUT_DIR%" +dir "%OUTPUT_DIR%" +echo. +echo [build] SHA-256 +type "%OUTPUT_DIR%\SHA256SUMS.txt" +exit /b 0 + +:usage_ok +call :usage +exit /b 0 + +:usage +echo OpsXShell Windows 原生安装包构建脚本 +echo. +echo 用法: +echo build.bat [v2.1.x] [win-x64] [选项] +echo. +echo 选项: +echo --quick 构建前只运行快速门禁 +echo --skip-checks 跳过门禁,仅用于本地重复打包 +echo -h, --help 显示帮助 +echo. +echo 示例: +echo build.bat +echo build.bat v2.1.1 +echo build.bat v2.1.2 win-x64 --quick +echo. +echo 环境要求: +echo Windows 10/11 x64、Node.js 18+、Rust MSVC toolchain、 +echo Visual Studio 2022 Build Tools(Desktop development with C++)、 +echo Windows 10/11 SDK、WebView2 Runtime。 +echo. +echo 说明: +echo - 输出 NSIS/MSI 到 release\v版本\windows-x64 并生成 SHA256SUMS.txt。 +echo - 脚本不会创建或推送 Git tag,也不会上传 Release。 +echo - 正式签名需设置 REQUIRE_SIGNING=true 和 Windows 签名环境变量。 +exit /b 0 + +:log +echo. +echo [build] %~1 +exit /b 0 + +:fail +echo. +echo [build] ERROR: %~1 1>&2 +exit /b 1 diff --git a/docs/README.md b/docs/README.md index 8dc499f..6853067 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,12 +2,13 @@ 文档按使用、开发、验收和发布分开维护,避免把规划能力写成已支持功能。 -| 文档 | 内容 | -| ----------------------------------- | -------------------------------------------- | -| [使用指南](./user-guide.md) | SSH、SFTP、代理、串口、AI 助手和数据安全 | -| [架构说明](./architecture.md) | Tauri/Rust 分层、原生服务、AI 与安全边界 | -| [SSH Agent CLI](./ai-agent.md) | ReAct 执行循环、工具、风险、记忆取舍与评测 | -| [测试验收](./testing-acceptance.md) | 本地门禁、真实 SSH 探针和发版验收 | -| [发布指南](./release.md) | macOS/Windows 构建、签名、产物和临时文件清理 | +| 文档 | 内容 | +| -------------------------------------------- | ------------------------------------------------- | +| [使用指南](./user-guide.md) | SSH、SFTP、代理、串口、AI 助手和数据安全 | +| [架构说明](./architecture.md) | Tauri/Rust 分层、原生服务、AI 与安全边界 | +| [SSH Agent CLI](./ai-agent.md) | ReAct 执行循环、工具、风险、记忆取舍与评测 | +| [产品体验全面评估](./ux-foundation-audit.md) | 全产品评分、大众操作习惯、P0/P1/P2 与统一回归清单 | +| [测试验收](./testing-acceptance.md) | 本地门禁、真实 SSH 探针和发版验收 | +| [发布指南](./release.md) | macOS/Windows 构建、签名、产物和临时文件清理 | 项目定位、功能概览和开发命令见仓库根目录的 [README](../README.md),未完成事项只在 [ROADMAP](../ROADMAP.md) 维护。 diff --git a/docs/ai-agent.md b/docs/ai-agent.md index a356518..86c5176 100644 --- a/docs/ai-agent.md +++ b/docs/ai-agent.md @@ -7,7 +7,7 @@ OpsXShell 适合吸收 aiops Agent CLI 的执行 Harness,不适合照搬它的 WebSocket、服务器资产库、审计、审批、Kubernetes 专用工具和默认持久化记忆。 -产品目标是:用户在已连接的 SSH 终端中输入自然语言任务,Agent 通过少量、边界清晰的工具获取真实证据,在每次真实执行前展示命令和风险并等待确认,失败后根据输出换方法,最后给出有证据的结论。 +产品目标是:用户在已连接的 SSH 终端中输入自然语言任务,Agent 通过少量、边界清晰的工具获取真实证据,按用户选择的安全模式决定确认阈值,失败后根据输出换方法,最后给出有证据的结论。无论选择哪种模式,目标主机始终可见,critical 命令始终硬拒绝。 ## 参考来源 @@ -35,7 +35,7 @@ OpsXShell 适合吸收 aiops Agent CLI 的执行 Harness,不适合照搬它的 -> LLM 选择下一条单行命令,或返回 FINAL -> 五级命令风险评估 -> critical 破坏性操作硬拒绝 - -> 显示目标、风险和命令,用户显式确认 + -> 按本次运行锁定的安全模式决定是否确认 -> ssh_execute 使用当前 connectionId -> 返回 stdout + stderr + exit_code -> 输出过长时本地 analyze_output @@ -43,7 +43,7 @@ OpsXShell 适合吸收 aiops Agent CLI 的执行 Harness,不适合照搬它的 -> 失败换方法 / 继续验证 / FINAL ``` -- 单次任务最多 8 步。aiops 默认 16 轮适合后端异步任务;OpsXShell 每步都显式确认,8 步更符合桌面 GUI 的注意力和成本边界。 +- 单次任务最多 8 步。aiops 默认 16 轮适合后端异步任务;OpsXShell 是前台桌面工具,8 步更符合注意力、时延和成本边界。 - 相同命令最多出现 2 次,第 3 次在执行前停止。 - 第 4 轮检查是否已有足够证据,第 8 轮强制优先收敛;连续失败时 Prompt 要求更换排查路径。 - 单步输出进入 Agent 前有 12,000 字符上限,任务观察上下文有 48,000 字符上限。 @@ -51,15 +51,15 @@ OpsXShell 适合吸收 aiops Agent CLI 的执行 Harness,不适合照搬它的 ### 安全边界 -| 等级 | 示例 | 当前处理 | -| -------- | --------------------------------- | ---------------------- | -| safe | `df -h`、`journalctl -u nginx` | 显示后确认 | -| low | 未知自定义查询命令 | 显示后确认 | -| medium | 文件写入、普通权限变更 | 风险提示后确认 | -| high | 服务重启、删除、包安装、K8s 变更 | 红色风险确认 | -| critical | 格式化磁盘、删除根目录、fork bomb | 硬拒绝,不显示放行按钮 | +| 等级 | 示例 | 严格模式 | 均衡模式(默认) | 宽松模式 | +| -------- | --------------------------------- | -------- | ---------------- | -------- | +| safe | `df -h`、`journalctl -u nginx` | 自动执行 | 自动执行 | 自动执行 | +| low | 未知自定义查询命令 | 自动执行 | 自动执行 | 自动执行 | +| medium | 文件写入、普通权限变更 | 先确认 | 自动执行 | 自动执行 | +| high | 服务重启、删除、包安装、K8s 变更 | 先确认 | 先确认 | 自动执行 | +| critical | 格式化磁盘、删除根目录、fork bomb | 硬拒绝 | 硬拒绝 | 硬拒绝 | -此处不直接引入 aiops 的 `strict / balanced / permissive` 三级模式。原因是 OpsXShell 已对用户承诺“AI 每次真实执行前显式确认”,`permissive` 静默执行会破坏这条简单、可理解的规则。未来如需减少确认,应引入边界更明确的“仅允许 Rust 认定的纯只读命令自动执行”,而不是模糊的宽松模式。 +安全模式在任务开始时锁定,运行中不能切换;需要确认时,对话框必须显示本次锁定模式、目标、风险和完整单行命令。宽松模式明确标注“仅适合隔离测试环境”,它不会降低 critical 硬红线。 终端输出被标记为不可信数据,不得将日志中的“忽略规则”等文字当作指令。常见密码、Token、Authorization Header、AWS Key 和私钥块在 Rust 发送 Provider 前脱敏。 @@ -74,10 +74,10 @@ OpsXShell 适合吸收 aiops Agent CLI 的执行 Harness,不适合照搬它的 | OpenAI / Anthropic LLM 适配 | 已吸收 | 保留 OpenAI Chat、Responses、Anthropic Compatible 及自定义 Provider | | `ssh_execute` | 已吸收 | 固定使用当前活跃 `connectionId`,不重新管理一套凭据 | | `analyze_output` | 已适配 | 本地确定性摘要,降低 token 和幻觉 | -| ReAct 16 轮 | 适配为 8 步 | GUI 每步确认,优先快速收敛 | +| ReAct 16 轮 | 适配为 8 步 | GUI 前台执行,优先快速收敛 | | 失败反馈和循环检测 | 已吸收 | 退出码和真实输出回传,连续失败换路,相同命令限次 | -| 五级风险 | 已吸收 | critical 硬拒绝,其他均显式确认 | -| 三级安全模式 | 暂不吸收 | 保持单一可理解的“每步确认”规则 | +| 五级风险 | 已吸收 | critical 硬拒绝,其他等级按安全模式阈值确认 | +| 三级安全模式 | 已适配 | 严格从 medium、均衡从 high 确认;宽松仍不可绕过 critical | | WebSocket 事件流 | 不照搬 | 本地 React/Tauri 没有跨程服务器会话,使用内存状态即可 | | 审计、审批、RBAC | 不引入 | 属于 aiops/堡垒机,不是本地 SSH 客户端职责 | | podexec/K8s 专用工具 | 不引入 | `kubectl/helm` 可沿远程 SSH 工具使用,不引入第二套目标和凭据边界 | diff --git a/docs/architecture.md b/docs/architecture.md index 1d97c1d..1711286 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -68,7 +68,7 @@ XTerm 当前焦点分屏 -> AiService (OpenAI Chat / Responses / Anthropic) -> 下一条唯一单行命令 / FINAL 结论 -> 五级风险;critical 硬拒绝 - -> 展示目标、命令和风险,用户确认 + -> 按本次运行锁定的安全模式决定是否确认 -> 当前 connectionId 的 ssh_exec -> stdout/stderr/exit_code + 本地 analyze_output -> 真实观察脱敏后交给 AI 判断下一步 @@ -83,8 +83,8 @@ XTerm 当前焦点分屏 - AI 对话、提示词、终端上下文、命令和工具输出只存在当前面板内存中;关闭面板即清空。 - 新 Provider 的测试命令接收临时 API Key 并发出最小真实完成请求;测试过程不调用凭据存储。只有当前连接字段快照测试成功,界面才允许保存。 - 模型返回内容按不可信文本处理;终端输出也标记为不可信,其中的指令不能覆盖 system Prompt。 -- Agent 只接受唯一单行命令,目标固定为当前活跃 SSH connection;每次调用 `ssh_exec` 前都显示目标、命令和风险确认。 -- 命令风险为 safe/low/medium/high/critical 五级;格式化磁盘、删除根目录等 critical 操作硬拒绝。当前不引入会静默执行的“宽松模式”。 +- Agent 只接受唯一单行命令,目标固定为当前活跃 SSH connection;本次运行开始后安全模式锁定,确认框必须显示目标、命令、风险和锁定模式。 +- 命令风险为 safe/low/medium/high/critical 五级。严格模式从 medium 起确认;默认均衡模式从 high 起确认;宽松模式仅适合隔离测试环境。格式化磁盘、删除根目录等 critical 操作在任何模式下都硬拒绝。 - 相同命令第 3 次在执行前拦截;连续失败时要求更换排查路径。长输出先经本地 `analyze_output` 保留错误行和首尾。 - 不创建独立 Kubernetes 客户端或 podexec。集群运维命令沿当前 SSH 会话执行,避免引入本机 kubeconfig 与第二套目标/凭据边界。 diff --git a/docs/release.md b/docs/release.md index 4a8a564..b82de3a 100644 --- a/docs/release.md +++ b/docs/release.md @@ -9,14 +9,14 @@ # macOS Apple Silicon 指定版本 ./build.sh v2.1.1 mac-arm64 -# Windows x86_64(在 Windows Git Bash 中执行) -./build.sh v2.1.1 win-x64 +# Windows x86_64(推荐在 Windows CMD/PowerShell 中执行) +build.bat v2.1.1 win-x64 # 本地重复构建时缩短门禁;正式发布不要跳过 ./build.sh v2.1.1 mac-arm64 --quick ``` -脚本面向 v2.1.x,会检查 Node、npm、Rust target 和五处版本元数据。显式传入不同于当前源码的新版本时,脚本会同步版本文件;它只负责构建,不会自动创建 tag 或推送远程仓库。安装包和 `SHA256SUMS.txt` 输出到: +`build.sh` 与 `build.bat` 面向 v2.1.x,会检查 Node、npm、Rust target 和五处版本元数据。显式传入不同于当前源码的新版本时,脚本会同步版本文件;它们只负责构建,不会自动创建 tag 或推送远程仓库。安装包和 `SHA256SUMS.txt` 输出到: ```text release/v2.1.1/macos-arm64/ @@ -25,6 +25,27 @@ release/v2.1.1/windows-x64/ 平台安装包必须在目标系统构建和验证。macOS 不能替代 Windows x86_64 实机打包。 +### Windows 构建环境 + +建议使用 Windows 10/11 x64,并安装: + +- Node.js 18+ 与 npm +- Rust stable 的 `x86_64-pc-windows-msvc` toolchain +- Visual Studio 2022 Build Tools,勾选 **Desktop development with C++** +- Windows 10/11 SDK +- Microsoft Edge WebView2 Runtime + +首次准备: + +```bat +rustup default stable-x86_64-pc-windows-msvc +rustup target add x86_64-pc-windows-msvc +npm ci +build.bat v2.1.1 win-x64 +``` + +默认执行完整门禁;`--quick` 适合开发期重复构建,正式发布不要使用 `--skip-checks`。输出位于 `release\v2.1.1\windows-x64\`。没有代码签名证书时可以生成开发安装包,但 Windows SmartScreen 可能提示未知发布者。 + ## Gitee Release 构建、提交并推送对应 tag 后执行: diff --git a/docs/testing-acceptance.md b/docs/testing-acceptance.md index b65ef61..5941663 100644 --- a/docs/testing-acceptance.md +++ b/docs/testing-acceptance.md @@ -36,7 +36,7 @@ npm run acceptance:full - API Key 只进入 Rust secrets,不出现在 `ai-providers-v1`、日志或 WebView 返回值中 - Agent CLI 单任务入口、回车发送、选区上下文、本地脱敏、单行命令协议和最终结论 - 新 Provider 测试前不落盘,真实测试成功后才启用保存;任一连接字段变化会使验证快照失效 -- Agent 固定当前 SSH connection,每步确认前不得调用 `ssh_exec`,退出码与真实输出反馈下一轮且不进入历史 +- Agent 固定当前 SSH connection;达到当前安全模式确认阈值时,确认前不得调用 `ssh_exec`;退出码与真实输出反馈下一轮且不进入历史 - Agent 五级命令风险、critical 硬拒绝、连续失败换路、相同命令循环拦截和长输出 `analyze_output` - 远程 Shell EOF/TMOUT 事件不得误判为整个 SSH 连接断开;只为对应分屏提供一键重开 - macOS 原生“编辑”菜单以及所有输入框的复制、剪切、粘贴、全选、撤销;应用快捷键不得覆盖这些系统操作 @@ -58,7 +58,7 @@ npm run acceptance:full 8. models.dev 三个源全部失败时不影响已保存 Provider;部分成功时仍能生成可导入目录。 9. 新增 Provider 后测试前“保存”禁用;测试实际发送“你是什么模型?请简短回答。”,期间 settings 与 secrets 均无写入;只有非空成功回答显示 HTTP 200,随后保存才写入加密 Key。 10. 切换 Endpoint、模型、协议或 API Key 后“保存”立即重新禁用。 -11. 每一轮只接受唯一单行命令;确认对话框打开前和用户取消后 `ssh_exec` 调用次数必须为 0。 +11. 每一轮只接受唯一单行命令;默认均衡模式下 safe/low/medium 自动执行、high 先确认;严格模式从 medium 起确认;宽松模式不允许绕过 critical 硬拒绝。需要确认时,对话框打开前和用户取消后 `ssh_exec` 调用次数必须为 0。 12. 确认后只向当前 connectionId 执行;真实输出和 exit code 必须交给下一轮,直到 FINAL 或 8 步上限。 13. `rm -rf /`、`mkfs`、对磁盘覆盖的 `dd` 等 critical 命令必须在确认对话框前硬拒绝。 14. 相同命令第 3 次必须在执行前拦截;连续失败的下一轮 Prompt 必须要求更换路径。 diff --git a/docs/user-guide.md b/docs/user-guide.md index 2a55d59..ce29fc4 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -74,10 +74,12 @@ SFTP 使用双栏文件管理器。上传和下载先写入 `.opsx-part` 临时 - `Cmd/Ctrl + D`:分屏当前终端 - `Cmd/Ctrl + W`:关闭当前标签 +- 终端右键菜单提供复制、粘贴、全选、清屏、向右/向下分屏、最大化/恢复和关闭当前分屏;没有选区时“复制”保持禁用,不会一次右键就直接粘贴。 +- “广播”作用于所有已打开且已连接的 SSH 终端标签和分屏,按钮显示实时目标数;本地 Shell 不加入远程广播。目标不足两个时广播自动关闭。 - 会话侧栏进入批量模式后,可同时打开终端、SFTP,或在多个主机执行同一条命令 - 如果远端 Bash 因 `TMOUT` 显示 `timed out waiting for input: auto-logout`,OpsXShell 会标记只有该 Shell/分屏退出。SSH 连接、其他分屏、SFTP 和隧道仍可用,点击“重新打开 Shell”即可继续。 -批量命令结果只保留在当前弹窗。OpsXShell 不持久化终端命令历史。 +批量命令弹框可以再次搜索、全选或调整主机,执行区逐主机显示连接/运行状态、输出、耗时与总进度。结果只保留在当前弹窗;OpsXShell 不持久化终端命令历史。 ## 本地终端与串口 @@ -99,7 +101,7 @@ SFTP 使用双栏文件管理器。上传和下载先写入 `.opsx-part` 临时 - 直接输入任务,例如“排查 nginx 为什么启动失败并修复”。回车发送,`Shift+回车` 换行;不需要先选“解释 / 生成命令 / 排障 / 执行”。 - 有终端选区时会作为初始上下文;没有选区时不抓取整屏,Agent 通过真实 `ssh-exec` 自主取证。 -- Agent 最多执行 8 步。每步只能准备一条单行命令,界面会显示当前主机、命令和五级风险;你确认后才真实执行。 +- Agent 最多执行 8 步。每步只能准备一条单行命令,界面会显示当前主机、命令和五级风险。默认“均衡”模式仅高风险命令确认;“严格”从中风险开始确认;“宽松”仅保留 critical 硬拒绝,适合隔离测试环境。 - 格式化磁盘、删除根目录等 critical 命令会被硬拒绝,不提供确认放行。相同命令第 3 次会在执行前拦截。 - 执行后的真实输出和退出码会回给 AI,用于判断下一步或收敛结论。连续失败时 Agent 会被要求换方法。 - 长输出由本地 `analyze_output` 提取错误行并保留首尾;Rust 在发给云模型前做常见敏感信息脱敏。 diff --git a/docs/ux-foundation-audit.md b/docs/ux-foundation-audit.md new file mode 100644 index 0000000..7143a67 --- /dev/null +++ b/docs/ux-foundation-audit.md @@ -0,0 +1,173 @@ +# 产品体验与基础能力全面评估 + +> 更新:2026-07-31 +> 目标:OpsXShell 做成“麻雀虽小、五脏俱全”的轻量 SSH/SFTP 工具。常用操作直接、结果明确、失败可恢复,不用花哨概念增加学习成本。 + +## 评估结论 + +当前开发分支已具备 SSH、SFTP、串口、分屏、批量命令、SOCKS5、跳板和 AI Agent 的完整产品骨架,已经不是功能演示,但距离可长期替代 Tabby、Netcatty、Xshell 或 WindTerm 仍有一段可靠性和跨平台发布差距。 + +综合评分为 **7.1/10**。评分针对当前代码和自动化基线,不等于正式安装包质量;Windows 实机、macOS 安装版、断网/休眠和真实大文件传输未统一验收前,不能按成熟商业终端评价。 + +| 维度 | 分数 | 判断 | +| ------------------ | ---: | ---------------------------------------------------------------------------------- | +| 产品聚焦 | 8.5 | SSH/SFTP/代理/串口边界清楚,没有堡垒机、审计和 CMDB 噪音 | +| 首次连接与会话管理 | 7.8 | 快速连接和保存会话路径短,支持密码、私钥、代理和跳板 | +| 终端基础体验 | 7.2 | 复制粘贴、右键、分屏、广播和 Shell 生命周期已补齐;搜索和高压稳定性仍不足 | +| SFTP | 6.8 | 双栏、双向拖拽、上传下载、权限和远程编辑已具备;冲突、重试和任务恢复不足 | +| 隧道与 SOCKS5 | 7.5 | 规则与运行实例分离、真实启停和状态统计方向正确;重连和复用还需压测 | +| 批量运维 | 7.0 | 多主机选择、并发、逐机结果清楚;取消和独立超时未完成 | +| AI Agent | 6.8 | 单入口、当前 SSH 执行、风险模式和 ReAct 基线可用;执行内核仍在 React,真正取消不足 | +| 性能与可靠性 | 6.2 | Tauri/Rust 基础占优,但断网、休眠、大输出和频繁切换仍缺持续压测 | +| 跨平台发布 | 5.8 | macOS/Windows 构建脚本已具备;Windows 实机、签名、公证、更新和崩溃诊断未闭环 | +| 文档与自动化 | 7.8 | 有契约、组件、Rust 和真实探针分层;仍需把实机矩阵纳入每次候选发布 | + +## 产品主旨 + +OpsXShell 的默认界面只围绕七件事:连接主机、管理会话、操作终端、传输文件、开隧道、批量执行、必要时让 AI 在当前主机协助处理。 + +必须坚持: + +1. 高频动作一步可达,高级选项默认收起。 +2. 当前操作目标始终可见,尤其是广播、批量命令、SFTP 和 AI 执行。 +3. 成功、进行中、失败、断开是四种不同状态,不能只靠颜色或开发日志表达。 +4. 空闲操作不滥用确认框;删除、覆盖、关闭运行任务和高风险 AI 命令才确认。 +5. 不保存终端命令、广播输入、批量命令、AI 对话或工具输出。 +6. 不加入审计、录像、审批、CMDB、独立 podexec 或 Kubernetes 凭据。 + +## 大众操作习惯基线 + +### 鼠标与窗口 + +- 单击负责选择和聚焦,双击负责打开会话/目录或切换窗口最大化,不能让一次双击触发两次相反操作。 +- 右键必须显示与当前位置相关的菜单;普通区域不得出现 WebView 的 Reload/Inspect Element。 +- macOS 使用原生红黄绿窗口控制,Windows 使用系统最小化/最大化/关闭;标题栏空白区可拖动。 +- 拖拽必须显示源、目标和允许/禁止状态;文件落到目录行时传入该目录,落到空白区时传入当前目录。 + +### 键盘 + +| 动作 | macOS | Windows/Linux | +| -------------------- | -------------- | ------------------------------------------------- | +| 复制/粘贴终端 | `Cmd+C/V` | `Ctrl+Shift+C/V` | +| 普通输入框复制/粘贴 | `Cmd+C/V` | `Ctrl+C/V` | +| 新建会话 | `Cmd+N` | `Ctrl+N` | +| 快速连接 | `Cmd+K` | `Ctrl+K` | +| 关闭标签 | `Cmd+W` | `Ctrl+W` | +| 分屏 | `Cmd+D` | `Ctrl+D`,但必须避免与远端 EOF 语义冲突并允许改键 | +| 终端搜索 | `Cmd+F` | `Ctrl+F` | +| 开发工具(开发构建) | `Cmd+Option+I` | `F12` | + +- 快捷键不能抢占输入框的复制、粘贴、全选、撤销和输入法组合状态。 +- 菜单和提示必须显示当前平台实际快捷键,不能同时展示两套组合键让用户猜。 +- `Escape` 关闭当前最上层弹框/菜单;`Enter` 触发主动作,文本域需要明确换行组合键。 + +### 表单 + +- SSH 目标允许直接粘贴 `user@host:port`,端口默认 22。 +- 保存会话只展示“密码/密钥”两个常用认证入口;代理、跳板、标签和启动命令放在高级区。 +- 端口、路径和 Endpoint 在失焦或提交时给出就地错误,不能无提示恢复默认值。 +- 取消或重新打开快速连接、批量命令、AI 面板时,敏感输入必须清空。 +- 按钮文案说明结果,如“连接”“创建并保存”“执行到 3 台”,避免只显示含义不明的图标。 + +### 状态与错误 + +- 错误必须包含“什么失败、作用于谁、如何恢复”,例如“Host A 命令超时,可重试”,而不是 `unknown`。 +- 剪贴板失败、广播目标变为 0、Shell 已退出、隧道端口占用和 SFTP 冲突必须在界面提示。 +- 重试不能重复创建连接、Shell、SFTP 通道或 Listener;状态必须以 Rust 真实资源为准。 +- 加载超过 300ms 显示进行中,超过设置超时显示具体阶段;不能无限转圈。 + +## 核心用户旅程 + +| 旅程 | 目标操作长度 | 当前状态 | 验收标准 | +| ----------- | ----------------------------: | -------------- | ------------------------------------------------------- | +| 临时连接 | 快捷键 → 粘贴 → 回车 | 已实现 | `user@host:port` 可粘贴;取消重开后密码为空 | +| 保存会话 | 新建 → 目标/凭据 → 保存 | 已实现 | 默认 22;常用项不超过一屏;错误就地显示 | +| 重复连接 | 双击侧栏会话 | 已实现 | 单击只选择,双击只打开一个终端标签 | +| 终端操作 | 右键或快捷键 | 已实现 | 复制、粘贴、全选、清屏、分屏、最大化、关闭均明确 | +| 多终端广播 | 开广播 → 输入 | 已实现,待增强 | 覆盖所有打开 SSH Shell,显示目标数,本地 Shell 永不参与 | +| 批量命令 | 选主机 → 命令 → 执行 | 已实现,待增强 | 最多并发 5,逐机结果和耗时;关闭后命令销毁 | +| SFTP | 会话右键 → SFTP 文件 | 已实现,待实机 | 本地↔远程拖拽、按钮和右键都走明确 upload/download 方向 | +| SOCKS5 | 会话 → 转发 → 启动 | 已实现,待压测 | 默认 `127.0.0.1:1080`;停止后真实端口不可连接 | +| 串口 | 快速启动 → 串口 → 参数 → 连接 | 已实现,待实机 | 可枚举设备,默认常用参数,断开和重连状态明确 | +| AI 处理任务 | 当前 SSH → AI → 描述 → 执行 | 实验可用 | 目标固定、安全模式明确、critical 硬拒、关闭后上下文销毁 | + +## 当前已完成的基础修正 + +- 全局广播覆盖所有打开且已连接的 SSH 标签与分屏,排除本地 Shell;目标不足两个时自动关闭。 +- 终端右键菜单包含复制、粘贴、全选、清屏、向右/向下分屏、最大化/恢复和关闭。 +- 关闭分屏会调用 Rust `ssh_close_shell`,远端关闭失败会记录明确错误,不只移除前端格子。 +- 远端 Shell 的 EOF、`TMOUT` 和 `Ctrl+D` 不再等同整个 SSH 连接断开,可只重开当前 Shell。 +- 批量命令支持搜索、全选当前结果、调整目标、最大并发 5、逐机状态/输出/耗时和总进度。 +- 快速连接和批量命令重新打开会清空敏感输入,结果不落盘。 +- Agent 支持严格、均衡、宽松三级安全模式;默认均衡只确认 high,critical 永久硬拒绝。 +- 普通 WebView 区域禁止 Reload/Inspect Element,文本输入保留系统编辑菜单。 +- 应用版本由 `package.json` 构建时注入,不再在状态栏硬编码旧版本。 + +## P0:下一候选版本必须完成 + +| ID | 优化项 | 当前缺口 | 完成标准 | +| ----- | --------------------- | -------------------------------------------------- | ---------------------------------------------------------------- | +| P0-01 | 连接生命周期压测 | 断网、睡眠唤醒、快速切标签和连接复用证据不足 | 自动覆盖连接/重连/挂起/恢复;100 次切换无重复连接和监听器泄漏 | +| P0-02 | 终端搜索接通 | `SearchAddon` 已加载,但分屏工作区没有可用搜索 UI | `Cmd/Ctrl+F` 搜索当前焦点分屏,支持上一项/下一项/关闭 | +| P0-03 | 终端高输出稳定性 | 仍出现过 CPU 高占用和操作卡顿 | 持续输出 10 分钟 UI 可操作;后台标签限流;CPU/内存有基准阈值 | +| P0-04 | 用户可见 I/O 错误 | 部分剪贴板、关闭 Shell 和事件错误只写日志 | toast/覆盖层说明失败对象和恢复动作;不得产生未处理 Promise | +| P0-05 | SFTP 基础传输实机门禁 | 自动测试有方向契约和探针,但真实安装版仍需重复验证 | 上传、下载、覆盖、目录落点、拖拽、取消后临时文件处理全部通过 | +| P0-06 | SFTP 冲突规则 | 同名文件处理不够显式 | 每次任务可选择覆盖/跳过/重命名;目录递归不静默覆盖 | +| P0-07 | 批量任务超时与取消 | 当前只能等待 worker 完成 | 区分连接/命令超时;停止后不再启动新主机,当前命令标为等待完成 | +| P0-08 | 隧道重连与清理 | Listener 真实启停已做,网络恢复策略不足 | 主连接断开立即清理;重连后按用户选择恢复;端口冲突显示占用地址 | +| P0-09 | 最小窗口适配 | 960×720 基础可用,AI 打开后终端区域偏窄 | 960×640 无遮挡;打开 AI 时可自动收起侧栏或提供一键扩展 | +| P0-10 | 双平台安装验收 | macOS 结果不能代表 Windows | macOS arm64 与 Windows x86_64 分别完成安装、升级、卸载和核心冒烟 | + +## P1:提升日常效率 + +| ID | 优化项 | 建议 | +| ----- | --------------- | ----------------------------------------------------------------------------- | +| P1-01 | 广播目标预览 | 点击目标数查看主机/分屏,允许本次临时排除;默认仍是全部打开 SSH | +| P1-02 | 批量失败重试 | 一键仅重试失败主机,可从分组/标签建立目标集合 | +| P1-03 | 终端标题与路径 | 安全解析 OSC 标题/当前目录,允许锁定会话名,防止远端伪装安全提示 | +| P1-04 | 关闭保护 | 仅当前台有运行任务时提示关闭会结束远端 Shell;空闲时一步关闭 | +| P1-05 | SFTP 任务中心 | 统一暂停/继续/取消/重试,显示速度、剩余时间和失败原因;重启后可恢复未完成任务 | +| P1-06 | SFTP 易用性 | 多选拖拽、复制路径、在终端打开目录、远程编辑冲突提示、校验和验证 | +| P1-07 | SSH Config 导入 | 展示将导入字段、冲突处理和不可支持项;支持 ProxyJump 链预览 | +| P1-08 | 隧道便捷入口 | 状态栏一键查看/复制 SOCKS5 地址;复制规则永不复制运行态 | +| P1-09 | Agent Runtime | 从 React 抽离状态机,增加单步超时、真正取消、结构化 tool calling 和 eval | +| P1-10 | 无障碍 | 所有图标按钮有名称;焦点圈清楚;弹框焦点锁定/返回;状态不只靠颜色 | + +## P2:专业兼容与发布闭环 + +- Agent Forwarding、OpenSSH 证书、FIDO2 和更完整的算法策略。 +- 保存常用分屏布局,但不保存 Shell ID、命令或终端输出。 +- 隧道使用明确的共享连接/Multiplexer 策略,避免每个请求重新握手。 +- macOS Developer ID 签名与公证、Windows Authenticode、自动更新清单和回滚。 +- 用户主动导出的脱敏诊断包:版本、平台、错误码、资源统计,不包含命令、密码和终端输出。 +- 可选主题和键位导入;不为了生态而引入重量级插件框架。 + +## 明确不做 + +- 不恢复命令历史,不持久化广播、批量命令或 Agent 输入输出。 +- 不增加堡垒机审计、审批流、录像、CMDB、告警大盘或独立 podexec。 +- 不使用“有选区右键复制、无选区右键直接粘贴”这类隐藏行为。 +- 不让本地 Shell 加入远程广播,不允许 `ssh://` 携带密码或静默连接。 +- 不为“专业感”堆无用途状态卡、动画、统计图和多级配置。 + +## 统一回归场景 + +1. 快速连接粘贴 `root@opsbase.cn:55555`,取消后重开,目标和密码为空;通过 `ssh://` 唤起时只预填安全目标。 +2. 新建会话默认端口 22,密码/密钥切换不会把另一种凭据落盘;编辑高级配置不影响基础字段。 +3. 打开两个 SSH 标签,其中一个分两屏;广播输入 `date`,三个远端各收到一次,本地 Shell 不收到。 +4. 关闭一个分屏,Rust Shell 释放,广播目标立即减少,兄弟分屏、SFTP 和隧道保持可用。 +5. 终端右键复制、粘贴、全选、清屏、向右/向下分屏、最大化/恢复和关闭均可用,无 Reload/Inspect Element。 +6. `Cmd/Ctrl+F` 只搜索当前焦点分屏;切换分屏后结果随焦点切换。 +7. 批量选择两台执行 `date`,逐机显示耗时和结果;停止后不启动剩余主机;关闭重开命令为空。 +8. SFTP 按钮上传、右键上传、本地拖到远程、远程拖到本地结果一致;文件逐字节校验成功。 +9. 同名文件明确选择覆盖/跳过/重命名;失败不留下被误认为完整文件的目标。 +10. 启动 `127.0.0.1:1080` SOCKS5,代理请求成功且统计变化;停止后端口不可连接,重启应用显示未运行。 +11. 远端 `TMOUT` 只关闭当前 Shell,一键重开可用,SSH connection 和其他资源不被误断。 +12. Agent 均衡模式自动执行 `df -h`,`systemctl restart nginx` 先确认;严格模式修改文件先确认;宽松仍拒绝 `rm -rf /`。 +13. 断网后状态由“已连接”进入“重连中/失败”,恢复后不重复创建标签、连接、Shell 或 Listener。 +14. macOS 窗口可拖动,双击标题栏只切换一次最大化;Windows 系统按钮、快捷键和安装卸载符合平台习惯。 +15. 960×640、125%/150% Windows 缩放以及长主机名、中文分组下,无按钮遮挡、水平溢出或不可达操作。 + +## 发版判定 + +自动化全绿只代表代码门禁通过。候选发布还必须分别在 macOS arm64 和 Windows x86_64 完成上述核心旅程,并记录版本、安装包哈希、测试平台、失败项和回归结果。任何“只能偶现”“重试一般能好”的连接、终端输入、SFTP 或隧道问题都按未通过处理。 diff --git a/scripts/build-script.test.mjs b/scripts/build-script.test.mjs index 20787a1..1badbb3 100644 --- a/scripts/build-script.test.mjs +++ b/scripts/build-script.test.mjs @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' -import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' import test from 'node:test' import { dirname, resolve } from 'node:path' import { tmpdir } from 'node:os' @@ -20,6 +20,17 @@ test('build.sh is valid Bash and documents supported native targets', () => { assert.match(help.stdout, /不会创建或推送 Git tag/) }) +test('build.bat provides an equivalent native Windows x64 release path', () => { + const script = readFileSync(resolve(root, 'build.bat'), 'utf8') + assert.match(script, /x86_64-pc-windows-msvc/) + assert.match(script, /acceptance:full/) + assert.match(script, /release-preflight\.mjs windows/) + assert.match(script, /verify-release\.mjs windows/) + assert.match(script, /nsis\\\*\.exe/) + assert.match(script, /SHA256SUMS\.txt/) + assert.match(script, /不会创建或推送 Git tag/) +}) + test('release metadata versions stay synchronized', () => { const versions = Object.values(inspectVersions(root)) assert.ok(versions.length > 0) diff --git a/shared/types/ipc.types.ts b/shared/types/ipc.types.ts index 06732b7..0f5f0c1 100644 --- a/shared/types/ipc.types.ts +++ b/shared/types/ipc.types.ts @@ -218,6 +218,7 @@ export const IPC_CHANNELS = { SSH_CREATE_SHELL: 'ssh:createShell', SSH_SHELL_WRITE: 'ssh:shellWrite', SSH_SHELL_RESIZE: 'ssh:shellResize', + SSH_SHELL_CLOSE: 'ssh:shellClose', SSH_FORWARD_LOCAL: 'ssh:forwardLocal', SSH_FORWARD_REMOTE: 'ssh:forwardRemote', SSH_FORWARD_DYNAMIC: 'ssh:forwardDynamic', diff --git a/src-tauri/src/commands/ssh.rs b/src-tauri/src/commands/ssh.rs index cb7fbe7..6746d7b 100644 --- a/src-tauri/src/commands/ssh.rs +++ b/src-tauri/src/commands/ssh.rs @@ -121,6 +121,11 @@ pub fn ssh_shell_resize( operation(state.ssh.shell_resize(&shell_id, cols, rows)) } +#[tauri::command] +pub fn ssh_close_shell(state: State<'_, AppState>, shell_id: String) -> OperationResponse { + operation(state.ssh.close_shell(&shell_id)) +} + #[tauri::command] pub fn sftp_connect(state: State<'_, AppState>, connection_id: String) -> IdResponse { match state.ssh.create_sftp(&connection_id) { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 41f6cee..e65079c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -73,6 +73,7 @@ pub fn run() { commands::ssh::ssh_create_shell, commands::ssh::ssh_shell_write, commands::ssh::ssh_shell_resize, + commands::ssh::ssh_close_shell, commands::ssh::sftp_connect, commands::ssh::sftp_list_directory, commands::ssh::sftp_upload, diff --git a/src-tauri/src/services/ssh.rs b/src-tauri/src/services/ssh.rs index e19a93c..7989e71 100644 --- a/src-tauri/src/services/ssh.rs +++ b/src-tauri/src/services/ssh.rs @@ -212,6 +212,19 @@ impl SshService { .map_err(|error| format!("调整远程终端尺寸失败: {error}")) } + pub fn close_shell(&self, shell_id: &str) -> Result<(), String> { + let mut shell = self + .shells + .lock() + .map_err(lock_error)? + .remove(shell_id) + .ok_or_else(|| format!("远程 Shell 不存在: {shell_id}"))?; + let _ = shell.channel.send_eof(); + let _ = shell.channel.close(); + let _ = shell.session.disconnect(None, "shell closed by user", None); + Ok(()) + } + pub fn forward_local( &self, connection_id: &str, diff --git a/src/components/ai/AiTerminalPanel.tsx b/src/components/ai/AiTerminalPanel.tsx index 91b52ab..ff30fad 100644 --- a/src/components/ai/AiTerminalPanel.tsx +++ b/src/components/ai/AiTerminalPanel.tsx @@ -7,7 +7,9 @@ import { Play, Server, ShieldAlert, + ShieldCheck, Sparkles, + Square, TerminalSquare, X, } from 'lucide-react' @@ -15,8 +17,16 @@ import { useAiProviderStore } from '../../stores/aiProviderStore' import { extractBashBlocks, singleLineTerminalInsert, terminalContextLineCount } from '../../utils/aiResponse' import { writeClipboardText } from '../../platform/clipboard' import { analyzeAgentOutput } from '../../utils/agentOutput' -import { AGENT_RISK_LABEL, assessAgentCommandRisk, type AgentCommandRisk } from '../../utils/agentCommandRisk' +import { + AGENT_RISK_LABEL, + AGENT_SAFETY_MODES, + agentCommandNeedsConfirmation, + assessAgentCommandRisk, + type AgentCommandRisk, + type AgentSafetyMode, +} from '../../utils/agentCommandRisk' import { useConfirmDialog } from '../common/ConfirmDialog' +import { useSettingsStore } from '../../stores/settingsStore' export interface TerminalAiContext { selection: string @@ -43,6 +53,7 @@ interface AgentStep { const MAX_AGENT_STEPS = 8 const MAX_TOOL_OUTPUT_CHARS = 12_000 const MAX_AGENT_CONTEXT_CHARS = 48_000 +const SAFETY_MODE_ORDER: AgentSafetyMode[] = ['strict', 'balanced', 'permissive'] function truncateMiddle(value: string, limit: number): string { if (value.length <= limit) return value @@ -102,6 +113,8 @@ export default function AiTerminalPanel({ isLocal = false, }: AiTerminalPanelProps) { const store = useAiProviderStore() + const safetyMode = useSettingsStore((state) => state.settings.agentSafetyMode) + const setSetting = useSettingsStore((state) => state.setSetting) const [prompt, setPrompt] = useState('') const [result, setResult] = useState('') const [contextNote, setContextNote] = useState('仅在开始时读取当前选中内容') @@ -150,6 +163,7 @@ export default function AiTerminalPanel({ : '未附带终端内容 · Agent 将通过 ssh-exec 自主检查' ) setRunning(true) + const runSafetyMode = safetyMode const completedSteps: AgentStep[] = [] const commandCounts = new Map() @@ -204,21 +218,24 @@ export default function AiTerminalPanel({ if (risk === 'critical') { throw new Error(`命令命中破坏性操作硬红线,不提供确认放行:${command}`) } - const awaiting: AgentStep = { round, command, risk, status: 'awaiting' } - setSteps([...completedSteps, awaiting]) - const approved = await confirm({ - title: `第 ${round} 步:确认远程执行 · ${AGENT_RISK_LABEL[risk]}`, - message: `目标:SSH · ${targetLabel}\n风险:${AGENT_RISK_LABEL[risk]}\n\n命令:${command}\n\n执行结果将交给 AI 判断下一步;命令和输出不会写入历史。`, - danger: risk === 'high', - confirmText: risk === 'high' ? '确认风险并执行' : '执行', - }) - if (!approved) { - setSteps([...completedSteps]) - setResult(`任务已停止:你取消了第 ${round} 步命令。`) - return + const proposed: AgentStep = { round, command, risk, status: 'running' } + if (agentCommandNeedsConfirmation(risk, runSafetyMode)) { + const awaiting = { ...proposed, status: 'awaiting' as const } + setSteps([...completedSteps, awaiting]) + const approved = await confirm({ + title: `第 ${round} 步:确认远程执行 · ${AGENT_RISK_LABEL[risk]}`, + message: `目标:SSH · ${targetLabel}\n安全模式:${AGENT_SAFETY_MODES[runSafetyMode].label}\n风险:${AGENT_RISK_LABEL[risk]}\n\n命令:${command}\n\n执行结果将交给 AI 判断下一步;命令和输出不会写入历史。`, + danger: risk === 'high', + confirmText: risk === 'high' ? '确认风险并执行' : '执行', + }) + if (!approved) { + setSteps([...completedSteps]) + setResult(`任务已停止:你取消了第 ${round} 步命令。`) + return + } } - setSteps([...completedSteps, { ...awaiting, status: 'running' }]) + setSteps([...completedSteps, proposed]) const executed = await window.opsxApi.sshExec(connectionId, command) if (runIdRef.current !== runId) return const rawOutput = executed.success @@ -259,8 +276,14 @@ export default function AiTerminalPanel({ window.setTimeout(() => setCopied(false), 1200) } + const stopAgent = () => { + runIdRef.current += 1 + setRunning(false) + setResult('任务已停止;正在执行的单条 SSH 命令不会被强制中断,但不会再继续下一步。') + } + return ( -