
























@@ -32,6 +32,7 @@ const CODEX_PLUGIN_NATIVE_CONFIG_PATH = [
3232"config",
3333"codexPlugins",
3434] as const;
35+const MIGRATION_REASON_PLUGIN_EXISTS = "plugin exists";
35363637export type CodexPluginMigrationConfigEntry = {
3738configKey: string;
@@ -107,12 +108,42 @@ function uniquePluginConfigKey(
107108return sanitizeName(`${base}-${next}`) || base;
108109}
109110110-function buildPluginItems(plugins: readonly CodexPluginSource[]): MigrationItem[] {
111+function readExistingCodexPluginEntries(
112+config: MigrationProviderContext["config"],
113+): Record<string, unknown> {
114+const entries = readMigrationConfigPath(config as Record<string, unknown>, [
115+ ...CODEX_PLUGIN_NATIVE_CONFIG_PATH,
116+"plugins",
117+]);
118+return isRecord(entries) ? entries : {};
119+}
120+121+function hasExistingCodexPluginEntry(
122+existingEntries: Record<string, unknown>,
123+configKey: string,
124+pluginName: string,
125+): boolean {
126+if (existingEntries[configKey] !== undefined) {
127+return true;
128+}
129+return Object.values(existingEntries).some((entry) => {
130+if (!isRecord(entry)) {
131+return false;
132+}
133+return entry.pluginName === pluginName;
134+});
135+}
136+137+function buildPluginItems(
138+ctx: MigrationProviderContext,
139+plugins: readonly CodexPluginSource[],
140+): MigrationItem[] {
111141const baseCounts = new Map<string, number>();
112142for (const plugin of plugins.filter((entry) => entry.migratable)) {
113143const base = sanitizeName(plugin.pluginName ?? plugin.name) || "codex-plugin";
114144baseCounts.set(base, (baseCounts.get(base) ?? 0) + 1);
115145}
146+const existingPluginEntries = readExistingCodexPluginEntries(ctx.config);
116147const usedCounts = new Map<string, number>();
117148let manualIndex = 0;
118149const items: MigrationItem[] = [];
@@ -123,11 +154,16 @@ function buildPluginItems(plugins: readonly CodexPluginSource[]): MigrationItem[
123154plugin.pluginName
124155) {
125156const configKey = uniquePluginConfigKey(plugin, baseCounts, usedCounts);
157+const conflict =
158+!ctx.overwrite &&
159+hasExistingCodexPluginEntry(existingPluginEntries, configKey, plugin.pluginName);
126160items.push(
127161createMigrationItem({
128162id: `plugin:${configKey}`,
129163kind: "plugin",
130164action: "install",
165+status: conflict ? "conflict" : "planned",
166+reason: conflict ? MIGRATION_REASON_PLUGIN_EXISTS : undefined,
131167source: plugin.source,
132168target: `plugins.entries.codex.config.codexPlugins.plugins.${configKey}`,
133169message: `Install Codex plugin "${plugin.pluginName}" in the OpenClaw-managed Codex app-server runtime.`,
@@ -188,9 +224,15 @@ function readExistingAllowDestructiveActions(
188224return typeof value === "boolean" ? value : undefined;
189225}
190226227+function isRecord(value: unknown): value is Record<string, unknown> {
228+return Boolean(value && typeof value === "object" && !Array.isArray(value));
229+}
230+191231export function buildCodexPluginsConfigValue(
192232entries: readonly CodexPluginMigrationConfigEntry[],
193-params: { config?: MigrationProviderContext["config"] } = {},
233+params: {
234+config?: MigrationProviderContext["config"];
235+} = {},
194236): Record<string, unknown> {
195237const plugins = Object.fromEntries(
196238entries
@@ -204,18 +246,19 @@ export function buildCodexPluginsConfigValue(
204246},
205247]),
206248);
249+const config: Record<string, unknown> = {
250+codexPlugins: {
251+enabled: true,
252+allow_destructive_actions:
253+params.config === undefined
254+ ? false
255+ : (readExistingAllowDestructiveActions(params.config) ?? false),
256+ plugins,
257+},
258+};
207259return {
208260enabled: true,
209-config: {
210-codexPlugins: {
211-enabled: true,
212-allow_destructive_actions:
213-params.config === undefined
214- ? false
215- : (readExistingAllowDestructiveActions(params.config) ?? false),
216- plugins,
217-},
218-},
261+ config,
219262};
220263}
221264@@ -231,14 +274,51 @@ export function hasCodexPluginConfigConflict(
231274return true;
232275}
233276const nativeConfig = (value.config as Record<string, unknown> | undefined)?.codexPlugins;
234-return hasMigrationConfigPatchConflict(config, CODEX_PLUGIN_NATIVE_CONFIG_PATH, nativeConfig);
277+if (!isRecord(nativeConfig)) {
278+return hasMigrationConfigPatchConflict(config, CODEX_PLUGIN_NATIVE_CONFIG_PATH, nativeConfig);
279+}
280+const existingNativeConfig = readMigrationConfigPath(
281+config as Record<string, unknown>,
282+CODEX_PLUGIN_NATIVE_CONFIG_PATH,
283+);
284+if (existingNativeConfig === undefined) {
285+return false;
286+}
287+if (!isRecord(existingNativeConfig)) {
288+return true;
289+}
290+if (existingNativeConfig.enabled !== undefined && existingNativeConfig.enabled !== true) {
291+return true;
292+}
293+const allowDestructiveActions = nativeConfig.allow_destructive_actions;
294+if (
295+existingNativeConfig.allow_destructive_actions !== undefined &&
296+existingNativeConfig.allow_destructive_actions !== allowDestructiveActions
297+) {
298+return true;
299+}
300+const plugins = nativeConfig.plugins;
301+if (!isRecord(plugins)) {
302+return false;
303+}
304+return Object.entries(plugins).some(([configKey, plugin]) => {
305+if (!isRecord(plugin)) {
306+return existingNativeConfig[configKey] !== undefined;
307+}
308+return hasExistingCodexPluginEntry(
309+readExistingCodexPluginEntries(config),
310+configKey,
311+typeof plugin.pluginName === "string" ? plugin.pluginName : configKey,
312+);
313+});
235314}
236315237316function buildPluginConfigItem(
238317ctx: MigrationProviderContext,
239318pluginItems: readonly MigrationItem[],
240319): MigrationItem | undefined {
241320const entries = pluginItems
321+.filter((item) => item.status === "planned")
242322.map((item) => readCodexPluginMigrationConfigEntry(item, true))
243323.filter((entry): entry is CodexPluginMigrationConfigEntry => entry !== undefined);
244324if (entries.length === 0) {
@@ -280,7 +360,7 @@ export async function buildCodexMigrationPlan(
280360overwrite: ctx.overwrite,
281361})),
282362);
283-const pluginItems = buildPluginItems(source.plugins);
363+const pluginItems = buildPluginItems(ctx, source.plugins);
284364items.push(...pluginItems);
285365const pluginConfigItem = buildPluginConfigItem(ctx, pluginItems);
286366if (pluginConfigItem) {
@@ -303,7 +383,7 @@ export async function buildCodexMigrationPlan(
303383const warnings = [
304384 ...(items.some((item) => item.status === "conflict")
305385 ? [
306-"Conflicts were found. Re-run with --overwrite to replace conflicting skill targets after item-level backups.",
386+"Conflicts were found. Re-run with --overwrite to replace conflicting migration targets after item-level backups.",
307387]
308388 : []),
309389 ...(source.plugins.length > 0
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。