233fxr-collab/dsh-launch-in-one-click

dsh-launch-in-one-click

A DeepSeek Harness plugin for Windows: writes a self-contained one-click launcher, and refuses to start a second instance when the target port already serves one. / 一个 Windows 专用的 DeepSeek Harness 插件:写入自包含的一键启动器;目标端口已有 Harness 实例时拒绝启动第二个。

待分类

dsh-Launch in One Click

English | 中文

随时切换生成的启动器语言 —— /launch --lang zh、/launch --lang en,或 /launch --lang auto 跟随控制台。

一个 Windows 专用的 DeepSeek Harness 插件:在你的桌面写入一个自包含的一键启动器(.bat), 并在目标端口已经有 Harness 实例时拒绝启动第二个实例。

它写出的启动器不会引用本插件。卸载插件后,桌面上的 .bat 照常可用——它就是一个文件, 需要的东西全都写在里面。

它做什么

装上它,启动器就出现在桌面上

插件市场的热挂载只接受纯 insert 的 bundle patch,所以本插件在你点下"安装"的那一刻就已经生效—— 启动器也就是这时候出现的。只有桌面上没有启动器时才会创建:已经存在的(不管是你自己写的还是本插件 旧版写的)一律原样保留,因为"插件加载"不是你的决定。想让它安静加载,把 provisionOnLoad 设为 false。

它还会让那个启动器保持最新

启动器里记录着写它的插件版本。当本插件的新版本加载时发现启动器是旧版写的,就会用当前模板重写这个文件—— 修好的文案、修好的代码页路径就是这样到达桌面的,而不是只停留在仓库里。

重写的是那一个启动器:里面记录的端口、工作区、启动方式、语言、浏览器行为全部沿用, 所以刷新模板永远不会搬动你的设置。改设置仍然是显式的 /launch。把 autoUpdate 设为 false 可以让文件冻结;doctor 仍然会报告它已过期。

有两件事刻意不碰:版本已匹配的启动器,以及版本读不出来的启动器——靠猜是手工修改被覆盖的原因。

另外,启动器每次运行都会用最新的 harness:npx @deepseek-ai/dsh 每次都会解析已发布的版本, 而不是钉死在写文件时的那个版本。

安装过程本身做了什么

launcher_install 按 Windows 自己的定义解析你的桌面目录(包括被 OneDrive 重定向或本地化的 桌面文件夹),确认该目录可以新建文件,按控制台代码页编码启动器,原子写入, 然后用试运行模式把它真正跑一次,证明它能被解析、能走到自己的逻辑。 自检不通过的启动器会被回滚,不会留在桌面上。

之后每次双击生成的启动器,它都会:

  1. 检查 node 能运行、启动命令(npx 或 dsh)有响应;
  2. 检查配置的工作目录仍然存在并进入它;
  3. 先绑定端口做探测,再给端口上已有的服务做指纹识别:
    • 端口空闲 —— 启动 Harness;
    • 是 Harness 实例 —— 拒绝启动,并告诉你该用那个实例自己的地址;
    • 被别的程序占用 —— 拒绝启动,并报出占用进程的 PID;
    • 探测没有结论 —— 拒绝启动,而不是靠猜。

只有第一种情况会启动任何东西。没有任何参数能启动第二个实例。

为什么端口检查不是客套

dsh web 每次启动都会生成进程级的访问令牌,并打印带上它的完整地址;服务器对任何 未认证请求都回答 401 dsh web authentication required。所以实例已经在跑时,直接打开 http://127.0.0.1:3080 看到的是一张 401 页面而不是你的会话——而启动第二个实例本来也会 在绑定端口时失败。端口被占用时,启动器只有两个诚实的选项,本插件选第二个: 告诉你该用哪个地址,然后什么都不启动。

指纹就是那个 401 响应体,所以「这是个 Harness 实例」是检测出来的而不是假设的—— 占用端口的是别的程序时,就会如实报告成别的程序,并附上 PID。

安装

dsh plugin --profile web add dsh-launch-in-one-click

