Skip to content

[Feature] 远程同步:新增“跟随远端删除插件”开关(默认关闭) #132

Closed

Description

@LingYingNX

使用场景

我在两台机器(A / B)上用同一个私有 Git 仓库同步 DSH 配置,plugins 分区已勾选,自动同步开着。

在 A 上卸载插件 X 后,自动同步会推送一份不含 X 的新快照;但在 B 上拉取或自动合并后,X 仍然装着。想让 B 也跟着删掉 X,只能去 B 上手动卸载。插件少的时候还能接受,插件多、机器多的时候很容易漏。

我希望这个行为是可选的:默认保持现在的“只增不删”,需要镜像插件清单的人自己打开开关。

问题

当前 0.7.0 的插件同步/导入是只增不删的:

  • src/adapters/plugins.ts:808-867 的 analyzeImport() 只遍历备份里的 data.plugins:
    • 目标机没装 → Install
    • 版本相同 → Skip
    • 版本不同 → Conflict
    • 没有“目标机有、备份里没有 → 删除”的分支。
  • src/adapters/plugins.ts:1000-1075 的 applyItem() 只处理安装/升级,没有卸载入口。
  • src/sync/merge.ts:299-301 的 GRANULAR_MERGES 目前只有 workspaces;plugins 数组走通用 JSON 合并,无法按包名表达“远端删除了某个插件”,容易整分区冲突。
  • src/sync/sync-engine.ts:1398 的 hasLocalChanges() 能检测到本地卸载并推送新快照(所以 A 侧没问题),但这份“删除”不会在 B 侧被应用。
  • 现有的插件卸载只存在于快照恢复:src/core/restore.ts:678-702 用 beforePlugins 基线卸载“导入期间新增”的插件。它是撤销导入,不是同步删除,而且不会重装“被删除”的插件。

结果就是:A 卸载后,远端最新快照里没有 X,但 B 永远不会自动删 X;用户只能手动逐台处理。

期望方案

在同步设置 / plugins 分区旁增加一个开关(默认关闭):

插件同步
  [x] 同步插件清单
  [ ] 跟随远端删除本机插件
      关闭:只安装/升级,不删除(当前行为)
      开启:仅删除“上次同步基线中有、本次远端快照中没有、本机未改过”的插件

行为表:

场景 开关关闭 开关开启
A 卸载 X,B 拉取最新快照 B 保留 X B 卸载 X
B 本地新装 Y,还没同步过 保留 Y 保留 Y(祖先里没有,不能证明是远端删除)
B 本地把 X 升级了,远端删除了 X 保留 X 不自动删,报冲突让用户选
官方内置 / bundlesOnly / dsh-config-manager 自身 保留 保留
删除失败(权限、占用、桌面保留档案) — 逐项报错,不阻断其余项

判定规则(建议):

  1. 以上一次成功同步的共同祖先快照为基线,而不是“远端没有就删”。
  2. 只删除同时满足以下条件的插件:
    • 祖先快照的 plugins 里有;
    • 本次要应用的远端快照的 plugins 里没有;
    • 本机当前仍安装;
    • 本机记录(版本 / spec)与祖先一致,说明不是本地改过;
    • 不是 bundlesOnly、不是 dsh-config-manager 自身、不是桌面端不可移除的内置包。
  3. 本机新装、祖先里没有的插件永不删除,避免把“刚装还没上传”的插件误删。
  4. 没有共同祖先(首次同步)时一律不删,只给提示。
  5. 只对“祖先 → 更新快照”的正向应用生效;手动选历史旧快照时不自动删,避免把后来新增的插件当成“远端删除”。
  6. 删除前在导入确认页单独列出 将删除:X@1.2.3,默认勾选(因为开关已开启),允许取消。
  7. 删除后写入本次 apply 的 undo 记录;回滚时重新安装被删插件。
  8. 自动同步:建议第一版遇到删除项时暂停待确认,不要无人值守静默删除;如果维护者希望全自动,可再加一个“自动同步也执行删除”的二级开关,默认关闭。

这个默认关闭的设计也符合 README 里“宁可少迁移一个配置,也不要破坏你现有的配置”的原则。

