/** * Backup — 文件备份与还原 * * 代码级扩展修改宿主 minified bundle 前,先备份原始文件。 * 卸载时从备份恢复。OC 升级时备份随 control-ui/ 一起被清除。 */ import { copyFileSync, unlinkSync, existsSync, readFileSync } from 'node:fs'; import { logger } from '../../util/logger.js'; const BACKUP_SUFFIX = '.clawlink-bak'; /** Patterns that indicate clawlink modifications in the bundle. * NOTE: __clawlinkMarked is NOT listed here — it's handled by the * removed-extensions scanner for marked-expose cleanup. */ const CLAWLINK_FINGERPRINTS = [ '__clawlinkRenderToolCard', '__clawlinkOwnsToolCards', '__tcid', '__clawlinkIsRemoteToolName', '__clawlinkToolCallIds', '__clawlinkKeepToolPayload', ]; const CLAWLINK_MARKER_PATTERN = '/*clawlink:'; /** * 检测文件是否包含无 marker 的 clawlink 手工补丁。 * 如果文件包含 clawlink 特征字符串但没有对应的 marker 注释, * 说明这是一个被手工修改过的文件,不应该作为"原始备份"保存。 */ function isDirtyBundle(filepath: string): boolean { try { const content = readFileSync(filepath, 'utf-8'); const hasFingerprint = CLAWLINK_FINGERPRINTS.some(fp => content.includes(fp)); const hasMarker = content.includes(CLAWLINK_MARKER_PATTERN); // 有 clawlink 特征但没有 marker → 手工补丁 return hasFingerprint && !hasMarker; } catch { return false; } } /** * 备份文件。如果备份已存在则跳过(保持最早的原始版本)。 * 如果文件包含无 marker 的手工补丁,拒绝备份。 * * @returns true = 备份就绪(已存在或新创建),false = 检测到脏 bundle,调用方必须中止 apply。 */ export function backupFile(filepath: string): boolean { const backupPath = filepath + BACKUP_SUFFIX; if (existsSync(backupPath)) { logger.debug(`[ui-ext] Backup already exists: ${backupPath}`); return true; } // 检测脏 bundle:包含 clawlink 代码但没有 marker if (isDirtyBundle(filepath)) { logger.warn( `[ui-ext] Dirty bundle detected: ${filepath} contains unmarked clawlink patches. ` + `Refusing to backup or patch. Please reinstall OpenClaw to get a clean bundle, then restart.` ); return false; } copyFileSync(filepath, backupPath); logger.debug(`[ui-ext] Backed up: ${filepath}`); return true; } /** * 从备份还原文件。 * 还原后删除备份文件。 * 如果备份不存在,记录警告但不抛异常。 */ export function restoreFile(filepath: string): boolean { const backupPath = filepath + BACKUP_SUFFIX; if (!existsSync(backupPath)) { logger.debug(`[ui-ext] No backup found: ${backupPath}`); return false; } copyFileSync(backupPath, filepath); unlinkSync(backupPath); logger.debug(`[ui-ext] Restored from backup: ${filepath}`); return true; } /** * 检查备份是否存在。 */ export function hasBackup(filepath: string): boolean { return existsSync(filepath + BACKUP_SUFFIX); }