或者直接从仓库安装:

dsh plugin --profile web add github:233fxr-collab/dsh-launch-in-one-click

从你的 profile 里删掉这一行即可卸载。已经生成的启动器照常可用——它们是自包含的。

使用

三个工具,外加一个斜杠命令。

工具作用
launcher_doctor只读诊断。报告 Node、npx、控制台代码页、解析出的桌面目录、它是否可写、目标端口被谁占用、npx 缓存里是哪个 harness 版本、以及已安装启动器的状态。可选地在试运行模式下运行已安装的启动器,或向 registry 查询已发布版本。
launcher_install写入启动器(原子写入、先备份、自检)。
launcher_uninstall删除本插件写的启动器以及它的备份。不是它写的文件默认拒绝删除,除非加 force。
/launch                      用部署默认值安装
/launch doctor               等同于 launcher_doctor 的报告
/launch list                 列出本插件放到桌面上的每一个启动器及其设置
/launch --port 3111          换一个端口启动
/launch --lang zh            用中文写入启动器(--lang en | --lang auto)
/launch --name "Work.bat"    指定文件名
/launch --overwrite          覆盖一个不是本插件写的文件
/launch --dry-run            只显示将要写入的内容

--language 是 --lang 的完整写法。切换语言会重写本插件写出的那个启动器(保留带时间戳的备份), 其余部分不变:端口、工作区、启动方式都保持原样。

启动器自己的参数

参数作用
--port N换端口启动
--workdir DIR换工作区
--no-open不自动打开浏览器
--silent不显示控制台窗口启动,过程写进日志
--dry-run只检查并打印计划,不启动
--help显示帮助

每次运行都会留下日志

%LOCALAPPDATA%\dsh-launch\launcher.log 记录启动器做过的判断——环境检查、工作区、端口结论、 要执行的命令、退出码——因为控制台窗口一关就什么都看不到了。--silent 完全没有窗口,写的也是它。 失败退出时会打印日志路径。

启动器本身接受 --port N、--workdir DIR、--no-open、--dry-run、--help。

退出码

退出码就是启动器的契约:快捷方式包装脚本或计划任务可以直接据此分支, 不需要解析本地化文本。

退出码含义
0服务正常运行并正常退出,或试运行打印了将要执行的命令
1找不到 node 或启动命令
2端口上已有 Harness 实例——没有启动任何东西
3端口被其它程序占用——没有启动任何东西
4参数非法
5工作目录不存在或无法进入
9端口探测没有得出结论
20Harness 自身启动失败,它自己的退出码打印在上面

配置

可选,写在 bundle 的 patch 行里:

- id: launch-in-one-click
  name: dsh-launch-in-one-click
  config:
    defaultPort: 3080                # 生成的启动器使用的端口
    language: auto                   # auto | en | zh
    runner: npx                      # npx | dsh
    packageSpec: '@deepseek-ai/dsh'  # npx 解析的包名
    openBrowser: true                # 是否让 `dsh web` 自己打开浏览器
    provisionOnLoad: true            # 加载时创建启动器(仅当桌面没有时)
    autoUpdate: true                 # 用旧版启动器时自动刷新为当前模板

⚠️ 在这里加 config: 行是有代价的。 市场只热挂载纯 id/name insert,所以带配置的 bundle patch 要等下次重启才生效,而不是点完即生效。这也是上面所有默认值都写在代码里而不是这个文件里的原因: 一行配置都不加地安装,启动器照样在你点"安装"的那一刻出现。

