"""Refresh reviewed sources atomically without resetting saved options or history.""" import argparse import copy import json from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from console import catalog, native_plans from console.settings import Settings from console.store import Store def refresh(store, backup, apply=False): original = {s['id']: s for s in store.scripts()} class Draft: settings = store.settings items = copy.deepcopy(original) def scripts(self): return list(self.items.values()) def save_script(self, item): self.items[item['id']] = item draft = Draft() scan = catalog.discover(draft) for item in draft.items.values(): profile = native_plans.profile_for(store.settings, item) if not profile: continue if item['id'] not in original: item['config'] = native_plans.validate_config(store.settings, item, {'native_options': {}, 'timeout_seconds': profile.get('timeout_seconds', 3600)}) item['enabled'] = profile.get('available', True) if not profile.get('available', True): item.update(enabled=False, blocked=True, reason=profile.get('unavailable_reason', '原环境待配置')) if item['enabled']: item.update(status='ready', reason='') elif item['status'] != 'missing': item['status'] = 'needs_configuration' changes = [] for identity, value in draft.items.items(): before = original.get(identity) if before and {k:v for k,v in before.items() if k != 'updated_at'} == {k:v for k,v in value.items() if k != 'updated_at'}: continue changes.append((identity, before, value)) if not apply: return {'scan': scan, 'changed': len(changes), 'applied': False} with store.connect(write=True) as db: if db.execute("SELECT 1 FROM runs WHERE state IN ('queued','running')").fetchone(): raise ValueError('请先等待排队和运行任务结束') records = [] for identity, before, after in changes: row = db.execute('SELECT data FROM scripts WHERE id=?', (identity,)).fetchone() if (json.loads(row[0]) if row else None) != before: raise ValueError('配置有并发修改,本次刷新未写入') records.append({'id': identity, 'before': row[0] if row else None, 'after': json.dumps(after)}) if not records: return {'scan': scan, 'changed': 0, 'applied': True} backup.parent.mkdir(parents=True, exist_ok=True) with backup.open('x', encoding='utf-8') as handle: json.dump({'scripts': records}, handle, ensure_ascii=False, indent=2) backup.chmod(0o600) for record in records: db.execute('INSERT OR REPLACE INTO scripts VALUES(?,?)', (record['id'], record['after'])) return {'scan': scan, 'changed': len(changes), 'applied': True} def restore(store, backup): records = json.loads(backup.read_text(encoding='utf-8'))['scripts'] with store.connect(write=True) as db: if db.execute("SELECT 1 FROM runs WHERE state IN ('queued','running')").fetchone(): raise ValueError('请先等待排队和运行任务结束') for record in records: row = db.execute('SELECT data FROM scripts WHERE id=?', (record['id'],)).fetchone() current = row[0] if row else None if current not in (record['before'], record['after']): raise ValueError('刷新后配置已修改,请人工核对,禁止覆盖') if record['before'] is None and db.execute('SELECT 1 FROM runs WHERE script_id=?', (record['id'],)).fetchone(): raise ValueError('新增项目已有运行历史,不能删除登记,请人工恢复') for record in records: if record['before'] is None: db.execute('DELETE FROM scripts WHERE id=?', (record['id'],)) else: db.execute('UPDATE scripts SET data=? WHERE id=?', (record['before'], record['id'])) return {'restored': len(records), 'history_preserved': True} if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--apply', action='store_true') parser.add_argument('--restore', action='store_true') parser.add_argument('--backup', type=Path, required=True) args = parser.parse_args() if args.apply and args.restore: parser.error('选择刷新或恢复之一') store = Store(Settings.from_env()) print(json.dumps(restore(store, args.backup) if args.restore else refresh(store, args.backup, args.apply)))