dsh-ssh-workbench 1.6.4 → 1.7.8

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,139 @@
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.8] - 2026-11-14
8
+
9
+ ### 变更(远程执行能力补齐)
10
+
11
+ - `remote_exec` 的每个 task 支持 `run_in_background`:批量任务里可以混跑前台命令和后台进程,后台任务立即返回远端 pid 与日志,其余 task 继续并发;`timeoutMs` 只约束启动,不再需要为长任务单独切到 `remote_bash` / `remote_pwsh`。
12
+ - 远程写入原子化(POSIX):`remote_write` / `remote_edit` 先写同目录临时文件,再 `rename` 覆盖目标,并尽量保留原权限位;连接中断或写入失败时原文件保持不变。Windows 目标保持直接写入;软链目标写穿链接而不是替换链接。
13
+ - POSIX 后台日志从 `/tmp` 移到 `~/.dsh/ssh-workbench/logs`(`DSH_SSH_WORKBENCH_LOG_DIR` 可覆盖),避免系统 tmp 清理;该目录不可写时自动回退 `/tmp`。
14
+
15
+ ### 文档
16
+
17
+ - README / quickstart / architecture 同步上述行为;明确 composer 下方 pill 只反映工具调用,后台进程启动后 pill 消失不代表远端进程结束。
18
+
19
+ ## [1.7.7] - 2026-11-14
20
+
21
+ ### 新增(工作区文件补齐原生能力)
22
+
23
+ - 页头补回搜索:走 Host 的 `workspaceFiles/search`(ripgrep),按文件名或路径匹配,输入即搜、结果按层级缩进、右侧显示所在目录,支持清空与 Esc 关闭;结果过多或搜索未完成时给出提示。默认排除 `node_modules`、`dist` 等目录,与官方一致。
24
+ - 「在应用中打开」由单个按钮升级为官方同款拆分按钮:主按钮用默认应用打开当前目录并显示该应用图标与名称,右侧箭头列出 Host 上可用的全部应用(来自 `session.workspacePathApplications`,含 Finder/VS Code/GitHub Desktop/终端等),选哪个用哪个。没有可用应用列表时回退为单按钮,按系统默认应用打开。
25
+ - 两处都沿用 `remote.workspaceFiles` / `remote.session` 命名空间,只读取 DSH 已提供的宿主能力,不新增后端接口。
26
+
27
+ ## [1.7.6] - 2026-11-14
28
+
29
+ ### 变更(设置页改为即时生效)
30
+
31
+ - 插件管理页去掉「保存」按钮:每个开关拨动后立即提交该字段并回读生效值。旧 Host 不支持新字段时仍提示重启,而不是谎报成功。
32
+ - 三个开关保持一致的三态偏好语义(设置页覆盖优先,未设置回落到 `cordis.patch.yml`)。
33
+
34
+ ### 变更(层级模式按本地/远程分工)
35
+
36
+ - 「工作区文件」层级页是**本机**目录浏览,因此隐藏只对远程有意义的「上传 / 新建文件夹 / 传输记录」,也不再显示行菜单里的「下载 / 复制文件」——这些仍保留在远程工作台的 SFTP 面板。
37
+ - 该页补回 DSH 原生的「在应用中打开」:页头按钮打开当前目录,行菜单对文件/目录用系统默认应用打开(走 Host 的 `session.openWorkspacePath`,因此只对 Host 本地路径生效)。远程 SFTP 页面不显示该入口。
38
+
39
+ ## [1.7.5] - 2026-11-14
40
+
41
+ ### 修复(层级模式对齐官方观感)
42
+
43
+ - 指南页(开始页)入口改回 DSH 官方的 `workspace` 条目:沿用官方 id、`workspace.files` 命令、`order: 10` 与官方图标,位置回到第一格而不是掉到列表末尾。
44
+ - 页签图标改用官方 `FileTypeIcon` 的 folder 图形(16px),与 DSH 自己的「工作区文件」页签一致,不再使用插件自绘图标。
45
+ - 页签标题与指南文案仍为「工作区文件 / Workspace files」,与原生页同名。
46
+
47
+ ## [1.7.2] - 2026-11-13
48
+
49
+ ### 新增(系统文件浏览层级模式)
50
+
51
+ - 插件管理页新增「系统文件浏览使用层级模式」开关,默认关闭。开启后,右侧栏的「工作区文件」页由插件接管,改为带面包屑、后退/上级/根目录导航的层级浏览器;关闭后恢复 DSH 原生文件页。
52
+ - 接管页只浏览当前会话目录:根目录固定为会话 `cwd`,输入路径或点击上级都不能越界;文件通过 DSH 资源协议 `dsh-resource://file/session/...` 交给官方预览打开,Markdown/代码/图片/PDF 等仍享受官方预览能力。
53
+ - 该开关与 `remoteFileWrite` 一样支持三态偏好:设置页覆盖优先,未设置时回落到 `systemFileHierarchy` 配置(默认 `false`)。
54
+ - `POST /dsh-ssh-workbench/prefs` 增加 `systemFileHierarchy` 字段;`GET /state` 的 `config.systemFileHierarchy` 返回实际生效值。
55
+
56
+ ### 修复
57
+
58
+ - 层级浏览器在接管模式下隐藏资源选择器,避免从「工作区文件」页切换到其他本机/远程资源而绕过会话目录边界。
59
+
60
+ ## [1.7.1] - 2026-11-13
61
+
62
+ ### 新增(右键菜单)
63
+
64
+ - 远程/本地文件行复用 DSH 官方的 `contextMenu` 服务:右键、`Shift+F10` 与行尾省略号按钮打开同一套菜单,不再使用插件私有的 `primitives.Menu` 弹层。菜单项为打开、下载、复制文件、复制路径、重命名、删除;删除标红并单独分组。
65
+ - 菜单动作在打开时按当前行解析,标签跟随界面语言;单个动作失败会写回面板错误行,不会静默失败。
66
+ - 官方菜单服务不可用时(例如宿主版本较旧)自动回退到原有省略号菜单,功能不缺失。
67
+
68
+ ### 新增
69
+
70
+ - 插件管理页新增「允许远程写入」开关:`remote_write` / `remote_edit` 不必再改配置文件,直接在「插件 → 远程工作台」里打开或关闭。开关存在本地偏好(`~/.dsh/ssh-workbench/prefs.json`),未设置时回落到 `remoteFileWrite` 配置,因此既可以用 UI 临时启用,也可以用配置文件做默认值。
71
+ - `GET /state` 的 `config.remoteFileWrite` 现在返回**实际生效**的值(偏好优先、配置兜底),设置页、模型上下文与工具拒绝逻辑共用同一判定。
72
+ - 写开关变化会立即生效:工具执行时按调用解析,模型上下文每轮重新读取,不需要重启 DSH。
73
+ - 设置页在保存后会回读实际生效的值:如果当前运行的 DSH 还是旧版本、后端不接受这个开关,界面会提示「重启 DSH 后生效」,而不是谎报保存成功。
74
+
75
+ ### 新增(文件预览)
76
+
77
+ - CSV / TSV 直接在预览页签里渲染成表格:逗号/制表符分隔、支持引号包裹与转义、字段内换行、CRLF;可在「表格 / 源码」之间切换。大表按 500 行、40 列截断并分别提示是行还是列被截断。
78
+ - SVG 按图片渲染,同时保留「源码」视图;其余矢量/位图格式行为不变。
79
+
80
+ ### 修复
81
+
82
+ - 文件预览不再静默截断:超过读取上限时,头部显示「已截断」徽标(悬停给出说明),底部行数改用「前 N 行」。之前 1 MB 以上的日志只显示开头部分,但界面看不出任何区别。
83
+ - 图片超过预览上限时不再渲染残缺图片,改为明确提示「图片超过预览上限,无法完整显示;请下载后查看」,底部也不再显示无意义的行数。
84
+ - 被读取上限截断的表格,底部从「N 行 · M 列」改用「前 N 行 · M 列」,与文本类预览口径一致。
85
+
86
+ ### 变更
87
+
88
+ - 写工具与上下文里「写入已关闭」的提示不再指向裸配置项,改为提示在插件设置里打开开关。
89
+ - `POST /dsh-ssh-workbench/prefs` 现在同时接受 `sidebar` 与 `remoteFileWrite`,返回体带上生效后的 `config`;只传 `sidebar` 仍然兼容。
90
+
91
+ ## [1.7.0] - 2026-11-13
92
+
93
+ 以 1.6.4 为基线的一个中版本:远程执行改为「每次调用显式带目标」,并补齐远程文件工具、多资源并行与执行指示。
94
+
95
+ ### 执行模型:每个远程调用显式带目标
96
+
97
+ - 移除会话级 active target、`ssh_workbench_target` 工具和原生 `bash` 的远程拦截。原生 `bash` / `pwsh` 现在始终只在本机运行;远程工作只有一条路:显式指定 `server`。
98
+ - 工具矩阵统一为 `remote_bash({ server, command })`(POSIX 目标)、`remote_pwsh({ server, command })`(Windows 目标)、`remote_exec({ tasks: [{ server, command }] })`(多目标并行);两种宿主都注册全部远程工具。
99
+ - `@资源名` 不再改变执行状态,只作为本轮目标提示进入上下文;后续无 `@` 的追问不会把命令静默带走。切回本机不需要任何「切换目标」动作。
100
+ - 每轮上下文始终注入纳管资源清单(名称 / 平台 / 状态)与显式 server 工具规则;无 `@` 时模型也能直接选目标,并明确禁止读取 `servers.json` 猜配置。
101
+ - 删除旧的 `TARGET_NOT_SELECTED`、target store replay/POST `/target`、native bash router 及其测试。
102
+
103
+ ### 远程文件工具
104
+
105
+ - 新增 `remote_read`、`remote_glob`、`remote_grep`:远程文件读取/查找/搜索,全部显式 `server`,通过 SFTP(read/glob)或目标方言命令(grep)执行,并受结果/输出上限约束。
106
+ - 新增 `remote_write`、`remote_edit`:默认关闭,需在配置中显式设置 `remoteFileWrite: true`;`remote_edit` 默认要求 oldText 唯一匹配,可用 `replaceAll: true` 覆盖。
107
+ - 原生 `read`/`write`/`edit`/`glob`/`grep` 仍只作用于本地;远程文件能力是显式的 `remote_*` 工具,不做 SSHFS 或透明重定向。
108
+ - 新增配置:`remoteFileReadBytes`(262144)、`remoteFileEditBytes`(2097152)、`remoteFileWrite`(false)、`remoteGlobMaxResults`(200)、`remoteGlobMaxDepth`(4)、`remoteGrepMaxResults`(200)。
109
+
110
+ ### 多资源并行
111
+
112
+ - `remote_exec({ tasks: [{ server, command, workdir?, timeoutMs? }], concurrency? })` 一次调用跨多台服务器并行采集;不读取也不改变任何会话目标状态。
113
+ - 全量预检后再执行:任一 task 的目标/命令/目录/超时非法时整批拒绝,不会执行一半;单任务失败、传输错误或超时只影响该任务,结果按输入顺序返回。
114
+ - 声明 `isConcurrencySafe: true`,由 DSH 工具批处理调度并行;工具内部再按 `batchConcurrency`(默认 4,上限 8)跨服务器并发,并受单连接 `maxChannelsPerServer` 约束。
115
+ - 配置新增 `maxBatchTasks`、`batchConcurrency`、`maxBatchOutputBytes`;输出预算按任务均分,避免一次 fan-out 撑爆上下文。
116
+
117
+ ### 状态查询性能
118
+
119
+ - `remote_exec` 新增 `probe:false`:跳过方言探测和 `timeoutGuard` 能力探测,直接用 `facts.dialect` / 记录平台判断。
120
+ - 新增 `connectTimeoutMs`:按调用限制 SSH 建连时间,一路透传到 broker 的 `readyTimeout`;状态类批量查询可用 `probe:false + connectTimeoutMs: 4000`,避免不可达目标拖满 15–30 秒。
121
+
122
+ ### 执行指示(composer pill)
123
+
124
+ - 底部 pill 是执行账本的只读投影,结构对齐 DSH Token 用量卡:标题「远程执行」+ 总次数、分隔线、目标名 → 次数;不显示成功/失败或用时,不承担切换目标或打开工作台。
125
+ - 远程调用进行中显示实时目标:单目标 `host165 · 执行中`,多目标 `2 个目标 · 执行中`;结束后回退为本会话次数(`远程执行 · N 次`)。1.5 秒轮询。
126
+ - 通过 around-dispatch 的 `tools/execute` 钩子维护内存 in-flight 记录,覆盖全部 `remote_*` 工具;不恢复 active target,不改变执行语义。
127
+
128
+ ### 安全
129
+
130
+ - 黑名单 guard 从工具参数解析显式 `server`;`remote_exec` 逐 task 校验,任一 task 命中则整批拒绝;方言事实缺失时同时跑 POSIX 与 PowerShell 规则。
131
+ - 原生 `bash` / `pwsh` 是本地工具,不再经过远程黑名单。
132
+
133
+ ### 整理
134
+
135
+ - 模块按当前职责重命名:`mentions.js`(@ 提示)、`posix-shell.js`(POSIX 命令构造)、`remote-shell-tool.js`(远程 shell 工具)、`managed-context.js`(每轮上下文注入)、`execution-stats.js`(执行统计)。
136
+ - 删除遗留死代码与过时注释,统一文档与代码中的执行模型表述。
137
+ - 测试 220/220,SSH/SFTP 集成测试通过。
6
138
 
7
139
  ## [1.6.4] - 2026-11-09
8
140
 
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,38 @@ 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?, run_in_background? }]`;每个任务**显式指定目标**,按各自方言选择 POSIX/Windows 执行器,并发执行(默认 4,上限 8),逐任务超时、输出上限与错误隔离;不依赖也不修改任何会话目标状态。任务可单独立 `run_in_background`,返回远端 pid 与日志 |
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 的文本结果 |
23
- | 后台任务 | `run_in_background: true` 在远端起**脱离会话**的进程:POSIX 用 `nohup` 重定向到 `/tmp` 日志,Windows 用 `Start-Process` + 日志文件;结果返回 pid、日志路径和读取/停止命令(`tail -f` / `kill`、`Get-Content -Wait` / `Stop-Process`)。它不注册到 DSH 本地 `job_*` 注册表,进程在工具调用返回后继续运行 |
29
+ | 结果输出 | 原生 `bash` / `pwsh` 保留 DSH 原生终端卡片;`remote_bash` / `remote_pwsh` / `remote_exec` 返回带 server / cwd / 退出码 / stdout / stderr 的文本结果 |
30
+ | 后台任务 | `run_in_background: true` 在远端起**脱离会话**的进程:POSIX 用 `nohup` 重定向到 `~/.dsh/ssh-workbench/logs`(`DSH_SSH_WORKBENCH_LOG_DIR` 可覆盖,不可写时回退 `/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
  | 侧边栏开关 | 插件管理 → 远程工作台 → 页面里的「在侧边栏显示远程工作台」可关闭左侧导航入口;关闭后 `@资源名` 远程执行、设置页与模型工具不受影响 |
