# dsh-Launch in One Click [English](README.md) | **中文** 随时切换生成的启动器语言 —— `/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。 ## 安装 ```sh dsh plugin --profile web add dsh-launch-in-one-click ``` 或者直接从仓库安装: ```sh 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 | 端口探测没有得出结论 | | 20 | Harness 自身启动失败,它自己的退出码打印在上面 | ## 配置 可选,写在 bundle 的 patch 行里: ```yaml - 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 根本跑不了的那个脚本永远不会被选中。 ## 开发 ```sh 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