前言¶
在 DSH 裏做日常研發,一件麻煩事是工作區裏那些 Maven 模塊和前端工程的生命週期散落在各處:項目在 IDEA 裏配一套運行配置,發包另開一個終端敲 mvn package,傳到服務器又得手動開 scp 或者寫腳本。每次切換 DSH 會話之後,這些動作都不在眼前。
dsh-devship 的處理方式是把這件事收進一個面板:在 DSH 右側欄加一個「應用運行」頁籤,自動識別當前工作區裏可啓動的項目,一行一個卡片,啓動、停止、打包、看日誌、部署、發佈都在卡片上,不需要跳出 DSH。下面介紹這個插件的安裝、面板行爲和配置項。
這是什麼¶
dsh-devship(插件內自稱「研發直髮臺」)由 dragoncdj 維護,是一個 DSH 客戶端側插件。它做的事可以拆成三段:
- 掃描工作區,找出 Java(Spring Boot 主類所在模塊)和前端(
package.json帶scripts.dev)項目; - 在右側欄面板裏提供啓動 / 停止 / 打包 / 日誌,並支持批量與脫離模式;
- 通過 SSH/SFTP 把產物發到多臺命名的部署環境,重啓容器並校驗狀態。
插件在社區目錄裏歸類爲「客戶端」,GitHub 倉庫顯示 1 star,許可證是 MIT。這裏說的「客戶端」指它掛在 DSH 客戶端界面裏,不是獨立命令行工具。
技術實現上,狀態查詢與命令執行在 Host 側(routes.js / lib/*.js),右側欄界面由 client.js 渲染,二者走同一個插件進程的接口。
安裝與啓用¶
安裝命令直接來自 README,按你用的 DSH 形態選一條:
# DSH Desktop
dsh plugin --profile desktop add dsh-devship
# dsh web
dsh plugin --profile web add dsh-devship
裝完需要重啓 DSH,右側欄會出現「應用運行」頁籤,插件激活後默認自動打開。如果面板被關掉,可以從右側欄頂部的「+」菜單裏重新打開。
開發插件本體時可以直接指向本地目錄,改完代碼重啓即生效:
dsh plugin --profile desktop add E:\workspace\dsh-devship
面板與項目識別¶
掃描規則¶
插件按固定規則發現項目,不需要手工登記:
- Java:根
pom.xml的<modules>裏、且在src/main/java下存在*Application.java的模塊,二級模塊(父模塊 / 子模塊-server)也算;端口從application-dev.yaml頂層的server.port讀。 - 前端:根目錄或兩層子目錄下帶
scripts.dev的package.json(典型如ui/*、web/*),用pnpm dev啓動。
掃描結果緩存 5 分鐘(SCAN_TTL_MS),同一工作區的重複掃描會合並。面板右上「刷新」按鈕強制重新掃描,新增模塊不需要重啓插件。
卡片與批量操作¶
每個項目一張卡片,包含勾選框、狀態點、類型徽標(Java / 前端)、端口、實際會執行的命令,以及 啓動 / 停止 / 打包 / 日誌 / 清除 / 隱藏 按鈕。
批量操作分三層:
- 頂部「全選 / 啓動選中 / 停止選中」配合卡片勾選框;啓動按順序逐個執行,避免多個 Maven 同時寫本地倉庫,失敗項彙總提示「哪個項目 + 什麼原因」;
- 底部「執行任務」列表右上角有「一鍵清除」,一次清掉所有打包 / 部署 / 發佈記錄;有任務在跑時按鈕就地變成「確認清除?會中斷任務」+ 是 / 否(3 秒不點自動收回),點「是」先中斷子進程 / SSH 上傳再移除條目,不會出現「列表裏沒了、進程還在後臺跑」的情況;
- 「完成自動清除」爲打包 / 部署 / 發佈各一個開關,另加一個延遲(立即 / 5 / 15 / 30 / 60 秒);開關存在
<工作區>/.dsh-devship/prefs.json,由 Host 側計時,面板關掉也生效。失敗的任務不會自動清除;開了自動清除的任務狀態後面顯示「N 秒後收起」倒計時。
另外每行有「隱藏 / 取消隱藏」,隱藏列表同樣持久化在 prefs.json;頂部按需出現「顯示已隱藏 (n)」開關,隱藏項半透明顯示。
端口被外部實例(IDEA / 終端起的)佔用時,該行會標「端口占用」,點啓動會先給出明確提示,而不是白跑一遍 Maven。
輪詢與連接佔用¶
面板每 2.5 秒拉一次運行狀態和日誌尾部,有文件在傳時降到 1.2 秒。輪詢是「一輪只允許一條在路上」:上一條 /status 沒回來就跳過這一跳,每條請求 9 秒超時,連續失敗按 2.5 → 5 → 10 → 15 秒退避,瀏覽器標籤頁在後臺時不輪詢。
這個限制是有意的:瀏覽器對同一個源最多開 6 條 HTTP/1.1 連接,面板把連接佔滿之後,DSH 自己的接口就要排隊,連接生成超過 15 秒會被判超時重連。走 HTTPS 局域網訪問時最容易撞上,表現就是頁面反覆「重新連接中 → 連接成功」。標籤頁設置了 keepMounted,切走再切回不會丟輪詢狀態。
日誌區固定 260px 高、內部滾動,不隨內容伸縮。
啓動與停止¶
Java 的啓動流程¶
啓動 Java 項目分兩個階段,共用一份日誌尾部,進入運行階段時清掉構建日誌只留應用輸出:
mvn -pl <模塊> -am -DskipTests -Dspring-boot.repackage.skip=true install
--am先構建上游模塊,避免本地倉庫裏是過期快照;
--Dspring-boot.repackage.skip=true讓模塊 jar 保持普通 jar,BOOT-INF嵌套包會讓聚合殼啓動時缺 Bean。mvn -pl <模塊> spring-boot:run,可帶-Dspring-boot.run.profiles=<profile>。
啓動前用 800ms 探測端口,已被佔用就直接提示「可能已在運行」,不再白跑一遍 Maven。
構建和運行共用日誌尾部這一點值得注意:構建階段的輸出不會留在運行日誌裏,排查啓動問題時要回到「執行任務」列表看日誌文件。
Windows 上的窗口與進程¶
Windows 上啓動不彈黑色控制台窗口:.cmd 由隱藏窗口的 powershell -WindowStyle Hidden 執行,DSH 自己起的子進程也是隱藏啓動的,退出碼照常回傳。
停止時兩種模式對應兩種收尾方式:
| 模式 | 停止方式 |
|---|---|
| 附帶(隨 DSH 結束) | handle.terminate(),DSH 的作業對象連帶整棵 powershell → cmd → mvn → java / node 一起結束 |
| 脫離(關 DSH 不停止) | taskkill /T /F 結束整棵樹 |
啓動模式:關 DSH 不停止¶
面板上「clean / 快速打包」同一行有「關 DSH 不停止」複選框,默認勾選。它決定了服務是否隨 DSH 一起退出。
- 勾選(默認,脫離模式):寫一個
.cmd,輸出重定向到logs/run-*.detached.log,外面套一層.ps1認領 pid,再用 WMIWin32_Process.Create起wscript.exe隱藏啓動。WMI 起的進程父進程是系統服務WmiPrvSE,不在 DSH 的作業對象裏,所以 DSH 關了也不受影響;啓動腳本里用WScript.Shell.Run(..., 0, False)(窗口樣式 0 = 隱藏)起 powershell,全程不彈控制台窗口。要跟蹤的進程是那個 powershell,它第一件事就是把自己的 PID 寫進run-*-detached.pid,Host 輪詢這個文件認領。WSH 被策略關掉時退回直接起 powershell(-WindowStyle Hidden),實測同樣沒有窗口。 - 不勾(附帶模式):走
ctx.subprocess管道,由隱藏窗口的 powershell 執行.cmd,DSH 退出後服務一併結束,日誌走管道。
脫離模式下日誌改爲讀日誌文件(增量)。
打包與發佈¶
打包命令¶
- Java:
mvn -pl <模塊> -am package -DskipTests -T 1C(多線程),勾 clean 變clean package; - 「快速打包」勾上後去掉
-am,只編目標模塊(依賴須已 install 過),保留-T 1C; - 前端:直接
node ./node_modules/vite/bin/vite.js build --mode prod(不經 pnpm,避免多開可見窗口); - 全量打包:根 reactor 執行
mvn package -DskipTests -T 1C。
前端打包繞過 pnpm 的原因是它會再開一個可見窗口,而插件的目標是全程不出窗。
一鍵發佈¶
卡片上的「發佈」把四步合到一個任務條目裏:打包 → 部署 → 重啓容器 → 校驗,日誌帶階段編號:
15:22:10 ① 打包(快速,不帶 -am) …
15:22:10 ① 打包完成
15:22:10 ② 部署 …
15:22:10 產物:...\target\xxx.jar
15:22:10 部署完成:1 個文件,155.88 MB
15:22:10 容器 xxx 已重啓,校驗狀態 …
15:22:10 容器狀態:Up 8 seconds
✔ 發佈完成
發佈期間任務列表裏會出現三張卡:打包(build)、部署(deploy)、發佈(release)。發佈卡是父卡,把子卡的日誌和上傳進度鏡像過來,因此:
- 進度條只有一條,畫在「發佈」卡上,「部署」子卡帶
progressInParent標記、不再重複畫;單獨點「部署」時那張卡照常顯示進度; - 狀態文案按任務種類區分:打包卡「打包中 / 打包完成」,部署卡「部署中 / 部署完成 / 部署失敗」,發佈卡「發佈中 / 發佈完成 / 發佈失敗」;
- 子卡保留是爲了各自的日誌與「檢出 / 中斷」,跑完按「完成自動清除」的設置收起。
部署到多臺服務器¶
面板右上「部署配置」打開一個多環境編輯器,配置存在 <工作區>/.dsh-devship/deploy.json。一臺服務器是一個環境,最多 12 臺;每臺有名稱、id、地址、端口、用戶、密碼、dsh-ssh 別名、重啓命令、「受保護」勾選、「設爲默認」、「刪除」。
環境選擇與受保護¶
推送環境的方式是「先選後發」:
- 配了多臺時,點「部署 / 發佈」會就地展開一排環境按鈕(
發到哪臺? [開發] [測試] [生產],10 秒不點自動收回),點哪臺才真發; - 只配了一臺(老
deploy.json)時不彈,點了就發; - 按住 Shift 點按鈕跳過選擇器,直接發到「默認發佈環境」下拉選中的那臺;Shift 直髮、卡片上的遠端目錄、
user@host:port都指向這臺,按鈕懸停標題會點名生產 · root@10.0.0.9 → /data/apps/demo/server; - 「受保護」勾選後,選擇器裏點「生產」第一下變成紅色的「確認發到 生產」,再點一次才真發;部署 / 發佈 / 批量四條路都同樣攔,發完或切環境自動復位。
「拆成開發 / 測試 / 生產」一次生成三臺(現在這臺填進「開發」,「生產」默認勾上受保護),也可以「加一個環境」自己加。只有一臺老配置時會被讀成 id 爲 default 的一個環境。
遠端目錄與產物¶
目錄解析順序是 servers[環境].targets[項目].remoteDir → 頂層 targets[項目].remoteDir。每臺環境有自己的覆蓋表,留空就用頂層 targets;兩邊都沒配的項目,卡片上不出現「部署」按鈕。環境的 id 只能用小寫字母、數字和 -,最長 24。
對應 JSON:
{
"defaultServer": "dev",
"servers": [
{
"id": "dev", "label": "開發", "host": "10.0.0.9", "port": 22, "user": "root", "password": "***",
"targets": { "app-server": { "remoteDir": "/data/apps/dev/server" } }
},
{
"id": "prod", "label": "生產", "alias": "prod-ssh", "protected": true,
"restartCommand": "docker restart {container}",
"targets": { "app-server": { "remoteDir": "/data/apps/demo/server", "container": "demo-server" } }
}
],
"targets": {
"app-server": { "remoteDir": "/data/apps/demo/server" },
"ui/admin-web": { "remoteDir": "/data/apps/demo/web/admin" }
}
}
產物默認是 Java = <模塊>/target/<artifactId>.jar,前端 = dist-prod / dist,可以用 artifact 覆蓋(每個環境各一份)。重啓命令裏的 {container} 會被替換成目標容器名。
部署走 ssh2 / SFTP:目錄先本地打 tar(不壓縮,局域網帶寬足,比 gzip 快)→ 傳一個包 → 遠端解包;tar 不可用時退回 8 路併發逐文件上傳;單個 jar 直接 put,上傳前會 cp x.jar x.jar.bak 留回滾副本。jar 尾部找不到 BOOT-INF/ 時日誌裏給 ⚠ 警告(不是 Spring Boot 可執行包)。重啓後用 docker ps 校驗容器狀態,不在運行列表就帶 docker logs --tail 20 報錯。批量部署時多個項目可以同時上傳(不再是串行),一條 SSH 會話可複用。
上傳進度反映在任務卡上:百分比、「已傳 / 總量」、實時速度;日誌裏每 2 秒記一行 ↑ 42% 12.3 MB / 29.1 MB · 4.8 MB/s,傳完一定記最後一行。速度按最近 5 秒窗口算,不是整輪平均。每次部署日誌第一行是 目標:生產 → root@10.0.0.9:22 · 受保護(alias 只顯示別名,實際連哪臺由 dsh-ssh 決定),建連後還有 連接 root@… / SSH 已連接 兩行,翻日誌可以覈對發到了哪臺。
任務與日誌分開:默認那臺用 deploy:<項目> → logs/deploy-<項目>.log,其它環境用 deploy_<環境>_<項目> → logs/deploy_<環境>_<項目>.log;同一個項目發兩臺是兩條獨立任務,日誌不會互相覆蓋。批量「部署選中 / 發佈選中」同樣先展開環境選擇器,把選中的項目全發到你點的那臺(不跨環境扇出),按順序逐個執行,失敗項彙總提示。
老形狀配置(頂層一臺 + 一份 targets)讀進來會合成默認那一臺,任務 id、日誌文件名、不帶 serverId 的請求行爲都和以前逐字節一致。有一個限制要注意:2.1.x 舊面板只會髮頂層字段,用它「保存配置」會把多環境配置寫回成單臺形狀(默認那臺的密碼和重啓命令繼承下來,不會丟憑據);用當前版本面板保存就不會回落。
可選配置¶
工作區根目錄配置¶
工作區根目錄放 .dsh-devship.json,可覆蓋自動探測結果:
{
"maven": "D:/ProgramFiles/apache-maven-3.9.15/bin/mvn.cmd",
"pnpm": "C:/Users/me/AppData/Roaming/npm/pnpm.cmd",
"javaHome": "C:/Users/me/.jdks/ms-25.0.4"
}
不寫也能用:Maven 依次找 PATH → M2HOME / MAVEN_HOME 的 bin → 常見安裝目錄 → IDEA 自帶 Maven。JDK 按根 pom 的 java.version 找匹配版本,常見目錄按平臺給:Windows 是 ~/.jdks、C:\Program Files\Java、Adoptium / Zulu / Corretto 等;macOS 是 /Library/Java/JavaVirtualMachines(含 Contents/Home 那一層)和 Homebrew;Linux 是 /usr/lib/jvm 與 sdkman(JAVA_HOME 版本不合適時會自動換)。
運行時產生的啓動腳本放在 <工作區>/.dsh-devship/。
偏好與部署配置¶
prefs.json 可以手工改:
{
"hidden": ["ui/admin"],
"autoClear": { "build": true, "deploy": true, "release": false, "delayMs": 15000 }
}
delayMs 只認 0 / 5000 / 15000 / 30000 / 60000,其它值回落到 15000。自動清除只處理成功和已停止的收尾,失敗的任務留在列表裏,仍可手動清除或「一鍵清除」。
適用場景與注意¶
適合的場景:Java 多模塊 + Vite 前端的工作區,日常要反覆起停本地服務、打包、往開發 / 測試 / 生產機發包並重啓容器,希望這些動作不離開 DSH 界面。面板上的批量啓動、按環境拆分的部署、受保護環境、脫離模式關掉 DSH 不停止,都是爲這類重複操作準備的。
幾點需要知道:
- 這個插件運行在當前 DSH 進程的權限下,能起子進程、寫工作區文件、經 SSH 把包傳到你指定的服務器。它的行爲決定了你要把哪些目錄、哪些機器交給它;
- 裝之前先讀一遍源碼和許可證(MIT),確認啓動命令、部署配置和命令執行路徑符合你的工作區約定;
- 部署配置默認是空的,README 裏的服務器地址都是示例,需要自己填;
- 端口占用提示只針對外部啓動的實例,服務如由別的工具拉起,插件不會替你接管;
- SFTP 走
ssh2,遠端需要有可用的 shell 與tar;tar不可用時會退回逐文件上傳,速度快慢取決於環境; - 脫離模式依賴 Windows WMI 與 WSH,被策略關掉時會退回直接起 powershell(隱藏窗口)。
收尾¶
回到開頭的問題:dsh-devship 的價值不在於又寫了一套啓動腳本,而在於把「一個工作區裏哪些服務在跑、包發到了哪臺機器、容器有沒有重啓」這些狀態集中到 DSH 右側欄的一個頁籤裏,並且可以用同一份配置把開發 / 測試 / 生產分開管理。識別規則是約定式的(*Application.java 就算 Java,scripts.dev 就算前端),遇到非常規結構時用 .dsh-devship.json 覆蓋即可。
目錄頁:https://www.skillhub.cn/plugins/dragoncdj/dsh-devship
源碼與文檔:https://github.com/dragoncdj/dsh-devship