dsh-ssh-workbench 1.6.4 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,55 @@
2
2
 
3
3
  记录 `dsh-ssh-workbench` 的重要变更。版本号遵循语义化版本。
4
4
 
5
- > 产品对外名称:**SSH Workbench / 远程工作台**(v1.6.4 起统一)。包名 `dsh-ssh-workbench`、模型工具 `ssh_workbench_target`、数据目录 `~/.dsh/ssh-workbench`。历史条目中的旧标识已统一为当前名称,便于检索。
5
+ > 产品对外名称:**SSH Workbench / 远程工作台**(v1.6.4 起统一)。包名 `dsh-ssh-workbench`、数据目录 `~/.dsh/ssh-workbench`。历史条目中的旧标识已统一为当前名称,便于检索。
6
+
7
+ ## [1.7.0] - 2026-11-13
8
+
9
+ 以 1.6.4 为基线的一个中版本:远程执行改为「每次调用显式带目标」,并补齐远程文件工具、多资源并行与执行指示。
10
+
11
+ ### 执行模型:每个远程调用显式带目标
12
+
13
+ - 移除会话级 active target、`ssh_workbench_target` 工具和原生 `bash` 的远程拦截。原生 `bash` / `pwsh` 现在始终只在本机运行;远程工作只有一条路:显式指定 `server`。
14
+ - 工具矩阵统一为 `remote_bash({ server, command })`(POSIX 目标)、`remote_pwsh({ server, command })`(Windows 目标)、`remote_exec({ tasks: [{ server, command }] })`(多目标并行);两种宿主都注册全部远程工具。
15
+ - `@资源名` 不再改变执行状态,只作为本轮目标提示进入上下文;后续无 `@` 的追问不会把命令静默带走。切回本机不需要任何「切换目标」动作。
16
+ - 每轮上下文始终注入纳管资源清单(名称 / 平台 / 状态)与显式 server 工具规则;无 `@` 时模型也能直接选目标,并明确禁止读取 `servers.json` 猜配置。
17
+ - 删除旧的 `TARGET_NOT_SELECTED`、target store replay/POST `/target`、native bash router 及其测试。
18
+
19
+ ### 远程文件工具
20
+
21
+ - 新增 `remote_read`、`remote_glob`、`remote_grep`:远程文件读取/查找/搜索,全部显式 `server`,通过 SFTP(read/glob)或目标方言命令(grep)执行,并受结果/输出上限约束。
22
+ - 新增 `remote_write`、`remote_edit`:默认关闭,需在配置中显式设置 `remoteFileWrite: true`;`remote_edit` 默认要求 oldText 唯一匹配,可用 `replaceAll: true` 覆盖。
23
+ - 原生 `read`/`write`/`edit`/`glob`/`grep` 仍只作用于本地;远程文件能力是显式的 `remote_*` 工具,不做 SSHFS 或透明重定向。
24
+ - 新增配置:`remoteFileReadBytes`(262144)、`remoteFileEditBytes`(2097152)、`remoteFileWrite`(false)、`remoteGlobMaxResults`(200)、`remoteGlobMaxDepth`(4)、`remoteGrepMaxResults`(200)。
25
+
26
+ ### 多资源并行
27
+
28
+ - `remote_exec({ tasks: [{ server, command, workdir?, timeoutMs? }], concurrency? })` 一次调用跨多台服务器并行采集;不读取也不改变任何会话目标状态。
29
+ - 全量预检后再执行:任一 task 的目标/命令/目录/超时非法时整批拒绝,不会执行一半;单任务失败、传输错误或超时只影响该任务,结果按输入顺序返回。
30
+ - 声明 `isConcurrencySafe: true`,由 DSH 工具批处理调度并行;工具内部再按 `batchConcurrency`(默认 4,上限 8)跨服务器并发,并受单连接 `maxChannelsPerServer` 约束。
31
+ - 配置新增 `maxBatchTasks`、`batchConcurrency`、`maxBatchOutputBytes`;输出预算按任务均分,避免一次 fan-out 撑爆上下文。
32
+
33
+ ### 状态查询性能
34
+
35
+ - `remote_exec` 新增 `probe:false`:跳过方言探测和 `timeoutGuard` 能力探测,直接用 `facts.dialect` / 记录平台判断。
36
+ - 新增 `connectTimeoutMs`:按调用限制 SSH 建连时间,一路透传到 broker 的 `readyTimeout`;状态类批量查询可用 `probe:false + connectTimeoutMs: 4000`,避免不可达目标拖满 15–30 秒。
37
+
38
+ ### 执行指示(composer pill)
39
+
40
+ - 底部 pill 是执行账本的只读投影,结构对齐 DSH Token 用量卡:标题「远程执行」+ 总次数、分隔线、目标名 → 次数;不显示成功/失败或用时,不承担切换目标或打开工作台。
41
+ - 远程调用进行中显示实时目标:单目标 `host165 · 执行中`,多目标 `2 个目标 · 执行中`;结束后回退为本会话次数(`远程执行 · N 次`)。1.5 秒轮询。
42
+ - 通过 around-dispatch 的 `tools/execute` 钩子维护内存 in-flight 记录,覆盖全部 `remote_*` 工具;不恢复 active target,不改变执行语义。
43
+
44
+ ### 安全
45
+
46
+ - 黑名单 guard 从工具参数解析显式 `server`;`remote_exec` 逐 task 校验,任一 task 命中则整批拒绝;方言事实缺失时同时跑 POSIX 与 PowerShell 规则。
47
+ - 原生 `bash` / `pwsh` 是本地工具,不再经过远程黑名单。
48
+
49
+ ### 整理
50
+
51
+ - 模块按当前职责重命名:`mentions.js`(@ 提示)、`posix-shell.js`(POSIX 命令构造)、`remote-shell-tool.js`(远程 shell 工具)、`managed-context.js`(每轮上下文注入)、`execution-stats.js`(执行统计)。
52
+ - 删除遗留死代码与过时注释,统一文档与代码中的执行模型表述。
53
+ - 测试 220/220,SSH/SFTP 集成测试通过。
6
54
 
7
55
  ## [1.6.4] - 2026-11-09
8
56
 
package/CONTRIBUTING.md CHANGED
@@ -18,12 +18,13 @@ npm run pack # 生成 dist/dsh-ssh-workbench-<version>.tgz
18
18
  ## 目录结构
19
19
 