35
+ | 系统文件层级模式 | 插件管理 → 远程工作台 → 「系统文件浏览使用层级模式」默认关闭。开启后右侧栏「工作区文件」改为带面包屑/上级导航的层级浏览器,只浏览当前会话目录;保留 DSH 原生的刷新、搜索与「在应用中打开」(含选择其他应用);关闭恢复 DSH 原生页面 |
28
36
  | 危险命令黑名单 | `rm -rf /`、`mkfs`、`dd of=/dev/*`、`fdisk/parted/wipefs`、改根目录权限、fork bomb 等同步拦截;多行脚本、`&`、引号/转义、路径变体、`$()`/`eval` 都会先还原再判断;其他命令交给 DSH 审批 |
29
37
 
30
- **不做**:诊断包、HTTP 探测、批量执行、远程 `read/write/edit/glob/grep`(这些工具始终只作用于 DSH 主机;远程文件操作用对应的远程 shell 工具,如 `cat`、`tee`、`sed`、`Get-Content`)。也**不做**命令分级(allow/guarded)、AI 风险解释和审计。
38
+ **不做**:诊断包、HTTP 探测、跨主机文件分发、远程 IDE/SSHFS/透明代理(远程文件能力是显式的 `remote_*` 工具,不把本地 `read`/`write` 隐式重定向到远端)、命令分级(allow/guarded)、AI 风险解释和审计。
31
39
 
