惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
有赞技术团队
有赞技术团队
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
Check Point Blog
博客园 - 【当耐特】
The GitHub Blog
The GitHub Blog
Recent Announcements
Recent Announcements
The Cloudflare Blog
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
Vercel News
Vercel News
IT之家
IT之家
MyScale Blog
MyScale Blog
博客园_首页
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
罗磊的独立博客

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(migrate): humanize conflict-status messaging across m...
sjf · 2026-05-14 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -576,7 +576,7 @@ export async function discoverCodexSource(

576576

const hooksPath = path.join(codexHome, "hooks", "hooks.json");

577577

const codexSkills = await discoverSkillDirs({

578578

root: codexSkillsDir,

579-

sourceLabel: "Codex CLI skill",

579+

sourceLabel: "Codex skill",

580580

excludeSystem: true,

581581

});

582582

const personalAgentSkills = await discoverSkillDirs({

Original file line numberDiff line numberDiff line change

@@ -103,7 +103,7 @@ function codexSkillPlan(overrides: Partial<MigrationPlan> = {}): MigrationPlan {

103103

target: "/tmp/openclaw/workspace/skills/alpha",

104104

details: {

105105

skillName: "alpha",

106-

sourceLabel: "Codex CLI skill",

106+

sourceLabel: "Codex skill",

107107

},

108108

},

109109

{

@@ -676,7 +676,7 @@ describe("migrateApplyCommand", () => {

676676

const optionsByValue = new Map(pluginPrompt.options?.map((option) => [option.value, option]));

677677

expect(optionsByValue.get("plugin:google-calendar")?.label).toBe("google-calendar");

678678

expect(String(optionsByValue.get("plugin:google-calendar")?.hint)).toContain(

679-

"conflict: plugin exists",

679+

"already installed in workspace",

680680

);

681681

expect(optionsByValue.get("plugin:gmail")?.label).toBe("gmail");

682682

const appliedPlan = firstAppliedPlan();

Original file line numberDiff line numberDiff line change

@@ -132,15 +132,34 @@ const REASON_CODE_MESSAGES: Record<string, string> = {

132132

"not selected for migration": "Skipped because it was not selected for migration.",

133133

};

134134
135+

// Phrase-form conflict reasons, used as-is in selection-prompt hints

136+

// (`<source label> <phrase>`) and wrapped into sentence form for preview

137+

// /result rows. Keep one map so the two surfaces never drift.

138+

export const MIGRATION_CONFLICT_REASON_PHRASES: Record<string, string> = {

139+

"target exists": "already installed in workspace",

140+

"plugin exists": "already installed in workspace",

141+

};

142+
143+

function conflictReasonSentence(reason: string): string | undefined {

144+

const phrase = MIGRATION_CONFLICT_REASON_PHRASES[reason];

145+

if (!phrase) {

146+

return undefined;

147+

}

148+

return `${phrase.charAt(0).toUpperCase()}${phrase.slice(1)}.`;

149+

}

150+
135151

function humanizeReason(reason: string | undefined): string | undefined {

136152

if (!reason) {

137153

return undefined;

138154

}

139-

return REASON_CODE_MESSAGES[reason] ?? reason;

155+

return REASON_CODE_MESSAGES[reason] ?? conflictReasonSentence(reason) ?? reason;

140156

}

141157
142158

function formatItemMessage(item: MigrationItem, mode: FormatMode): string | undefined {

143159

if (mode === "preview") {

160+

if (item.status === "conflict" || item.status === "skipped" || item.status === "error") {

161+

return humanizeReason(item.reason) ?? item.message;

162+

}

144163

if (item.kind === "skill" && item.action === "copy") {

145164

return "Copy Codex skill into OpenClaw";

146165

}

@@ -177,29 +196,26 @@ const RESULT_STATUS_GLYPHS: Record<string, string> = {

177196

conflict: "⚠️ ",

178197

};

179198
180-

function formatItemPrefix(item: MigrationItem, mode: FormatMode): string {

199+

function formatItemPrefix(item: MigrationItem): string {

181200

if (item.kind === "manual") {

182201

return "🔍 ";

183202

}

184203

if (item.kind === "archive") {

185204

return "📖 ";

186205

}

187-

if (mode === "result") {

188-

const glyph = RESULT_STATUS_GLYPHS[item.status];

189-

if (glyph) {

190-

return `${glyph} `;

191-

}

192-

return "• ";

206+

const glyph = RESULT_STATUS_GLYPHS[item.status];

207+

if (glyph) {

208+

return `${glyph} `;

193209

}

194-

return item.status === "planned" ? "• " : `• ${item.status}: `;

210+

return "• ";

195211

}

196212
197213

function formatMigrationItem(item: MigrationItem, mode: FormatMode): string {

198214

const name = formatItemDisplayName(item);

199215

const message = formatItemMessage(item, mode);

200216

const messageSuffix = message ? ` ${theme.muted(`(${message})`)}` : "";

201217

const sensitive = item.sensitive ? " [sensitive]" : "";

202-

const prefix = formatItemPrefix(item, mode);

218+

const prefix = formatItemPrefix(item);

203219

return `${prefix}${name}${sensitive}${messageSuffix}`;

204220

}

205221
Original file line numberDiff line numberDiff line change

@@ -36,7 +36,7 @@ function skillItem(params: {

3636

reason: params.reason,

3737

details: {

3838

skillName: params.name,

39-

sourceLabel: "Codex CLI skill",

39+

sourceLabel: "Codex skill",

4040

},

4141

};

4242

}

@@ -491,7 +491,7 @@ describe("applyMigrationPluginSelection", () => {

491491

"plugin:gmail",

492492

]);

493493

expect(formatMigrationPluginSelectionHint(items[1])).toBe(

494-

"openai-curated; conflict: plugin exists",

494+

"openai-curated plugin already installed in workspace",

495495

);

496496

});

497497
Original file line numberDiff line numberDiff line change

@@ -1,6 +1,7 @@

11

import path from "node:path";

22

import { markMigrationItemSkipped, summarizeMigrationItems } from "../../plugin-sdk/migration.js";

33

import type { MigrationItem, MigrationPlan } from "../../plugins/types.js";

4+

import { MIGRATION_CONFLICT_REASON_PHRASES } from "./output.js";

45
56

export const MIGRATION_SKILL_NOT_SELECTED_REASON = "not selected for migration";

67

export const MIGRATION_PLUGIN_NOT_SELECTED_REASON = "not selected for migration";

@@ -253,28 +254,34 @@ export function formatMigrationSkillSelectionLabel(item: MigrationItem): string

253254

return readMigrationSkillName(item) ?? item.id.replace(/^skill:/u, "");

254255

}

255256
257+

function humanizeMigrationConflictReason(reason: string | undefined): string {

258+

if (!reason) {

259+

return "conflict";

260+

}

261+

return MIGRATION_CONFLICT_REASON_PHRASES[reason] ?? reason;

262+

}

263+
256264

export function formatMigrationSkillSelectionHint(item: MigrationItem): string | undefined {

257-

const parts = [readMigrationSkillSourceLabel(item)];

265+

const sourceLabel = readMigrationSkillSourceLabel(item);

258266

if (item.status === "conflict") {

259-

parts.push(item.reason ? `conflict: ${item.reason}` : "conflict");

267+

const reason = humanizeMigrationConflictReason(item.reason);

268+

return sourceLabel ? `${sourceLabel} ${reason}` : reason;

260269

}

261-

return (

262-

parts

263-

.filter((value): value is string => typeof value === "string" && value.length > 0)

264-

.join("; ") || undefined

265-

);

270+

return sourceLabel ?? undefined;

266271

}

267272
268273

export function formatMigrationPluginSelectionHint(item: MigrationItem): string | undefined {

269274

const pluginName = readMigrationPluginName(item);

270275

const configKey = readMigrationPluginConfigKey(item);

276+

const marketplace = readMigrationPluginMarketplaceName(item);

277+

if (item.status === "conflict") {

278+

const reason = humanizeMigrationConflictReason(item.reason);

279+

return marketplace ? `${marketplace} plugin ${reason}` : reason;

280+

}

271281

const parts = [

272-

readMigrationPluginMarketplaceName(item),

282+

marketplace,

273283

configKey && configKey !== pluginName ? `config: ${configKey}` : undefined,

274284

];

275-

if (item.status === "conflict") {

276-

parts.push(item.reason ? `conflict: ${item.reason}` : "conflict");

277-

}

278285

return (

279286

parts

280287

.filter((value): value is string => typeof value === "string" && value.length > 0)