前言¶
在 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