32
40
  ## 安装
33
41
 
@@ -42,15 +50,17 @@ dsh plugin --profile desktop add dsh-ssh-workbench
42
50
  ```bash
43
51
  npm test
44
52
  npm run pack
45
- # 然后在 DSH 的插件管理里安装 dist/dsh-ssh-workbench-1.6.4.tgz
53
+ # 然后在 DSH 的插件管理里安装 dist/dsh-ssh-workbench-1.7.8.tgz
46
54
  ```
47
55
 
48
- 发布包是**自包含**的:SSH 传输依赖(纯 JS 的 ssh2 及其依赖)已放在 `vendor/` 里随包携带,安装时不会运行 `node-gyp`、不需要 Xcode Command Line Tools,也不会额外下载依赖;包体积因此略大(约 0.5 MB),换来任何机器开箱即装。组件与许可证见 `vendor/NOTICE.md`。
56
+ 发布包是**自包含**的:SSH 传输依赖(纯 JS 的 ssh2 及其依赖)已放在 `vendor/` 里随包携带,安装时不会运行 `node-gyp`、不需要 Xcode Command Line Tools,也不会额外下载依赖;包体积因此略大(约 1.1 MB),换来任何机器开箱即装。组件与许可证见 `vendor/NOTICE.md`。
49
57
 