20
20
  ```
21
- lib/index.js host 半:插件入口、Config、工具注册(ssh_workbench_target / remote_bash / remote_pwsh)
21
+ lib/index.js host 半:插件入口、Config、工具注册(remote_bash / remote_pwsh / remote_exec / remote_* 文件工具)
22
22
  lib/routes.js UI 数据面:资源、凭据、连接测试、SFTP、终端、偏好
23
23
  lib/client.js client 半:工作台、设置页、侧栏面板、工具卡片、i18n
24
- lib/core/ 传输与领域模块:broker/ssh/dialect/shell-adapters/remote-bash(-tool)/
25
- background/win32-adapter/gate(-pwsh)/guard/targets/server-reference/
26
- store/secrets/known-hosts/activity/terminals/localfs/routes 支撑模块
24
+ lib/core/ 传输与领域模块:broker/ssh/dialect/shell-adapters/posix-shell/
25
+ remote-shell-tool/remote-exec-tool/remote-file-tools/managed-context/
26
+ mentions/execution-stats/live-executions/background/win32-adapter/
27
+ gate(-pwsh)/guard/store/secrets/known-hosts/activity/terminals/localfs
27
28
  test/ 单元测试、集成测试、基准脚本、真机回归脚本
28
29
  docs/ 用户文档(docs/quickstart.md、docs/architecture.md)
29
30
  vendor/ 随包携带的纯 JS 依赖(ssh2 及 xterm 构建输入);版本/许可证见 vendor/NOTICE.md
package/README.md CHANGED
@@ -4,30 +4,37 @@ DeepSeek Harness 的**远程工作台**:SSH 终端、本地与远程文件、@
4
4
 
5
5
  不是又一个 SSH 客户端:它把远程执行接进 DSH 的对话与审批,把终端、SFTP 文件管理和本地文件放在同一个工作台里;安装包自包含、开箱即用。
6
6
 
7
- 插件不改 DSH 的本地工具语义;左侧导航提供一个独立的「远程工作台」,把终端与文件管理放在一起(本地和远程都能用)。对话里 `@资源名` 之后,命令在目标上执行:POSIX 宿主复用原生 `bash`(结果仍是原生终端卡片);Windows 宿主没有内置 `bash`,改用插件自有的 `remote_bash`。Windows 目标(PowerShell)在两种宿主上都经 `remote_pwsh` 执行。原生 `bash` / `pwsh` 始终只在本机生效。没有 `@` 时一切行为不变。模型侧新增的工具是 `ssh_workbench_target`(切换目标)和按需出现的 `remote_bash` / `remote_pwsh`。另外保留一道**危险命令黑名单**作为兜底。
7
+ 插件不改 DSH 的本地工具语义;左侧导航提供一个独立的「远程工作台」,把终端与文件管理放在一起(本地和远程都能用)。**所有远程执行都显式带目标**:原生 `bash` / `pwsh` 只在本机生效;对话里 `@资源名` 只是告诉模型“这轮可能用哪台”,模型实际调用 `remote_bash({ server, command })`、`remote_pwsh({ server, command })`、`remote_exec({ tasks })`,必要时用 `remote_read` / `remote_glob` / `remote_grep` 查远程文件。没有会话级 active target,也没有“切回本地”的状态残留:多轮对话里目标不会静默漂移。另外保留一道**危险命令黑名单**作为兜底。
8
8
 
9
9
  - 适用:DSH 0.2.1-rc.1 ~ 0.2.x(`engines.dsh: >=0.2.1-rc.1 <0.3.0`)
10
10
  - 平台:macOS / Linux / Windows 上的 DSH;远程目标支持 POSIX 与 Windows(PowerShell)
11
11
 
12
+ ![远程工作台:远程 SSH 终端与 SFTP 文件管理](docs/images/remote-workbench.png)
13
+
14
+ ![在对话里 @资源 执行,结果直接回到会话](docs/images/chat-at-resource.png)
15
+
12
16
  ## 能力
13
17
 
14
18
  | 能力 | 说明 |
15
19
  |---|---|
16
20
  | 远程资源纳管 | 右侧栏/设置页:增删改查、连接测试、host key 指纹确认、密码/私钥凭据(0600 存储);连接测试/选中目标后会采集 hostname 等信息(hostname 仅用于目标上下文;内核/磁盘只存储,不在列表展示) |
17
- | `@资源` 绑定 | 消息里 `@web-1` 后,命令在 web-1 上执行(资源名支持中文/非 ASCII);`@` 多个资源时需用 `ssh_workbench_target` 选择,未选择前远程工作台会报 `TARGET_NOT_SELECTED` 而不是静默走本地 |
18
- | Windows 宿主 | Windows 上的 DSH 没有原生 `bash`(DSH 按平台禁用):插件注册 `remote_bash` 用于 POSIX 目标,参数与行为对齐原生 bash 路由;原生 `pwsh` 仍只作用于本机 |
21
+ | `@资源` 提示 | 消息里 `@web-1` 会让模型在工具调用里带上 `server: "web-1"`(资源名支持中文/非 ASCII);`@` 多个资源时用 `remote_exec` 一次并行执行。`@` 只是本轮的提示,不是全局 active target;模型仍可按上下文显式选择其他资源 |
22
+ | 执行目标 | 每个远程工具调用自己带 `server`:`remote_bash({ server, command })`、`remote_pwsh({ server, command })`、`remote_exec({ tasks: [{ server, command }] })`。不存在“当前目标”,因此切回本机只是改调原生 `bash` / `pwsh`,不需要 `ssh_workbench_target`,也不会把本地命令误发到远程 |
23
+ | Windows 宿主 | Windows 上的 DSH 没有原生 `bash`(DSH 按平台禁用):插件在两种宿主上都注册 `remote_bash` / `remote_pwsh` / `remote_exec`,所以 Windows 宿主也能操作 POSIX 目标;原生 `pwsh` 仍只作用于本机 |
19
24
  | Windows 目标 | Windows 目标经插件自有的 `remote_pwsh` 执行:命令包成 UTF-16LE 的 `-EncodedCommand` 脚本(避免引号转义),强制 UTF-8 输出并回传真实退出码;PowerShell 危险命令另有黑名单 |
20
25
  | 方言探测 | 目标实际是 POSIX 还是 Windows 由连接后的 `uname -s` / `cmd /c ver` 探测并写入 `facts.dialect`,不依赖手工填写的 `platform`;探测结果用于选择 `remote_bash` / `remote_pwsh`、超时包装和目标上下文文案 |
26
+ | 远程文件工具 | `remote_read` / `remote_glob` / `remote_grep` 直接操作远程文件(显式 `server`);`remote_write` / `remote_edit` 默认关闭,需开启 `remoteFileWrite`。原生 `read`/`write`/`edit`/`glob`/`grep` 仍只作用于本地 |
27
+ | `remote_exec` 多资源并行 | 一次调用传 `tasks: [{ server, command, workdir?, timeoutMs? }]`;每个任务**显式指定目标**,按各自方言选择 POSIX/Windows 执行器,并发执行(默认 4,上限 8),逐任务超时、输出上限与错误隔离;不依赖也不修改任何会话目标状态。仅前台执行,长任务仍用后台工具 |
21
28
  | `remote_bash` / `remote_pwsh` | `command`、`workdir`(绝对路径 / `~/x` / 相对路径)、`timeoutMs`、取消沿用同一套语义;POSIX 目标超时后远端命令组会被终止(远端有 `timeout` 时含 `-k` 补 KILL),Windows 目标只能客户端停止等待 |
22
- | 结果输出 | POSIX 宿主上的 POSIX 目标仍是 DSH 原生 bash 终端卡片;`remote_bash` / `remote_pwsh` 返回带 target / cwd / 退出码 / stdout / stderr 的文本结果 |
29
+ | 结果输出 | 原生 `bash` / `pwsh` 保留 DSH 原生终端卡片;`remote_bash` / `remote_pwsh` / `remote_exec` 返回带 server / cwd / 退出码 / stdout / stderr 的文本结果 |
23
30
  | 后台任务 | `run_in_background: true` 在远端起**脱离会话**的进程:POSIX 用 `nohup` 重定向到 `/tmp` 日志,Windows 用 `Start-Process` + 日志文件;结果返回 pid、日志路径和读取/停止命令(`tail -f` / `kill`、`Get-Content -Wait` / `Stop-Process`)。它不注册到 DSH 本地 `job_*` 注册表,进程在工具调用返回后继续运行 |
24
31
  | 平台徽标 | 资源列表/设置页显示 **Linux / Windows** 徽标(来自连接后探测的 `facts.dialect`,不是手填字段);连接测试结果也显示探测到的平台 |
25
- | 目标切换 | `ssh_workbench_target({ server: "web-1" })`;`server: "local"` 回到本机;切换后首条命令的 stderr、bash 卡片 cwd 前缀和 composer 下方参数行都会标出当前目标 |
32
+ | 执行统计 | composer 下方的 pill 只读展示本会话的远程执行次数,结构对齐 DSH Token 用量弹窗:标题「远程执行」+ 总次数、分隔线、目标名 → 次数;没有执行时不显示。执行中 pill 会实时显示当前目标(单目标为 `host165 · 执行中`,多目标为 `2 个目标 · 执行中`),1.5 秒轮询;它不切换目标、不打开工作台,也不显示成功/失败或用时 |
26
33
  | 远程工作台 | 左侧导航的独立页面:左侧终端(本地终端 / 远程 SSH 终端),右侧文件管理(本地用户主目录 / 远程 SFTP,远程默认打开登录 home),文件可**在工作台内直接预览**,拖动分隔条调整宽度;每个页签一个终端会话,仅活动页签轮询、非活动页签暂停并在切回时续读(UI 能力,不占模型工具) |
27
34
  | 侧边栏开关 | 插件管理 → 远程工作台 → 页面里的「在侧边栏显示远程工作台」可关闭左侧导航入口;关闭后 `@资源名` 远程执行、设置页与模型工具不受影响 |
28
35
  | 危险命令黑名单 | `rm -rf /`、`mkfs`、`dd of=/dev/*`、`fdisk/parted/wipefs`、改根目录权限、fork bomb 等同步拦截;多行脚本、`&`、引号/转义、路径变体、`$()`/`eval` 都会先还原再判断;其他命令交给 DSH 审批 |
29
36
 
30
- **不做**:诊断包、HTTP 探测、批量执行、远程 `read/write/edit/glob/grep`(这些工具始终只作用于 DSH 主机;远程文件操作用对应的远程 shell 工具,如 `cat`、`tee`、`sed`、`Get-Content`)。也**不做**命令分级(allow/guarded)、AI 风险解释和审计。
37
+ **不做**:诊断包、HTTP 探测、跨主机文件分发、远程 IDE/SSHFS/透明代理(远程文件能力是显式的 `remote_*` 工具,不把本地 `read`/`write` 隐式重定向到远端)、命令分级(allow/guarded)、AI 风险解释和审计。
31
38
 
32
39
  ## 安装
33
40
 
@@ -42,7 +49,7 @@ dsh plugin --profile desktop add dsh-ssh-workbench
42
49
  ```bash
43
50
  npm test
44
51
  npm run pack
45
- # 然后在 DSH 的插件管理里安装 dist/dsh-ssh-workbench-1.6.4.tgz
52
+ # 然后在 DSH 的插件管理里安装 dist/dsh-ssh-workbench-1.7.0.tgz
46
53
  ```
47
54
 
48
55
  发布包是**自包含**的:SSH 传输依赖(纯 JS 的 ssh2 及其依赖)已放在 `vendor/` 里随包携带,安装时不会运行 `node-gyp`、不需要 Xcode Command Line Tools,也不会额外下载依赖;包体积因此略大(约 0.5 MB),换来任何机器开箱即装。组件与许可证见 `vendor/NOTICE.md`。
@@ -51,6 +58,8 @@ npm run pack
51
58
 
52
59
  安装后在 **设置 → 远程工作台** 里添加资源。
53
60
 
61
+ ![在 DSH 插件页中的远程工作台](docs/images/plugin.png)
62
+
54
63
  ## 用法
55
64
 
56
65
  1. 先说目标,再说要做什么:
@@ -59,29 +68,29 @@ npm run pack
59
68
  @web-1 看下磁盘和内存
60
69
  ```
61
70
 
62
- 模型会调用 `bash`(Windows 宿主上是 `remote_bash`;Windows 目标是 `remote_pwsh`),命令实际跑在目标上。`read` / `write` / `edit` / `glob` / `grep` 仍然作用于本机;要操作远程文件,直接用远程 shell 工具:
71
+ 模型会调用 `remote_bash({ server: "web-1", command })`(Windows 目标是 `remote_pwsh`),命令实际跑在目标上。`read` / `write` / `edit` / `glob` / `grep` 仍然作用于本机;要读写远程文件,模型会改用 `remote_read` / `remote_glob` / `remote_grep`(写入需开启 `remoteFileWrite`):
63
72
 
64
73
  ```
65
74
  @web-1 把 /etc/nginx/nginx.conf 里 worker_connections 改成 2048,改完 nginx -t
66
75
  ```
67
76
 
68
- 2. 同时提到多个资源时,模型需要先选择目标:
77
+ 2. 同时提到多个资源时,让模型用 `remote_exec` 一次并行执行,不必逐个切换:
69
78
 
70
79
  ```
71
80
  @web-1 @db-1 各自看下磁盘
72
- → 模型调用 ssh_workbench_target({ server: "web-1" }) → bash df -h
73
- → 模型调用 ssh_workbench_target({ server: "db-1" }) → bash df -h
81
+ → 模型调用 remote_exec({ tasks: [
82
+ { server: "web-1", command: "df -h" },
83
+ { server: "db-1", command: "df -h" }
84
+ ] })
74
85
  ```
75
86
 
76
- 3. 切回本机:
87
+ 每个 task 自带目标,命令在各自服务器上并发执行,结果按任务顺序返回到同一条工具卡片;不读取也不改变任何会话目标状态。composer 下方的 pill 只读展示本会话执行次数,点开是按目标列出的计数卡;不负责切换或打开工作台。
77
88
 
78
- ```
79
- ssh_workbench_target({ server: "local" })
80
- ```
89
+ 3. 切回本机:直接让模型用原生 `bash` / `pwsh`,不需要任何“切目标”动作;远程调用只有显式带 `server` 才会发出。
81
90
 
82
91
  ## 安全模型
83
92
 
84
- - **黑名单兜底**:远程 `bash` / `remote_bash` / `remote_pwsh` 执行前经过同步、单调的 `ctx.tools.guard()`,命中机器毁灭类命令直接拒绝;该拦截不受审批策略影响。POSIX 规则先按 shell 语义还原(去引号/转义、剥包装、路径规范化、`$()`/`eval` 递归)再匹配,PowerShell 规则覆盖 `Format-Volume`、`Clear-Disk`、`diskpart`、`bcdedit`、整盘递归删除等;两者都只是 best-effort。
93
+ - **黑名单兜底**:`remote_bash` / `remote_pwsh` / `remote_exec` 执行前经过同步、单调的 `ctx.tools.guard()`:每个显式 `server`(以及 `remote_exec` 的每个 task)都会按对应方言检查,命中机器毁灭类命令直接拒绝;该拦截不受审批策略影响。原生 `bash` / `pwsh` 是本地工具,不经过远程黑名单。POSIX 规则先按 shell 语义还原(去引号/转义、剥包装、路径规范化、`$()`/`eval` 递归)再匹配,PowerShell 规则覆盖 `Format-Volume`、`Clear-Disk`、`diskpart`、`bcdedit`、整盘递归删除等;两者都只是 best-effort。
85
94
  - **审批**:插件不做自己的 ask 清单,远程命令是否弹审批完全由 DSH 的会话预设(`ask` / `never`)决定。
86
95
  - **凭据**:密码存于 `~/.dsh/ssh-workbench/`(0600 文件 + 本机密钥),私钥只存路径;servers.json 0600。
87
96
  - **主机身份**:严格校验 known_hosts;未知指纹会显示 SHA256 并询问是否信任,由用户决定;信任后写入插件自己的 known_hosts。**如果服务器重装或更换导致指纹变化,连接会被拒绝并提示「服务器主机密钥已变化」**,核对后可在界面点「更新信任」。
@@ -92,18 +101,19 @@ ssh_workbench_target({ server: "local" })
92
101
 
93
102
  - **stdout/stderr 顺序**:两者是独立管道,收集后分开返回(与原生 bash 的值契约一致),交错顺序会丢失;看 `make`/`pip install` 这类日志时需要留意。
94
103
  - **Windows 本地路径**:工作台本地文件面板已按 Windows 分隔符处理面包屑与文件名;极少数外部工具生成的混合分隔符路径仍可能显示成一段,可在路径编辑框直接输入绝对路径。
104
+ - **`remote_exec` 仅前台**:不支持 `run_in_background`;一次最多 8 个 task、默认并发 4,所有结果共享 `maxBatchOutputBytes` 预算,超出会按任务截断。长时间任务仍应分别用后台工具或终端。
95
105
  - **后台任务**:`run_in_background` 返回的是远端 pid + 日志,不是 DSH 本地 job id;`timeoutMs` 和取消只影响启动调用,不作用于已脱离的进程。POSIX 的 `/tmp` 日志可能被系统清理(按 tmp 策略),重要输出请自行重定向到持久目录。
96
106
  - **超时终止**:远端有 `timeout` 时,超时会连同远端进程组一起终止(支持 `-k` 时再补一次 KILL,缓冲时间可用 `timeoutKillAfterMs` 调整);远端没有 `timeout` 时只能停止等待,stderr 会提示"命令可能仍在运行"。**取消(Esc)不会立刻杀远端**:通道关闭后远端仍会计时到自己的 deadline。
97
107
  - **stdin 恒为 EOF**:远程命令的 stdin 在启动后立即关闭(等价 `</dev/null`),`cat`/`read` 等读 stdin 的命令会直接得到 EOF。
98
- - **文件工具仍在本地**:`read`/`write`/`edit`/`glob`/`grep` 不受目标影响;远程文件请走对应的远程 shell 工具或右侧栏 SFTP 面板。`working_directory` 只影响本地工具,远程目录用远程 shell 工具的 `workdir` 参数。
99
- - **并发由 DSH 调度**:插件为单台服务器提供最多 6 条并发通道,但同一轮里多个 `bash` 调用是否并行由 DSH 的工具调度决定。
100
- - **目标标识**:DSH 的 bash 卡片是官方渲染且没有装饰插槽,插件不替换卡片;当前用“切换后首条命令 stderr 行 + cwd 前缀(`web-1:/var/log`)+ composer 下方参数行目标指示”标出目标。
108
+ - **文件工具分本地/远程**:原生 `read`/`write`/`edit`/`glob`/`grep` 只作用于本地;远程文件用显式的 `remote_read`/`remote_glob`/`remote_grep`,写入用 `remote_write`/`remote_edit`(默认关闭)。远程写入不是原子操作,修改配置前请备份;大文件/二进制仍建议走右侧栏 SFTP 面板。
109
+ - **并发由 DSH 调度**:`remote_exec` 显式声明并行安全,因此一次批量任务会真正并发(受 `batchConcurrency` 上限约束);原生 `bash` 与 `remote_bash` / `remote_pwsh` 仍是 exclusive 工具,同一轮里的多个 shell 调用按 DSH 调度串行。插件为单台服务器提供最多 6 条并发通道。
110
+ - **目标标识**:DSH 的工具卡片是官方渲染且没有装饰插槽,插件不替换卡片;远程调用的目标由工具参数(`server`)与结果里的 `target` / `cwd` 行体现,composer 下方的 pill 另提供本会话执行次数与执行中的实时目标。
101
111
  - **侧边栏开关**:开关存放在 `~/.dsh/ssh-workbench/prefs.json`,在插件管理里保存后当前窗口立即生效;关闭时左侧导航不再出现远程工作台入口,右侧栏的「远程工作台」「远程文件」页签保持不变。
102
112
  - **终端生命周期**:终端会话跟随工作台页面/页签,关闭页签或离开工作台会结束会话;页面刷新/关闭时通过 `sendBeacon` 尽力补发关闭,但进程崩溃或 Host 异常退出后仍依赖 30 分钟空闲回收(尚未做跨页面保活或重连恢复)。本地终端依赖 Host 的 PTY 服务,不可用时给出提示。
103
113
  - **本地文件边界**:工作台的本地文件面板与本地终端默认以**本地用户主目录**为根(`$HOME`,与常规 SSH 客户端的「本地」面板一致,面包屑显示真实路径),越界路径与指向外部的符号链接都会被拒绝;需要固定其他目录时用 `localRoot` 配置(绝对路径或 `~/...`),需要操作主目录以外的本地路径时用本地终端。远程文件仍走 SFTP,远程文件面板默认打开登录 home。
104
114
  - **终端渲染资源**:xterm 随客户端 bundle 内置(`vendor/xterm.js` 只是构建输入,不再有运行时脚本路由),不依赖额外脚本路由或 CSP 放行;首次打开工作台时加载。
105
115
  - **终端操作边界**:本地/远程终端是用户主动输入,不经过 agent 的审批与黑名单;SSH 权限边界不变。
106
- - **黑名单是 best-effort**:`bash -c`、base64、脚本文件、解释器 heredoc 正文不会被解析;heredoc 正文会按可执行文本检查(fail-closed,写脚本时可能误拦)。真正的边界是 SSH 最小权限 + host-key + DSH 审批。
116
+ - **黑名单是 best-effort**:`bash -c`、base64、脚本文件、解释器 heredoc 正文不会被解析;heredoc 正文会按可执行文本检查(fail-closed,写脚本时可能误拦)。`remote_exec` 会对每个 task 按自己目标方言逐条过 guard,但仍是 best-effort。真正的边界是 SSH 最小权限 + host-key + DSH 审批。
107
117
 
108
118
  ## 配置
109
119
 
@@ -116,6 +126,15 @@ ssh_workbench_target({ server: "local" })
116
126
  | `timeoutKillAfterMs` | `5000` | 远端 `timeout -k` 的宽限:TERM 后多久补 SIGKILL;0 = 只用 TERM |
117
127
  | `maxOutputBytes` | `102400` | stdout 保留上限(头尾保留,中间截断) |
118
128
  | `maxStderrBytes` | `16384` | stderr 独立保留上限(默认明显小于 stdout,保护上下文) |
129
+ | `maxBatchTasks` | `8` | 一次 `remote_exec` 最多任务数 |
130
+ | `batchConcurrency` | `4` | `remote_exec` 默认并发数;模型可请求更小值,不能超过此配置 |
131
+ | `maxBatchOutputBytes` | `262144` | 一次 `remote_exec` 所有任务共享的 stdout/stderr 总预算 |
132
+ | `remoteFileReadBytes` | `262144` | `remote_read` 单次读取上限(字节) |
133
+ | `remoteFileEditBytes` | `2097152` | `remote_write` / `remote_edit` 的文件上限(字节) |
134
+ | `remoteFileWrite` | `false` | 是否启用 `remote_write` / `remote_edit`;默认关闭 |
135
+ | `remoteGlobMaxResults` | `200` | `remote_glob` 结果上限 |
136
+ | `remoteGlobMaxDepth` | `4` | `remote_glob` 递归深度上限 |
137
+ | `remoteGrepMaxResults` | `200` | `remote_grep` 结果上限 |
119
138
  | `transferChunkTimeoutMs` | `30000` | SFTP 分块读写看门狗;卡死时关闭会话,UI 可从 offset 续传 |
120
139
  | `connectTimeoutMs` | `15000` | SSH 建连超时 |
121
140
  | `hostKeyMode` | `strict` | `strict` / `accept-new` |
@@ -124,7 +143,7 @@ ssh_workbench_target({ server: "local" })
124
143
  | `poolMaxServers` | `8` | 连接池最多缓存的目标数 |
125
144
  | `maxChannelsPerServer` | `6` | 单连接并发通道上限 |
126
145
  | `prewarm` | `true` | UI 选中服务器时预热连接 |
127
- | `targetContext` | `true` | 自动绑定 `@资源名` 并注入目标上下文;关掉后不自动绑定(仍可手动用 `ssh_workbench_target`) |
146
+ | `targetContext` | `true` | 每轮注入纳管资源清单与 `@资源名` 提示;关掉后不注入上下文(工具仍可显式调用) |
128
147
  | `localRoot` | `""`(空 = 用户主目录) | 工作台本地文件面板与本地终端的根目录;必须是绝对路径或 `~/...`,非法值回退用户主目录 |
129
148
 
130
149
  > 右侧栏的工作台开关不在 `cordis.patch.yml` 里,而是在 **插件管理 → 远程工作台** 页面里的「在侧边栏显示远程工作台」;它只影响 UI 入口,不影响模型工具。
@@ -148,7 +167,7 @@ npm run test:ssh:sftp # SFTP 集成(进程内 ssh2.Server)
148
167
  npm run pack # 打包到 dist/
149
168
  ```
150
169
 
151
- 更多文档:[快速上手](docs/quickstart.md) · [架构说明](docs/architecture.md) · [贡献指南](CONTRIBUTING.md) · [安全说明](SECURITY.md) · [更新日志](CHANGELOG.md)。
170
+ 更多文档:[快速上手](docs/quickstart.md) · [执行模型](docs/execution-model.md) · [架构说明](docs/architecture.md) · [贡献指南](CONTRIBUTING.md) · [安全说明](SECURITY.md) · [更新日志](CHANGELOG.md)。
152
171
 
153
172
  ## 参与贡献
154
173
 
package/SECURITY.md CHANGED
@@ -9,7 +9,9 @@
9
9
  - **凭据**:使用 AES-256-GCM 加密存储在 `$DSH_HOME/ssh-workbench/secrets.json`,密钥在 `secrets.key`(0600);纳管资源清单不存明文密码。
10
10
  - **主机指纹**:插件维护自己的 `known_hosts`,默认 `strict`(未知指纹拒绝);UI 显示 SHA256 指纹并询问是否信任,由用户决定;信任后保存。指纹变化时连接会被拒绝并提示「服务器主机密钥已变化」,用户核对后可显式「更新信任」。
11
11
  - **审批**:插件不是权限边界,只硬阻断机器级黑名单;其余远程命令是否弹审批完全由 DSH 会话预设决定(ask / never)。当前不提供命令分级、AI 风险解释或审计。
12
+ - **批量执行**:`remote_exec` 对每个 task 按其目标方言单独跑黑名单,任一 task 命中即整批拒绝;批量工具不依赖任何会话目标状态,但每个 task 都必须是已纳管的远程资源(`server: "local"` 被拒绝)。它仅前台执行,并受 `maxBatchTasks` / `batchConcurrency` / `maxBatchOutputBytes` 限制;这些限制不是安全边界。
12
13
  - **黑名单是 best-effort**:`;`、`&&`、`|`、单个 `&`、换行都会逐段检查;每段会先按 shell 语义还原(去掉引号/反斜杠、剥离 `sudo`/`timeout`/`env`/`!`/赋值等包装、路径词法规范化/glob 前缀、`$()`/反引号/`eval` 递归),所以 `"mkfs.ext4"`、`\rm -rf /`、`rm -rf /etc/`、`cat > "/dev/sda"`、`echo "$(mkfs...)"` 都会命中。但 `bash -c`、base64、脚本文件、解释器 heredoc 正文不会被解析,真正的安全边界仍是 SSH 最小权限 + host-key 固定 + 网络隔离 + DSH 审批。
14
+ - **远程文件工具**:`remote_read`/`remote_glob`/`remote_grep` 只是把显式 `server` 的 SFTP/远程搜索暴露给模型;`remote_write`/`remote_edit` 默认关闭,开启后仍以 SSH 账号权限直接写目标文件,不做原子替换、不做备份。读写路径必须绝对路径或 `~`,但仍然没有远端目录白名单;真正的边界是 SSH 最小权限、host-key 固定和 DSH 审批。
13
15
  - **本地↔远端传输**(右侧栏文件面板的 SFTP 上传/下载)以 DSH 宿主进程的 OS 权限读写本地文件;请把宿主账号视为可信。
14
16
  - **用户终端**:远程工作台的终端是用户主动输入,不经过 agent 的审批、沙箱或黑名单;远程终端使用同一份 SSH 凭据与主机校验,本地终端以 Host 进程权限运行。终端会话随页签/页面关闭而结束,并受空闲回收约束。
15
17
  - **本地文件管理**:工作台的本地文件浏览默认限制在宿主用户主目录内(插件配置 `localRoot` 可改为其他绝对路径),解析真实路径后拒绝越界与符号链接逃逸;它不是模型工具,也不暴露给 agent。
@@ -1,4 +1,4 @@
1
- # SSH Workbench(dsh-ssh-workbench)架构(v1.6.4)
1
+ # SSH Workbench(dsh-ssh-workbench)架构(v1.7.0)
2
2
 
3
3
  > 产品定位与用法见 [README.md](../README.md);本文只讲实现分层。
4
4
 
@@ -6,14 +6,19 @@
6
6
 
7
7
  ```
8
8
  lib/
9
- ├── index.js 插件入口:Config、apply、工具注册(ssh_workbench_target + 按宿主/方言出现的 remote_*)
9
+ ├── index.js 插件入口:Config、apply、工具注册(remote_exec + remote_bash + remote_pwsh + remote 文件工具)
10
10
  ├── routes.js UI 数据面:资源管理 / 连接测试 / SFTP 文件面板 / 侧边栏偏好
11
11
  ├── client.js Client 半片:左侧导航远程工作台、设置页、@ 引用 chip、右侧栏文件面板
12
12
  └── core/
13
- ├── targets.js 每会话目标绑定:@ 解析、切换、历史回放
14
- ├── server-reference.js agent/pre-step:绑定 + 注入目标上下文
15
- ├── remote-bash.js tools/execute:把原生 bash 路由到 SSH,映射原生输出值
16
- ├── remote-bash-tool.js 插件自有 remote_bash / remote_pwsh 工具(Windows 宿主与 Windows 目标)
13
+ ├── mentions.js 每会话 @ 提示:mention 解析、去重、历史恢复(不绑定执行目标)
14
+ ├── managed-context.js agent/pre-step:每轮注入资源清单与 @ 提示上下文
15
+ ├── posix-shell.js POSIX 远程 shell 辅助:workdir、超时包装
16
+ ├── remote-shell-tool.js 显式 server 的 remote_bash / remote_pwsh 工具
17
+ ├── remote-exec-tool.js 插件自有 remote_exec:显式目标、一轮 fan-out、并发池、逐任务 guard/超时/输出
18
+ ├── remote-file-tools.js 远程文件工具:remote_read/write/edit/glob/grep(SFTP + 方言 grep;写开关)
19
+ ├── execution-stats.js 按 Session 聚合 activity 中的远程执行次数(composer dock)
20
+ ├── live-executions.js 进行中的远程调用(内存 in-flight,供 composer pill 实时显示,不参与执行)
21
+ ├── server-ref.js 资源名/ID 解析与 requireServerRef 错误
17
22
  ├── shell-adapters.js 方言适配器入口:POSIX 适配器、按方言选择、宿主平台判断
18
23
  ├── win32-adapter.js Windows 适配器:PowerShell -EncodedCommand、UTF-8、退出码、workdir
19
24
  ├── dialect.js 目标方言:facts.dialect 优先、platform 仅作提示
@@ -43,40 +48,52 @@ vendor/
43
48
 
44
49
  `ssh2` 不通过 `dependencies` 安装,而是以纯 JS 形式 vendor 进发布包(`vendor/ssh2` + `vendor/node_modules/`);broker 用相对路径动态 import。这样插件安装不触发 `node-gyp`、不需要本机编译工具链、也不依赖 npm 网络,代价是包体积略大。ssh2 的可选原生模块(`cpu-features`、`nan`、`sshcrypto.node`)全部未携带,ssh2 会回退到纯 JS 实现。
45
50
 
46
- ## 一次远程 bash 的完整路径
51
+ ## 一次远程工具执行的完整路径
47
52
 
48
53
  1. 用户消息进入 `agent/pre-step`。
49
- 2. `server-reference.js`:
54
+ 2. `managed-context.js`:
50
55
  - 从宿主消息里提取 `@名字`,匹配服务器清单;
51
- - 1 个 → 绑定为 active target;多个 → 记住候选并要求模型用 `ssh_workbench_target` 选择;
52
- - 需要时注入一条 user-role 目标上下文(source kind `ssh-workbench-target`),并 spread 下游 decision。
53
- 3. 模型调用原生 `bash`。
56
+ - 仅记录为 @ 提示(candidates),不产生 active target;每轮注入资源清单与显式 server 工具规则,模型自行选择目标;
57
+ - 注入一条 user-role 资源上下文(source kind `ssh-workbench-target`),并 spread 下游 decision。
58
+ 3. 模型选择一个远程工具并显式传 `server`:`remote_bash` / `remote_pwsh` / `remote_exec`,或远程文件工具 `remote_read` / `remote_glob` / `remote_grep`(写入另需 `remoteFileWrite`)。原生 `bash` / `pwsh` 不经过插件,始终本地执行。
54
59
  4. DSH 审批(本插件不参与)。
55
- 5. `ctx.tools.guard()`(`guard.js`):若本会话有 active target 且命令命中黑名单 → 直接拒绝。`gate.js` 先把每段还原成 argv words(去引号/转义、剥 `sudo`/`timeout` 等包装、路径规范化、重定向按 token、`$()`/`eval` 递归)再匹配。
56
- 6. `tools/execute` waterfall(`remote-bash.js`;仅 POSIX 宿主、仅 POSIX 目标):
57
- - 非 `bash` / 无目标 → `next()`(本地行为不变);`@a @b` 后未选目标 → `TARGET_NOT_SELECTED` 错误,不静默跑本地;
58
- - `run_in_background` → 结构化错误(远程不支持);
59
- - 处理 `workdir`(绝对路径 / `~` / 相对路径 → `$HOME` 前缀);
60
- - 探测远端 `timeout` 能力(`kill`/`basic`/`none` + 登录 shell 路径,按服务器缓存),可用时把命令包成 `timeout [-k N] <秒> '<shell>' -c '<命令>'`,让客户端超时同时终止远端进程组;
61
- - `runtime.exec(server, command, { timeoutMs, signal, cap, stderrCap })`;broker 建通道后立即关闭 stdin(等价 `</dev/null`),把远端 `timeout` 的 exit 124 映射回 timeout 结果,并对 stdout/stderr 分别限长;
62
- - 映射成原生 bash foreground value,或结构化错误结果;value 的 cwd 带目标前缀(`web-1:/var/log`);超时/取消结果在 stderr 说明远端是否已终止;目标切换后的首条命令追加 `[remote] target` 行。
63
- 7. DSH 管道用原生 bash 的输出 schema 校验、原生 renderer 渲染终端卡片。
60
+ 5. `ctx.tools.guard()`(`guard.js`):从 `arguments.server`(或 `remote_exec.tasks[]` 的每个 server)解析目标,按对应方言检查黑名单 → 命中直接拒绝。`gate.js` 先把每段还原成 argv words(去引号/转义、剥 `sudo`/`timeout` 等包装、路径规范化、重定向按 token、`$()`/`eval` 递归)再匹配。
61
+ 6. 工具实现(`remote-shell-tool.js` / `remote-exec-tool.js` / `remote-file-tools.js`):
62
+ - `remote_bash` / `remote_pwsh` 处理 `workdir`(绝对路径 / `~` / 相对路径 → `$HOME` 前缀);
63
+ - 逐 server 解析方言与 `timeout` 能力(`kill`/`basic`/`none` + 登录 shell 路径;`remote_exec` 的 `probe:false` 跳过探测);
64
+ - 可用时把命令包成 `timeout [-k N] <秒> '<shell>' -c '<命令>'`,让客户端超时同时终止远端进程组;
65
+ - `runtime.exec(server, command, { timeoutMs, signal, cap, stderrCap, connectTimeoutMs })`;broker 建通道后立即关闭 stdin(等价 `</dev/null`),把远端 `timeout` 的 exit 124 映射回 timeout 结果,并对 stdout/stderr 分别限长;
66
+ - 返回带 server / dialect / cwd / 退出码 / stdout / stderr 的文本结果;超时/取消说明远端是否已终止。
67
+ 7. `remote_exec` 的多个 task 在同一个 assistant step 内并发执行(最多 `batchConcurrency`),结果按输入顺序渲染;`isConcurrencySafe` 让 DSH 的并行调度器可以真正重叠调用。
64
68
 
65
- ## 多宿主与多方言(v1.5)
69
+ ## 多资源并行
66
70
 
67
- DSH 的内置 shell 工具按宿主平台二选一:POSIX 宿主有 `bash`、没有 `pwsh`;Windows 宿主有 `pwsh`、没有 `bash`。因此“谁是远程执行入口”由宿主与目标方言共同决定:
71
+ `remote_exec({ tasks: [{ server, command, workdir?, timeoutMs? }], concurrency? })` 是插件自有的 `defineTool`,不走 `tools/execute`:
68
72
 
69
- | 宿主 \ 目标 | POSIX | Windows |
73
+ - **显式目标**:每个 task 自己带 `server`,不依赖也不修改任何会话目标状态;`server: "local"` 被拒绝,任务只能落在纳管资源上。
74
+ - **全量预检**:先解析全部 server、方言、workdir、timeout;任一 task 非法则整批拒绝,不会执行一半。
75
+ - **真正并发**:工具声明 `isConcurrencySafe: () => true`,DSH 的 batch 调度会把它和其他 parallel-safe 调用一起放进有界并发池;工具内部再按 `batchConcurrency`(默认 4,最大 8)跨服务器并发。单台服务器仍受 `maxChannelsPerServer` 约束。
76
+ - **逐任务隔离**:每个 task 按自己的 `facts.dialect` 选择 POSIX/Windows 适配器与 guard 规则;超时、传输失败、非零退出码只影响该任务,结果按输入顺序渲染。
77
+ - **输出预算**:一次 batch 共享 `maxBatchOutputBytes`(默认 256 KiB),每个 task 先取 `maxOutputBytes` 与均分预算的较小值,避免一次 fan-out 撑爆上下文。
78
+ - **仅前台**:不支持 `run_in_background`;长任务按目标分别用后台工具或分别在终端里跑。
79
+ - **UI**:composer 下方 pill 是执行账本的只读投影,结构对齐 DSH 内置 Token 用量弹窗:标题行「远程执行」+ 本 Session 总次数、0.5px 分隔线、目标名 → 执行次数。不显示成功/失败、用时或模型指标,也不承担切换或打开工作台;终端/文件从左侧工作台进入。弹层通过 `react-dom.createPortal` 挂到 `document.body`,避免被 composer 的 stacking context 裁切。没有执行记录时不显示,因此不存在“切本地后残留”。
80
+
81
+ ## 多宿主与多方言
82
+
83
+ DSH 的内置 shell 工具按宿主平台二选一:POSIX 宿主有 `bash`、没有 `pwsh`;Windows 宿主有 `pwsh`、没有 `bash`。远程执行不依赖宿主自带的 shell——三种远程工具在**两种宿主上都注册**,按目标方言分派:
84
+
85
+ | 目标方言 | 工具 | 执行器 |
70
86
  |---|---|---|
71
- | macOS/Linux | 原生 `bash`(`tools/execute` 路由,保原生卡片) | 插件自有 `remote_pwsh` |
72
- | Windows | 插件自有 `remote_bash` | 插件自有 `remote_pwsh` |
87
+ | POSIX | `remote_bash` | `posixAdapter` + 可选远端 `timeout` 包装 |
88
+ | Windows | `remote_pwsh` | `win32Adapter`(UTF-16LE `-EncodedCommand`) |
89
+ | 混合(多目标) | `remote_exec` | 逐 task 按各自方言选适配器 |
73
90
 
74
91
  - 目标方言由连接后的探测决定(`uname -s` 成功 → posix;否则 `cmd /c ver` 成功 → windows),写入 `facts.dialect`;`platform` 字段只作探测前提示。`effectiveDialect()` 是唯一读取入口。
75
92
  - 方言决定命令构造:POSIX 用 `cd -- ... && ...` + 可选 `timeout` 包装;Windows 用 UTF-16LE base64 的 `powershell -EncodedCommand` 脚本(内含 UTF-8 输出、`Set-Location`、显式退出码),没有远端 `timeout`。
76
- - 目标上下文与 `ssh_workbench_target` 文案按方言/宿主生成正确的工具名;`remote_bash` 与 `remote_pwsh` 互相拒绝错误方言并给出改用建议。
77
- - `ctx.tools.guard()` 按工具名选择 POSIX 或 PowerShell 规则集。
93
+ - `remote_bash` 与 `remote_pwsh` 互相拒绝错误方言并给出改用建议;即使 `platform` 手填错误,探测结果也会纠正。
94
+ - `ctx.tools.guard()` 按每个显式 server 的方言选择 POSIX 或 PowerShell 规则集;facts 缺失时两套都跑。
78
95
 
79
- ## 后台任务(v1.6)
96
+ ## 后台任务
80
97
 
81
98
  `run_in_background: true` 不再报错,而是启动一个**脱离 SSH 会话**的远端进程:
82
99
 
@@ -91,21 +108,26 @@ DSH 的内置 shell 工具按宿主平台二选一:POSIX 宿主有 `bash`、
91
108
  - `agent/pre-step`:返回 `{ kind: 'enter', messages }`,改写必须 spread。
92
109
  - `ctx.tools.guard()`:同步、单调的拒绝;在审批之后执行。
93
110
  - 原生 `bash` 输出值:`{ kind:'foreground', cwd, exitCode, signal, timedOut, aborted, timeoutMs, stdout, stderr }`。
94
- - 模型可见工具:`ssh_workbench_target` 恒在;POSIX 宿主上另有 `remote_pwsh`,Windows 宿主上另有 `remote_bash` + `remote_pwsh`。
111
+ - 模型可见工具:`remote_exec`、`remote_bash`、`remote_pwsh`、`remote_read`、`remote_glob`、`remote_grep`、`remote_write`、`remote_edit` 在两种宿主上都注册;默认只读工具可用,写工具在 `remoteFileWrite=true` 前明确拒绝。原生 `bash` / `pwsh` / `read` / `write` / `edit` / `glob` / `grep` 保持本地语义。`remote_exec` 与只读文件工具声明 `isConcurrencySafe:true`,shell 与写文件工具保持 exclusive。
95
112
 
96
- ## 目标状态生命周期
113
+ ## @ 提示生命周期
97
114
 
98
- - 存储:进程内 `Map<sessionId, {candidates, active, version, injectedVersion, seeded}>`。
99
- - 来源:人类消息里的 `@`(权威、可回放)+ `ssh_workbench_target` 工具。
115
+ - 存储:进程内 `Map<sessionId, {candidates, version, injectedVersion, seeded}>`;只有 @ 提示,没有执行绑定。
116
+ - 来源:人类消息里的 `@名字`,由 `mentions.js` 解析并匹配清单;`managed-context.js` 每轮把它连同资源清单写进上下文。
100
117
  - 恢复:会话恢复后首次 `agent/pre-step` 用 `session.deriveMessages()` 回放历史人类消息。
101
118
  - 清理:`agent/disposed` 或插件卸载。
102
119
 
120
+ ## 执行账本
121
+
122
+ - `activity.js` 记录每次远程调用(`server` / `sessionId` / `durationMs` / `outcome`),是 composer pill 的唯一数据源。
123
+ - `execution-stats.js` 按 Session 聚合出 `{count, servers:[{name,count}]}`;`live-executions.js` 只保存进行中的调用,两者都不参与执行决策,也不写入 Session。
124
+
103
125
  ## 降级行为
104
126
 
105
127
  | 情况 | 行为 |
106
128
  |---|---|
107
- | 没有目标 | 原生工具完全本地,零开销 |
108
- | `targetContext:false` | 不绑定、不注入;`ssh_workbench_target` 仍可手动使用 |
129
+ | 没有 @ 提示 | 原生工具完全本地,零开销 |
130
+ | `targetContext:false` | 不注入资源清单/`@` 提示;显式 server 工具仍可用 |
109
131
  | `ctx.tools.guard` 不可用(旧 DSH) | 记录 warning,黑名单关闭,其余功能不变 |
110
132
  | 目标被删除/禁用/方言不匹配 | 返回结构化错误,提示重新选择目标或改用另一种 remote 工具 |
111
133
  | 远端没有 `timeout` 工具 | 命令不加包装;超时只停止等待,stderr 提示远端可能仍在运行 |
@@ -113,12 +135,12 @@ DSH 的内置 shell 工具按宿主平台二选一:POSIX 宿主有 `bash`、
113
135
  | host facts 采集失败 | 只影响 UI/上下文摘要,不影响执行;下次测试/选中目标时重试 |
114
136
  | SFTP 分块卡死 | `transferChunkTimeoutMs` 到点关闭会话并报 `SFTP_TIMEOUT`,UI 可从 offset 续传 |
115
137
  | 远程 home 解析失败 | `/sftp/home` 返回可读错误,工作台文件面板回退到 `/` |
116
- | 多个 `@` 未选择目标 | router 返回 `TARGET_NOT_SELECTED`,避免静默在本地执行 |
138
+ | 多个 `@` | 模型按资源清单选择目标,或直接发 `remote_exec` 并行;没有可切换的 active target,不会静默跑本地 |
117
139
  | 服务器清单损坏 | 路由返回错误结果,不影响本地工具 |
118
140
 
119
141
  ## 安全边界
120
142
 
121
- - 黑名单只拦机器毁灭类命令,不是权限边界。
143
+ - 黑名单只拦机器毁灭类命令,不是权限边界。`remote_exec` 的每个 task 都会按目标方言过 guard,但批量并发不改变权限边界。
122
144
  - 远程命令不受 DSH 本地沙箱约束;是否审批由 DSH 会话预设决定。
123
145
  - 凭据与主机身份:见 `secrets.js` / `known-hosts.js`;未知指纹不会被静默信任。
124
146
  - 插件不写 Session 事件、不做审计日志;`activity.json` 只是 UI 概览。
@@ -0,0 +1,83 @@
1
+ # 执行模型与设计取舍
2
+
3
+ 本文说明「远程执行如何确定目标」这件事的设计结论,以及为此放弃的方案和原因。实现分层见 [architecture.md](architecture.md);用户用法见 [quickstart.md](quickstart.md)。
4
+
5
+ ## 一句话结论
6
+
7
+ **每个远程工具调用自己带 `server`。** 不存在会话级「当前目标」,也不存在把原生工具透明改道到远端。
8
+
9
+ ## 为什么不要「当前目标」
10
+
11
+ 早期版本用一个会话级 active target 表示「现在操作哪台机器」,`@资源名` 会改写它。它在三个地方同时承担职责:
12
+
13
+ 1. **执行绑定** —— 原生 `bash` 被路由到 active target;
14
+ 2. **上下文状态** —— 每轮把 active target 写进提示;
15
+ 3. **UI 状态** —— composer 下方显示当前目标。
16
+
17
+ 三者耦合带来一组必然问题:
18
+
19
+ - 多目标任务只能逐个 `switch → 执行 → switch`,模型把时间花在切换上,人也看不出「这台到底跑了什么」;
20
+ - 多个 `@` 时 active 被清空,模型既不知道该跑哪台,也拿不到资源清单;
21
+ - 切回本地是状态变更,容易残留成「以为在本机、其实在远端」;
22
+ - 目标会漂移,事后无法从对话里复现「这条命令跑在哪台」。
23
+
24
+ 所以把它整体去掉。现在:
25
+
26
+ | 场景 | 做法 |
27
+ |---|---|
28
+ | 单个目标 | `remote_bash({ server, command })` |
29
+ | 多个目标并行 | `remote_exec({ tasks: [{ server, command }] })` |
30
+ | 本机操作 | 原生 `bash` / `pwsh` / `read` / `write` / … |
31
+ | 远程文件 | `remote_read` / `remote_glob` / `remote_grep`(写需 `remoteFileWrite`) |
32
+
33
+ **每个调用的目标都写在参数里**,因此并行安全、可审计、可复现,也不存在「状态残留」。执行工具在两种宿主上都注册,避免宿主自带 shell 平台差异(Windows 宿主没有原生 `bash`)影响可用性。
34
+
35
+ ## `@资源名` 是什么
36
+
37
+ **提示,不是绑定。** 用户 `@web-1` 时:
38
+
39
+ - `mentions.js` 解析出候选名字,只作为本会话的提示存储;
40
+ - `managed-context.js` 每轮把它连同资源清单写进上下文;
41
+ - 模型据此在工具参数里填 `server`。
42
+
43
+ `@` 不改变任何执行状态,所以后续无 `@` 的追问不会把命令静默带到别的机器;用户仍可随时 `@` 另一台或直接说「在本机看」。
44
+
45
+ ## 上下文注入
46
+
47
+ 每轮都注入一份紧凑资源清单(名称 / 方言 / 状态),而不是只在「有 active target」或「多个候选」时才注入。理由是:没有任何提示时,模型只能去读 `~/.dsh/ssh-workbench/servers.json` 猜配置——既慢,又把凭据目录暴露给模型。上下文里同时明确:
48
+
49
+ - 远程操作只能用 `remote_*` 工具,且必须显式写 `server`;
50
+ - 原生 shell / 文件工具只在本机生效;
51
+ - 目标不明确时按清单选择,或在有歧义时先问。
52
+
53
+ ## 执行账本与 pill
54
+
55
+ composer 下方的 pill 是**只读投影**,不是控制器:
56
+
57
+ - 数据来自 `activity.js`(每次远程调用的 `server` / `sessionId` / `durationMs` / `outcome`),由 `execution-stats.js` 按 Session 聚合;
58
+ - 进行中的调用由 `live-executions.js` 在内存里记录,仅用于显示「正在连哪台」,不参与执行;
59
+ - 视觉对齐 DSH 内置的 Token 用量弹窗:标题 + 总次数、分隔线、目标名 → 次数;
60
+ - 不显示成功/失败、用时或模型指标(这些是 DSH 会话的私有数据,插件不伪造),不承担切换目标或打开工作台。
61
+
62
+ ## 状态类批量查询的快路径
63
+
64
+ 「看看几台机器状态」这类请求最大的延迟来源不是 SSH 跑命令,而是**对不可达目标的探测**:方言探测(每次最多一个 `connectTimeoutMs`)+ `timeoutGuard` 能力探测(再一次)+ 真正建连(再一次)。
65
+
66
+ 因此 `remote_exec` 提供两个参数:
67
+
68
+ - `probe:false` —— 跳过方言与 `timeoutGuard` 探测,直接用 `facts.dialect` / 记录平台判断;
69
+ - `connectTimeoutMs` —— 按调用限制建连时间,一路透传到 broker 的 `readyTimeout`。
70
+
71
+ 组合后 6 台目标里 3 台不可达时,整体从接近一分钟降到秒级;不可达任务只让自己失败,不阻塞其他任务。
72
+
73
+ ## 有意的非目标
74
+
75
+ | 不做 | 原因 |
76
+ |---|---|
77
+ | 把 `read` / `write` / `edit` 透明改道到远端 | 会让「改文件」的语义取决于隐式状态,比多做几个工具危险得多;需要远程文件就用显式的 `remote_*` |
78
+ | SSHFS / 远程 IDE 式挂载 | 需要额外守护进程与权限模型,和「零安装依赖」的发布约束冲突 |
79
+ | 命令分级(allow/guarded)与风险解释 | 白名单可被 shell 语法绕过,容易制造错误的安全感;真正的边界是 SSH 权限、host-key 与 DSH 审批 |
80
+ | 插件自有审批清单 | 审批策略属于 DSH 会话预设,插件重复实现会互相矛盾 |
81
+ | 伪造模型指标(TTFT / TPS / 轮次) | 这些数据只存在于 DSH 宿主,插件无法准确获得 |
82
+
83
+ 黑名单 guard 保留为兜底:同步、单调、按每个显式目标的方言逐条检查(`remote_exec` 的每个 task 都过),但它**不是权限边界**——见 [SECURITY.md](../SECURITY.md)。
Binary file
Binary file
Binary file
Binary file
@@ -10,7 +10,7 @@ npm test
10
10
  npm run pack
11
11
  ```
12
12
 
13
- 然后安装 `dist/dsh-ssh-workbench-1.6.4.tgz`。安装后重启/重载插件。
13
+ 然后安装 `dist/dsh-ssh-workbench-1.7.0.tgz`。安装后重启/重载插件。
14
14
 
15
15
  发布包自带纯 JS 的 SSH 依赖,安装过程不需要编译(不需要 Xcode Command Line Tools)、也不额外下载 npm 包,离线机器同样可以安装。
16
16
 
@@ -32,7 +32,7 @@ npm run pack
32
32
  @web-1 把 /etc/nginx/nginx.conf 的 worker_connections 改成 2048,然后 nginx -t
33
33
  ```
34
34
 
35
- 模型会调用原生 `bash`(Windows 宿主上是 `remote_bash`),命令实际在 web-1 上执行;POSIX 宿主的结果卡片与本地 bash 一致(退出码、超时、截断)。
35
+ 模型会调用 `remote_bash({ server: "web-1", command })`(Windows 目标是 `remote_pwsh`),命令实际在 web-1 上执行;`@` 只是告诉模型这一轮的目标,不产生全局状态。原生 `bash` / `pwsh` 始终在本机运行,所以想查本机时不需要“切回来”。
36
36
 
37
37
  ## 4. 多目标
38
38
 
@@ -40,9 +40,18 @@ npm run pack
40
40
  @web-1 @db-1 各自看下磁盘
41
41
  ```
42
42
 
43
- 模型会先调用 `ssh_workbench_target({ server: "web-1" })` 再执行,然后切换到 `db-1`。可以随时用 `ssh_workbench_target({ server: "local" })` 回到本机。
43
+ 模型会优先用 `remote_exec` 一次并行执行:
44
44
 
45
- 注意:一次提到多个资源后,当前目标会先被清空;在模型调用 `ssh_workbench_target` 选择之前,直接调用远程 shell 工具(POSIX 宿主上的原生 `bash` 或 `remote_bash`)会返回 `TARGET_NOT_SELECTED` 错误(不会静默在本机执行)。资源名支持中文/非 ASCII(如 `@测试机`)。
45
+ ```
46
+ remote_exec({ tasks: [
47
+ { server: "web-1", command: "df -h" },
48
+ { server: "db-1", command: "df -h" }
49
+ ] })
50
+ ```
51
+
52
+ 每个 task 自己指定目标,命令在各自服务器上并发执行,结果按任务顺序返回;没有会话级 active target,所以不存在“切目标”状态,也不会把后续本地命令误发到远程。资源名支持中文/非 ASCII(如 `@测试机`)。
53
+
54
+ composer 下方的 pill 只读展示本会话各目标的远程执行次数,结构对齐 DSH 的 Token 用量卡(标题 + 总数、分隔线、目标名 → 次数);它不切换目标、不打开工作台。终端/文件从左侧「远程工作台」进入。
46
55
 
47
56
  ## 5. 远程工作台
48
57
 
@@ -57,8 +66,8 @@ npm run pack
57
66
 
58
67
  ## 6. 常见问题
59
68
 
60
- **为什么 `read` / `write` / `edit` 读不到远程文件?**
61
- 这些文件工具始终作用于 DSH 主机(POSIX 宿主上的原生 `bash` 或远程 shell 工具都不会改变这一点),误用 `write` 会写到本机。远程文件用对应的远程 shell 工具操作(POSIX 目标:`cat`、`tee`、`sed`;Windows 目标:`Get-Content`、`Set-Content`),或用右侧栏 SFTP 面板。
69
+ **远程文件怎么读写?**
70
+ 原生 `read` / `write` / `edit` / `glob` / `grep` 始终作用于 DSH 主机;远程文件请让模型使用显式的远程文件工具:`remote_read`、`remote_glob`、`remote_grep`。写入默认关闭,需在插件配置里设置 `remoteFileWrite: true` 后才可使用 `remote_write` / `remote_edit`;后者默认要求 oldText 唯一匹配。大文件、二进制和上传/下载仍建议用右侧栏 SFTP 面板。远程写入不是原子操作,修改关键配置前先备份。
62
71
 
63
72
  **命令很慢/超时?**
64
73
  远程命令默认 120s,可用远程 shell 工具的 `timeoutMs` 参数(原生 `bash` / `remote_bash` / `remote_pwsh`)或插件配置 `commandTimeoutMs` 调整。连接池会复用连接,第一次会稍慢。超时后如果远端有 `timeout`,整个远端进程组会被终止(TERM 后按 `timeoutKillAfterMs` 再补 KILL);没有 `timeout` 的服务器(如 macOS)只能停止等待,命令可能还在跑,结果 stderr 会提示这一点。**取消(Esc)不会立即杀远端**,远端仍会计时到自己的 deadline。
@@ -67,10 +76,13 @@ npm run pack
67
76
  远程命令的 stdin 在启动后立即关闭(等价 `</dev/null`),读 stdin 的命令会立刻得到 EOF,不会挂到超时。
68
77
 
69
78
  **怎么确认命令跑在哪台机器?**
70
- 目标切换后的第一条远程命令会带有目标提示(原生 bash 卡片显示 stderr 行与 cwd 前缀,如 `[remote] target: web-1 (...)`、`web-1:/var/log`;远程 shell 工具在结果文本里带 target/cwd);composer 下方参数行常驻显示当前目标。也可以点 `ssh_workbench_target` 选 `local` 回到本机。
79
+ 每个远程工具结果都带 server / target / cwd(如 `web-1:/var/log`),工具卡片也会显示执行的资源;composer 下方的 pill 只统计本会话执行次数。原生 `bash` / `pwsh` 的结果永远来自本机。
80
+
81
+ **能不能一次让多个资源并行执行?**
82
+ 可以,让模型调用 `remote_exec`:每个 task 显式带 `server`,默认并发 4、最多 8,单次最多 8 个 task;每个任务独立超时、独立输出、互不污染,结果按输入顺序返回。它只支持前台命令,长任务仍走后台工具。`remote_bash` / `remote_pwsh` 也各自显式带 `server`,不存在需要先切换的单 active target。
71
83
 
72
84
  **怎么跑长时间任务 / 后台服务?**
73
- 让模型带 `run_in_background: true` 调用远程 shell 工具(`remote_bash` / `remote_pwsh`,POSIX 宿主上的原生 `bash` 路由同样支持)。工具会立刻返回远端 pid 和日志路径:
85
+ 让模型带 `run_in_background: true` 调用远程 shell 工具(`remote_bash` / `remote_pwsh`)。工具会立刻返回远端 pid 和日志路径:
74
86
  - POSIX:`tail -f <log>` 看输出,`kill <pid>` 停止;
75
87
  - Windows:`Get-Content -LiteralPath '<log>' -Wait` 看输出,`Stop-Process -Id <pid> -Force` 停止。
76
88
  后台进程与本次工具调用脱离,调用返回后继续运行;`timeoutMs` 只约束启动调用本身。它不是 DSH 本地 job,`job_*` 工具看不到它。