处理了哪些边界情况

  • 被重定向或本地化的桌面目录——依次通过 shell 的 known-folder API、两个注册表键、 %USERPROFILE%\Desktop 解析。指向不存在目录的来源会被跳过,而不是被信任。
  • 控制台代码页——启动器以系统 OEM 代码页为目标:新开的控制台用的就是它,双击批处理 文件时读文件用的也是它。这刻意不是安装它的那个进程的控制台代码页——在中文 Windows 上 从 UTF-8 终端安装,会得到一个英文 UTF-8 启动器(这是开发过程中实测到的)。只有在注册表读 不到时才退回到安装进程的控制台代码页,doctor 会把两者都报出来。文件会在第一个非 ASCII 字节之前把自己的代码页切过去。在 Windows 11 上实测:即使控制台报告的代码页是 936, cmd 仍会按 UTF-8 读取批处理文件,直到文件自己声明为止。
  • 语言——auto(默认)跟随控制台:支持中文的代码页得到中文,英文控制台得到英文且不产生 任何警告,因为"贴住控制台"本身就是目的,不是回退。显式指定 language: zh 时,即使控制台 打不出中文也会照做——改用 UTF-8 写入并把控制台切过去。
  • 双字节尾字节——在 GBK 及其同类编码里,一个汉字的第二个字节可能是 |、&、<、>、 ^、%、(、) 或 "。cmd.exe 不知道这两个字节是一个字,于是会起一个管道、一次重定向 或一次变量展开。每个文件在写入前都会扫描这种情况。
  • 代码页装不下的路径——例如英文控制台下的中文用户目录。换成英文并不能解决路径问题, 所以启动器会把自己切换成 UTF-8,而不是直接失败。
  • 不是本插件写的文件——拒绝覆盖并保持原样。加 overwrite 时才会替换,并保留带时间戳的备份。
  • 只有时间戳变化的重复安装——内容相同的启动器不会被重写,force 可以强制刷新。
  • 瞬时的重命名失败——索引服务或杀毒软件持有目标文件时 MoveFileEx 会返回 EPERM, 因此重命名带退避重试。
  • 双击打开的窗口——失败时会等待按键,让你看清信息;脚本调用时不会出现提示, 因为只有在 cmd 就是被启动来运行这个文件时才暂停。
  • npx 与 npx.cmd——Node 会在 npx.cmd 旁边放一个没有扩展名的 POSIX 脚本。 路径解析优先尝试 PATHEXT 里的候选,所以 cmd 根本跑不了的那个脚本永远不会被选中。

开发

npm install --legacy-peer-deps   # 只有 iconv-lite,其余都是 peer 依赖
npm test

测试共 90 项。其中最有价值的部分会生成启动器,用真实的 cmd.exe 去运行它, 并对着真实的监听者验证——一个会返回 401 指纹的假 Harness 服务、一个无关的 HTTP 服务、 以及一个空闲端口——然后断言退出码。这正是单元测试抓不到的两个缺陷的发现方式: 用裸 LF 结尾写出的文件(cmd 会把整个文件当成一行)和把代码页切换放在第一个多字节字符之后。

test/plugin.test.js 校验插件市场会检查的 manifest 约定,其中包含 peer 版本范围。 那个范围故意写得很长:

>=0.0.1-rc.1 <0.2.0-0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-rc.1 <0.2.0-0 ||
>=0.1.2-alpha.1 <0.2.0-0 || >=0.1.3-alpha.1 <0.2.0-0 || >=0.1.5-alpha.1 <0.2.0-0

node-semver 只有在范围里某个比较符与该版本的 major.minor.patch 元组完全一致、 且自身也带预发布标签时,才会放行预发布版本。所以看起来宽松的范围——比如 >=0.0.1-rc.1 <0.2.0,甚至 >=0.0.0-0——都会排除掉已发布的每一个 0.1.x 预发布版本, 用户会遇到需要手工绕过的 ERESOLVE。上面这种枚举写法覆盖了每一条已发布的线; 测试会断言本机安装的 harness 版本所在的元组就在其中。

当 harness 的包可以解析时,同一个文件还会用真实的 defineTool 编译器校验每个工具 schema, 否则会报告跳过。

运行要求

  • Windows 10 或 11。
  • 插件本身需要 Node 20+;harness 自身面向 Node 22。
  • 默认的 npx 启动方式需要 Node 自带 npx;若设置 runner: dsh,则需要已安装的 dsh 命令。

许可证

MIT