【Bug已解决】openclaw: migration failed / Schema version mismatch — OpenClaw 数据库迁移失败解决方案1. 问题描述在使用 OpenClaw 升级或执行数据库迁移时系统报出迁移失败或 Schema 版本不匹配错误# 迁移失败 $ openclaw --migrate 执行数据库迁移 Error: migration failed Migration 001_add_users_table failed Column email already exists in table users # Schema 版本不匹配 $ openclaw 启动服务 Error: Schema version mismatch Expected: 5, Actual: 3 Please run: openclaw --migrate # 迁移冲突 $ openclaw --migrate 应用新迁移 Error: Migration conflict Migration 004_add_index depends on 003_add_column But 003_add_column has not been applied # 迁移锁 $ openclaw --migrate 并行迁移 Error: Migration lock held by another process Lock acquired at: 2024-07-07T10:00:00Z Another migration is in progress这个问题在以下场景中特别常见版本升级后未执行数据库迁移多个开发者同时修改 Schema迁移文件之间有依赖冲突生产环境迁移被中断数据库 Schema 与代码版本不同步迁移过程中数据冲突2. 原因分析OpenClaw升级版本 ↓ 代码期望Schema v5 ←──── 但数据库是v3 ↓ 检测版本不匹配 ←──── 版本差异 ↓ 执行迁移 ←──── 可能失败 ↓ 迁移冲突/数据问题 → 迁移失败原因分类具体表现占比版本不同步版本不匹配约 30%重复迁移对象已存在约 25%依赖冲突顺序错误约 15%数据冲突约束违反约 15%迁移中断部分应用约 10%锁竞争并行迁移约 5%深层原理数据库迁移系统通过维护一个迁移历史表如_migrations或schema_migrations记录已应用的迁移。每个迁移文件有唯一的版本号和顺序迁移系统按顺序应用未执行的迁移。当 OpenClaw 代码升级后可能需要新的表、列或索引如果数据库 Schema 版本与代码期望的版本不匹配运行时就会出错。迁移失败的原因包括尝试创建已存在的对象如列、表、索引迁移之间的依赖顺序错误数据类型变更导致现有数据违反新约束迁移被中断导致部分应用数据库在 v3 和 v4 之间多进程同时迁移导致锁竞争。3. 解决方案方案一检查和修复迁移状态最推荐# 检查当前 Schema 版本 openclaw --migration-status # 输出: # Current version: 3 # Expected version: 5 # Pending migrations: 004_add_index, 005_add_audit_table # 检查迁移历史 openclaw --migration-history # 输出: # 001_add_users_table | 2024-07-01 10:00 | ✅ Applied # 002_add_projects_table | 2024-07-01 10:01 | ✅ Applied # 003_add_sessions_table | 2024-07-01 10:02 | ✅ Applied # 004_add_index | - | ⏳ Pending # 005_add_audit_table | - | ⏳ Pending # 检查数据库实际 Schema python3 -c import sqlite3 conn sqlite3.connect(.openclaw/data.db) cursor conn.cursor() # 查看表 cursor.execute(\SELECT name FROM sqlite_master WHERE typetable\) tables cursor.fetchall() print(表:, [t[0] for t in tables]) # 查看迁移历史表 try: cursor.execute(SELECT * FROM _migrations ORDER BY version) migrations cursor.fetchall() print(已应用迁移:) for m in migrations: print(f {m}) except sqlite3.OperationalError: print(无迁移历史表) conn.close() # 修复中断的迁移 # 如果迁移被中断可能需要手动修复 openclaw --migration-repair # 或手动标记迁移状态 openclaw --migration-mark 004_add_index --applied openclaw --migration-mark 004_add_index --pending方案二安全执行迁移# 执行迁移安全模式 openclaw --migrate --safe # 安全模式: # - 事务包装失败自动回滚 # - 迁移前自动备份 # - 逐个迁移执行失败即停止 # 执行单个迁移 openclaw --migrate --only 004_add_index # 执行迁移到指定版本 openclaw --migrate --to 5 # 回滚迁移 openclaw --migrate-rollback 004_add_index # 回滚到指定版本 openclaw --migrate-rollback --to 3 # 强制执行迁移跳过冲突检查 openclaw --migrate --force # ⚠️ 危险: 跳过冲突检查可能导致数据不一致 # 配置迁移选项 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[migration] { autoMigrate: False, # 不自动迁移 safeMode: True, # 安全模式 backupBeforeMigrate: True, # 迁移前备份 transactionPerMigration: True, # 每个迁移一个事务 stopOnError: True, # 出错停止 lockTimeout: 60000, # 迁移锁超时60秒 retryOnLock: True, # 锁等待重试 maxRetries: 3, # 最大重试 validateChecksums: True # 验证迁移文件校验和 } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(迁移配置: 安全模式自动备份事务包装出错停止) 方案三处理重复迁移错误# 重复迁移错误: Column already exists # 原因: 迁移历史表记录不完整实际数据库已有该对象 # 方法1: 手动检查数据库状态 python3 -c import sqlite3 conn sqlite3.connect(.openclaw/data.db) cursor conn.cursor() # 检查表结构 cursor.execute(PRAGMA table_info(users)) columns cursor.fetchall() print(users 表列:) for col in columns: print(f {col[1]} ({col[2]})) # 检查索引 cursor.execute(\SELECT name FROM sqlite_master WHERE typeindex AND tbl_nameusers\) indexes cursor.fetchall() print(索引:, [i[0] for i in indexes]) conn.close() # 方法2: 修改迁移文件添加 IF NOT EXISTS # 编辑迁移文件 004_add_index.js # 将 CREATE INDEX 改为 CREATE INDEX IF NOT EXISTS # 将 ALTER TABLE ADD COLUMN 改为先检查再添加 # 方法3: 标记迁移为已应用如果对象已存在 openclaw --migration-mark 004_add_index --applied --force # 这会跳过该迁移的执行直接标记为已应用 # 方法4: 创建修复迁移 cat .openclaw/migrations/004_fix_add_index.js EOF // 修复迁移: 安全添加索引 module.exports { up: function(db) { // 检查索引是否已存在 const indexes db.exec(SELECT name FROM sqlite_master WHERE typeindex AND nameidx_users_email); if (indexes.length 0) { db.exec(CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)); console.log(✅ 索引已创建); } else { console.log(⏭️ 索引已存在跳过); } }, down: function(db) { db.exec(DROP INDEX IF EXISTS idx_users_email); } }; EOF方案四处理依赖冲突# 检查迁移依赖 openclaw --migration-check-dependencies # 方法1: 调整迁移顺序 # 迁移文件命名: 001_, 002_, 003_... # 确保依赖的迁移排在前面 # 方法2: 在迁移文件中声明依赖 cat .openclaw/migrations/005_add_audit_table.js JEOF module.exports { // 声明依赖 dependencies: [003_add_sessions_table, 004_add_index], up: function(db) { // 验证依赖 const required [sessions, idx_users_email]; for (const req of required) { const exists db.exec( SELECT name FROM sqlite_master WHERE name${req} ); if (exists.length 0) { throw new Error(依赖缺失: ${req}。请先执行相关迁移。); } } // 执行迁移 db.exec( CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, table_name TEXT NOT NULL, action TEXT NOT NULL, record_id INTEGER, changes TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ); ); }, down: function(db) { db.exec(DROP TABLE IF EXISTS audit_log); } }; JEOF # 方法3: 使用迁移 DAG有向无环图 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[migration][useDAG] True config[migration][dependencyCheck] True config[migration][topologicalSort] True config[migration][allowCircular] False with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(迁移 DAG 已启用: 依赖检查拓扑排序) 方案五迁移前备份和回滚# 迁移前自动备份 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[migration][backup] { enabled: True, backupDir: .openclaw/backups/, maxBackups: 10, # 保留10个备份 compress: True, # 压缩备份 includeData: True, # 包含数据 includeSchema: True # 包含 Schema } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(迁移备份已配置: 压缩数据Schema, 保留10个) # 手动备份 openclaw --backup --before-migration # 迁移失败后从备份恢复 openclaw --restore .openclaw/backups/db_backup_20240707_100000.db.gz # 创建回滚脚本 cat .openclaw/migration_rollback.sh EOF #!/bin/bash # 迁移回滚脚本 BACKUP_DIR.openclaw/backups DB_FILE.openclaw/data.db # 找到最近的备份 LATEST_BACKUP$(ls -t $BACKUP_DIR/db_backup_*.db.gz 2/dev/null | head -1) if [ -z $LATEST_BACKUP ]; then echo ❌ 无可用备份 exit 1 fi echo 恢复备份: $LATEST_BACKUP # 关闭 OpenClaw openclaw --daemon-stop 2/dev/null # 备份当前数据库 cp $DB_FILE ${DB_FILE}.failed # 恢复备份 gunzip -c $LATEST_BACKUP $DB_FILE echo ✅ 恢复完成 openclaw --migration-status EOF chmod x .openclaw/migration_rollback.sh方案六创建迁移测试工具# 创建迁移测试和验证工具 import sqlite3 import os import json import time import shutil class MigrationTester: 迁移测试工具 def __init__(self, db_path.openclaw/data.db): self.db_path db_path self.test_db_path .openclaw/test_db.db def create_test_db(self): 创建测试数据库 # 复制当前数据库 if os.path.exists(self.db_path): shutil.copy2(self.db_path, self.test_db_path) else: # 创建空数据库 conn sqlite3.connect(self.test_db_path) conn.close() print(f测试数据库: {self.test_db_path}) def test_migration(self, migration_name): 测试单个迁移 self.create_test_db() conn sqlite3.connect(self.test_db_path) cursor conn.cursor() # 记录迁移前状态 before self.get_schema_snapshot(cursor) # 执行迁移模拟 print(f测试迁移: {migration_name}) try: # 加载迁移文件 migration_path f.openclaw/migrations/{migration_name}.js if not os.path.exists(migration_path): return False, f迁移文件不存在: {migration_path} # 执行 up 函数 # 实际使用 node 执行 import subprocess result subprocess.run( [node, -e, f const sqlite3 require(better-sqlite3); const db sqlite3({self.test_db_path}); const migration require(./{migration_path}); migration.up(db); db.close(); ], capture_outputTrue, textTrue, timeout30 ) if result.returncode ! 0: return False, result.stderr # 记录迁移后状态 after self.get_schema_snapshot(cursor) # 比较变化 changes self.compare_schemas(before, after) print(f Schema 变化:) for change in changes: print(f {change}) # 测试回滚 # 执行 down 函数 rollback_result subprocess.run( [node, -e, f const sqlite3 require(better-sqlite3); const db sqlite3({self.test_db_path}); const migration require(./{migration_path}); migration.down(db); db.close(); ], capture_outputTrue, textTrue, timeout30 ) if rollback_result.returncode 0: print(f ✅ 回滚成功) else: print(f ⚠️ 回滚失败: {rollback_result.stderr}) return True, 迁移测试通过 except Exception as e: return False, str(e) finally: conn.close() # 清理测试数据库 if os.path.exists(self.test_db_path): os.remove(self.test_db_path) def get_schema_snapshot(self, cursor): 获取 Schema 快照 snapshot {tables: {}, indexes: []} cursor.execute(SELECT name FROM sqlite_master WHERE typetable) tables [t[0] for t in cursor.fetchall()] for table in tables: cursor.execute(fPRAGMA table_info({table})) snapshot[tables][table] [ {name: col[1], type: col[2]} for col in cursor.fetchall() ] cursor.execute(SELECT name, tbl_name FROM sqlite_master WHERE typeindex) snapshot[indexes] [ {name: i[0], table: i[1]} for i in cursor.fetchall() ] return snapshot def compare_schemas(self, before, after): 比较两个 Schema 快照 changes [] # 新增表 new_tables set(after[tables]) - set(before[tables]) for t in new_tables: changes.append(f 新增表: {t}) # 删除表 removed_tables set(before[tables]) - set(after[tables]) for t in removed_tables: changes.append(f- 删除表: {t}) # 修改表 for table in set(before[tables]) set(after[tables]): before_cols {c[name]: c[type] for c in before[tables][table]} after_cols {c[name]: c[type] for c in after[tables][table]} for col, col_type in after_cols.items(): if col not in before_cols: changes.append(f 新增列: {table}.{col} ({col_type})) elif before_cols[col] ! col_type: changes.append(f~ 修改列: {table}.{col} {before_cols[col]} - {col_type}) for col in before_cols: if col not in after_cols: changes.append(f- 删除列: {table}.{col}) # 索引变化 before_idx {i[name] for i in before[indexes]} after_idx {i[name] for i in after[indexes]} for idx in after_idx - before_idx: changes.append(f 新增索引: {idx}) for idx in before_idx - after_idx: changes.append(f- 删除索引: {idx}) return changes if __name__ __main__: tester MigrationTester() success, message tester.test_migration(004_add_index) print(f\n结果: {✅ if success else ❌} {message})4. 各方案对比总结方案适用场景推荐指数方案一检查修复诊断问题⭐⭐⭐⭐⭐方案二安全迁移通用执行⭐⭐⭐⭐⭐方案三重复迁移对象已存在⭐⭐⭐⭐方案四依赖冲突顺序问题⭐⭐⭐⭐方案五备份回滚安全保障⭐⭐⭐⭐⭐方案六测试工具预防验证⭐⭐⭐⭐5. 常见问题 FAQ5.1 Windows 上数据库路径问题Windows 路径格式不同# Windows 上 SQLite 路径使用反斜杠 $env:OPENCLAW_DB_PATH C:\Users\zhubo\.openclaw\data.db # 确保路径正确 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[database][path] .openclaw/data.db # 使用正斜杠 with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) # 检查数据库文件 Test-Path .openclaw\data.db5.2 Docker 中迁移持久化容器内迁移需要持久化存储# 迁移文件和数据库使用 Volume docker run -v openclaw_data:/app/.openclaw openclaw --migrate # Docker Compose services: openclaw: volumes: - data:/app/.openclaw command: openclaw --migrate --safe volumes: data: # 迁移后重新启动 docker-compose run openclaw --migrate docker-compose restart openclaw5.3 CI/CD 中自动迁移CI 中需要安全执行迁移# CI 迁移步骤 steps: - name: Backup database run: openclaw --backup - name: Run migrations run: | openclaw --migrate --safe --stop-on-error openclaw --migration-status - name: Verify migrations run: | STATUS$(openclaw --migration-status --format json) echo $STATUS | jq .pending | length # 如果有待执行迁移失败 if [ $(echo $STATUS | jq .pending | length) -gt 0 ]; then echo ❌ 有未执行的迁移 exit 1 fi - name: Rollback on failure if: failure() run: openclaw --restore --latest-backup5.4 多环境迁移差异开发/测试/生产环境的 Schema 可能不同# 为不同环境配置不同迁移 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[migration] { environments: { development: { autoMigrate: True, # 开发环境自动迁移 safeMode: False, # 不需要安全模式 allowDrop: True # 允许删除 }, production: { autoMigrate: False, # 生产不自动迁移 safeMode: True, # 安全模式 allowDrop: False, # 不允许删除 requireApproval: True # 需要确认 } } } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(多环境迁移策略已配置) # 指定环境执行迁移 OPENCLAW_ENVproduction openclaw --migrate --safe5.5 迁移文件版本控制迁移文件需要纳入版本控制# 迁移文件命名规范 # .openclaw/migrations/ # ├── 001_add_users_table.js # ├── 002_add_projects_table.js # ├── 003_add_sessions_table.js # ├── 004_add_index.js # └── 005_add_audit_table.js # 迁移文件应纳入 Git git add .openclaw/migrations/ git commit -m 添加数据库迁移文件 # 防止修改已应用的迁移 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[migration][immutableApplied] True # 已应用的迁移不可修改 config[migration][checksumValidation] True # 校验和验证 with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(迁移文件不可变: 已应用的迁移修改将被拒绝) 5.6 迁移过程中数据丢失某些迁移可能导致数据丢失# 安全的列类型变更 # SQLite 不直接支持 ALTER TABLE 修改列类型 # 需要: 创建新表 - 迁移数据 - 删除旧表 - 重命名 # 创建数据安全迁移 cat .openclaw/migrations/006_change_email_type.js JEOF module.exports { up: function(db) { // 1. 创建新表 db.exec( CREATE TABLE users_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ); // 2. 迁移数据 const count db.exec(SELECT COUNT(*) FROM users)[0].values[0][0]; console.log(迁移 ${count} 条记录...); db.exec( INSERT INTO users_new (id, email, name, created_at) SELECT id, email, name, created_at FROM users; ); // 3. 验证数据 const newCount db.exec(SELECT COUNT(*) FROM users_new)[0].values[0][0]; if (newCount ! count) { throw new Error(数据迁移不一致: ${count} - ${newCount}); } // 4. 切换表 db.exec(DROP TABLE users); db.exec(ALTER TABLE users_new RENAME TO users); console.log(✅ 列类型变更完成); }, down: function(db) { // 回滚: 恢复旧表结构 // ... } }; JEOF5.7 集群环境迁移协调多节点同时迁移需要协调# 使用迁移锁防止多节点同时迁移 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[migration][clusterLock] { enabled: True, backend: redis, # redis | file | db lockKey: openclaw:migration:lock, lockTTL: 600000, # 10分钟锁 retryCount: 30, # 重试30次 retryDelay: 2000 # 2秒间隔 } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(集群迁移锁已配置: Redis后端, 10分钟TTL) # 或者只在主节点执行迁移 # 使用 Leader Election 确保只有一个节点迁移5.8 迁移日志和审计记录迁移历史用于审计# 配置迁移日志 python3 -c import json with open(.openclaw/config.json, r) as f: config json.load(f) config[migration][logging] { enabled: True, logFile: .openclaw/logs/migrations.log, logLevel: info, includeSQL: True, # 记录执行的SQL includeDuration: True, # 记录执行时间 includeUser: True, # 记录执行者 includeHost: True # 记录主机名 } with open(.openclaw/config.json, w) as f: json.dump(config, f, indent2) print(迁移日志已配置: SQL时间用户主机) # 查看迁移日志 cat .openclaw/logs/migrations.log | tail -20 # 迁移审计报告 openclaw --migration-audit # 输出: 迁移历史、执行时间、执行者、变更内容排查清单速查表□ 1. 检查迁移状态: openclaw --migration-status □ 2. 检查迁移历史: openclaw --migration-history □ 3. 检查数据库 Schema: PRAGMA table_info □ 4. 执行安全迁移: openclaw --migrate --safe □ 5. 迁移前备份: openclaw --backup □ 6. 处理重复: CREATE INDEX IF NOT EXISTS □ 7. 检查依赖: 声明 dependencies □ 8. 测试迁移: 使用测试数据库验证 □ 9. 配置迁移锁防止并行 □ 10. 记录迁移日志用于审计6. 总结最常见原因Schema 版本不同步30%和重复迁移导致对象已存在25%安全迁移使用--safe模式事务包装自动备份出错停止重复处理迁移文件中使用IF NOT EXISTS或标记已应用跳过依赖管理迁移文件中声明dependencies使用拓扑排序确保顺序最佳实践建议迁移前自动备份数据库迁移后验证数据完整性生产环境禁止自动迁移和删除操作多节点使用 Redis 分布式锁协调迁移故障排查流程图flowchart TD A[迁移失败] -- B[检查迁移状态] B -- C[openclaw --migration-status] C -- D{版本匹配?} D --|是| E[检查迁移错误] D --|否| F[执行迁移] E -- G[查看错误类型] G -- H{重复对象?} H --|是| I[使用IF NOT EXISTS] H --|否| J{依赖冲突?} I -- K[标记已应用] J --|是| L[调整顺序] J --|否| M{数据冲突?} L -- F K -- F M --|是| N[修复数据] M --|否| O[检查锁竞争] N -- F O -- P[等待锁释放] P -- F F -- Q[openclaw --migrate --safe] Q -- R{迁移成功?} R --|是| S[验证Schema] R --|否| T[从备份恢复] S -- U[✅ 问题解决] T -- V[手动修复] V -- F U -- W[配置迁移策略] W -- X[安全模式备份锁] X -- Y[✅ 长期稳定]