设计系统的 AI 迁移助手:自动将旧组件映射到新 Token 体系
设计系统的 AI 迁移助手自动将旧组件映射到新 Token 体系一、300 个旧组件的 Token 迁移——人肉干的边界公司决定将设计系统从 1.x 升级到 2.0核心变化是 Token 体系的重命名和重组$color-primary→--color-brand-primary$spacing-md→--space-4$font-size-base→--text-body-md问题是仓库中有 300 个业务组件直接引用了旧 Token 名称。手动替换的工作量估算300 个组件 × 平均 8 处引用 2400 次查找替换。还不算那些通过extend、mixin、动态拼接等方式间接引用的场景。AI 迁移助手要解决的核心问题不是替换字符串sed就能做而是理解Token 之间的语义映射关系——$color-primary是映射到--color-brand-primary还是--color-action-primary这取决于它的使用上下文。二、迁移引擎的三层映射架构三、迁移工具实现// migration/token-migration-engine.ts // 设计系统 Token 迁移引擎 // 读取旧项目代码根据映射表自动生成新 Token 引用 import fs from fs; import path from path; import { globSync } from glob; interface TokenMapping { /** 旧 Token 名称如 $color-primary */ oldToken: string; /** 匹配模式支持正则 */ pattern: RegExp; /** * 新 Token 候选项按优先级排序 * 第一个为默认映射后续为上下文相关映射 */ candidates: Array{ newToken: string; /** 在哪些 CSS 属性中使用时优先匹配此候选项 */ preferredProperties?: string[]; /** 在哪些选择器中使用时优先匹配 */ preferredSelectors?: string[]; }; } interface MigrationReport { file: string; line: number; column: number; oldValue: string; newValue: string; confidence: number; // 0~1映射置信度 reasoning: string; // 为什么选择这个映射 needsReview: boolean; // 是否需要人工审查 } /** * Token 迁移映射表 * 这是整个迁移系统的翻译词典——维护成本最高但最重要的数据 */ const TOKEN_MAPPINGS: TokenMapping[] [ { oldToken: $color-primary, pattern: /\$color-primary\b/g, candidates: [ { newToken: var(--color-brand-primary), preferredProperties: [color, background-color, border-color], preferredSelectors: [.btn-primary, .link-primary] }, { newToken: var(--color-action-primary), preferredProperties: [color], preferredSelectors: [.btn, a] } ] }, { oldToken: $spacing-md, pattern: /\$spacing-md\b/g, candidates: [ { newToken: var(--space-4), preferredProperties: [padding, margin, gap] } ] }, { oldToken: $font-size-base, pattern: /\$font-size-base\b/g, candidates: [ { newToken: var(--text-body-md), preferredProperties: [font-size] } ] } ]; /** * 迁移主函数 * 扫描所有样式文件逐一替换旧 Token 引用 */ async function migrateTokens( projectRoot: string, filePattern: string **/*.{css,scss,less} ): PromiseMigrationReport[] { const files globSync(path.join(projectRoot, filePattern), { ignore: [**/node_modules/**] }); const reports: MigrationReport[] []; for (const file of files) { const source fs.readFileSync(file, utf-8); const lines source.split(\n); for (const mapping of TOKEN_MAPPINGS) { // 重置正则的 lastIndex全局正则的状态问题 mapping.pattern.lastIndex 0; for (let lineNum 0; lineNum lines.length; lineNum) { const line lines[lineNum]; let match: RegExpExecArray | null; while ((match mapping.pattern.exec(line)) ! null) { // 上下文分析找到当前声明所在的 CSS 规则 const cssProperty extractCSSProperty(line); const cssSelector extractCSSSelector(lines, lineNum); // 智能选择最佳候选映射 const { bestCandidate, confidence, reasoning } selectBestCandidate( mapping, cssProperty, cssSelector ); reports.push({ file: path.relative(projectRoot, file), line: lineNum 1, column: match.index 1, oldValue: match[0], newValue: bestCandidate.newToken, confidence, reasoning, needsReview: confidence 0.9 }); } } } } return reports; } /** * 上下文感知的候选映射选择 * * 策略 * 1. 精确属性匹配color 属性 color token→ 最高权重 * 2. 选择器语义匹配.btn-primary → action-primary→ 次高权重 * 3. 默认候选candidates[0]→ 兜底 */ function selectBestCandidate( mapping: TokenMapping, cssProperty: string, cssSelector: string ): { bestCandidate: TokenMapping[candidates][0]; confidence: number; reasoning: string } { // 候选只有一项 → 直接返回置信度 1.0 if (mapping.candidates.length 1) { return { bestCandidate: mapping.candidates[0], confidence: 1.0, reasoning: 唯一候选映射 }; } // 按权重打分属性匹配 选择器匹配 默认 const scored mapping.candidates.map((cand) { let score 0; let reasons: string[] []; if (cand.preferredProperties?.includes(cssProperty)) { score 10; reasons.push(CSS 属性 ${cssProperty} 匹配); } if (cand.preferredSelectors?.some(s cssSelector.includes(s))) { score 5; reasons.push(选择器 ${cssSelector} 匹配); } return { candidate: cand, score, reasons }; }); scored.sort((a, b) b.score - a.score); const best scored[0]; return { bestCandidate: best.candidate, // 分数越高置信度越高10 分 1.05 分 0.90 分 0.7 confidence: best.score 10 ? 1.0 : best.score 5 ? 0.9 : 0.7, reasoning: best.reasons.join( ) || 默认候选 }; } /** * 从单行 CSS 中提取属性名 * 如 color: var(--old-token); → color */ function extractCSSProperty(line: string): string { const match line.match(/^\s*([a-z-])\s*:/); return match ? match[1] : ; } /** * 向上查找当前 CSS 声明所在规则的选择器 * 从当前行向上遍历找到包含花括号的规则行 */ function extractCSSSelector(lines: string[], currentLine: number): string { for (let i currentLine; i 0; i--) { const line lines[i].trim(); if (line.includes({) !line.endsWith({)) { return line.replace(/\{.*/, ).trim(); } if (line.endsWith({)) { return line.replace({, ).trim(); } } return ; } /** * 生成迁移后的文件——实际写回磁盘 */ function applyMigration( projectRoot: string, reports: MigrationReport[] ): void { // 按文件分组 const byFile new Mapstring, MigrationReport[](); for (const report of reports) { if (report.needsReview) continue; // 跳过低置信度项 const absPath path.join(projectRoot, report.file); if (!byFile.has(absPath)) byFile.set(absPath, []); byFile.get(absPath)!.push(report); } // 逐个文件替换 for (const [absPath, fileReports] of byFile) { let source fs.readFileSync(absPath, utf-8); const lines source.split(\n); // 从后往前替换避免行号偏移 fileReports.sort((a, b) b.line - a.line); for (const report of fileReports) { const lineIdx report.line - 1; lines[lineIdx] lines[lineIdx].replace(report.oldValue, report.newValue); } fs.writeFileSync(absPath, lines.join(\n)); } }四、迁移助手的信任边界正则匹配存在漏报。$font-size-base * 1.5Sass 中的变量运算、map-get($tokens, color-primary)Sass maps、模板字符串拼接——这些非标准引用方式正则是搞不定的。覆盖率为 70%~80%剩余需要人工补充。语义推断错误会传播。如果映射表本身有问题$color-primary→--color-brand-primary是错的那么迁移引擎会系统性地把所有引用都映射错。因此映射表必须由设计系统 Owner 审核签字后才能执行迁移。建议的执行流程先生成完整报告高置信度自动替换 低置信度列审查清单工程师逐条审查低置信度项确认后一键执行。五、总结AI 迁移助手的价值不在批量替换sed 也能做而在上下文感知的语义映射。同一个$color-primary在color属性中映射到--color-brand-primary在background-color中可能需要映射到--color-bg-brand。三个关键设计决策映射表是唯一的真理源——不是 AI 模型推理出来的而是设计系统团队明确定义的置信度分层处理——高置信度自动替换低置信度列清单审查不做半吊子的AI 自动决策运行在 PR 生成流程中——不是一次性批量替换后就删除的工具而是一个可重复执行的 CI 任务