50
58
  本地开发时,也可以在 DSH 的插件管理页直接安装本目录(Bundle 安装会读取 `package.json` 的 `dsh` 字段)。
51
59
 
52
60
  安装后在 **设置 → 远程工作台** 里添加资源。
53
61
 
62
+ ![在 DSH 插件页中的远程工作台](docs/images/plugin.png)
63
+
54
64
  ## 用法
55
65
 
56
66
  1. 先说目标,再说要做什么:
@@ -59,29 +69,29 @@ npm run pack
59
69
  @web-1 看下磁盘和内存
60
70
  ```
61
71
 
62
- 模型会调用 `bash`(Windows 宿主上是 `remote_bash`;Windows 目标是 `remote_pwsh`),命令实际跑在目标上。`read` / `write` / `edit` / `glob` / `grep` 仍然作用于本机;要操作远程文件,直接用远程 shell 工具:
72
+ 模型会调用 `remote_bash({ server: "web-1", command })`(Windows 目标是 `remote_pwsh`),命令实际跑在目标上。`read` / `write` / `edit` / `glob` / `grep` 仍然作用于本机;要读写远程文件,模型会改用 `remote_read` / `remote_glob` / `remote_grep`(写入需在插件管理里打开「允许远程写入」):
63
73
 
64
74
  ```
65
75
  @web-1 把 /etc/nginx/nginx.conf 里 worker_connections 改成 2048,改完 nginx -t
66
76
  ```
67
77
 
68
- 2. 同时提到多个资源时,模型需要先选择目标:
78
+ 2. 同时提到多个资源时,让模型用 `remote_exec` 一次并行执行,不必逐个切换:
69
79
 
70
80
  ```
71
81
  @web-1 @db-1 各自看下磁盘
72
- → 模型调用 ssh_workbench_target({ server: "web-1" }) → bash df -h
73
- → 模型调用 ssh_workbench_target({ server: "db-1" }) → bash df -h
82
+ → 模型调用 remote_exec({ tasks: [
83
+ { server: "web-1", command: "df -h" },
84
+ { server: "db-1", command: "df -h" }
85
+ ] })
74
86
  ```
75
87
 
76
- 3. 切回本机:
88
+ 每个 task 自带目标,命令在各自服务器上并发执行,结果按任务顺序返回到同一条工具卡片;不读取也不改变任何会话目标状态。composer 下方的 pill 只读展示本会话执行次数,点开是按目标列出的计数卡;不负责切换或打开工作台。
77
89
 
78
- ```
79
- ssh_workbench_target({ server: "local" })
80
- ```
90
+ 3. 切回本机:直接让模型用原生 `bash` / `pwsh`,不需要任何“切目标”动作;远程调用只有显式带 `server` 才会发出。
81
91
 
82
92
  ## 安全模型
83
93
 
84
- - **黑名单兜底**:远程 `bash` / `remote_bash` / `remote_pwsh` 执行前经过同步、单调的 `ctx.tools.guard()`,命中机器毁灭类命令直接拒绝;该拦截不受审批策略影响。POSIX 规则先按 shell 语义还原(去引号/转义、剥包装、路径规范化、`$()`/`eval` 递归)再匹配,PowerShell 规则覆盖 `Format-Volume`、`Clear-Disk`、`diskpart`、`bcdedit`、整盘递归删除等;两者都只是 best-effort。
94
+ - **黑名单兜底**:`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
95
  - **审批**:插件不做自己的 ask 清单,远程命令是否弹审批完全由 DSH 的会话预设(`ask` / `never`)决定。
