Repository navigation
Closed
Labels
enhancementNew feature or requestNew feature or requestfixed已在某个版本中修复(fixed in a release)已在某个版本中修复(fixed in a release)
Description
使用场景
我在两台机器(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 自身 |
保留 | 保留 |
| 删除失败(权限、占用、桌面保留档案) | — | 逐项报错,不阻断其余项 |
判定规则(建议):
- 以上一次成功同步的共同祖先快照为基线,而不是“远端没有就删”。
- 只删除同时满足以下条件的插件:
- 祖先快照的
plugins里有; - 本次要应用的远端快照的
plugins里没有; - 本机当前仍安装;
- 本机记录(版本 / spec)与祖先一致,说明不是本地改过;
- 不是
bundlesOnly、不是dsh-config-manager自身、不是桌面端不可移除的内置包。
- 祖先快照的
- 本机新装、祖先里没有的插件永不删除,避免把“刚装还没上传”的插件误删。
- 没有共同祖先(首次同步)时一律不删,只给提示。
- 只对“祖先 → 更新快照”的正向应用生效;手动选历史旧快照时不自动删,避免把后来新增的插件当成“远端删除”。
- 删除前在导入确认页单独列出
将删除:X@1.2.3,默认勾选(因为开关已开启),允许取消。 - 删除后写入本次 apply 的 undo 记录;回滚时重新安装被删插件。
- 自动同步:建议第一版遇到删除项时暂停待确认,不要无人值守静默删除;如果维护者希望全自动,可再加一个“自动同步也执行删除”的二级开关,默认关闭。
这个默认关闭的设计也符合 README 里“宁可少迁移一个配置,也不要破坏你现有的配置”的原则。
实现要点(供参考)
- 配置持久化:在
SyncSelection(src/sync/sync-selection.ts)增加 per-channel 的removePlugins: boolean,默认false;同步页读写这个字段。或放进sync-config.json的通道配置,二选一。 - 记录级合并:给
src/sync/merge.ts的GRANULAR_MERGES增加plugins: mergePluginsSection,按包名做三方合并,输出added / updated / removed / conflicts,不要整个数组判冲突。 - 导入计划:
analyzeImport()增加Remove(或Delete)kind;只对removed且本机==祖先的条目生成。开关关闭时降级为Skip,并在 detail 里说明“远端已删除,开关未开启,保留本机插件”。 - 执行卸载:
applyItem()处理 Remove,调用宿主新增的ctx.target.plugins.remove(name, signal);桌面desktopprofile 走桌面插件管理器(避免desktop-profile-reserved),失败按现有warning逐项报告。 - 回滚:
src/core/restore.ts的planRestore()增加“baseline 有、当前没有 → 重装”的pluginInstallaction;删除项带name@version/spec,恢复时用官方dsh plugin add装回。 - 测试:至少覆盖
- A 卸载 → B 拉取删除;
- B 本地新装不删;
- B 本地改版不删/报冲突;
- 开关关闭不删;
bundlesOnly/ self / 不可移除包不删;- 删除失败不阻断其余项;
- 回滚能重装被删插件;
- 自动同步有删除项时按约定暂停/确认。
最小可行版本
如果不想一次做太大,第一版可以只做:
- 一个开关,默认关闭;
- 只作用于手动「从远端合并到本机」;
- 自动同步遇到删除项只提示,不自动删;
- 删除前必须确认,删除后写 undo 记录。
等这个跑稳,再放开自动同步。
备选方案
- 保持现状 + 手动卸载:最安全,但多机场景麻烦;也是不想加开关时的默认。
- 独立 CLI
sync prune --plugins:适合高级用户/脚本,但普通用户仍需要 UI 开关;可以和开关共用同一套判定。 - 整机镜像模式:把所有分区都做成镜像,包括删除本地新装插件;风险更高,不建议作为默认,也不建议和这个开关混在一起。
- 只在手动导入时删除,自动同步永不删除:更保守的第一版;如果担心无人值守误删,可以先这样做。
补充
- 版本:
dsh-config-manager0.7.0 / DSH Desktop 0.2.2-rc.1,Git 通道,plugins分区已勾选。 - 我已搜索现有 issue、PR 和 Discussions(含“插件卸载 / 删除插件 / plugin uninstall / plugin removal / 镜像 / 同步删除”等关键词),没有发现重复建议;最接近的 [Bug] 进行备份时会打开DSH中我关闭的插件 #111、[Bug] 同步 plugins 分区会带上 pnpm-workspace.yaml 的 patchedDependencies,但不同步对应 patch 文件 → 目标机 pnpm 拒绝一切插件安装(且一键同步仍报成功) #35 都不是这个诉求。
进度
- 进度: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 test5559 pass 0 fail / build 0 / smoke 12/12 / 体量棘轮 F-01 通过。 - 已知偏离:删除不走
ImportPlan的 Remove 计划项(在导入成功后执行,才有快照与 undo 基线);自动同步在开关开启时直接删,不额外弹确认(开关即授权,维护者裁决 2026-11-10)。 - 未发布:等下一个版本;发布后按
.dsh/issue-ledger.md回版本号并换fixed。
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requestfixed已在某个版本中修复(fixed in a release)已在某个版本中修复(fixed in a release)
Activity
github-actions commentedon Nov 10, 2026
感谢反馈!🙏 我通常会在 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:
xiajiajun516 commentedon Nov 10, 2026
Triage Notes
已核对范围:本票正文与唯一评论(github-actions 自动回复,无实质信息);
main当前代码(HEAD78c881f,工作区干净);全部本地与远端分支、gh pr list --search 132;全仓grepremovePlugins/followRemoteDelete/prunePlugins零命中;.out-of-scope/、.dsh/issue-ledger.md;相邻 #111、#35(均与本票无关,你的判断成立)。结论:尚未实现,需求成立,技术可行。 你引用的 5 处代码逐条核对无误:
src/adapters/plugins.ts:808-867—analyzeImport只产Install(目标未装)/Skip(同版本)/Conflict(版本不同),确实没有「目标机有、备份没有 → 删除」分支。src/adapters/plugins.ts:995-1073—applyItem只有安装/升级分支。src/sync/merge.ts:299-301—GRANULAR_MERGES只有workspaces;通用 JSON 合并对数组两侧各自改即整分区冲突。src/sync/sync-engine.ts:1398—hasLocalChanges存在,A 侧卸载能被检出并推送。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.mdG-20、known-limits.md:77)能解释机制,但没有「同步删除插件」的判定与缺口,故按 /research 口径补了一次源码级核查,结论落在docs/evidence/sync-channel.md的「跟随远端删除插件(issue #132 评估)」一节,含 9 条待实现守卫清单(A 卸载→B 删、B 新装不删、B 改版报冲突、开关关降级 Skip、bundlesOnly/自身不删、缺祖先不删、失败逐项 warning、自动档恒不删、回滚能重装)。标签:维持
enhancement+needs-triage,等维护者对下面三个问题拍板后再动。xiajiajun516 commentedon Nov 10, 2026
Agent brief
目标:
plugins分区同步支持「跟随远端删除插件」。开关默认关闭;开启后手动合并与自动同步都会产生删除项;关闭时行为与当前完全一致(只增不删)。范围(必须做)
SyncSelection增 per-channelremovePlugins(缺省false),贯通自动同步配置读写(src/sync/autosync-config.ts)与同步页 UI 开关 + 文案。plugins进GRANULAR_MERGES(src/sync/merge.ts:299-301),按包名做「祖先 / 本地 / 远端」三方合并;祖先前置不变(src/sync/ancestor.ts:41-52,读不到祖先 → 两方合并 → 一律不删)。removalCandidates(祖先有、远端没有、本地未改)并作为入参传给analyzeImport。adapter 不得自推基线 ——analyzeImport也被文件导入复用(src/index.ts:3421、src/ui/import-wizard.ts:349),那条路没有祖先,自推基线会让「导入旧快照」开始删插件。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 补齐,指纹冻结测试只许追加)。applyItem的Remove分支走runDshPlugin(profileDir, profile, ['remove', name])(src/core/restore.ts:429-442)+classifyDshPluginFailure;desktop-profile-reserved记 warning 并继续,不阻断其余项。planRestore补「基线有、当前没有 → 用name@version/spec装回」的pluginInstall分支(src/core/restore.ts:675-706现有分支只有反向);每次删除逐项写 undo 记录。已删除: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 评估)」一节。xiajiajun516 commentedon Nov 10, 2026
已实施并验证(本地未推送)
提交:
23df0bdfeat(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 typecheck0;npm test5561 tests / 5559 pass / 0 fail;npm run build0(TEMP/TMP 指向.tmp/buildtmp);npm run smoke12/12;体量棘轮(F-01)通过。文档:
docs/contracts/sync-channel-v1.md§5 增deletePlugins;docs/evidence/sync-channel.md增「已交付」与设计偏离说明(不走 ImportPlan 的 Remove 计划项);docs/known-issues/known-gaps.mdG-20 增 plugins 例外(前提=可回滚 + 逐项 undo + 报告列明);CHANGELOG.md## [Unreleased]双语条目。仍未覆盖:自动同步的「有删除项先确认」未做——按维护者裁决(2026-11-10)开关本身即授权,自动同步直接删;未发布到 npm(等下一个版本)。
xiajiajun516 commentedon Nov 10, 2026
实现已在本地完成并验证通过(提交 23df0bd),随下个版本发布
checkpoint: docs(evidence): issue #132 同步删除插件的现状、闸门与回滚缺口评估
checkpoint: docs: 记录 #132 维护者裁决(开启后自动同步也可删),G-20 增插件例外
checkpoint: feat(sync): 跟随远端删除插件(#132)——开关、引擎后置卸载、undo 装回与逐项报告
checkpoint: ledger 记 #132 待发布回填版本号
xiajiajun516 commentedon Nov 10, 2026
已在 v0.8.0 修复并发布。
现在同步可以在你打开开关后跟随远端删除插件:远端删掉、你这边没改过的插件会在合并或自动同步时一并卸载,报告里逐项写「已删除:包@版本」,删掉的都会记进当轮快照,恢复快照时自动装回同一版本(非 registry 来源也按原来源装回)。开关默认关闭;桌面档案被系统拒时只记一条警告,不影响其余项。
升级:
dsh plugin --profile <档案> add dsh-config-manager@0.8.0自证动作(三步,约两分钟):
如果第 3 步没删或没装回,把同步报告截图与
包名 + 版本贴回来,我按报告里的逐项原因继续查。chore(ledger): 删除 #132 行(v0.8.0 已回版本号)