dsh-remote-ssh:把遠端機器當本地用的 DSH 插件

前言

DSH(DeepSeek Harness)跑的活,默認都落在本機工作區:agent 讀寫本地文件、終端在本地起。一旦任務的目標機器在另一臺服務器上,就要在本地工作區和遠端 SSH 會話之間反覆手工搬東西——開多個終端窗口、手動 scp、靠記憶記端口轉發。也有另一類做法:乾脆把整個 harness 搬到遠端執行。這個插件走的是中間路線:本地工作區仍是執行現場,遠端是它的「影子」,面板、終端、文件、同步、轉發都收進 DSH 側邊欄。

下面介紹的是 dsh-remote-ssh,由 LinHao100076 維護(倉庫 LinHao100076/dsh-ssh-remote,GitHub 3 stars,分類客戶端,MIT 許可)。它不是通用 SSH 客戶端,而是給 DSH 增加一個「遠程 SSH」面板,覆蓋文件樹、遠端終端、編輯同步、傳輸斷點續傳和 -L / -D / -R 三種轉發。

這是什麼

一句話定位:在 DSH 側邊欄裏瀏覽遠端文件系統、開遠端終端、按映射方向同步本地改動、並在本機與遠端之間建立端口轉發。維護者 LinHao100076,倉庫公開,MIT 許可。

倉庫對自身定位寫得很明確(見 docs/design.md 與 README.md 的「核心判斷」):

  1. 這不是 SSH 客戶端,而是「本地工作區的遠端影子」,因爲 agent 和側邊欄都在本地工作。
  2. DSH 有 ctx.fs / ctx.subprocess 執行世界抽象,理論上可以把整個 harness 搬到遠端;但 dsh-better-sidebar 繞過 ctx.fs 直讀本地,兩條路線混用會產生 split-brain。
  3. 因此 0.x 版本走「本地鏡像 + 旁掛遠端通道」,執行世界替換留作 1.0 的獨立 profile 實驗。

版本情況:package.json 裏是 0.7.0;CHANGELOG.md 中 0.7.0 標註日期爲 2026-11-06。README 正文仍寫着「當前版本 0.6.1 —— 第一個正式版」,兩個位置的說法不一致,以倉庫實際文件爲準。0.7.0 的主要增量是密碼 / OTP 認證和多跳跳板鏈路;0.6.1 修掉了 Node 20 上宿主端 bundle 加載即拋的問題,0.6.0 補 socks5 動態轉發與 -R,0.5.0 加斷點續傳,0.4.0 加遠端搜索。

入口與界面

插件提供一個「遠程 SSH」面板,在兩個位置出現,取哪條路取決於 dsh-better-sidebar 有沒有裝:

裝了什麼 出現在哪 怎麼打開
有 dsh-better-sidebar better-sidebar 側欄裏一個名爲「遠程 SSH」的標籤頁 側欄裏點「遠程 SSH」
沒有 / 停用了 dsh-better-sidebar DSH 自帶的右側邊欄,同一個面板 會話頭部右上角的服務器圖標按鈕

兩條路徑互斥:若 better-sidebar 在啓動後才激活,插件會把原生入口撤掉、換成同一個標籤頁,不會同時出現兩個入口。面板由主機頭 + 一條標籤條(文件 / 終端 / 同步 / 轉發 / 設置)+ 狀態欄組成,寬度按 424px 基準、跟着右側欄走,深色模式複用同一套 token 自動跟隨 DSH 主題。界面基準是倉庫裏的 ui/prototype.html(單文件可直接打開,支持 ?view=terminal 等直達鏈接)。

安裝與啓用

README 給的安裝方式是發佈包 + dsh plugin 命令,沒有提供 dsh plugin add github:owner/repo 形式的直接安裝:

npm run pack:rel     # 生成 dist/dsh-remote-ssh-<版本>.tgz
dsh plugin --profile <profile> add -w "/path/to/plugin.tgz"

兩點要注意:-w 不能省,profile 裏有 pnpm-workspace.yaml,不帶它會報 ERR_PNPM_ADDING_TO_ROOT;不要解壓 tgz 後指向解壓出來的目錄,pnpm 會寫成 link:,鏈接目錄不安裝自身依賴。0.3.1 起後一條路也已可用——發佈包內零運行期依賴,ssh2、ws、chokidar 全部被 esbuild 打進 lib/server.mjs(約 1 MiB)。

