前言¶
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 的「核心判斷」):
- 這不是 SSH 客戶端,而是「本地工作區的遠端影子」,因爲 agent 和側邊欄都在本地工作。
- DSH 有
ctx.fs/ctx.subprocess執行世界抽象,理論上可以把整個 harness 搬到遠端;但dsh-better-sidebar繞過ctx.fs直讀本地,兩條路線混用會產生 split-brain。 - 因此 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