@zhangfengshun/dsh-remote-ssh 2.4.10 → 2.4.12
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 +31 -5
- package/README.md +6 -3
- package/README_EN.md +6 -3
- package/lib/client.js +1 -1
- package/lib/index.js +261 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
本文件的版本号与 `package.json` 的 `version` 保持一致。每个版本对应一个 Cordis Package 快照(`pkg-N`)。
|
|
4
4
|
|
|
5
|
+
## [2.4.12] — 公开产物脱敏(无功能改动)
|
|
6
|
+
### 变更
|
|
7
|
+
- **移除文档、代码注释与测试夹具中的真实标识**:此前 CHANGELOG / README / 代码注释 / issue 回复里出现了真实的项目名、HPC 主机名、内网 IP、真实远程路径与镜像目录 ID。现已全部替换为通用占位符(`proj-a` / `my-project` / `project-b`、`~/proj`、`hpc-a.example.com`、`192.0.2.10`(TEST-NET-1 文档网段)、`wmirror1`…、`solver`),并在 `MAINTENANCE.md` 写入硬规则:**公开产物一律用占位符**,发布前跑一次私有标识扫描。README 七夕段落里刻意保留的署名不受影响。
|
|
8
|
+
- **本版不含任何功能改动**:与 2.4.11 的代码逐字相同,仅为让 npm 上的 `latest` 产物不再包含上述标识(2.4.11 的 tarball 因 npm 撤回策略限制无法删除,见 `MAINTENANCE.md`)。
|
|
9
|
+
|
|
10
|
+
## [2.4.11] — `fs.search` 返回契约对齐 better-sidebar:补上 `matches`(issue #13)
|
|
11
|
+
### 修复
|
|
12
|
+
- **「文件」页签的「按文件名搜索」不再崩掉整块页签**(感谢 @Linhaojing 的契约考古与最小复现):better-sidebar 客户端契约是 `{ matches: string[], truncated }`(其注释原文:*matches are cwd-relative '/'-separated paths*,自 v0.13.0 引入搜索框起未变),并在渲染阶段直接读 `results.matches.length` / `results.matches.map(rel => …)`;而本插件的 `fs.search` 拦截只返回 `{ entries: [{path,isDir}], truncated }`,于是 `undefined.length` 抛 TypeError 被 `RenderBoundary` 兜住 → 整个页签变成错误条、搜索框与文件树一起消失。因为拦截是**无条件**的(不区分远程/本地会话),**本地工作区同样复现**,与 better-sidebar 版本无关。
|
|
13
|
+
- 两条分支统一经 `fsSearchResult()` 产出:`matches` **必定存在**且为 **cwd 相对、`/` 分隔**的字符串数组(与 better-sidebar 自带 `searchFiles` 的行为一致,结果行显示相对路径),`entries`(绝对路径 + `isDir`)保留作向前兼容;
|
|
14
|
+
- 本地分支:`matches` 由 `toPosixRelative(sessionCwd, entry.path)` 从绝对路径裁剪而来(Windows 盘符路径大小写不敏感、POSIX 保持大小写敏感、前缀相近的非子路径不误裁、非 ASCII 安全);
|
|
15
|
+
- **附带修复远程分支的路径 bug**:`remoteGlob` 的 `files` 是 `find` 以命令行起点拼出的**远端绝对路径**,此前却用 `join(mirrorBase, f)` 拼成了畸形镜像路径(如 `<镜像根>/home/user/proj/src/a.jl`),远程搜索结果的路径本来就是错的;现在 `remoteGlob` 新增 `opts.relative`(`-printf '%P\n'`)取相对路径,`entries` 再据此拼镜像绝对路径(`remote_ssh_glob` 工具仍走默认的 `%p`,行为不变)。
|
|
16
|
+
- README 兼容性表中原先笼统的 `fs.tree/read/write/search(4 端点契约)` 一行,已改为显式写明 `fs.search` 的 `{ matches, truncated }` 契约。
|
|
17
|
+
- **远程工作区的搜索不再一直「加载中…」**(同 issue 的实测跟进:崩溃修好后,在真实的大型远程项目工作区里搜索变成永久 loading)。旧命令有两个致命点:① `… | sort | head -n 500` 里的 `sort` 会缓冲**全部** `find` 输出后才吐第一行,`head` 的提前短路完全失效;② 没有深度/时间约束,而 `find` 是深度优先——巨型子目录(`processor*` / 数据目录)会吃光预算,连顶层文件都轮不到。实测该工作区:**旧命令 5 分钟零输出**(还占住池化 SSH 会话,把文件树/读写一起拖住)。现在:
|
|
18
|
+
- **浅层优先 + 有结果即返回**:第一趟 `-maxdepth 3`(3s 预算,实测冷 0.68s / 热 0.11s)——**只要有命中就立刻返回**,深挖转**后台预热缓存**(同一 query 的后续请求会拿到更全的结果);只有浅层**一无所获**时才同步等深挖趟(`-maxdepth 8`,5s 预算,实测会吃满)。此前条件是「浅层命中 < 200 就同步深挖」,而文件名片段极少命中 200 个 → 几乎每次查询都要多等一趟 ≤5s 的深挖(用户实测反馈「有点慢」),现已消除;两趟结果合并去重后本地排序(不再需要远端 `sort`)。实测同一工作区:**浅层 0.96s 返回 43 条命中**(旧命令 5 分钟 0 条);
|
|
19
|
+
- **遍历前剪噪声目录**:`REMOTE_SEARCH_SKIP_DIRS` = 本插件索引排除表 ∪ 上游 `SEARCH_SKIP_DIRS`(`.git` / `node_modules` / `dist` / `.next` / `.pnpm-store` / `.turbo` / …);
|
|
20
|
+
- **远端墙钟预算**:`timeout`(macOS 回退 `gtimeout`,都没有则退回无预算版本由 SSH 层兜底)到点杀掉 `find`,并把**已收集到的部分结果**照常返回 + `truncated: true`——宁可给部分结果,也不让 UI 无限转圈;
|
|
21
|
+
- **命中上限 200**(与上游 `DEFAULT_MAX_MATCHES` 一致)、**SSH 层 15s 超时**(不再占用池化会话 120s)、**同 query 30s 短缓存 + 并发合并**(连打键盘不会堆起一串 `find`),缓存挂 `cacheEpoch`(写/exec 后自动失效)。
|
|
22
|
+
- **`@文件名` 在某些远程项目里完全没有候选**(用户实测反馈:同一台机器上两个远程项目对照:一个正常、另一个全空;而单独输入 `@` 正常——因为 `@` 走目录列举、模糊查询走索引)。根因:索引在 git 仓库里首选 `git ls-files --cached --others --exclude-standard`,而 `--others` 需要**遍历整棵工作树**枚举未跟踪文件,在巨型项目上根本跑不完(实测该目录 **10s 被超时杀掉、零输出**;`git ls-files --cached` 反而只要 0.085s 且为 0 条——仓库里没有任何已跟踪文件)→ 20s 构建预算内拿不到条目 → 索引为空。现在三级降级,每级都有墙钟预算:
|
|
23
|
+
- ① 完整 git(6s)→ ② 仅索引 git(3s,恒定快)→ ③ 有界 `find`(`maxdepth 3` + 5s;原为 `maxdepth 5` 且无预算,实测该目录 5 层遍历 >180s 都跑不完);
|
|
24
|
+
- 实测同一项目:新命令 **921 条 / 6.68s**(其中 916 条含 `solver`、`node_modules` 零残留),三级合计 ≤14s < 20s 构建预算;
|
|
25
|
+
- **另外加了即时兜底**:模糊查询在索引尚未就绪(大仓库要 6.7s 才建好,而单次查询只等 900ms)时,**不再直接返回空列表**,而是用与侧栏搜索同一套有界 find(剪噪声 + 无 `sort` + `maxdepth 3` + 5s 预算,实测 **0.65s**)立刻给出候选;索引建好后自动切换到更全的索引结果。
|
|
26
|
+
|
|
27
|
+
### 测试
|
|
28
|
+
- 新增 `tests/fs-search-contract.test.mjs`(**34 条断言**):`toPosixRelative`(POSIX / 尾部斜杠 / Windows 反斜杠 / 盘符大小写不敏感 / POSIX 大小写敏感 / 非子路径不误裁 / 空 base / null / 非 ASCII)、`fsSearchResult` 形状(`matches` 恒为字符串数组——**原崩溃点**、缺参数时仍为空数组而非 undefined、过滤空串与非字符串、`truncated` 归一化)、**契约模拟**(客户端 `.length` / `.map(rel)` 渲染相对路径 / 点击解析回 `entries.path`)、接线断言(两条分支、`remoteGlob` 的 `relative` 选项与 `%p` 默认值、不再有只回 `entries` 的旧分支)与 README 契约描述。
|
|
29
|
+
- 新增 `tests/remote-search.test.mjs`(**36 条断言**):两趟命令生成(`-maxdepth 3/8`、3s/5s 预算、无 `sort`、`%P`、`head 200`、`-prune`、`timeout`/`gtimeout` 探测与回退)、**延迟策略**(浅层有结果立即返回、后台预热写回缓存、浅层零命中才同步深挖、浅层满额不再深挖、预算到点返回部分结果 + `truncated`)、硬失败不深挖且不缓存、缓存命中与 `cacheEpoch` 失效、三个并发同 query 只跑一趟遍历、以及 `fs.search` 分支的接线。
|
|
30
|
+
|
|
5
31
|
## [2.4.10] — 非 loopback 访问不再全线 403:信任判定改用宿主 webRuntime.trustedHosts(issue #12)
|
|
6
32
|
### 修复
|
|
7
33
|
- **信任判定与 `/api` 网关、better-sidebar 同源**(感谢 @Linhaojing 的验证矩阵与自我更正后的方案):此前 `isTrusted()` 硬编码 loopback 白名单(`localhost` / `127.0.0.1` / `::1` / `0.0.0.0`),从局域网(例如经配对设备或反向代理访问)打开 GUI 时,本插件的 **5 类路由全部 403** —— `/remote-ssh/api/*` 全部 RPC、6 个被拦截的 `fs.*`、`git.*`、文件上传、`/sidebar/file`,也就是文件树 / 读写 / 搜索 / 重命名 / 删除 / git / 上传全线失效,插件几乎等于没装。而同一 Host 下 DSH 官方路由正常,说明是本插件单方面比宿主更严。
|
|
@@ -16,7 +42,7 @@
|
|
|
16
42
|
|
|
17
43
|
## [2.4.9] — 「添加工作区」目录选择器支持新建目录(issue #11)
|
|
18
44
|
### 新增
|
|
19
|
-
- **「添加工作区」弹窗的目录选择器新增「新建目录」**(感谢 @Linhaojing 的定位与接线建议):此前本地 / 远程两个 tab 都只有「打开 / 上级 / 选择此目录」,要把**尚不存在**的目录加为工作区(例如远端起新项目 `~/
|
|
45
|
+
- **「添加工作区」弹窗的目录选择器新增「新建目录」**(感谢 @Linhaojing 的定位与接线建议):此前本地 / 远程两个 tab 都只有「打开 / 上级 / 选择此目录」,要把**尚不存在**的目录加为工作区(例如远端起新项目 `~/work/new-project`)必须先跳出 DSH 用别的终端 `mkdir`。现在底部操作区多一个「📁 新建目录」按钮:点开输入行 → 填名字(回车即提交)→ 在当前 `path` 下创建 → **自动进入新目录**,紧接着点「选择此目录」即可成为工作区,全程不离开 DSH(对齐 VSCode「新建文件夹」的交互预期)。
|
|
20
46
|
- **两个 tab 都支持**:远程走既有 `api("mkdir")`(`mkdir -p` + 缓存失效,与远程资源管理器的「新建目录」同一条已验证链路);本地新增宿主 API `mkdirLocal`(`fs.mkdir(recursive)`),补齐此前本地侧完全没有入口的缺口;
|
|
21
47
|
- **名字校验**:只接受单个路径段——空名、`.`、`..`、含 `/` 或 `\` 一律拦截并给出中英文提示(避免「在 path 下新建」变成任意位置写入);
|
|
22
48
|
- **路径拼接**:`joinChild()` 沿用基路径的分隔符(POSIX `/`、Windows `\`,含 `C:\` 根与 `~` 前缀),不改变远端路径语义;
|
|
@@ -51,7 +77,7 @@
|
|
|
51
77
|
|
|
52
78
|
## [2.4.6] — 「文件」页签树根显示远程目录名(issue #9)
|
|
53
79
|
### 修复
|
|
54
|
-
- **树根标签不再显示本地镜像目录 ID**(感谢 @Linhaojing 的链路定位):better-sidebar「文件」页签的树根标签取自会话 cwd(= 本地镜像路径)的 basename,于是显示成 `
|
|
80
|
+
- **树根标签不再显示本地镜像目录 ID**(感谢 @Linhaojing 的链路定位):better-sidebar「文件」页签的树根标签取自会话 cwd(= 本地镜像路径)的 basename,于是显示成 `wmirror3` 之类的镜像目录名。这条链路**不经过任何 `fs.*` 路由**,且 Host 侧 `session.cwd` 虽已返回 `root` 字段但客户端只消费 `cwd`(`api.sessionCwd(...).then(r => setFetchedCwd(r.cwd))`,另有 `useSessionCwd` 直接读客户端会话列表的 `cwd`),因此只能在渲染层替换文本。现由客户端取 `listWorkspaces` 的 `mirrorPath`/`remotePath` 建立「镜像 basename → 远程 basename」映射,把树根那一行换成**远程目录名**(如 `my-project`),并给该行加 `title` 显示完整远程路径(如 `~/work/my-project`)。
|
|
55
81
|
- **定位不依赖构建期哈希类名**:根行是树里唯一带内联 `padding-left:6px` 的行,仅当该行内纯文本节点恰好等于某远程工作区的镜像 basename 时才替换——同名子条目、本地工作区、无根行标志(上游改版)时一律保守不动;
|
|
56
82
|
- **同步替换 + 幂等自愈**:与设置导航图标同样在 MutationObserver 回调(微任务、绘制前)内完成,不会先闪一下镜像 ID;React 重渲染还原文本时由 1s 复检与 30s 工作区轮询恢复。
|
|
57
83
|
|
|
@@ -185,7 +211,7 @@
|
|
|
185
211
|
|
|
186
212
|
## [2.2.0] — 远程命令连接复用(实测 ≈15× 提速)+ 协议/安全加固
|
|
187
213
|
### 性能
|
|
188
|
-
- **`remote_ssh_exec` 走持久会话池**:不再每次调用做完整 SSH 握手。真实 HPC(ssh.
|
|
214
|
+
- **`remote_ssh_exec` 走持久会话池**:不再每次调用做完整 SSH 握手。真实 HPC(ssh.hpc-a.example.com,2222 端口)实测:一次性连接单命令平均 **1292ms**,连接复用后单命令平均 **84.3ms**,**约 15.3× 提速**;`stdin` 参数经 heredoc 走同一条复用通道。
|
|
189
215
|
- **stderr/stdout 分离语义保留**:stderr 重定向到远端临时文件、退出码哨兵后 cat 回传并以第二个哨兵收尾(双哨兵协议)。
|
|
190
216
|
- **读路径保持轻量**:`ls`/`cat`/`grep`/`glob` 继续走 `2>&1` 合并协议,热路径避开子 shell 与临时文件开销。
|
|
191
217
|
- **大文件读取先 stat**:超过 4MB 只读前 4MB(`head -c` 截断并标记 truncated),不再让持久会话缓冲无限增长。
|
|
@@ -220,11 +246,11 @@
|
|
|
220
246
|
|
|
221
247
|
## [2.1.4] — 修复远程工作区标题卡在镜像目录名
|
|
222
248
|
### 修复
|
|
223
|
-
- `workspaceRegistry.create` 只对新建记录应用 title,旧工作区(早期版本未传 title)标题卡在镜像目录名(如 `
|
|
249
|
+
- `workspaceRegistry.create` 只对新建记录应用 title,旧工作区(早期版本未传 title)标题卡在镜像目录名(如 `wmirror1`)。创建时显式 `setTitle`,并新增启动自愈 `healWorkspaceTitles`(仅在标题等于镜像目录 basename 时修复,不覆盖用户自定义标题)。
|
|
224
250
|
|
|
225
251
|
## [2.1.3] — 远程工作区标题默认取远程路径最后一段
|
|
226
252
|
### 改进
|
|
227
|
-
- 创建远程工作区时的默认标题改为远程路径最后一段目录名(如 `~/
|
|
253
|
+
- 创建远程工作区时的默认标题改为远程路径最后一段目录名(如 `~/proj/DSH_Test → DSH_Test`),取不到有效段时回退为 `连接名:远程路径`;原生工作区标题带 🌐 前缀。
|
|
228
254
|
|
|
229
255
|
## [2.1.2] — 修复 ssh config RemoteForward 端口占用导致连接失败
|
|
230
256
|
### 修复
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
**一条命令安装**(无需 token、API Key 或额外配置):
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.
|
|
49
|
+
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.12
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
安装后**重启 DSH**。`@zhangfengshun/dsh-remote-ssh` 必须在 bundles 列表中排在 `dsh-better-sidebar` **之后**。
|
|
@@ -183,7 +183,7 @@ dsh plugin --profile <name> remove @zhangfengshun/dsh-remote-ssh
|
|
|
183
183
|
| --- | --- | --- |
|
|
184
184
|
| DSH | 0.1.5-rc.1(DSH Desktop v2.0.9) | ✅ 主机服务 / settings / tools / slot / 上传下载拦截全部咬合 |
|
|
185
185
|
| DSH | 0.1.2-rc.1 稳定线 | ✅(插件 2.3.x 时代基线) |
|
|
186
|
-
| dsh-better-sidebar | 0.15.0 – 0.18.0 | ✅ `fs.tree
|
|
186
|
+
| dsh-better-sidebar | 0.15.0 – 0.18.0 | ✅ `fs.tree`/`fs.read`/`fs.write` + `fs.search`(`{ matches: cwd 相对 '/'-分隔路径, truncated }` 契约,2.4.11 起;此前只回 `entries` 会让「按文件名搜索」崩掉整块页签) |
|
|
187
187
|
| dsh-better-sidebar | 0.19.x | ⚠️ 插件侧已适配 6 端点(含 `fs.rename`/`fs.remove`);但 0.19.0/0.19.1 自身在 DSH Desktop 上主机半边无法加载,需等上游修复(见[安装](#安装)的警告) |
|
|
188
188
|
| 远程主机 sshd | 标准 OpenSSH(Linux / 超算 / Windows) | ✅ 密钥认证;密码认证需本机 `sshpass`(POSIX) |
|
|
189
189
|
|
|
@@ -198,11 +198,14 @@ dsh plugin --profile <name> remove @zhangfengshun/dsh-remote-ssh
|
|
|
198
198
|
| 侧边栏文件页签显示「这类内容还没有可用的查看方式。」 | `dsh-better-sidebar` 主机半边未加载:0.18.1 / 0.19.0 / 0.19.1 在 DSH Desktop 上会因 `SessionLogOffset` 运行时导入失败——降到 0.18.0 或使用修复版(上游 PR [#641](https://github.com/omdsh-dev/DSH-better-sidebar/pull/641)) |
|
|
199
199
|
| 内置「终端」页签连不上 | 终端为 `ssh -tt` 交互式通道,**仅支持密钥认证**;密码认证的连接会回退为本地 shell 并打印一行提示(避免把本地 shell 误认为已连上远程),密码认证请改用「文件」页签与模型工具 |
|
|
200
200
|
| 终端落在远程 `$HOME` 而不是工作区目录 | 2.4.5 起已修复(wrapper 会 `cd` 到工作区 `remotePath`,目录不存在时回退 `$HOME`);若仍停在 `$HOME`,确认 2.4.5 已装入并重启 DSH |
|
|
201
|
-
| 「文件」页签树根显示镜像目录 ID(如 `
|
|
201
|
+
| 「文件」页签树根显示镜像目录 ID(如 `wmirror3`) | 2.4.6 起已修复:树根改为显示**远程目录名**(如 `my-project`),悬停可见完整远程路径;该标签不经过 `fs.*` 路由,由客户端渲染层替换 |
|
|
202
202
|
| `@` 补全只搜到镜像里那几个文件 | 2.4.7 起已修复:远程工作区会话的 `@` 补全改列远端文件(索引缓存 60s + 900ms 查询预算);若仍只有镜像文件,确认 2.4.7 已装入并重启 DSH |
|
|
203
203
|
| 大仓里 `@` 搜不到真实文件(如根目录 `AGENTS.md`、`src/**`) | 2.4.8 起已修复:此前排除目录发生在截断之后,`node_modules/` 这类目录会吃光索引配额;现在排除由远端 `grep`/`-prune` 在截断前完成,并会在索引达上限时打 warn 提示 |
|
|
204
204
|
| 想加的远程目录还不存在,「添加工作区」里没法创建 | 2.4.9 起「目录选择器」底部有「📁 新建目录」(本地 / 远程 tab 均有):输入名字即可就地创建并自动进入 |
|
|
205
205
|
| 从局域网 / 另一台设备访问时插件文件能力全部报 403 | 2.4.10 起已修复:信任判定改用宿主 `ctx.webRuntime.trustedHosts`(与 `/api` 网关同源)。把访问地址加进 DSH 信任列表即可:启动时加 `--trusted-host <host[:port]>`(或经配对设备访问);未配置时行为与之前一致(仅本机 loopback) |
|
|
206
|
+
| 「文件」页签的「按文件名搜索」一输入就报 `Cannot read properties of undefined (reading 'length')` | 2.4.11 起已修复:`fs.search` 拦截此前只返回 `entries`,而 better-sidebar 客户端契约是 `{ matches, truncated }`;现在补上 `matches`(cwd 相对、`/` 分隔,与上游自带实现一致)并保留 `entries` |
|
|
207
|
+
| 远程工作区里「按文件名搜索」一直转圈(大工作区) | 2.4.11 起已修复:改为浅层优先(`-maxdepth 3`,实测冷 0.68s / 热 0.11s)且**有命中就立即返回**(深挖转后台预热缓存,浅层零命中才同步等深挖 `-maxdepth 8`),遍历前剪噪声目录、去掉会阻塞短路的 `sort`,并加远端墙钟预算——到点返回**已收集的部分结果**并标记不完整。实测某大型远程项目工作区:旧实现 5 分钟零输出 → 现在 **0.96s 返回 43 条** |
|
|
208
|
+
| 远程会话里 `@文件名` 没有候选,但单独输入 `@` 有 | 2.4.11 起已修复:模糊查询依赖索引,而索引首选 `git ls-files --cached --others`(`--others` 要遍历整棵工作树,巨型项目上跑不完 → 索引为空)。现在三级降级(完整 git 6s → 仅索引 git 3s → 有界 `find` `maxdepth 3` + 5s),并在索引未就绪时用有界 find 即时兜底(实测 0.65s),不再出现「全空」 |
|
|
206
209
|
| 安装时提示 `minimumReleaseAge` 或「No matching version」(刚发布) | npm 供应链新鲜度策略,等 1–5 分钟后重试即可 |
|
|
207
210
|
| 命令卡住不返回 | 默认 120s 超时后自动丢弃会话;长时任务用 `timeoutMs: 0`,随时可用 `remote_ssh_kill` 强杀 |
|
|
208
211
|
| 大文件读取被截断 | 单文件读取上限 4MB、下载池化路径约 6.29MB(更大自动回落一次性连接);用 `remote_ssh_exec` + `head`/`tail` 分段处理 |
|
package/README_EN.md
CHANGED
|
@@ -46,7 +46,7 @@ A **DSH** plugin like **VSCode Remote-SSH**: connect to remote HPC / servers via
|
|
|
46
46
|
**One command** (no token, API key or extra configuration needed):
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.
|
|
49
|
+
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@2.4.12
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
**Restart DSH** after installation. `@zhangfengshun/dsh-remote-ssh` must come **after** `dsh-better-sidebar` in the bundles list.
|
|
@@ -183,7 +183,7 @@ All SSH commands default to a **120-second** timeout (issue #5): a hung remote c
|
|
|
183
183
|
| --- | --- | --- |
|
|
184
184
|
| DSH | 0.1.5-rc.1 (DSH Desktop v2.0.9) | ✅ host services / settings / tools / slots / upload & download interception all compatible |
|
|
185
185
|
| DSH | 0.1.2-rc.1 stable line | ✅ (the 2.3.x-era baseline) |
|
|
186
|
-
| dsh-better-sidebar | 0.15.0 – 0.18.0 | ✅ `fs.tree
|
|
186
|
+
| dsh-better-sidebar | 0.15.0 – 0.18.0 | ✅ `fs.tree`/`fs.read`/`fs.write` + `fs.search` (the `{ matches: cwd-relative '/'-separated paths, truncated }` contract, since 2.4.11; returning only `entries` used to crash the whole Files tab on search) |
|
|
187
187
|
| dsh-better-sidebar | 0.19.x | ⚠️ this plugin already supports the 6-endpoint contract (incl. `fs.rename`/`fs.remove`); 0.19.0/0.19.1 themselves cannot load their host half on DSH Desktop until upstream fixes it (see the warning under [Installation](#installation)) |
|
|
188
188
|
| Remote sshd | standard OpenSSH (Linux / HPC / Windows) | ✅ key auth; password auth needs `sshpass` on the host (POSIX) |
|
|
189
189
|
|
|
@@ -198,11 +198,14 @@ The plugin never patches DSH sources or injects into the profile dependency tree
|
|
|
198
198
|
| Sidebar Files tab says "Nothing here can view this kind of content yet." | `dsh-better-sidebar` host half failed to load: 0.18.1 / 0.19.0 / 0.19.1 hit the `SessionLogOffset` runtime import on DSH Desktop — downgrade to 0.18.0 or use a fixed build (upstream PR [#641](https://github.com/omdsh-dev/DSH-better-sidebar/pull/641)) |
|
|
199
199
|
| Built-in Terminal tab cannot connect | The terminal is an `ssh -tt` interactive channel and supports **key auth only**; password-auth profiles fall back to a local shell and now print a one-line notice (so a local shell is not mistaken for a remote one) — use the Files tab and the model tools for password auth |
|
|
200
200
|
| Terminal opens in the remote `$HOME` instead of the workspace directory | Fixed in 2.4.5 (the wrapper `cd`s into the workspace `remotePath`, falling back to `$HOME` when it no longer exists); if it still starts in `$HOME`, make sure 2.4.5 is installed and DSH restarted |
|
|
201
|
-
| Files tab tree root shows the mirror directory id (e.g. `
|
|
201
|
+
| Files tab tree root shows the mirror directory id (e.g. `wmirror3`) | Fixed in 2.4.6: the root row now shows the **remote directory name** (e.g. `my-project`) with the full remote path on hover; that label never passes through the `fs.*` routes, so the client renders the replacement |
|
|
202
202
|
| `@` completion only finds the few files in the mirror | Fixed in 2.4.7: in a remote-workspace session `@` now lists remote files (60s index cache + 900ms query budget); if only mirror files show up, make sure 2.4.7 is installed and DSH restarted |
|
|
203
203
|
| In a large repo `@` cannot find real files (e.g. root `AGENTS.md`, `src/**`) | Fixed in 2.4.8: exclusion used to run *after* truncation, so a `node_modules/` tree could exhaust the index quota; exclusion now happens remotely (`grep`/`-prune`) before truncation, and hitting the cap logs a warning |
|
|
204
204
|
| The remote directory you want does not exist yet and "Add Workspace" cannot create it | Fixed in 2.4.9: the directory picker has a "📁 New folder" button (both tabs) — type a name to create it in place and enter it automatically |
|
|
205
205
|
| Accessing from the LAN / another device makes every file capability return 403 | Fixed in 2.4.10: the trust fence now reads the host's `ctx.webRuntime.trustedHosts` (same source as the `/api` gateway). Add the address to the DSH trust list — start with `--trusted-host <host[:port]>`, or access through your paired remote-access setup; with nothing configured the behaviour is unchanged (loopback only) |
|
|
206
|
+
| The Files tab's "search by file name" crashes with `Cannot read properties of undefined (reading 'length')` | Fixed in 2.4.11: the `fs.search` interception returned only `entries`, while better-sidebar's client contract is `{ matches, truncated }`; it now provides `matches` (cwd-relative, `/`-separated, matching upstream's own implementation) and keeps `entries` |
|
|
207
|
+
| "Search by file name" spins forever in a remote workspace (large trees) | Fixed in 2.4.11: shallow-first (`-maxdepth 3`, measured 0.68s cold / 0.11s warm) **returning as soon as anything matches** (the deeper pass becomes a background cache warm-up and only runs synchronously when the shallow pass finds nothing), noise directories pruned before traversal, the short-circuit-blocking `sort` removed, and a remote wall-clock budget that returns the **partial results collected so far** and flags them as incomplete. Measured on an 大型项目 workspace: 5 minutes with zero output before → **43 matches in 0.96s** now |
|
|
208
|
+
| In a remote session `@filename` shows no candidates, while a bare `@` works | Fixed in 2.4.11: fuzzy queries rely on the index, and the index preferred `git ls-files --cached --others` (`--others` must walk the whole working tree and never finishes on huge projects → empty index). It now degrades in three budgeted steps (full git 6s → index-only git 3s → bounded `find` `maxdepth 3` + 5s) and falls back to a bounded `find` (measured 0.65s) whenever the index is not ready yet, so candidates are never empty |
|
|
206
209
|
| Install fails with `minimumReleaseAge` or "No matching version" right after a release | npm supply-chain freshness policy — retry after 1–5 minutes |
|
|
207
210
|
| A command hangs forever | The 120s timeout discards the pooled session automatically; use `timeoutMs: 0` for long jobs and `remote_ssh_kill` at any time |
|
|
208
211
|
| Large files are truncated | 4MB per read, ≈6.29MB on the pooled download path (larger files fall back to a one-shot connection); use `remote_ssh_exec` with `head`/`tail` to page through |
|
package/lib/client.js
CHANGED
|
@@ -1333,7 +1333,7 @@ window.__ModuleLoader__.load({
|
|
|
1333
1333
|
|
|
1334
1334
|
// ---- 文件树根标签:显示远程目录名,而不是本地镜像目录 ID(issue #9)----
|
|
1335
1335
|
// better-sidebar「文件」页签的树根标签取自会话 cwd(= 本地镜像路径)的 basename,
|
|
1336
|
-
// 于是显示成镜像目录 ID(如
|
|
1336
|
+
// 于是显示成镜像目录 ID(如 wmirror3)。这条链路不经过任何 fs.* 路由,
|
|
1337
1337
|
// 而 Host 侧 session.cwd 虽然已返回 root 字段,客户端只消费 cwd
|
|
1338
1338
|
// (client-registry 的 api.sessionCwd(...).then(r => setFetchedCwd(r.cwd)),
|
|
1339
1339
|
// 另有 useSessionCwd 直接读客户端会话列表的 cwd),因此只能在渲染层替换文本。
|
package/lib/index.js
CHANGED
|
@@ -877,9 +877,25 @@ async function remoteGrep(runner, p, pattern, path, opts) {
|
|
|
877
877
|
return { ok: true, pattern: pattern, path: target, matches: matches, truncated: truncated, error: "" };
|
|
878
878
|
}
|
|
879
879
|
|
|
880
|
+
/** 侧栏文件名搜索要剪掉的噪声目录:定义见下方 REF_EXCLUDED_DIRS 之后
|
|
881
|
+
* (模块级 const 不能引用尚未初始化的绑定 —— 曾因展开 REF_EXCLUDED_DIRS 触发 TDZ,
|
|
882
|
+
* 导致整个插件树加载失败,故这里只留说明,实体定义放在其依赖之后)。 */
|
|
883
|
+
|
|
880
884
|
/**
|
|
881
885
|
* 在远端按 glob 模式查找文件(find -name)。find 已递归;pattern 为 basename 通配
|
|
882
886
|
* (如 .py 后缀),自动剥离前导双星号-斜杠与星号-斜杠以适配 POSIX find -name。
|
|
887
|
+
*
|
|
888
|
+
* 选项:
|
|
889
|
+
* - `relative`:用 `-printf '%P\n'` 输出**相对起始目录**的路径(issue #13:侧栏文件名搜索
|
|
890
|
+
* 要 cwd 相对路径;默认仍输出 find 原样的绝对路径,供 remote_ssh_glob 工具使用)。
|
|
891
|
+
* - `sorted !== false`:管道里加 `sort`。搜索路径必须关掉它 —— `sort` 会缓冲**全部** find
|
|
892
|
+
* 输出后才吐第一行,使 `head` 的提前短路完全失效(issue #13 实测:一个大型远程项目工作区
|
|
893
|
+
* 5 分钟都跑不完,UI 一直「加载中…」,还占住池化会话阻塞其他操作)。
|
|
894
|
+
* - `pruneNoise`:遍历前剪掉 REMOTE_SEARCH_SKIP_DIRS。
|
|
895
|
+
* - `maxDepth`:`-maxdepth N`(浅层优先搜索用;不设则全深度)。
|
|
896
|
+
* - `timeBudgetSec`:远端墙钟预算(`timeout`/`gtimeout`,存在才用),到点杀掉 find 并返回
|
|
897
|
+
* **已收集到的部分结果** + truncated,避免把用户吊死在一次遍历上。
|
|
898
|
+
* - `timeoutMs`:SSH 层命令超时(默认沿用全局 120s)。
|
|
883
899
|
*/
|
|
884
900
|
async function remoteGlob(runner, p, pattern, path, opts) {
|
|
885
901
|
opts = opts || {};
|
|
@@ -887,10 +903,37 @@ async function remoteGlob(runner, p, pattern, path, opts) {
|
|
|
887
903
|
let pat = String(pattern || "").trim();
|
|
888
904
|
pat = pat.replace(/^(\*\*\/|\*\/)+/, "");
|
|
889
905
|
const max = parseInt(String(opts.maxResults || 500), 10) || 500;
|
|
890
|
-
const
|
|
891
|
-
const
|
|
892
|
-
const
|
|
893
|
-
|
|
906
|
+
const depth = parseInt(String(opts.maxDepth || 0), 10) || 0;
|
|
907
|
+
const fmt = opts.relative ? "%P" : "%p";
|
|
908
|
+
const prune = opts.pruneNoise
|
|
909
|
+
? "\\( " + Array.from(REMOTE_SEARCH_SKIP_DIRS).map((n) => "-name " + shellQuote(n)).join(" -o ") + " \\) -prune -o "
|
|
910
|
+
: "";
|
|
911
|
+
const findCmd = "find " + shellQuotePath(target) + " " + (depth > 0 ? "-maxdepth " + depth + " " : "")
|
|
912
|
+
+ prune + "-name " + shellQuote(pat) + " -printf '" + fmt + "\\n' 2>/dev/null";
|
|
913
|
+
const pipeline = findCmd + (opts.sorted === false ? "" : " | sort") + " | head -n " + max;
|
|
914
|
+
let cmd = pipeline;
|
|
915
|
+
const budget = parseInt(String(opts.timeBudgetSec || 0), 10) || 0;
|
|
916
|
+
if (budget > 0) {
|
|
917
|
+
// timeout 是 GNU coreutils(Linux 常见),macOS 需 gtimeout;都没有时退回无预算版本,
|
|
918
|
+
// 由 SSH 层超时兜底(此时不会拿到部分结果)。
|
|
919
|
+
cmd = [
|
|
920
|
+
"T=$(command -v timeout || command -v gtimeout || true)",
|
|
921
|
+
"if [ -n \"$T\" ]; then",
|
|
922
|
+
"\"$T\" " + budget + " " + pipeline,
|
|
923
|
+
"else",
|
|
924
|
+
pipeline,
|
|
925
|
+
"fi"
|
|
926
|
+
].join("\n");
|
|
927
|
+
}
|
|
928
|
+
const r = await runner(p, cmd, undefined, MAX_BYTES, false, opts.timeoutMs);
|
|
929
|
+
const files = String(r && r.stdout ? r.stdout : "").split("\n").filter(function (s) { return !!s; });
|
|
930
|
+
// 预算到点(timeout → 124)或被 kill 时,stdout 里已有部分结果:照常返回并标记 truncated,
|
|
931
|
+
// 让用户看到「部分结果」而不是一个错误或永久加载。
|
|
932
|
+
if (r && r.ok === false && files.length === 0) {
|
|
933
|
+
return { ok: false, pattern: pattern, path: target, files: [], truncated: false, error: (r && r.error) || "glob failed" };
|
|
934
|
+
}
|
|
935
|
+
const budgetHit = !!(r && (r.ok === false || r.exitCode === 124 || r.timeout));
|
|
936
|
+
return { ok: true, pattern: pattern, path: target, files: files, truncated: !!((r && r.truncated) || budgetHit || files.length >= max), error: "" };
|
|
894
937
|
}
|
|
895
938
|
|
|
896
939
|
/**
|
|
@@ -902,6 +945,111 @@ function spawnOne(subprocess, argv, stdio, cwd) {
|
|
|
902
945
|
return subprocess.spawn({ argv: argv, cwd: cwd || process.cwd(), stdio: stdio, graceMs: 60000 });
|
|
903
946
|
}
|
|
904
947
|
|
|
948
|
+
/** 侧栏文件名搜索的预算与缓存(issue #13 实测:一个大型远程项目工作区 `find` 5 分钟都跑不完,
|
|
949
|
+
* UI 一直「加载中…」,而且池化 SSH 会话被这条命令占住,其他操作一起卡)。
|
|
950
|
+
*
|
|
951
|
+
* 实测该工作区:顶层仅 12 个条目、命中集中在 depth 1–3(0.02–0.42s),depth 4 起耗时跳到 3s+
|
|
952
|
+
* 且不再有新命中——`find` 是深度优先,巨型子目录会吃光预算,连顶层文件都轮不到。
|
|
953
|
+
* 因此分两趟:先浅层(快、通常已够);**浅层一无所获才同步等深挖**,有结果时深挖转后台预热
|
|
954
|
+
* 缓存(否则常见查询都要多等一趟 ≤5s 的深挖,实测「有点慢」)。两趟都受远端墙钟预算约束。 */
|
|
955
|
+
const REF_SEARCH_MAX_MATCHES = 200; // 与上游 DEFAULT_MAX_MATCHES 一致
|
|
956
|
+
const REF_SEARCH_SHALLOW_DEPTH = 3; // 第一趟深度
|
|
957
|
+
const REF_SEARCH_SHALLOW_BUDGET_SEC = 3; // 第一趟墙钟预算
|
|
958
|
+
const REF_SEARCH_DEEP_DEPTH = 8; // 第二趟深度(仅在浅层未找满时)
|
|
959
|
+
const REF_SEARCH_DEEP_BUDGET_SEC = 5; // 第二趟墙钟预算
|
|
960
|
+
const REF_SEARCH_TIMEOUT_MS = 15000; // SSH 层超时(单趟预算 + 余量),避免占住池化会话
|
|
961
|
+
const REF_SEARCH_TTL_MS = 30000; // 相同 query 的短缓存(连打键盘/重复打开搜索框时省一次遍历)
|
|
962
|
+
const remoteSearchCache = new Map(); // key -> { at, epoch, result }
|
|
963
|
+
const remoteSearchInflight = new Map(); // key -> Promise(并发合并同一 query)
|
|
964
|
+
|
|
965
|
+
/** 远端文件名搜索:浅层优先 + 按需深挖 + 剪噪声目录 + 无 sort(head 可提前短路)+ 部分结果降级。 */
|
|
966
|
+
async function remoteSearch(runner, profile, remoteRoot, query) {
|
|
967
|
+
const pk = profileKey(profile);
|
|
968
|
+
const key = pk + "|" + String(remoteRoot || "~") + "|" + String(query || "");
|
|
969
|
+
const epoch = cacheEpoch(pk);
|
|
970
|
+
const hit = remoteSearchCache.get(key);
|
|
971
|
+
if (hit && hit.epoch === epoch && Date.now() - hit.at <= REF_SEARCH_TTL_MS) return hit.result;
|
|
972
|
+
let inflight = remoteSearchInflight.get(key);
|
|
973
|
+
if (!inflight) {
|
|
974
|
+
inflight = (async () => {
|
|
975
|
+
try {
|
|
976
|
+
const q = String(query || "");
|
|
977
|
+
const common = {
|
|
978
|
+
relative: true,
|
|
979
|
+
sorted: false,
|
|
980
|
+
pruneNoise: true,
|
|
981
|
+
maxResults: REF_SEARCH_MAX_MATCHES,
|
|
982
|
+
timeoutMs: REF_SEARCH_TIMEOUT_MS
|
|
983
|
+
};
|
|
984
|
+
const first = await remoteGlob(runner, profile, "*" + q + "*", remoteRoot, Object.assign({}, common, {
|
|
985
|
+
maxDepth: REF_SEARCH_SHALLOW_DEPTH,
|
|
986
|
+
timeBudgetSec: REF_SEARCH_SHALLOW_BUDGET_SEC
|
|
987
|
+
}));
|
|
988
|
+
const files = (first && first.files) || [];
|
|
989
|
+
let truncated = !!(first && first.truncated);
|
|
990
|
+
let error = first && first.ok ? "" : (first && first.error) || "search failed";
|
|
991
|
+
// 深挖策略(延迟关键):**有结果就立刻返回**,深挖转后台预热缓存。
|
|
992
|
+
// 此前条件是「浅层命中 < 200 就深挖」,而文件名片段极少能命中 200 个 → 几乎每次查询都要
|
|
993
|
+
// 再等一趟最多 5s 的深挖(实测浅层 0.96s + 深挖 ≤5s ≈ 最多 6s,用户反馈「有点慢」)。
|
|
994
|
+
// 现在:浅层一无所获时才同步等深挖(此时用户没别的可看);否则后台跑,同一 query 的
|
|
995
|
+
// 后续请求/重复打开搜索框会命中更全的缓存结果。
|
|
996
|
+
const deepOpts = Object.assign({}, common, {
|
|
997
|
+
maxDepth: REF_SEARCH_DEEP_DEPTH,
|
|
998
|
+
timeBudgetSec: REF_SEARCH_DEEP_BUDGET_SEC
|
|
999
|
+
});
|
|
1000
|
+
const warmDeepPass = () => {
|
|
1001
|
+
Promise.resolve()
|
|
1002
|
+
.then(() => remoteGlob(runner, profile, "*" + q + "*", remoteRoot, deepOpts))
|
|
1003
|
+
.then((deep) => {
|
|
1004
|
+
if (!deep || !deep.ok || !deep.files || deep.files.length === 0) return;
|
|
1005
|
+
if (cacheEpoch(pk) !== epoch) return; // 期间发生过写/exec:结果已过期,不写入
|
|
1006
|
+
const cur = remoteSearchCache.get(key);
|
|
1007
|
+
const merged = new Set((cur && cur.result && cur.result.files) || files);
|
|
1008
|
+
for (const f of deep.files) merged.add(f);
|
|
1009
|
+
remoteSearchCache.set(key, {
|
|
1010
|
+
at: Date.now(),
|
|
1011
|
+
epoch: epoch,
|
|
1012
|
+
result: {
|
|
1013
|
+
ok: true,
|
|
1014
|
+
pattern: "*" + q + "*",
|
|
1015
|
+
path: remoteRoot,
|
|
1016
|
+
files: Array.from(merged).slice(0, REF_SEARCH_MAX_MATCHES).sort(),
|
|
1017
|
+
truncated: !!(deep.truncated || merged.size >= REF_SEARCH_MAX_MATCHES),
|
|
1018
|
+
error: ""
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
})
|
|
1022
|
+
.catch(() => {});
|
|
1023
|
+
};
|
|
1024
|
+
if (first && first.ok && files.length === 0) {
|
|
1025
|
+
// 浅层零命中:同步等深挖(用户此时没有可看的结果)
|
|
1026
|
+
const deep = await remoteGlob(runner, profile, "*" + q + "*", remoteRoot, deepOpts);
|
|
1027
|
+
const seen = new Set(files);
|
|
1028
|
+
for (const f of ((deep && deep.files) || [])) { if (!seen.has(f)) { seen.add(f); files.push(f); } }
|
|
1029
|
+
truncated = truncated || !!(deep && deep.truncated);
|
|
1030
|
+
if (deep && !deep.ok && files.length === 0) error = deep.error || error;
|
|
1031
|
+
} else if (first && first.ok && files.length > 0 && files.length < REF_SEARCH_MAX_MATCHES) {
|
|
1032
|
+
warmDeepPass(); // 后台预热:不增加本次响应延迟(浅层已满额时无需再深挖)
|
|
1033
|
+
}
|
|
1034
|
+
const result = {
|
|
1035
|
+
ok: files.length > 0 || !!(first && first.ok),
|
|
1036
|
+
pattern: "*" + q + "*",
|
|
1037
|
+
path: remoteRoot,
|
|
1038
|
+
files: files.slice(0, REF_SEARCH_MAX_MATCHES).sort(),
|
|
1039
|
+
truncated: truncated || files.length >= REF_SEARCH_MAX_MATCHES,
|
|
1040
|
+
error: files.length > 0 ? "" : error
|
|
1041
|
+
};
|
|
1042
|
+
if (result.ok) remoteSearchCache.set(key, { at: Date.now(), epoch, result: result });
|
|
1043
|
+
return result;
|
|
1044
|
+
} finally {
|
|
1045
|
+
remoteSearchInflight.delete(key);
|
|
1046
|
+
}
|
|
1047
|
+
})();
|
|
1048
|
+
remoteSearchInflight.set(key, inflight);
|
|
1049
|
+
}
|
|
1050
|
+
return await inflight;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
905
1053
|
function readCollected(handle, name) {
|
|
906
1054
|
if (!handle || !handle.collected || !handle.collected[name]) return "";
|
|
907
1055
|
const r = handle.collected[name].readFrom(0);
|
|
@@ -1207,14 +1355,26 @@ const REF_INDEX_TTL_MS = 60000;
|
|
|
1207
1355
|
const REF_QUERY_BUDGET_MS = 900;
|
|
1208
1356
|
/** 后台建索引的预算。 */
|
|
1209
1357
|
const REF_INDEX_BUILD_TIMEOUT_MS = 20000;
|
|
1210
|
-
/**
|
|
1211
|
-
const
|
|
1358
|
+
/** 索引回退 find 的深度上限(原为 5,在巨型目录上 5 层遍历会跑不完——实测 >180s)。 */
|
|
1359
|
+
const REF_INDEX_FIND_MAXDEPTH = 3;
|
|
1360
|
+
/** 远端各步的墙钟预算(秒):完整 git → 仅索引 git → find 回退,合计 14s < 上面的 20s 构建预算。 */
|
|
1361
|
+
const REF_INDEX_GIT_FULL_BUDGET_SEC = 6;
|
|
1362
|
+
const REF_INDEX_GIT_CACHED_BUDGET_SEC = 3;
|
|
1363
|
+
const REF_INDEX_FIND_BUDGET_SEC = 5;
|
|
1212
1364
|
/** 排除目录:与 @deepseek-ai/dsh-file-reference-local 的 DEFAULT_FILE_SEARCH_EXCLUDED_DIRECTORIES 逐条一致。 */
|
|
1213
1365
|
const REF_EXCLUDED_DIRS = new Set([
|
|
1214
1366
|
".git", "node_modules", "dist", "build", "out", "coverage", "target",
|
|
1215
1367
|
".next", ".nuxt", ".turbo", ".venv", "__pycache__", ".pytest_cache",
|
|
1216
1368
|
".mypy_cache", ".gradle"
|
|
1217
1369
|
]);
|
|
1370
|
+
|
|
1371
|
+
/** 侧栏文件名搜索要剪掉的噪声目录(上游 SEARCH_SKIP_DIRS 与本地索引排除表的并集)。
|
|
1372
|
+
* 在远端遍历前剪枝,避免把配额与时间花在依赖/构建产物上。
|
|
1373
|
+
* **必须定义在 REF_EXCLUDED_DIRS 之后**:模块级 const 的展开会在模块求值期立即读取该绑定。 */
|
|
1374
|
+
const REMOTE_SEARCH_SKIP_DIRS = new Set([
|
|
1375
|
+
...REF_EXCLUDED_DIRS,
|
|
1376
|
+
".pnpm-store", ".yarn", ".turbopack", ".output", ".cache", ".parcel-cache", ".umi", ".umi-production"
|
|
1377
|
+
]);
|
|
1218
1378
|
const refIndexCache = new Map(); // key = profileKey|root -> { at, epoch, entries }
|
|
1219
1379
|
const refIndexBuilds = new Map(); // key -> Promise<entries>(同一 workspace 只建一次)
|
|
1220
1380
|
const refTruncationWarned = new Set(); // 已就索引截断告警过的 workspace(避免每次重建都刷日志)
|
|
@@ -1325,27 +1485,77 @@ function refExcludeRegex() {
|
|
|
1325
1485
|
return "(^|/)(" + alts + ")(/|$)";
|
|
1326
1486
|
}
|
|
1327
1487
|
|
|
1328
|
-
/** 远端建索引命令:git
|
|
1488
|
+
/** 远端建索引命令:git 仓库优先(尊重 .gitignore),跑不完/为空时回退到有界 find。
|
|
1329
1489
|
*
|
|
1330
|
-
* 排除必须发生在 `head` **之前**(issue #10 实测反馈):`git ls-files --cached --others`
|
|
1490
|
+
* ① 排除必须发生在 `head` **之前**(issue #10 实测反馈):`git ls-files --cached --others`
|
|
1331
1491
|
* 的输出**不是全局字典序**——未跟踪文件按 readdir 顺序先输出,`node_modules/` 这类目录
|
|
1332
1492
|
* 可能占满前两万行,把 `head` 的配额吃光,导致 `AGENTS.md`、`src/**` 等真实文件被整段切掉
|
|
1333
1493
|
* (实测仓库:node_modules 18,880 条 = 84.5%,`AGENTS.md` 落在第 20417 行)。
|
|
1334
|
-
*
|
|
1494
|
+
* ② 每一步都要有**墙钟预算**(用户实测反馈:某个远程项目里 `@文件名` 完全没有候选,
|
|
1495
|
+
* 而 `@` 单独输入正常 —— 因为模糊查询走索引、目录列举不走):`git ls-files --others`
|
|
1496
|
+
* 需要遍历整棵工作树枚举未跟踪文件,在巨型目录上**根本跑不完**(实测 10s 被超时杀掉、
|
|
1497
|
+
* 零输出),于是索引为空。现在:完整 git(6s)→ 仅索引 git(3s,恒定快)→ 有界 find
|
|
1498
|
+
* (`maxdepth 3` + 5s,实测该目录 921 条 / 0.65s),三级合计 ≤14s < 20s 构建预算。
|
|
1335
1499
|
* 客户端的 excludedSegment 仍保留作双保险(见 refParseIndexOutput)。 */
|
|
1336
1500
|
function refIndexCommand(root) {
|
|
1337
1501
|
const target = shellQuotePath(root || "~");
|
|
1338
1502
|
const prune = Array.from(REF_EXCLUDED_DIRS).map((n) => "-name " + shellQuote(n)).join(" -o ");
|
|
1503
|
+
const cap = REF_MAX_ENTRIES + 1;
|
|
1504
|
+
const gitPipe = "| grep -vE '" + refExcludeRegex() + "' | head -n " + cap;
|
|
1505
|
+
const findFallback = "find . -mindepth 1 -maxdepth " + REF_INDEX_FIND_MAXDEPTH
|
|
1506
|
+
+ " \\( " + prune + " \\) -prune -o -printf '%y\\t%P\\n' 2>/dev/null | head -n " + cap;
|
|
1339
1507
|
return [
|
|
1340
1508
|
"cd " + target + " 2>/dev/null || exit 3",
|
|
1509
|
+
"T=$(command -v timeout || command -v gtimeout || true)",
|
|
1510
|
+
"G=",
|
|
1341
1511
|
"if [ -d .git ] && command -v git >/dev/null 2>&1; then",
|
|
1342
|
-
"git ls-files --cached --others --exclude-standard 2>/dev/null
|
|
1343
|
-
"
|
|
1344
|
-
"
|
|
1512
|
+
" if [ -n \"$T\" ]; then G=$(\"$T\" " + REF_INDEX_GIT_FULL_BUDGET_SEC + " git ls-files --cached --others --exclude-standard 2>/dev/null " + gitPipe + "); else G=$(git ls-files --cached --others --exclude-standard 2>/dev/null " + gitPipe + "); fi",
|
|
1513
|
+
" if [ -z \"$G\" ]; then",
|
|
1514
|
+
" if [ -n \"$T\" ]; then G=$(\"$T\" " + REF_INDEX_GIT_CACHED_BUDGET_SEC + " git ls-files --cached 2>/dev/null " + gitPipe + "); else G=$(git ls-files --cached 2>/dev/null " + gitPipe + "); fi",
|
|
1515
|
+
" fi",
|
|
1516
|
+
"fi",
|
|
1517
|
+
"if [ -n \"$G\" ]; then printf '%s\\n' \"$G\" | sed 's/^/f\t/'; else",
|
|
1518
|
+
" if [ -n \"$T\" ]; then \"$T\" " + REF_INDEX_FIND_BUDGET_SEC + " " + findFallback + "; else " + findFallback + "; fi",
|
|
1345
1519
|
"fi"
|
|
1346
1520
|
].join("\n");
|
|
1347
1521
|
}
|
|
1348
1522
|
|
|
1523
|
+
// ---------------------------------------------------------------------------
|
|
1524
|
+
// fs.search 的返回契约(issue #13)
|
|
1525
|
+
//
|
|
1526
|
+
// better-sidebar 客户端契约(v0.13.0 起至今未变,其 client-registry 注释原文:
|
|
1527
|
+
// "matches are cwd-relative '/'-separated paths"):
|
|
1528
|
+
// fs.search → { matches: string[], truncated: boolean }
|
|
1529
|
+
// 而本插件此前的两条分支只返回 { entries: [{path,isDir}], truncated } —— 客户端在渲染阶段
|
|
1530
|
+
// 读 results.matches.length / results.matches.map(...),undefined 直接抛 TypeError,
|
|
1531
|
+
// 被 RenderBoundary 兜住 → 整个「文件」页签变成错误条(本地会话也复现,因为拦截是无条件的)。
|
|
1532
|
+
// 现在统一经 fsSearchResult() 产出:matches 必须存在且为 cwd 相对、'/' 分隔的字符串数组;
|
|
1533
|
+
// entries(绝对路径 + isDir)保留作向前兼容。
|
|
1534
|
+
// ---------------------------------------------------------------------------
|
|
1535
|
+
|
|
1536
|
+
/** 把绝对路径裁成相对 base 的 POSIX 路径;不在 base 之下时回退为归一化后的绝对路径。 */
|
|
1537
|
+
function toPosixRelative(base, target) {
|
|
1538
|
+
const t = String(target == null ? "" : target).replace(/\\/g, "/");
|
|
1539
|
+
const b = String(base == null ? "" : base).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1540
|
+
if (!b) return t;
|
|
1541
|
+
// Windows 盘符路径大小写不敏感;POSIX 保持大小写敏感
|
|
1542
|
+
const fold = /^[a-z]:\//i.test(b);
|
|
1543
|
+
const tt = fold ? t.toLowerCase() : t;
|
|
1544
|
+
const bb = fold ? b.toLowerCase() : b;
|
|
1545
|
+
if (tt === bb) return "";
|
|
1546
|
+
if (tt.startsWith(bb + "/")) return t.slice(b.length + 1);
|
|
1547
|
+
return t;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** 组装 fs.search 的响应:matches 永远存在且为字符串数组(客户端会直接 .length / .map)。 */
|
|
1551
|
+
function fsSearchResult(matches, entries, truncated) {
|
|
1552
|
+
return {
|
|
1553
|
+
matches: Array.isArray(matches) ? matches.filter((m) => typeof m === "string" && m !== "") : [],
|
|
1554
|
+
entries: Array.isArray(entries) ? entries : [],
|
|
1555
|
+
truncated: !!truncated
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1349
1559
|
// ---------------------------------------------------------------------------
|
|
1350
1560
|
// apply
|
|
1351
1561
|
// ---------------------------------------------------------------------------
|
|
@@ -1622,7 +1832,7 @@ function apply(ctx, config) {
|
|
|
1622
1832
|
if (!p) return { ok: false, error: "未找到连接配置" };
|
|
1623
1833
|
if (!a.remotePath) return { ok: false, error: "remotePath 为必填项" };
|
|
1624
1834
|
const wsId = "w" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
1625
|
-
// 默认标题取远程路径最后一段目录名(如 ~/
|
|
1835
|
+
// 默认标题取远程路径最后一段目录名(如 ~/proj/DSH_Test → DSH_Test);
|
|
1626
1836
|
// 取不到有效段(如 ~/ 或 /)时回退为 连接名:远程路径。
|
|
1627
1837
|
const lastSeg = String(a.remotePath).replace(/\/+$/, "").split("/").pop();
|
|
1628
1838
|
const title = a.title || (lastSeg && lastSeg !== "~" ? lastSeg : (p.name + ":" + a.remotePath));
|
|
@@ -1653,7 +1863,7 @@ function apply(ctx, config) {
|
|
|
1653
1863
|
const native = await workspaceRegistry.create(mirrorDir, wsTitle);
|
|
1654
1864
|
workspaceId = native.id;
|
|
1655
1865
|
// workspaceRegistry.create 仅首次创建时应用 title;已存在的旧工作区
|
|
1656
|
-
// (早期版本未传 title,标题卡在镜像目录名,如
|
|
1866
|
+
// (早期版本未传 title,标题卡在镜像目录名,如 wmirror1)会被直接
|
|
1657
1867
|
// 返回而不更新。这里显式 setTitle,保证标题始终是远程路径最后一段目录名。
|
|
1658
1868
|
if (native && typeof native.setTitle === "function" && native.title !== wsTitle) {
|
|
1659
1869
|
await native.setTitle(wsTitle);
|
|
@@ -2308,7 +2518,7 @@ function apply(ctx, config) {
|
|
|
2308
2518
|
})();
|
|
2309
2519
|
|
|
2310
2520
|
// ---- 启动自愈:把既有远程工作区的原生标题统一为「远程路径最后一段」----
|
|
2311
|
-
// 早期版本 create 工作区时未传 title,DSH 用镜像目录 basename(wsId,如
|
|
2521
|
+
// 早期版本 create 工作区时未传 title,DSH 用镜像目录 basename(wsId,如 wmirror1)
|
|
2312
2522
|
// 作标题;workspaceRegistry.create 只对新建记录应用 title,旧记录会被原样返回。
|
|
2313
2523
|
// 2.3.2 起标题不再带 🌐 前缀(图标由客户端地球角标文件夹 SVG 承担),自愈同时剥掉
|
|
2314
2524
|
// 旧版遗留的 🌐 前缀。仅在标题等于已知旧形态时修复,不覆盖用户自定义标题。
|
|
@@ -2562,11 +2772,16 @@ function apply(ctx, config) {
|
|
|
2562
2772
|
const rp = localToRemote(localPath, remoteBase, remoteInfo.remotePath);
|
|
2563
2773
|
result = await remoteWriteFile(runPooled, profile, rp, payload.content || "");
|
|
2564
2774
|
} else if (method === "fs.search") {
|
|
2775
|
+
// remoteSearch:相对路径(%P)+ 剪噪声目录 + 无 sort(head 提前短路)+ 8s 远端预算
|
|
2776
|
+
// + 15s SSH 超时 + 同 query 合并/短缓存(issue #13:大工作区此前会一直「加载中…」)。
|
|
2565
2777
|
const rp = remoteInfo.remotePath;
|
|
2566
|
-
const r = await
|
|
2567
|
-
|
|
2568
|
-
return
|
|
2569
|
-
})
|
|
2778
|
+
const r = await remoteSearch(runPooled, profile, rp, payload.query || "");
|
|
2779
|
+
const matches = (r.files || []).map(function (f) {
|
|
2780
|
+
return String(f).replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2781
|
+
}).filter(function (f) { return f !== ""; });
|
|
2782
|
+
result = fsSearchResult(matches, matches.map(function (rel) {
|
|
2783
|
+
return { path: join(remoteBase, rel), isDir: false };
|
|
2784
|
+
}), r.truncated);
|
|
2570
2785
|
} else if (method === "fs.rename") {
|
|
2571
2786
|
// better-sidebar 0.19 新增端点:重命名必须落在远端,绝不能让本地镜像目录被改。
|
|
2572
2787
|
const localPath = payload.path || "";
|
|
@@ -2620,7 +2835,11 @@ function apply(ctx, config) {
|
|
|
2620
2835
|
} else if (method === "fs.write") {
|
|
2621
2836
|
result = await localWriteFile(payload.path, payload.content || "");
|
|
2622
2837
|
} else if (method === "fs.search") {
|
|
2623
|
-
|
|
2838
|
+
// matches 必须是 cwd 相对、'/' 分隔的路径(better-sidebar 客户端直接 .map 渲染并据此打开);
|
|
2839
|
+
// entries 保留绝对路径(本插件 fs.read 拦截据此映射回远端)。
|
|
2840
|
+
const r = await localSearchFiles(sessionCwd, payload.query || "");
|
|
2841
|
+
const entries = (r && r.entries) || [];
|
|
2842
|
+
result = fsSearchResult(entries.map(function (e) { return toPosixRelative(sessionCwd, e.path); }), entries, r && r.truncated);
|
|
2624
2843
|
} else if (method === "fs.rename") {
|
|
2625
2844
|
result = await localRenameEntry(payload.path, String(payload.name || ""));
|
|
2626
2845
|
} else if (method === "fs.remove") {
|
|
@@ -3180,9 +3399,28 @@ function apply(ctx, config) {
|
|
|
3180
3399
|
return await remoteRefListDirectory(profile, root, split.directory, split.fragment);
|
|
3181
3400
|
}
|
|
3182
3401
|
const entries = await refIndexFor(profile, root);
|
|
3183
|
-
if (
|
|
3184
|
-
|
|
3185
|
-
|
|
3402
|
+
if (entries.length) {
|
|
3403
|
+
const visible = entries.filter((e) => refVisibleForGlobalQuery(e.path, split.fragment));
|
|
3404
|
+
return refRankCandidates(visible, split.fragment, REF_MAX_RESULTS);
|
|
3405
|
+
}
|
|
3406
|
+
// 索引尚未就绪时不能直接给空列表 —— 那正是「@文件名 没有候选、而 @ 单独输入正常」的观感
|
|
3407
|
+
// (用户实测反馈:某个远程项目里模糊查询全空)。原因:大仓库里索引的 git 那一步会吃满预算
|
|
3408
|
+
// (实测 6.7s 才建好),而单次查询只等 REF_QUERY_BUDGET_MS=900ms。
|
|
3409
|
+
// 这里用与侧栏搜索同一套**有界 find** 兜底:剪噪声目录 + 无 sort + 浅层深度 + 墙钟预算(实测 0.65s)。
|
|
3410
|
+
const r = await remoteGlob(runPooled, profile, "*" + split.fragment + "*", root, {
|
|
3411
|
+
relative: true,
|
|
3412
|
+
sorted: false,
|
|
3413
|
+
pruneNoise: true,
|
|
3414
|
+
maxResults: REF_MAX_RESULTS,
|
|
3415
|
+
maxDepth: REF_INDEX_FIND_MAXDEPTH,
|
|
3416
|
+
timeBudgetSec: REF_INDEX_FIND_BUDGET_SEC,
|
|
3417
|
+
timeoutMs: 10000
|
|
3418
|
+
});
|
|
3419
|
+
if (!r || !r.files || !r.files.length) return [];
|
|
3420
|
+
const quick = r.files.map(function (f) {
|
|
3421
|
+
return { path: String(f).replace(/\\/g, "/").replace(/^\.\//, ""), kind: "file" };
|
|
3422
|
+
}).filter(function (e) { return e.path !== "" && refVisibleForGlobalQuery(e.path, split.fragment); });
|
|
3423
|
+
return refRankCandidates(quick, split.fragment, REF_MAX_RESULTS);
|
|
3186
3424
|
}
|
|
3187
3425
|
|
|
3188
3426
|
/** 该 agent 是远程工作区会话则返回远端候选,否则返回 null(调用方委托原实现)。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangfengshun/dsh-remote-ssh",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.12",
|
|
4
4
|
"description": "DSH web plugin: VSCode Remote-SSH-like remote development (SSH to supercomputers/servers, remote workspace, file explorer, integrated terminal), integrated with dsh-better-sidebar and DSH settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|