86
96
  - **凭据**:密码存于 `~/.dsh/ssh-workbench/`(0600 文件 + 本机密钥),私钥只存路径;servers.json 0600。
87
97
  - **主机身份**:严格校验 known_hosts;未知指纹会显示 SHA256 并询问是否信任,由用户决定;信任后写入插件自己的 known_hosts。**如果服务器重装或更换导致指纹变化,连接会被拒绝并提示「服务器主机密钥已变化」**,核对后可在界面点「更新信任」。
@@ -92,18 +102,20 @@ ssh_workbench_target({ server: "local" })
92
102
 
93
103
  - **stdout/stderr 顺序**:两者是独立管道,收集后分开返回(与原生 bash 的值契约一致),交错顺序会丢失;看 `make`/`pip install` 这类日志时需要留意。
94
104
  - **Windows 本地路径**:工作台本地文件面板已按 Windows 分隔符处理面包屑与文件名;极少数外部工具生成的混合分隔符路径仍可能显示成一段,可在路径编辑框直接输入绝对路径。
95
- - **后台任务**:`run_in_background` 返回的是远端 pid + 日志,不是 DSH 本地 job id;`timeoutMs` 和取消只影响启动调用,不作用于已脱离的进程。POSIX 的 `/tmp` 日志可能被系统清理(按 tmp 策略),重要输出请自行重定向到持久目录。
105
+ - **`remote_exec` 批量规则**:一次最多 8 个 task、默认并发 4,所有结果共享 `maxBatchOutputBytes` 预算,超出会按任务截断;每个 task 都可以单独设置 `run_in_background`。
106
+ - **后台任务**:`run_in_background` 返回的是远端 pid + 日志,不是 DSH 本地 job id;`timeoutMs` 和取消只影响启动调用,不作用于已脱离的进程。POSIX 日志默认写在 `~/.dsh/ssh-workbench/logs`(可用 `DSH_SSH_WORKBENCH_LOG_DIR` 覆盖),只有该目录不可写时才回退 `/tmp`。
96
107
  - **超时终止**:远端有 `timeout` 时,超时会连同远端进程组一起终止(支持 `-k` 时再补一次 KILL,缓冲时间可用 `timeoutKillAfterMs` 调整);远端没有 `timeout` 时只能停止等待,stderr 会提示"命令可能仍在运行"。**取消(Esc)不会立刻杀远端**:通道关闭后远端仍会计时到自己的 deadline。
97
108
  - **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 下方参数行目标指示”标出目标。
109
+ - **文件工具分本地/远程**:原生 `read`/`write`/`edit`/`glob`/`grep` 只作用于本地;远程文件用显式的 `remote_read`/`remote_glob`/`remote_grep`,写入用 `remote_write`/`remote_edit`(默认关闭)。POSIX 目标的远程写入会先写同目录临时文件再 `rename` 原子替换,并尽量保留原权限位;Windows 目标没有可靠的覆盖 rename,仍为直接写入;软链目标会写穿链接而不是替换链接。修改配置前仍建议备份;大文件/二进制仍建议走右侧栏 SFTP 面板。
110
+ - **composer pill 与后台任务**:pill 只在工具调用开始/结束时更新;`run_in_background` 的进程在工具返回后继续运行,pill 不会保持“执行中”,这不代表远端进程已结束。
111
+ - **并发由 DSH 调度**:`remote_exec` 显式声明并行安全,因此一次批量任务会真正并发(受 `batchConcurrency` 上限约束);原生 `bash` 与 `remote_bash` / `remote_pwsh` 仍是 exclusive 工具,同一轮里的多个 shell 调用按 DSH 调度串行。插件为单台服务器提供最多 6 条并发通道。
112
+ - **目标标识**:DSH 的工具卡片是官方渲染且没有装饰插槽,插件不替换卡片;远程调用的目标由工具参数(`server`)与结果里的 `target` / `cwd` 行体现,composer 下方的 pill 另提供本会话执行次数与执行中的实时目标。
101
113
  - **侧边栏开关**:开关存放在 `~/.dsh/ssh-workbench/prefs.json`,在插件管理里保存后当前窗口立即生效;关闭时左侧导航不再出现远程工作台入口,右侧栏的「远程工作台」「远程文件」页签保持不变。
