AI 设计系统的自愈能力:自动修复 Token 不一致与样式漂移

📅 2026/7/17 17:52:47
AI 设计系统的自愈能力:自动修复 Token 不一致与样式漂移
AI 设计系统的自愈能力自动修复 Token 不一致与样式漂移一、Token 漂移——设计系统的温水煮青蛙三个版本迭代后我在 Chrome DevTools 里抽查了几个页面的--color-primary值。结果让人不安首页var(--color-brand-primary)→ computed #1677FF正确商品列表.btn-primary { background: #1890FF; }→ 硬编码已经偏离用户中心var(--color-primary)→ computed #1677FF但用的变量名是老版本的--color-primary而非当前标准--color-brand-primary这种现象叫Token 漂移——设计系统的规范在缓慢演化Token 名称变更、值调整、新增语义层而消费方的代码逐渐偏离。不是一次性断裂而是三行五行的我觉得这样写更方便逐渐累积。AI 的自愈能力不是魔法而是一个检测 → 诊断 → 修复的自动化闭环。它定期扫描代码仓库对照设计系统的 Token 规范发现偏离后自动生成修复 PR。二、自愈系统的架构三、自愈引擎实现// self-healing/self-healing-engine.ts // 设计系统自愈引擎 // 定期扫描代码修复 Token 引用问题 import fs from fs; import path from path; import { globSync } from glob; import { simpleGit, SimpleGit } from simple-git; interface TokenSpec { /** Token 名称如 --color-brand-primary */ name: string; /** Token 值如 #1677FF */ value: string; /** Token 别名集合历史版本名称 */ aliases: string[]; /** Token 类型 */ type: color | spacing | typography | shadow | radius; } interface HealingIssue { file: string; line: number; column: number; type: obsolete-token | hardcoded-value | value-drift; severity: critical | warning | info; currentCode: string; suggestedFix: string; confidence: number; explanation: string; } /** * 设计系统自愈引擎 * * 工作原理 * 1. 加载 Token 规范文件tokens.json * 2. 扫描所有样式文件CSS/SCSS/LESS/TSX * 3. 匹配各种 Token 引用模式 * 4. 对于发现的问题生成修复建议并创建 PR */ class SelfHealingEngine { private tokenSpecs: TokenSpec[] []; constructor(private projectRoot: string) {} /** * 加载 Token 规范 * * 从 tokens.json 中提取所有 Token 定义 * 构建 tokenSpecs 列表供后续匹配使用 */ async loadTokenSpecs(): Promisevoid { const tokensPath path.join(this.projectRoot, src/tokens/tokens.json); const tokensRaw JSON.parse(fs.readFileSync(tokensPath, utf-8)); this.tokenSpecs this.flattenTokens(tokensRaw); console.log(已加载 ${this.tokenSpecs.length} 个 Token 规范); } /** * 展平嵌套 Token JSON 为扁平列表 */ private flattenTokens(obj: any, prefix ): TokenSpec[] { const result: TokenSpec[] []; for (const [key, val] of Object.entries(obj)) { const fullPath prefix ? ${prefix}-${key} : key; if (val typeof val object value in val) { result.push({ name: --${fullPath}, value: val.value as string, aliases: (val.aliases as string[]) || [], type: (val.type as TokenSpec[type]) || color }); } else if (val typeof val object) { result.push(...this.flattenTokens(val, fullPath)); } } return result; } /** * 执行一次完整的自愈扫描 * * returns 检测到的问题列表 */ async scan(): PromiseHealingIssue[] { const issues: HealingIssue[] []; const styleFiles globSync( path.join(this.projectRoot, src/**/*.{css,scss,less,tsx,jsx}), { ignore: [**/node_modules/**] } ); for (const file of styleFiles) { const content fs.readFileSync(file, utf-8); const lines content.split(\n); for (let i 0; i lines.length; i) { const line lines[i]; // 检测 1废弃的 Token 引用旧名称 this.detectObsoleteTokens(line, file, i 1, issues); // 检测 2硬编码的 Token 值 this.detectHardcodedValues(line, file, i 1, issues); // 检测 3Token 值漂移手动覆盖了 Token 变量 this.detectValueDrift(line, file, i 1, issues); } } return issues; } /** * 检测废弃 Token 引用 * * 场景代码中使用了 --color-primary但规范中已改名 --color-brand-primary * 策略通过 aliases 字段找到新旧 Token 的映射关系 */ private detectObsoleteTokens( line: string, file: string, lineNum: number, issues: HealingIssue[] ): void { // 匹配 var(--xxx) 模式 const varPattern /var\((--[\w-])\)/g; let match; while ((match varPattern.exec(line)) ! null) { const usedToken match[1]; const usedValue match[0]; // 检查是否在 aliases 中旧名称 for (const spec of this.tokenSpecs) { if (spec.aliases.includes(usedToken)) { issues.push({ file, line: lineNum, column: match.index 1, type: obsolete-token, severity: warning, currentCode: usedValue, suggestedFix: var(${spec.name}), confidence: 0.95, explanation: Token ${usedToken} 已重命名为 ${spec.name}请更新引用 }); break; } } } } /** * 检测硬编码值 * * 场景代码中直接写了 #1677FF而规范中该颜色已有 Token --color-brand-primary * 策略颜色值完全匹配 属性语义匹配 */ private detectHardcodedValues( line: string, file: string, lineNum: number, issues: HealingIssue[] ): void { // 匹配 hex/rgb/rgba 颜色值 const colorPattern /(?:#([0-9a-fA-F]{3,8})|rgba?\([^)]\))\b/g; const propertyPattern /(color|background|border|outline|fill|stroke)[\s-]*:/i; let match; while ((match colorPattern.exec(line)) ! null) { const hardcodedColor match[0].toLowerCase(); // 检查是否有 Token 的值等于这个硬编码颜色 for (const spec of this.tokenSpecs) { if (spec.type ! color) continue; if (spec.value.toLowerCase() hardcodedColor) { // 检查是否在颜色相关属性中使用 if (propertyPattern.test(line)) { issues.push({ file, line: lineNum, column: match.index 1, type: hardcoded-value, severity: warning, currentCode: hardcodedColor, suggestedFix: var(${spec.name}), confidence: 0.85, explanation: 硬编码颜色 ${hardcodedColor} 可使用 Token ${spec.name} 替代 }); } break; } } } } /** * 检测 Token 值漂移 * * 场景代码中 var(--color-brand-primary) 被手动计算后覆盖 * 例如background: color-mix(in srgb, var(--color-brand-primary) 80%, white) * 这种派生值本身不会漂移正确用法但以下情况会 * background: #1a7aff; /* 注释这是 --color-brand-primary 的 hover 态 */ * * 检测策略硬编码的值接近 Token 值但不完全相等 注释暗示相关 */ private detectValueDrift( line: string, file: string, lineNum: number, issues: HealingIssue[] ): void { const hardcodedHex line.match(/#([0-9a-fA-F]{6})\b/); if (!hardcodedHex) return; const color hardcodedHex[1].toLowerCase(); for (const spec of this.tokenSpecs) { if (spec.type ! color) continue; const specValue spec.value.replace(#, ).toLowerCase(); // 计算颜色距离简化版RGB 通道差值之和 const distance this.colorDistance(color, specValue); // 距离在 30 以内 注释中提到 Token 名 → 高置信度漂移 if (distance 30 distance 0) { const commentMatch line.match(/\/\*.*?(token|color|primary|brand).*?\*\//i); if (commentMatch) { issues.push({ file, line: lineNum, column: hardcodedHex.index! 1, type: value-drift, severity: critical, currentCode: hardcodedHex[0], suggestedFix: var(${spec.name}), confidence: 0.75, explanation: 颜色 ${hardcodedHex[0]} 接近 Token ${spec.name}(${spec.value})疑似 Token 漂移 }); } } } } /** * 计算两个 hex 颜色之间的 RGB 距离 */ private colorDistance(hex1: string, hex2: string): number { const r1 parseInt(hex1.slice(0, 2), 16); const g1 parseInt(hex1.slice(2, 4), 16); const b1 parseInt(hex1.slice(4, 6), 16); const r2 parseInt(hex2.slice(0, 2), 16); const g2 parseInt(hex2.slice(2, 4), 16); const b2 parseInt(hex2.slice(4, 6), 16); return Math.abs(r1 - r2) Math.abs(g1 - g2) Math.abs(b1 - b2); } /** * 生成并提交自愈 PR * * 只处理高置信度的问题confidence 0.9 * 低置信度的仅生成报告供人工 Review */ async heal(issues: HealingIssue[]): Promisevoid { const highConfidence issues.filter(i i.confidence 0.9); const lowConfidence issues.filter(i i.confidence 0.9); if (highConfidence.length 0 lowConfidence.length 0) { console.log(自愈扫描未发现 Token 问题); return; } console.log( 发现 ${highConfidence.length} 个高置信度问题将自动修复、 ${lowConfidence.length} 个待审查问题 ); if (highConfidence.length 0) { await this.applyFixes(highConfidence); } } /** * 应用修复在文件中进行字符串替换 */ private async applyFixes(issues: HealingIssue[]): Promisevoid { // 按文件分组 const byFile new Mapstring, HealingIssue[](); for (const issue of issues) { if (!byFile.has(issue.file)) byFile.set(issue.file, []); byFile.get(issue.file)!.push(issue); } for (const [file, fileIssues] of byFile) { let content fs.readFileSync(file, utf-8); const lines content.split(\n); // 从后往前替换避免行号偏移 fileIssues.sort((a, b) b.line - a.line); for (const issue of fileIssues) { const lineIdx issue.line - 1; lines[lineIdx] lines[lineIdx].replace( issue.currentCode, issue.suggestedFix ); } fs.writeFileSync(file, lines.join(\n)); } console.log(已修复 ${issues.length} 个问题); } }四、自愈的边界——什么时候不该自动修复颜色值的近似匹配是高风险操作。colorDistance 30的阈值会误报——品牌规范中如果有两个语义不同但色号相近的颜色如--color-error: #FF4D4F和--color-danger: #FF4444自愈引擎可能会把#FF4D4F替换为var(--color-danger)。降低误报的策略优先匹配propertyName colorValue的联合模式。复杂表达式中嵌入的 Token 引用。rgba(var(--color-brand-primary-rgb), 0.5)这种正确的 Token 用法不应该被误判为硬编码 rgba。检测规则需要排除var(内部的颜色值引用。不要自动合入自愈 PR。即使置信度 95%也建议保留人工 Review 环节。Token 的语义不是颜色值能完全决定的——#1677FF在一个地方是品牌主色在另一个地方可能是某个 SVG 图标的 fill 色替换为 Token 变量可能反而降低了代码意图的可读性。五、总结设计系统的自愈不是让 AI 自动修改你的 CSS而是建立 Token 规范的持续合规检查闭环。三个核心动作检测——扫描全量代码找出引用废弃 Token、硬编码 Token 值、Token 值漂移三种问题诊断——按置信度分层95% 自动修复70-95% 生成 PR 待 Review70% 仅告警修复——对高置信度问题生成包含具体替换方案的 PR最终的架构目标Token 文件变更后自愈引擎自动触发扫描 → 为所有消费方生成迁移 PR → 工程师 Review 后合入。这就是设计系统发版前端自动跟上的终极形态。