☰
一个开源桌面壳的 10 个版本:我推翻过的 5 个决定
2026/10/14 13:28:59 网站建设 项目流程

项目: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-usagedsh-model-surplus
dsh-composer-keysdsh-keys-setting
dsh-opencode-go-pathdsh-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。

实测的失败链条是这样的:

  1. profile 里登记着 npm 版本,但 registry 上根本没有这个包;
  2. 插件从dsh.profile.bundles里消失,引擎少一个功能;
  3. 更严重的是,那条解析不了的依赖会让这个 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 个版本

本文事实来源(均可核查):

  1. 项目CHANGELOG.md—— 各版本的决策说明章节(含「为什么改」与「刻意没有改的东西」)
  2. 提交记录:aaf6167(壳改名)、950ce61/37a38b3/f23bcb7(插件改名)、0b6e0a2(插件改回内置)、a886d11(数据目录三选一)
  3. GitHub Releases 页 —— 10 个版本的发布时间与产物

说明:本文只写已经确认和落地的决策。同期还有些技术层面的反复(例如一处跨进程锁接错位置导致自死锁后撤回),属于实现细节而非产品决策,未纳入本文。


这是一个独立开源项目,与 DeepSeek Harness 官方无隶属关系。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询