裝完 package.json 會變成:

"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-remote-ssh"] } },
"dependencies": { "dsh-remote-ssh": "file:C:/.../dsh-remote-ssh-<版本>.tgz" }

插件把主機與映射存在 ~/.dsh/storages/dsh-remote-ssh/state.json。密碼和口令永不落盤,只存在瀏覽器內存裏,每次連接現取。

啓動命令(app 參數直接跟在後面,不寫 web):

dsh --profile <name> --no-open --port 4987

desktop profile 由桌面應用獨佔,CLI 會拒絕(profile "desktop" is managed exclusively by the Electron application),這種 profile 請在應用內插件頁安裝,或手工改 package.json 的 bundles 與 cordis.patch.yml 後重啓應用。開發期想直連源碼目錄時,可以只在 cordis.patch.yml 裏插入:

- insert:
    - id: dsh-remote-ssh
      name: "dsh-remote-ssh"

核心功能

文件樹與遠端搜索

文件視圖提供遠端文件樹、文本預覽、上傳下載。工具條上的放大鏡在遠端跑 rg(沒有則 grep -r),只把命中的行拿回來,不下載文件、不建本地索引,所以能搜的範圍永遠比已映射的目錄大。

1、默認字面量 + smart-case:輸入 foo 忽略大小寫,輸入 Foo 區分大小寫,Aa 強制區分,.* 才按正則解釋。
2、結果按文件分組,點一條即打開文件並滾到那一行;超長行圍繞命中位置裁剪。
3、默認範圍是當前目錄,rg 尊重 .gitignore;沒有 rg 時 grep 顯式跳過 .git、node_modules、.venv、__pycache__、target、dist、build、.next、.cache、.idea。
4、上限 200 條且明確標出截斷,切斷髮生在遠端(head -n)。
5、輸出通道用 rg --json / grep -rInZ,避免 path:line:text 在帶冒號路徑或正文時解析錯;老 rg 不認 --json 時降級到 grep 並記住。

終端與 tmux

遠端有 tmux 時自動執行 tmux new -A -s dsh-<host>,刷新頁面、IDE 重啓、網絡抖動之後都重新接回同一個 shell。遠端沒有 tmux 時退回普通登錄 shell,並在終端上方明確警告「刷新頁面或斷線後,本次會話的進程會一起結束」,而不是假裝一切正常。兩個分支都在 test/e2e-profile.mjs 覆蓋,包括實際下發的 argv 形狀。

認證與跳板鏈路

0.7.0 補上了密碼登錄與 keyboard-interactive(2FA / OTP):只開 PasswordAuthentication 的服務器、只問鍵盤交互的服務器都能連;服務器連續問多個問題就彈多個框;猜錯一次後改爲直接問人,後臺重連遇到需要憑據的場景返回 needs-credential 而不是卡在看不見的對話框上。

跳板不是獨立頁面,而是主機參數的一部分。支持多跳,每跳帶自己的端口、賬號與認證方式,摘要行形如 2 跳 bastion-a:22 → bastion-b:2222;~/.ssh/config 裏的 ProxyJump a,b 會展開成兩跳,首次連接後整條鏈路寫回成可編輯的 hops。每跳各記自己的 TOFU 指紋(跳板換鑰匙不會被當成目標機換鑰匙),指紋按跳保存,密碼與目標機密碼分開存在內存裏,都不寫 config.json。

同步語義

按映射決定同步方向,規則如下:

  • 默認單向本地 → 遠端,agent 改的文件自動推上去;打開雙向後遠端變更纔會被下載。
  • 本地是真相:衝突判定 = 本地改過 且 遠端 mtime 相對基線漂移超過 2s;衝突只報告,由 UI 給「保留本地 / 保留遠端 / 忽略」。
  • 刪除不傳播:本地刪除只在隊列裏記一筆,絕不在遠端 rm。
  • 寫入用臨時文件 + rename 落地,避免讀到半個文件;遠端可執行位按本地 mode 推(Windows 上靠 modeTemplate 補)。

斷點續傳

傳輸按 1 MiB 分塊順序寫,不是整包 fastPut / fastGet:

1、兩個方向都先寫同目錄的 part 文件(<名字>.dsh-part-<大小>-<mtime>),結束後才 rename 成真名。
2、part 文件的大小就是斷點:中斷、斷線、進程重啓後重試同一個文件,會從 part 末尾接着傳,只補發剩餘部分。
3、part 名字裏帶源文件的大小和 mtime,源文件變了指紋就變了,舊 part 不會被拿來拼接,寧可重傳。