102
114
  - **终端生命周期**:终端会话跟随工作台页面/页签,关闭页签或离开工作台会结束会话;页面刷新/关闭时通过 `sendBeacon` 尽力补发关闭,但进程崩溃或 Host 异常退出后仍依赖 30 分钟空闲回收(尚未做跨页面保活或重连恢复)。本地终端依赖 Host 的 PTY 服务,不可用时给出提示。
103
115
  - **本地文件边界**:工作台的本地文件面板与本地终端默认以**本地用户主目录**为根(`$HOME`,与常规 SSH 客户端的「本地」面板一致,面包屑显示真实路径),越界路径与指向外部的符号链接都会被拒绝;需要固定其他目录时用 `localRoot` 配置(绝对路径或 `~/...`),需要操作主目录以外的本地路径时用本地终端。远程文件仍走 SFTP,远程文件面板默认打开登录 home。
104
116
  - **终端渲染资源**:xterm 随客户端 bundle 内置(`vendor/xterm.js` 只是构建输入,不再有运行时脚本路由),不依赖额外脚本路由或 CSP 放行;首次打开工作台时加载。
105
117
  - **终端操作边界**:本地/远程终端是用户主动输入,不经过 agent 的审批与黑名单;SSH 权限边界不变。
106
- - **黑名单是 best-effort**:`bash -c`、base64、脚本文件、解释器 heredoc 正文不会被解析;heredoc 正文会按可执行文本检查(fail-closed,写脚本时可能误拦)。真正的边界是 SSH 最小权限 + host-key + DSH 审批。
118
+ - **黑名单是 best-effort**:`bash -c`、base64、脚本文件、解释器 heredoc 正文不会被解析;heredoc 正文会按可执行文本检查(fail-closed,写脚本时可能误拦)。`remote_exec` 会对每个 task 按自己目标方言逐条过 guard,但仍是 best-effort。真正的边界是 SSH 最小权限 + host-key + DSH 审批。
107
119
 
108
120
  ## 配置
109
121
 
@@ -116,6 +128,16 @@ ssh_workbench_target({ server: "local" })
116
128
  | `timeoutKillAfterMs` | `5000` | 远端 `timeout -k` 的宽限:TERM 后多久补 SIGKILL;0 = 只用 TERM |
117
129
  | `maxOutputBytes` | `102400` | stdout 保留上限(头尾保留,中间截断) |
118
130
  | `maxStderrBytes` | `16384` | stderr 独立保留上限(默认明显小于 stdout,保护上下文) |
131
+ | `maxBatchTasks` | `8` | 一次 `remote_exec` 最多任务数 |
132
+ | `batchConcurrency` | `4` | `remote_exec` 默认并发数;模型可请求更小值,不能超过此配置 |
133
+ | `maxBatchOutputBytes` | `262144` | 一次 `remote_exec` 所有任务共享的 stdout/stderr 总预算 |
134
+ | `remoteFileReadBytes` | `262144` | `remote_read` 单次读取上限(字节) |
135
+ | `remoteFileEditBytes` | `2097152` | `remote_write` / `remote_edit` 的文件上限(字节) |
136
+ | `remoteFileWrite` | `false` | `remote_write` / `remote_edit` 的默认开关;插件管理里的「允许远程写入」可覆盖它,默认关闭 |
137
+ | `systemFileHierarchy` | `false` | 系统「工作区文件」是否使用插件的层级浏览器;插件管理里的「系统文件浏览使用层级模式」可覆盖它 |
138
+ | `remoteGlobMaxResults` | `200` | `remote_glob` 结果上限 |
139
+ | `remoteGlobMaxDepth` | `4` | `remote_glob` 递归深度上限 |
140
+ | `remoteGrepMaxResults` | `200` | `remote_grep` 结果上限 |
119
141
  | `transferChunkTimeoutMs` | `30000` | SFTP 分块读写看门狗;卡死时关闭会话,UI 可从 offset 续传 |
120
142
  | `connectTimeoutMs` | `15000` | SSH 建连超时 |
121
143
  | `hostKeyMode` | `strict` | `strict` / `accept-new` |
@@ -124,7 +146,7 @@ ssh_workbench_target({ server: "local" })
124
146
  | `poolMaxServers` | `8` | 连接池最多缓存的目标数 |
125
147
  | `maxChannelsPerServer` | `6` | 单连接并发通道上限 |
126
148
  | `prewarm` | `true` | UI 选中服务器时预热连接 |
127
- | `targetContext` | `true` | 自动绑定 `@资源名` 并注入目标上下文;关掉后不自动绑定(仍可手动用 `ssh_workbench_target`) |
149
+ | `targetContext` | `true` | 每轮注入纳管资源清单与 `@资源名` 提示;关掉后不注入上下文(工具仍可显式调用) |
128
150
  | `localRoot` | `""`(空 = 用户主目录) | 工作台本地文件面板与本地终端的根目录;必须是绝对路径或 `~/...`,非法值回退用户主目录 |
129
151
 
130
152
  > 右侧栏的工作台开关不在 `cordis.patch.yml` 里,而是在 **插件管理 → 远程工作台** 页面里的「在侧边栏显示远程工作台」;它只影响 UI 入口,不影响模型工具。
@@ -148,7 +170,7 @@ npm run test:ssh:sftp # SFTP 集成(进程内 ssh2.Server)
148
170
  npm run pack # 打包到 dist/
149
171
  ```
150
172
 
151
- 更多文档:[快速上手](docs/quickstart.md) · [架构说明](docs/architecture.md) · [贡献指南](CONTRIBUTING.md) · [安全说明](SECURITY.md) · [更新日志](CHANGELOG.md)。
173
+ 更多文档:[快速上手](docs/quickstart.md) · [执行模型](docs/execution-model.md) · [架构说明](docs/architecture.md) · [贡献指南](CONTRIBUTING.md) · [安全说明](SECURITY.md) · [更新日志](CHANGELOG.md)。
152
174
 
153
175
  ## 参与贡献
154
176
 
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.8)
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,46 +48,58 @@ 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`(写入需在插件管理里打开「允许远程写入」,对应 prefs 覆盖或 `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?, run_in_background? }], 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
+ - **后台任务**:每个 task 可单独设置 `run_in_background`;启动后该任务立即返回远端 pid 与日志,其余 task 继续并发执行。
79
+ - **UI**:composer 下方 pill 是执行账本的只读投影,结构对齐 DSH 内置 Token 用量弹窗:标题行「远程执行」+ 本 Session 总次数、0.5px 分隔线、目标名 → 执行次数。不显示成功/失败、用时或模型指标,也不承担切换或打开工作台;它只反映工具调用的开始/结束,后台任务启动后 pill 消失不代表远端进程结束。终端/文件从左侧工作台进入。弹层通过 `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
 
83
- - POSIX:`sh -c '<脚本>'` 内用 `nohup "$SHELL" -lc` 启动命令,stdout/stderr 重定向到 `mktemp /tmp/dsh-ssh-workbench-bg.XXXXXX` 生成的日志;启动器打印 `__SDH_BG_PID__` / `__SDH_BG_LOG__` 标记。
100
+ - POSIX:`sh -c '<脚本>'` 内用 `nohup "$SHELL" -lc` 启动命令,stdout/stderr 重定向到 `~/.dsh/ssh-workbench/logs` 下 `mktemp` 生成的日志(`DSH_SSH_WORKBENCH_LOG_DIR` 可覆盖,目录不可写时回退 `/tmp`);启动器打印 `__SDH_BG_PID__` / `__SDH_BG_LOG__` 标记。
84
101
  - Windows:外层 PowerShell 用 `Start-Process -RedirectStandardOutput/-RedirectStandardError -WindowStyle Hidden -PassThru` 启动内层编码脚本,输出 `__SDH_BG_PID__` / `__SDH_BG_LOG__` / `__SDH_BG_ERR__`。
85
- - 工具/路由解析标记后返回 pid、日志路径与读取/停止命令;启动失败(没有 pid)返回 `REMOTE_BACKGROUND_FAILED`。
102
+ - 工具/路由解析标记后返回 pid、日志路径与读取/停止命令;`remote_exec` 的每个 task 各自解析并返回自己的 pid/日志;启动失败(没有 pid)返回 `REMOTE_BACKGROUND_FAILED`(原生 bash 路由)或该 task 的 `background start failed` 错误。
86
103
  - 远端进程不在 DSH 本地 `job_*` 注册表里:`job_output` 看不到它,模型用 `tail` / `Get-Content` 取输出、`kill` / `Stop-Process` 停止;`timeoutMs` 与取消只作用于启动调用。
87
104
 
88
105
  ## 关键契约(来自 DSH,非本插件发明)
@@ -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` 配置。原生 `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 概览。