
















@@ -1,5 +1,6 @@
11import { replaceConfigFile } from "../config/config.js";
22import type { OpenClawConfig } from "../config/types.openclaw.js";
3+import type { PluginInstallRecord } from "../config/types.plugins.js";
34import { type HookInstallUpdate, recordHookInstall } from "../hooks/installs.js";
45import { isPathInside } from "../infra/path-guards.js";
56import { enablePluginInConfig } from "../plugins/enable.js";
@@ -11,6 +12,11 @@ import {
1112import type { PluginInstallUpdate } from "../plugins/installs.js";
1213import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trace.js";
1314import { buildPluginSnapshotReport } from "../plugins/status.js";
15+import {
16+applyPluginUninstallDirectoryRemoval,
17+planPluginUninstall,
18+type PluginUninstallDirectoryRemoval,
19+} from "../plugins/uninstall.js";
1420import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
1521import { theme } from "../terminal/theme.js";
1622import { resolveUserPath, shortenHomePath } from "../utils.js";
@@ -110,6 +116,69 @@ function logShadowedNpmInstallWarning(params: {
110116);
111117}
112118119+function resolveComparableInstallPath(
120+install: Pick<PluginInstallRecord, "installPath" | "sourcePath">,
121+) {
122+return install.installPath ?? install.sourcePath;
123+}
124+125+function shouldPreserveReplacedInstallPath(params: {
126+removalTarget: string;
127+nextInstallPath: string;
128+}) {
129+const removalTarget = resolveUserPath(params.removalTarget);
130+const nextInstallPath = resolveUserPath(params.nextInstallPath);
131+return (
132+isPathInside(removalTarget, nextInstallPath) || isPathInside(nextInstallPath, removalTarget)
133+);
134+}
135+136+function resolveReplacedManagedInstallRemoval(params: {
137+pluginId: string;
138+previousInstall?: PluginInstallRecord;
139+nextInstall: Omit<PluginInstallUpdate, "pluginId">;
140+}): PluginUninstallDirectoryRemoval | null {
141+if (!params.previousInstall) {
142+return null;
143+}
144+const previousInstallPath = resolveComparableInstallPath(params.previousInstall);
145+const nextInstallPath = resolveComparableInstallPath(params.nextInstall);
146+if (!previousInstallPath || !nextInstallPath) {
147+return null;
148+}
149+if (
150+shouldPreserveReplacedInstallPath({
151+removalTarget: previousInstallPath,
152+ nextInstallPath,
153+})
154+) {
155+return null;
156+}
157+const plan = planPluginUninstall({
158+config: {
159+plugins: {
160+installs: {
161+[params.pluginId]: params.previousInstall,
162+},
163+},
164+} as OpenClawConfig,
165+pluginId: params.pluginId,
166+deleteFiles: true,
167+});
168+if (!plan.ok || !plan.directoryRemoval) {
169+return null;
170+}
171+if (
172+shouldPreserveReplacedInstallPath({
173+removalTarget: plan.directoryRemoval.target,
174+ nextInstallPath,
175+})
176+) {
177+return null;
178+}
179+return plan.directoryRemoval;
180+}
181+113182export async function persistPluginInstall(params: {
114183snapshot: ConfigSnapshotForInstallPersist;
115184pluginId: string;
@@ -138,6 +207,11 @@ export async function persistPluginInstall(params: {
138207() => loadInstalledPluginIndexInstallRecords(),
139208{ command: "install" },
140209);
210+const replacedInstallRemoval = resolveReplacedManagedInstallRemoval({
211+pluginId: params.pluginId,
212+previousInstall: installRecords[params.pluginId],
213+nextInstall: params.install,
214+});
141215const nextInstallRecords = recordPluginInstallInRecords(installRecords, {
142216pluginId: params.pluginId,
143217 ...params.install,
@@ -165,6 +239,23 @@ export async function persistPluginInstall(params: {
165239}),
166240{ command: "install" },
167241);
242+if (replacedInstallRemoval) {
243+const removalResult = await tracePluginLifecyclePhaseAsync(
244+"replaced install cleanup",
245+() => applyPluginUninstallDirectoryRemoval(replacedInstallRemoval),
246+{ command: "install", pluginId: params.pluginId },
247+);
248+for (const warning of removalResult.warnings) {
249+runtime.log(theme.warn(warning));
250+}
251+if (removalResult.directoryRemoved) {
252+runtime.log(
253+theme.muted(
254+`Removed previous plugin install directory: ${shortenHomePath(replacedInstallRemoval.target)}`,
255+),
256+);
257+}
258+}
168259await refreshPluginRegistryAfterConfigMutation({
169260config: next,
170261reason: "source-changed",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。