项目:DSH Ready GUI(DeepSeek Harness 的第三方桌面外壳)
时间跨度:2026-09-06 的 v0.1.0 到 2026-10-05 的 v0.8.0,共 10 个版本,约一个月
性质:个人开发、主要自用
文中每个决定的背景和推翻理由,都能在仓库的 CHANGELOG 与提交记录里查到。
摘要(先看结论)
- 这个项目一个月发了 10 个版本,其中有5 个决定后来被我自己推翻了。
- 最大的一次反转是**「把插件拆出去发 npm」→ 改回「随壳内置」**:拆包的好处很明确,但卡在发布渠道上,而「装了却装不回来」比「随壳发布的旧版」糟得多。
- 改名改了三次(壳一次、插件两次),但每次都坚持一条原则:改「对外标识」,不改「内部契约」。
- 有两处是默认值设错了:插件默认关闭、模型追加到列表末尾——都让「装上即用」变成了空话。
- 这几条教训合起来是一句话:决定的对错,取决于用户的实际路径,而不是设计原则本身有多正确。
一、改名改了三次,但有些字符串故意不改
1.1 壳的名字:DSH GUI→DSH Ready GUI
旧名字的问题不是"不好听",是搜不到自己:
dsh-gui在 GitHub 上已被四个别人的仓库占用(8★ / 4★ / 4★ / 3★);- 更要命的是
ScannerVpn/DeepSeekHarnessGui与旧仓库名只差大小写,GitHub 搜索视为同一个名字,而它星数是旧仓库的两倍——搜这个关键词的人先看到的是别人; - npm 上
dsh-gui、deepseek-harness-gui、dsh-desktop也全被占。
新名字dsh-ready-gui实测三项全零:npm 空闲、GitHub 无同名仓库、DSH 生态注册表(当时 4000+ 条目)里也没占用。
代价比想象中大。Electron 的userData目录由productName推导,改名会把目录从<appData>\DSH GUI变成<appData>\DSH Ready GUI——而引擎、dsh-home、settings.json、pnpm 工具链全在旧目录里。
不处理的话,已有安装会看起来像全新安装:重新下载几百 MB 的引擎,丢掉模型配置和会话历史。
所以加了启动时的一次性迁移:把旧目录里「新目录还没有」的条目搬过去,只 rename、绝不删除,幂等,而且迁移失败绝不让应用起不来。
这里还有个自己挖的坑:那个 7 项单测里有一条,专门守住「旧目录名必须是
DSH GUI」。因为一次批量改名曾把这个字符串也一起替换掉了,导致legacy和current指向同一个目录,迁移静默失效——不报错,就是不搬。
1.2 刻意没有改的字符串
改名时有一批字符串故意保持旧名,因为它们是内部契约:
| 保留的字符串 | 为什么不能改 |
|---|---|
// dsh-gui: | 写进引擎自身文件的补丁标记。改了旧补丁就识别不出、也清理不掉 |
<home>\.dsh-gui\bundled-plugins | 插件暂存目录,磁盘上真实存在,profile 的file:依赖直接指向它 |
DSH_SHELL_*环境变量 | 13 个,README 里公开过的接口 |
dsh-gui:*IPC 通道 | 内部标识,用户看不到,改了只会制造破坏 |
所以在新代码里grep dsh-gui仍会命中这些地方,那是有意保留的。
1.3 插件的名字:改了两次
原因更直接——名字被别人占了:
- npm 上的
dsh-opencode-go和dsh-composer-keys已被其他作者占用; dsh-model-usage虽然 npm 上还空着,但 GitHub 上已有三个别人的同名仓库,其中两个的package.json里name也写着这个名字——谁先 publish 谁拿到。
所以与其将来竞速,不如一次换掉:
| 旧名 | 新名 |
|---|---|
dsh-model-usage | dsh-model-surplus |
dsh-composer-keys | dsh-keys-setting |
dsh-opencode-go-path | dsh-gateway-models(后来它不只管 OpenCode Go 了) |
但有一条坚持不改:补丁层的行 id 和设置命名空间保持原样。
因为「已禁用」的选择、市场state.json里的开关、settings.yaml里保存的键位,全都记在那些旧名字下。包名只是安装标识——改它不该让用户的设置失效。
所以迁移逻辑里有一句关键规则:
改名不该改变用户的选择。旧包要摘干净(bundles 登记 +
file:依赖都要摘),同时把「已禁用」的状态搬到新包上。
摘登记还不够——引擎的 reconcile 会依据残留依赖把旧插件重新登记回来,实测会导致新旧同时加载。
二、最大的反转:从「拆包发 npm」改回「随壳内置」
原计划
把四个插件拆成独立包发布到 npm,壳改成像插件市场那样从 registry 安装。
好处很明确,而且当时看是明显更优的方案:
- 插件更新不必再等壳发版(原来插件代码打进
app.asar,只有装新版 GUI 才会更新); - 官方桌面端、Tauri 客户端、
dsh web、CLI 都能装; - 生态里 4000+ 插件都是这个形态,独立仓库也是上架注册表的前提(注册表按
owner/repo收录,插件埋在 monorepo 里无法上架)。
为什么推翻
卡在发布渠道上:npm 账号注册走不通,包发不出去。
如果只是"发不出去"还好办——麻烦的是已经登记的依赖会连累整个 profile。
实测的失败链条是这样的:
- profile 里登记着 npm 版本,但 registry 上根本没有这个包;
- 插件从
dsh.profile.bundles里消失,引擎少一个功能; - 更严重的是,那条解析不了的依赖会让这个 profile 里每一次安装操作一起失败——包括你在装别的插件的时候。
结论
「装了、却装不回来」的插件,比「随壳发布的旧版」糟得多。
所以这一轮改回内置:壳必须能自给自足。四个独立仓库保留(它们同时也是上架注册表的载体),等发布渠道通了再切。
这条教训值得单独记:依赖一个"本来应该能用"的外部渠道做架构规划,风险极高。发布渠道是硬约束,不是可以假设的前提。
三、默认值设错了:插件默认关闭
原来的设定
四个插件随壳打包,但默认一个都不装(目录里的勾选框全关)。
这个设定本身是对的——插件是以你的权限运行的代码,可以读写文件、访问网络,装之前应该让你确认。
为什么是错的
因为设置窗口藏在托盘菜单里。
结果是:用户下载、解压、打开之后,那四个插件一个都没生效——而它们恰恰是这个壳能提供的全部增量。
「打开就能用」变成了一句空话。
修正(v0.7.0)
不是改成默认安装,而是改成主动问:
- 引擎起来后,检测「还有该装没装的」;
- 主动把设置窗口弹出来,最上方一张卡片:「一键开启(N 个)」/「不用了,我自己选」;
- 点一键开启 = 装齐并自动重启一次引擎(引擎只在启动时组装插件列表,不重启等于没装);
- 点「不用了」一个插件都不动,只记录「问过了」,之后不再主动弹。
保留原则(不替用户决定),但把发现的成本降到零。
这里的关键区分是:「默认值」和「用户路径」是两件事。默认关闭在原则上是正确的,但如果用户走不到那个开关前面,原则就只是空话。
四、模型排序:从「追加到末尾」改到「排在最前」
原来的做法
插件会给opencode-go路由补上deepseek-v4.1-flash模型——追加到列表末尾。而且列表里一旦出现 v4.1,就完全不动。
问题
列表顺序就是模型选择器的顺序,第一项是默认选中项。
追加到末尾意味着:模型确实补上了,但用户还得手动把它拖到最前,或者每次手动切换。而且因为"出现了就不动",已经补过模型的用户,那条永远停在最后。
「装上即用」在这里又变成了空话。
修正
改成放在最前——这样「装上即用」才真正意味着V4.1 Flash 默认被选中。
同时守住两条:
- 已经是这个形状就不写(幂等,不会因为自己写的设置再触发一轮);
- 你自己那条同 id 的条目原样保留(你改过的名字或上下文长度不会被默认值覆盖)。
五、数据目录切换:从「自动搬」到「三选一」
最早的实现是自动迁移:切换数据目录时,把旧目录的数据搬过去。
后来改成问一句,而且给三个选项:
- 迁移并切换
- 只切换,不迁移
- 取消
为什么改:自动搬在多数情况下是对的,但它假设了用户切换目录的意图。有人只是想换个位置放新数据,不想动旧数据;有人磁盘空间不够,搬不动;有人搬一半失败,两头都不完整。
把决定权交回去,比替用户猜更安全——尤其是这种"搬错了会丢东西"的操作。
六、这几条放在一起,能看出什么
① 「对外标识」和「内部契约」要分开对待。
改名时该改的果断改(仓库名、包名、显示名),不该改的一个字都别碰(补丁标记、暂存路径、环境变量、IPC 通道、行 id)。判据是:改了之后,用户已有的数据还认得出来吗?
② 发布渠道是硬约束,不能当假设。
「拆包发 npm」在架构上更优,但渠道不通就全是空谈。规划时先确认渠道,再谈架构。
③ 默认值要跟着「用户实际路径」走。
默认关闭插件、模型追加到末尾——两个默认值在各自的逻辑里都说得通,但用户走不到那个"正确的状态"。原则正确不等于结果正确。
④ 「搬到一半失败」比「不搬」更糟。
所有涉及数据迁移的操作,都应该可取消、可选择、可回退。
⑤ 改名会连带一串东西。
一次改名要同时处理:仓库名、包名、显示名、userData 目录、迁移逻辑、以及一批故意不能改的内部字符串。少想一样就会静默出问题。
七、常见问题(FAQ)
Q1:这个项目多久发一次版本?
从 2026-09-06 的 v0.1.0 到 2026-10-05 的 v0.8.0,约一个月发了 10 个版本。属于早期快速迭代节奏。
Q2:为什么改名这么多次?
两次是被动的——心仪的名字在 npm 或 GitHub 上已被占用(dsh-gui有四个同名仓库、dsh-composer-keys被其他作者占用)。一次是主动的——插件的能力范围超出了原名字(opencode-go-path后来也管 Command Code)。
Q3:改名会让我的设置丢失吗?
不会。项目坚持「包名可改、行 id 不改」:已禁用的选择、市场开关、settings.yaml里的键位都记在旧的行 id 下,改名后由启动维护自动迁移。壳改名时也会自动迁移userData目录(只 rename、不删除、幂等)。
Q4:为什么插件不默认安装?
插件是以用户权限运行的第三方代码。默认关闭是刻意的安全选择。v0.7.0 起改为「首次启动主动询问 + 一键开启」,既保留用户决定权,又不让发现成本挡住使用。
Q5:npm 上的包什么时候能发?
取决于 npm 账号注册何时可用。目前插件以「随壳内置 + 独立 GitHub 仓库」两种形态分发,后者可以通过预构建 tarball 安装,不需要 npm。
Q6:这些决定是在哪记录的?
都在仓库的CHANGELOG.md和提交历史里,包括被推翻的那些。
八、背景与来源
项目:DSH Ready GUI —— DeepSeek Harness 的第三方桌面外壳,个人开发、主要自用。
- 仓库:https://github.com/itchenshi/dsh-ready-gui
- 版本:v0.8.0(MIT,与 DeepSeek Harness 官方无隶属关系)
- 发布记录:v0.1.0(2026-09-06)至 v0.8.0(2026-10-05),共 10 个版本
本文事实来源(均可核查):
- 项目
CHANGELOG.md—— 各版本的决策说明章节(含「为什么改」与「刻意没有改的东西」) - 提交记录:
aaf6167(壳改名)、950ce61/37a38b3/f23bcb7(插件改名)、0b6e0a2(插件改回内置)、a886d11(数据目录三选一) - GitHub Releases 页 —— 10 个版本的发布时间与产物
说明:本文只写已经确认和落地的决策。同期还有些技术层面的反复(例如一处跨进程锁接错位置导致自死锁后撤回),属于实现细节而非产品决策,未纳入本文。
这是一个独立开源项目,与 DeepSeek Harness 官方无隶属关系。