Skip to content

[Bug] 卸载插件市场(dshmarket)后重启仍被重新投影回来 #330

Closed

Description

@Lililizi0307

Bug 描述

在「设置 → 插件 → 插件市场」点击「卸载插件市场」并确认后,Harness 会停止、执行卸载、再自动重启。但重启完成后,插件市场仍然存在。

排查发现:卸载只在 pnpm 层面短暂生效(package.json 的 dependencies 与 node_modules/dshmarket 被移除),但 generation 注册表 desired.json 没有被清掉;重启时 projectGenerations 以 desired.json 为唯一权威重新投影,把 dshmarket 完整重建回来。

复现步骤

  1. 安装 dshmarket(generation 形态,即出现在 profiles/.generations/desired.json 中)。
  2. 打开「设置 → 插件 → 插件市场」,点击「卸载插件市场」。
  3. 在确认弹窗中点击「卸载」。
  4. 等待 Harness 自动重启。
  5. 重启后再次打开「设置 → 插件」,发现插件市场仍然存在。

期望结果

卸载后重启,插件市场彻底消失: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的那条:

  1. 原生 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。
  2. HTTP 路径(generation-aware,未被桌面端选中)

    • dsh-desktop-market-installer/index.js 的 runUninstall() → runPlugin(['remove', '--workspace-root', 'dshmarket'])。
    • runPlugin() 先经 projectedGenerationRemoval() 判定目标是 generation 插件,命中后走 disableGeneration()(从 desired.json 移除),这才是正确路径。

为什么删了又回来:

  1. removeProfilePluginWithDsh 只做 pnpm 删除,desired.json 仍含 dshmarket。
  2. pnpm runner 的 suspendGenerationProjectionForPnpm() + restore()(pnpm-runner.mjs)在每次 pnpm 操作前后把 generation 投影字段(dependencies + pnpm.overrides)临时摘除、结束再恢复,所以即使 pnpm remove 成功,这些字段也被写回。
  3. 重启时 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)。

附加信息

相关现象(同一根因主题)

最初还观察到另一个症状「在插件市场里卸载了全部插件,但磁盘上文件仍存在」:

  1. 通过市场卸载其它 generation 插件时,runPlugin() 走 disableGeneration() + publishGenerationManifest();
  2. publishGenerationManifest() 调用 syncProfileManifest(..., { syncBundles: false }),不重建 bundles,导致 dsh.profile.bundles 残留已卸载插件名;
  3. 下次冷启动 migrateProfileToGenerations() 里 communityPlugins() 用 dependencies ∪ bundles 收集社区插件,把这些「幽灵条目」当成待迁移的旧插件;
  4. 迁移中若某插件 verifyGenerationPeers() 失败(如 react resolves outside the installation closure),会 writeDeferred 冻结维护,prepareGenerationsForLaunch()(sweepRegistry + projectGenerations)被跳过;
  5. 于是无引用 generation 目录、老 junction、老 bundles 都留在磁盘上。

建议一并评估:卸载 generation 插件时同步重建 bundles,或让 communityPlugins() 只以 dependencies 为准(排除已在 generation 注册表中的名字),并避免单插件 peer 校验失败就整体冻结维护。

注:以上文件名/函数名与 0.7.2 发布包一致;行号基于发布包 out/main/index.js 与 node_modules/dsh-desktop-market-installer/* 的产物,源码对应 monorepo packages/…,行号可能有偏移。

Activity

  1. added a commit that references this issue on Sep 10, 2026
    b854c29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions