Repository navigation
Description
Bug 描述
在「设置 → 插件 → 插件市场」点击「卸载插件市场」并确认后,Harness 会停止、执行卸载、再自动重启。但重启完成后,插件市场仍然存在。
排查发现:卸载只在 pnpm 层面短暂生效(package.json 的 dependencies 与 node_modules/dshmarket 被移除),但 generation 注册表 desired.json 没有被清掉;重启时 projectGenerations 以 desired.json 为唯一权威重新投影,把 dshmarket 完整重建回来。
复现步骤
- 安装 dshmarket(generation 形态,即出现在
profiles/.generations/desired.json中)。 - 打开「设置 → 插件 → 插件市场」,点击「卸载插件市场」。
- 在确认弹窗中点击「卸载」。
- 等待 Harness 自动重启。
- 重启后再次打开「设置 → 插件」,发现插件市场仍然存在。
期望结果
卸载后重启,插件市场彻底消失:desired.json 为空、package.json 无 dshmarket、generation 目录被 sweep、node_modules/dshmarket 链接被 prune。
实际结果
卸载后重启,插件市场原样存在。重启后磁盘状态:
// profiles/.generations/desired.json —— 关键:仍是 dshmarket
[
"dshmarket+1.44.0+af88ab682bc0"
]// profiles/web/package.json —— 被重新投影回来
{
"name": "dsh-profile-web",
"private": true,
"dependencies": { "dshmarket": "1.44.0" },
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dshmarket"]
},
"desktop": {
"generationProjection": {
"version": 1,
"plugins": {
"dshmarket": {
"generationId": "dshmarket+1.44.0+af88ab682bc0",
"visibleVersion": "1.44.0",
"previousOverride": { "present": false }
}
}
}
}
},
"pnpm": {
"overrides": {
"dshmarket": "link:../.generations/live/dshmarket+1.44.0+af88ab682bc0/node_modules/dshmarket"
}
}
}同时 profiles/.generations/live/dshmarket+1.44.0+af88ab682bc0/ 目录仍在,profiles/web/node_modules/dshmarket junction 仍在。
环境信息
| 项 | 值 |
|---|---|
| DSH Desktop 版本 | 0.7.2 |
| 操作系统 | Windows |
| Node / pnpm | 24.9.0 / 10.34.5 |
| DSH_HOME | %APPDATA%\dsh-desktop\harness |
| dshmarket 安装形态 | generation(desired.json 中为 dshmarket+1.44.0+af88ab682bc0) |
根因分析(供开发者参考)
卸载存在两条路径,而 UI 优先选择了不感知 generation的那条:
-
原生 Electron 路径(当前实际生效)
dsh-desktop-market-installer/client.js的uninstall()优先调用globalThis.dshDesktop.uninstallMarket()。- IPC
market:uninstall(out/main/index.js)→uninstallMarketAndRestart()。 uninstallMarketAndRestart()调用removeProfilePluginWithDsh({...}, "dshmarket", true)。removeProfilePluginWithDsh()执行dsh plugin --profile web remove --workspace-root dshmarket,这是 pnpm 级删除,只改package.json的dependencies和node_modules,完全不碰desired.json。
-
HTTP 路径(generation-aware,未被桌面端选中)
dsh-desktop-market-installer/index.js的runUninstall()→runPlugin(['remove', '--workspace-root', 'dshmarket'])。runPlugin()先经projectedGenerationRemoval()判定目标是 generation 插件,命中后走disableGeneration()(从desired.json移除),这才是正确路径。
为什么删了又回来:
removeProfilePluginWithDsh只做 pnpm 删除,desired.json仍含 dshmarket。- pnpm runner 的
suspendGenerationProjectionForPnpm()+restore()(pnpm-runner.mjs)在每次 pnpm 操作前后把 generation 投影字段(dependencies+pnpm.overrides)临时摘除、结束再恢复,所以即使 pnpm remove 成功,这些字段也被写回。 - 重启时
prepareGenerationsForLaunch()→projectGenerations()(generations/projection.mjs)以desired.json为唯一权威,把 dshmarket 的 junction、dependencies、bundles、generationProjection、overrides全部重建。
关键:桌面端已有正确实现但未被使用
out/main/index.js 中已存在 generation-aware 的完整卸载函数:
removeProfilePluginCompletely()→removePluginSafely(),其中传入
uninstallGeneration: () => uninstallGenerationPlugin(...);uninstallGenerationPlugin()内部正是调用disableGeneration()+projectGenerations()+resolveEnabledGenerations()校验。
只要 uninstallMarketAndRestart() 复用这条路径(或对 generation 插件调用 disableGeneration),问题即可解决;但它却直接调用了只做 pnpm 删除的 removeProfilePluginWithDsh()。
建议修复
在 uninstallMarketAndRestart() 中对 generation 插件复用已有的 removeProfilePluginCompletely()(或 uninstallGenerationPlugin() → disableGeneration()),而不是裸调 removeProfilePluginWithDsh():
// 伪代码示意
async function uninstallMarketAndRestart() {
const dshHome = join(app.getPath("userData"), "harness");
await showSplash();
await runtime.stop();
// 关键:generation 插件必须先 disableGeneration(清 desired.json),再做投影
const result = await removeProfilePluginCompletely(dshHome, "dshmarket", "market-uninstall");
await launchHarness();
if (!result.ok) throw new Error(result.detail ?? "Plugin market removal failed.");
return { ok: runtime.snapshot().phase === "ready" };
}或更小的改动:在 removeProfilePluginWithDsh 之前/之后,若 isGenerationPlugin(dshHome, "dshmarket") 为真,则调用 uninstallGenerationPlugin(dshHome, "dshmarket", note)。
附加信息
相关现象(同一根因主题)
最初还观察到另一个症状「在插件市场里卸载了全部插件,但磁盘上文件仍存在」:
- 通过市场卸载其它 generation 插件时,
runPlugin()走disableGeneration()+publishGenerationManifest(); publishGenerationManifest()调用syncProfileManifest(..., { syncBundles: false }),不重建bundles,导致dsh.profile.bundles残留已卸载插件名;- 下次冷启动
migrateProfileToGenerations()里communityPlugins()用dependencies ∪ bundles收集社区插件,把这些「幽灵条目」当成待迁移的旧插件; - 迁移中若某插件
verifyGenerationPeers()失败(如react resolves outside the installation closure),会writeDeferred冻结维护,prepareGenerationsForLaunch()(sweepRegistry+projectGenerations)被跳过; - 于是无引用 generation 目录、老 junction、老
bundles都留在磁盘上。
建议一并评估:卸载 generation 插件时同步重建 bundles,或让 communityPlugins() 只以 dependencies 为准(排除已在 generation 注册表中的名字),并避免单插件 peer 校验失败就整体冻结维护。
注:以上文件名/函数名与 0.7.2 发布包一致;行号基于发布包
out/main/index.js与node_modules/dsh-desktop-market-installer/*的产物,源码对应 monorepopackages/…,行号可能有偏移。
Activity
fix(market): disable the generation pointer when uninstalling the mar…