实现要点(供参考)

  1. 配置持久化:在 SyncSelection(src/sync/sync-selection.ts)增加 per-channel 的 removePlugins: boolean,默认 false;同步页读写这个字段。或放进 sync-config.json 的通道配置,二选一。
  2. 记录级合并:给 src/sync/merge.ts 的 GRANULAR_MERGES 增加 plugins: mergePluginsSection,按包名做三方合并,输出 added / updated / removed / conflicts,不要整个数组判冲突。
  3. 导入计划:analyzeImport() 增加 Remove(或 Delete)kind;只对 removed 且本机==祖先的条目生成。开关关闭时降级为 Skip,并在 detail 里说明“远端已删除,开关未开启,保留本机插件”。
  4. 执行卸载:applyItem() 处理 Remove,调用宿主新增的 ctx.target.plugins.remove(name, signal);桌面 desktop profile 走桌面插件管理器(避免 desktop-profile-reserved),失败按现有 warning 逐项报告。
  5. 回滚:src/core/restore.ts 的 planRestore() 增加“baseline 有、当前没有 → 重装”的 pluginInstall action;删除项带 name@version/spec,恢复时用官方 dsh plugin add 装回。
  6. 测试:至少覆盖
    • A 卸载 → B 拉取删除;
    • B 本地新装不删;
    • B 本地改版不删/报冲突;
    • 开关关闭不删;
    • bundlesOnly / self / 不可移除包不删;
    • 删除失败不阻断其余项;
    • 回滚能重装被删插件;
    • 自动同步有删除项时按约定暂停/确认。

最小可行版本

如果不想一次做太大,第一版可以只做:

  • 一个开关,默认关闭;
  • 只作用于手动「从远端合并到本机」;
  • 自动同步遇到删除项只提示,不自动删;
  • 删除前必须确认,删除后写 undo 记录。

等这个跑稳,再放开自动同步。

备选方案

  • 保持现状 + 手动卸载:最安全,但多机场景麻烦;也是不想加开关时的默认。
  • 独立 CLI sync prune --plugins:适合高级用户/脚本,但普通用户仍需要 UI 开关;可以和开关共用同一套判定。
  • 整机镜像模式:把所有分区都做成镜像,包括删除本地新装插件;风险更高,不建议作为默认,也不建议和这个开关混在一起。
  • 只在手动导入时删除,自动同步永不删除:更保守的第一版;如果担心无人值守误删,可以先这样做。

补充

进度

  • 进度:40%(2026-11-10T17:04:12+01:00):triage 完成:裁决=做,开启后自动同步也可删;agent brief 已贴,转 ready-for-agent,待实施
  • 进度:10%(2026-11-10T16:46:08+01:00):triage:已确认未实现;源码级核查与最小设计写入 docs/evidence/sync-channel.md,待维护者拍板 3 项

交付

  • 2026-11-10 本地完成并验证:提交 23df0bd(47 文件,未推送),labels 改 confirmed,待随版本发布换 fixed。
  • 实现:SyncSelection.deletePlugins(缺省 false)+ src/sync/plugin-removals.ts 判据 + applyPluginRemovalStep() 在 executeImportPlan 成功后卸载(逐项 undo 写回滚快照),恢复计划 pluginInstall 走 dsh plugin add;引擎 sync-engine.ts:1966 起 4'/4'' 段。
  • 验证:typecheck 0 / npm test 5559 pass 0 fail / build 0 / smoke 12/12 / 体量棘轮 F-01 通过。
  • 已知偏离:删除不走 ImportPlan 的 Remove 计划项(在导入成功后执行,才有快照与 undo 基线);自动同步在开关开启时直接删,不额外弹确认(开关即授权,维护者裁决 2026-11-10)。
  • 未发布:等下一个版本;发布后按 .dsh/issue-ledger.md 回版本号并换 fixed。