端口轉發

轉發標籤頁裏一條規則的形狀是「一端監聽 → 一端目標」,類型決定哪一端在本機:

類型 監聽 目標 等價於
本地 本機 遠端 host:port ssh -L 6379:127.0.0.1:6379 me@server
動態 本機(SOCKS5) 每次連接再定 ssh -D 1080 me@server
反向 遠端 本機 host:port ssh -R 8080:127.0.0.1:3000 me@server

行爲細節:

  • 綁定地址默認 127.0.0.1;選 0.0.0.0 會在面板頂部標紅——對 -L / -D 是把遠端服務暴露給整個局域網,對 -R 是把你的本機暴露給服務器所在的網絡。
  • 默認隨主機連接自動啓動,也可關掉改成手動。
  • 走主機已建立的 SSH 連接(-L / -D 用 direct-tcpip,-R 用 forwarded-tcpip),無需二次認證。
  • 掉線恢復:本地監聽器不隨連接斷開銷燬,下一條入站連接惰性重連;-R 的監聽端口在服務器上、斷了就沒了,重連後自動重新申請,沒申請回來時面板顯示「服務器上的端口暫時不存在」。
  • 端口衝突按「監聽端 + 端口」判重,0.0.0.0:8080 與 127.0.0.1:8080 視爲衝突;-L 與 -R 用同一端口號不算衝突。
  • 停止規則或斷開主機時關閉監聽、掐斷隧道,-R 還會通知服務器停止監聽(cancel-tcpip-forward)。
  • SOCKS5 只實現 CONNECT,BIND / UDP ASSOCIATE 明確回 0x07 command not supported。
  • 遠端服務拒絕連接與 SSH 層失敗是兩種錯誤碼(target-refused / transport),面板分別提示。

典型用法:沒有服務器也能試

倉庫裏有本地假遠端 scripts/dev-remote.mjs:exec 走本機 sh(命令拼錯、引號寫錯當場暴露),SFTP 只讀(刪不掉真文件),但轉發能力是真的,direct-tcpip 從本機連出去、tcpip-forward 在本機開監聽再推通道回來:

node scripts/dev-remote.mjs /path/to/project 2202
# 面板裏新建主機 127.0.0.1:2202,用戶和密碼隨便填

建好後可以對着它真跑三種轉發、遠端搜索、文件傳輸斷點續傳,以及多跳跳板鏈路。

適用場景與注意事項

適合的場景:DSH agent 在本機跑、但要操作另一臺服務器(看日誌、改遠端配置、起遠端 dev server、把本地改動同步上去、臨時 tunnel 給服務器用)的開發者;希望這些操作都留在 DSH 側邊欄裏、不想另開 SSH 客戶端的人。

需要注意:

  • 插件運行在當前 dsh 進程內,以該進程的權限執行,宿主端 bundle(lib/server.mjs,約 1 MiB,ssh2 / ws / chokidar 全部內聯)由插件直接加載。安裝前應先檢查源碼與許可證(MIT)再投入使用。
  • 0.x 的同步默認是本地 → 遠端單向、本地爲真相,衝突不自動裁決,先理解這條語義再用雙向映射。
  • -R 會把本機端口暴露給服務器所在網絡,0.0.0.0 綁定需謹慎。
  • 密碼與口令只存瀏覽器內存,插件不負責記住密碼(0.7.0 明確「還沒做」),每次連接要重新輸入或回到連接頁問一次。
  • 生態背景:DSH 的理念是「一切皆插件」,社區目錄站點(如 SkillHub)是獨立站點,與 DeepSeek / 幻方無官方從屬關係;該插件是第三方開源項目,不代表官方應用商店收錄。

結尾

這套插件的價值在於把「本地工作區 + 遠端機器」收斂進同一個 DSH 界面:文件、終端、同步、轉發各佔一屏,而不是四個獨立工具。能力邊界也寫得很清楚:不做通用 SSH 客戶端,不替換 DSH 的執行世界,不自動裁決衝突,不記住密碼。

目錄頁:https://www.skillhub.cn/plugins/LinHao100076/dsh-ssh-remote

GitHub:https://github.com/LinHao100076/dsh-ssh-remote

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