Activity

  1. github-actions commented on Nov 10, 2026

    @github-actions
    Contributor

    感谢反馈!🙏 我通常会在 48 小时内回复;这条已加上 needs-triage,进度会体现在标签上。

    Thanks for the report! I usually reply within 48h. This issue is labeled needs-triage; progress shows up in the labels.

    如果还没写,补上这几项能少好几轮来回 / Adding these saves round trips:

    • 截图 / 录屏 Screenshot —— 直接把图片拖进编辑框,或 Ctrl+V 粘贴
    • 控制台报错 Console —— F12 → Console,复制文本或截图
    • 环境 Environment —— 插件版本(设置 → 备份与迁移 → 关于,可一键复制)、DSH 版本、系统、浏览器版本、显示缩放与浏览器缩放、深色/浅色

    ⚠️ 请务必抹掉 API Key / Token / 密码等敏感信息(包括截图里的)。
    Never paste real secrets — including inside screenshots.

  2. added
    needs-triage等待维护者分类与确认(awaiting maintainer triage)
    on Nov 10, 2026
  3. xiajiajun516 commented on Nov 10, 2026

    @xiajiajun516
    Owner

    This was generated by AI during triage.

    Triage Notes

    已核对范围:本票正文与唯一评论(github-actions 自动回复,无实质信息);main 当前代码(HEAD 78c881f,工作区干净);全部本地与远端分支、gh pr list --search 132;全仓 grep removePlugins / followRemoteDelete / prunePlugins 零命中;.out-of-scope/、.dsh/issue-ledger.md;相邻 #111、#35(均与本票无关,你的判断成立)。

    结论:尚未实现,需求成立,技术可行。 你引用的 5 处代码逐条核对无误:

    1. src/adapters/plugins.ts:808-867 — analyzeImport 只产 Install(目标未装)/ Skip(同版本)/ Conflict(版本不同),确实没有「目标机有、备份没有 → 删除」分支。
    2. src/adapters/plugins.ts:995-1073 — applyItem 只有安装/升级分支。
    3. src/sync/merge.ts:299-301 — GRANULAR_MERGES 只有 workspaces;通用 JSON 合并对数组两侧各自改即整分区冲突。
    4. src/sync/sync-engine.ts:1398 — hasLocalChanges 存在,A 侧卸载能被检出并推送。
    5. src/core/restore.ts:675-706 — planRestore 的插件撤销只算「导入期间新增 → pluginRemove」,没有「基线有 → 当前没有 → 重装」的反向分支。

    三处需要修正/补充的实现事实(不影响需求成立,但会改变方案形状):

    • ctx.target.plugins.remove 不存在:宿主 facade 只有 install(name, target, signal)(src/adapters/plugins.ts:1029)。本仓既有卸载通道是 runDshPlugin(profileDir, profile, ['remove', name])(src/core/restore.ts:429-442);桌面保留档案会命中 desktop-profile-reserved,recoverable: false(src/core/plugin-cli.ts:704/799)。
    • analyzeImport 不是同步专用:普通「从文件导入」也走它(src/index.ts:3421、src/ui/import-wizard.ts:349),而那条路径没有祖先。所以删除候选必须由同步引擎作为入参注入(例如 removalCandidates 名单),adapter 自己绝不推基线 —— 否则「导入一份旧快照」会开始删本机插件。这是最小可行版必须先钉住的边界。
    • 不需要为「自动同步不删」新增二级开关:风险分级已经够用 —— 注册表 riskTier(src/sync/risk.ts:1-25,high 恒不参与自动应用)+ autoApplyTier 缺省 medium(src/sync/autosync-config.ts:58-61,medium 时 plugins 会自动落盘)。把「含删除项的合并结果」判到阈值之上即可,与你方案第 8 点同效但更少开关。

    方案 2(记录级三方合并)也有仓内先例可循:GRANULAR_MERGES 的 workspaces 钩子刻意在 ancestor 闸门之前执行(src/sync/merge.ts ③' 段),所以「按包名三方合并 + 首轮两方同步仍能并集」是既有允许的形态。

    与既有产品裁决的冲突(这是本票真正的决策点):docs/known-issues/known-gaps.md:108-115 的 G-20 已明确「对端删除不代本机删除」为有意不修,理由是不可回滚,并把将来要做的前置条件写死为三条:① 适配器删除能力 + 审计;② 新的计划项种类 + 逐项确认 UI(不得并入自动应用);③ 与「导入前强制快照」的可回滚语义对齐。

    插件与会话的差别正好落在第三条:插件可回滚(beforePlugins 基线 + 重装通道),会话不可回滚。所以插件有资格破例、会话没有。但「自动同步也删」与②直接冲突。

    若要支持回滚,必须一起补的缺口:planRestore 补「基线有、当前没有 → 用 name@version/spec 装回」的 pluginInstall 分支;另注意 pluginRemove 记 external=true、不带指纹(docs/known-issues/known-limits.md:77),重装结果只能靠回读版本判成功。

    笔记:既有笔记(docs/evidence/sync-channel.md 的合并/风险分级、known-gaps.md G-20、known-limits.md:77)能解释机制,但没有「同步删除插件」的判定与缺口,故按 /research 口径补了一次源码级核查,结论落在 docs/evidence/sync-channel.md 的「跟随远端删除插件(issue #132 评估)」一节,含 9 条待实现守卫清单(A 卸载→B 删、B 新装不删、B 改版报冲突、开关关降级 Skip、bundlesOnly/自身不删、缺祖先不删、失败逐项 warning、自动档恒不删、回滚能重装)。

    标签:维持 enhancement + needs-triage,等维护者对下面三个问题拍板后再动。

  4. xiajiajun516 commented on Nov 10, 2026

    @xiajiajun516
    Owner

    This was generated by AI during triage.

    Agent brief

    目标:plugins 分区同步支持「跟随远端删除插件」。开关默认关闭;开启后手动合并与自动同步都会产生删除项;关闭时行为与当前完全一致(只增不删)。

    范围(必须做)

    1. 开关:SyncSelection 增 per-channel removePlugins(缺省 false),贯通自动同步配置读写(src/sync/autosync-config.ts)与同步页 UI 开关 + 文案。
    2. 三方判定:plugins 进 GRANULAR_MERGES(src/sync/merge.ts:299-301),按包名做「祖先 / 本地 / 远端」三方合并;祖先前置不变(src/sync/ancestor.ts:41-52,读不到祖先 → 两方合并 → 一律不删)。
    3. 删除候选注入:由同步引擎算出 removalCandidates(祖先有、远端没有、本地未改)并作为入参传给 analyzeImport。adapter 不得自推基线 —— analyzeImport 也被文件导入复用(src/index.ts:3421、src/ui/import-wizard.ts:349),那条路没有祖先,自推基线会让「导入旧快照」开始删插件。
    4. 新计划项种类 Remove:定义域 src/core/types.ts:652,连带 src/core/import-planning.ts:345、src/core/analyzer.ts:672/803(统计 + needsRestart)、src/ui/backup-inspect.ts:44/69、字典键(src/ui/i18n.ts 的 sync.kind.*;zh 逐字保留、en 补齐,指纹冻结测试只许追加)。
    5. 执行:applyItem 的 Remove 分支走 runDshPlugin(profileDir, profile, ['remove', name])(src/core/restore.ts:429-442)+ classifyDshPluginFailure;desktop-profile-reserved 记 warning 并继续,不阻断其余项。
    6. 回滚:planRestore 补「基线有、当前没有 → 用 name@version/spec 装回」的 pluginInstall 分支(src/core/restore.ts:675-706 现有分支只有反向);每次删除逐项写 undo 记录。
    7. 报告:拉取/合并报告逐条列明 已删除:X@1.2.3;开关关闭时删除候选降级 Skip 并在原因里写明「开关未开启」。

    非目标

    • sessions 分区不动,仍受 G-20 全约束(本票的例外只覆盖 plugins,已回填 docs/known-issues/known-gaps.md)。
    • 不引入第二个「自动同步是否删除」开关:开关本身就是授权。
    • 不加新的同步协议字段/版本协商。

    验收(每条一个测试,对照 src/adapters/plugins.test.ts 既有风格)

    ① A 卸载 → B 删除;② B 本地新装 → 不删;③ B 本地改版 → 不删且报 Conflict;④ 开关关 → 不删 + Skip 原因;⑤ bundlesOnly / 本插件自身 → 不删;⑥ 无共同祖先 → 不删;⑦ 卸载失败 → 逐项 warning 不阻断;⑧ desktop 档案被拒 → warning;⑨ undo 回滚能重装被删插件;⑩ 文件导入路径(无祖先)产生零个 Remove。

    验证顺序:npm run typecheck → npm test →(动 client/样式)npm run build。

    已知风险:plugins 导出会跑 npm pack(走内容缓存,见 docs/evidence/sync-channel.md 的打包缓存节);Remove 种类会穿过 analyzer 统计与 needsRestart,别漏改。

    判定依据与缺口全表:docs/evidence/sync-channel.md 的「跟随远端删除插件(issue #132 评估)」一节。

  5. added
    ready-for-agent完全描述清楚、可直接交给 AFK agent(fully specified, ready for an AFK agent)
    and removed
    needs-triage等待维护者分类与确认(awaiting maintainer triage)
    on Nov 10, 2026
  6. self-assigned this
    on Nov 10, 2026
  7. xiajiajun516 commented on Nov 10, 2026

    @xiajiajun516
    Owner

    This was generated by AI during implementation.

    已实施并验证(本地未推送)

    提交:23df0bd feat(sync): 跟随远端删除插件(#132)(47 文件,本地分支 main,未 push;回滚 git revert 23df0bd)。

    开关:SyncSelection.deletePlugins(缺省 false,落盘 sync-selection.json,POST /sync/selection 只认 deletePlugins === true);UI 在分区选择弹窗 plugins 行内,附「开启后删除不可回滚提示」。开关开 = 手动合并与自动同步都可删;关 = 一条都不删,且合并钩子整体退出(否则会丢条目、下一轮把已删插件当「本机独有」推回远端而复活)。

    判定与执行:src/sync/plugin-removals.ts 只对本机==祖先且远端已删的条目生成删除;本机新装 / 本机改版(冲突)/ bundlesOnly / self / 无祖先副本 → 一律不删并逐项说明原因。真正卸载发生在 executeImportPlan 成功之后(applyPluginRemovalStep,src/sync/sync-engine.ts 的 4'/4''段,早于基线重导出),逐项写 undo 到本次回滚快照;失败项按 warning 报告、不阻断其余项;desktop 档案被 CLI 拒同样只记 warning。

    undo 装回:Snapshot.pluginRemovals + RestoreActionKind 'pluginInstall',恢复计划用 pluginInstallTarget(name, { version, spec }) 走 dsh plugin add(非 registry 来源保留 spec);已装则 skip。GUI 恢复路径(executeRestorePlan)与 CLI 危险动作清单都补了 pluginInstall,WAL 目标判定统一走 isExternalPluginKind(不再把包名当 $DSH_HOME 下的路径)。

    验收对照:①A 卸载→B 删 ✔(引擎用例)②B 新装不删 ✔ ③B 改版本→冲突不删 ✔ ④开关关不删且说明 ✔ ⑤bundlesOnly/self 不删 ✔ ⑥无祖先不删 ✔ ⑦卸载失败逐项 warning ✔ ⑧desktop 被拒 warning ✔ ⑨undo 装回 ✔(含非 registry spec)⑩文件导入路径零 Remove ✔(删除只在引擎后置步骤,不进 ImportPlan)。

    验证:npm run typecheck 0;npm test 5561 tests / 5559 pass / 0 fail;npm run build 0(TEMP/TMP 指向 .tmp/buildtmp);npm run smoke 12/12;体量棘轮(F-01)通过。

    文档:docs/contracts/sync-channel-v1.md §5 增 deletePlugins;docs/evidence/sync-channel.md 增「已交付」与设计偏离说明(不走 ImportPlan 的 Remove 计划项);docs/known-issues/known-gaps.md G-20 增 plugins 例外(前提=可回滚 + 逐项 undo + 报告列明);CHANGELOG.md ## [Unreleased] 双语条目。

    仍未覆盖:自动同步的「有删除项先确认」未做——按维护者裁决(2026-11-10)开关本身即授权,自动同步直接删;未发布到 npm(等下一个版本)。

  8. xiajiajun516 commented on Nov 10, 2026

    @xiajiajun516
    Owner

    实现已在本地完成并验证通过(提交 23df0bd),随下个版本发布

  9. added
    confirmed已确认,会修复(confirmed, will be fixed)
    and removed
    ready-for-agent完全描述清楚、可直接交给 AFK agent(fully specified, ready for an AFK agent)
    on Nov 10, 2026
  10. added 4 commits that reference this issue on Nov 10, 2026
  11. xiajiajun516 commented on Nov 10, 2026

    @xiajiajun516
    Owner

    This was generated by AI during triage.

    已在 v0.8.0 修复并发布。

    现在同步可以在你打开开关后跟随远端删除插件:远端删掉、你这边没改过的插件会在合并或自动同步时一并卸载,报告里逐项写「已删除:包@版本」,删掉的都会记进当轮快照,恢复快照时自动装回同一版本(非 registry 来源也按原来源装回)。开关默认关闭;桌面档案被系统拒时只记一条警告,不影响其余项。

    升级:dsh plugin --profile <档案> add dsh-config-manager@0.8.0

    自证动作(三步,约两分钟):

    1. 设置 → 配置管理 → 同步 → 分区选择,找到 plugins 那一行的「跟随远端删除插件」开关,确认它默认是关的。
    2. 复制一份插件清单,在一台机器上卸载某个插件并同步一次;在另一台(开关仍关)手动同步 → 该插件应仍在,报告里不会出现删除。
    3. 打开开关后再同步一次 → 报告出现「已删除:包@版本」,插件从列表消失;随后恢复那一轮快照 → 插件被装回同一版本。

    如果第 3 步没删或没装回,把同步报告截图与 包名 + 版本 贴回来,我按报告里的逐项原因继续查。

  12. added
    fixed已在某个版本中修复(fixed in a release)
    and removed
    confirmed已确认,会修复(confirmed, will be fixed)
    on Nov 10, 2026
  13. added a commit that references this issue on Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestfixed已在某个版本中修复(fixed in a release)

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions