|
- import fs from 'node:fs';
- import crypto from 'node:crypto';
- import assert from 'node:assert/strict';
- import { login, call } from './role-permissions-live.mjs';
-
- // One-time cleanup of the explicitly audited local inventory. New projects are never included.
- const dir = 'reports/bridge-content-cleanup-20260925';
- const plan = JSON.parse(fs.readFileSync(dir + '/plan.json', 'utf8'));
- const protectedIds = new Set([...plan.keep, ...plan.blockers].map(p => p.id));
- for (let changed = true; changed;) {
- changed = false;
- for (const project of plan.all.filter(p => protectedIds.has(p.id))) {
- for (const id of project.dependencies) if (!protectedIds.has(id)) { protectedIds.add(id); changed = true; }
- }
- }
- const pending = plan.candidates.filter(p => !protectedIds.has(p.id));
- const order = [];
- while (pending.length) {
- const index = pending.findIndex(p => !pending.some(other => other.id !== p.id && other.dependencies.includes(p.id)));
- assert.ok(index >= 0, 'Dependency cycle: stop rather than deleting referenced content');
- order.push(...pending.splice(index, 1));
- }
- console.log(JSON.stringify({ removable: order.map(p => ({ id: p.id, type: p.type, name: p.name })), protectedCount: protectedIds.size }, null, 2));
- if (process.argv.includes('--apply-unreferenced')) {
- const session = await login('root');
- const request = (path, body, method) => call('tran/v1/' + path, session.accessToken, body, method);
- const resultPath = dir + '/deleted-unreferenced.json';
- const result = fs.existsSync(resultPath) ? JSON.parse(fs.readFileSync(resultPath, 'utf8')) : { startedAt: new Date().toISOString(), deleted: [] };
- const save = () => fs.writeFileSync(resultPath, JSON.stringify(result, null, 2));
- try {
- for (const project of order) {
- assert.ok(['MODEL', 'SCENE', 'TRAINING'].includes(project.type));
- assert.ok(!protectedIds.has(project.id));
- const backup = fs.readFileSync(plan.backupDirectory + '/project-' + project.id + '.json');
- assert.equal(crypto.createHash('sha256').update(backup).digest('hex'), project.backupSha256, 'Backup must match audited snapshot');
- const response = await request('content/projects/' + project.id);
- if (response.status === 404 && result.deleted.some(p => p.id === project.id)) continue;
- assert.equal(response.status, 200, 'Read project ' + project.id);
- let current = response.data.project;
- assert.equal(current.type, project.type);
- assert.equal(current.name, project.name);
- assert.equal(current.version, project.version, 'Concurrent change to project ' + project.id);
- if (current.status === 'PUBLISHED') {
- const revoked = await request('content/projects/' + project.id + '/review', {
- version: current.version, action: 'REVOKE', comment: '按用户要求清理非005~008的历史内容;已备份并核对无保留工程或教学任务引用。',
- });
- assert.equal(revoked.status, 200, 'Revoke project ' + project.id + ': ' + revoked.message);
- current = revoked.data.project;
- }
- const deleted = await request('content/projects/' + project.id + '?version=' + current.version, undefined, 'DELETE');
- assert.equal(deleted.status, 200, 'Delete project ' + project.id + ': ' + deleted.message);
- result.deleted.push({ id: project.id, name: project.name, type: project.type, deletedAt: new Date().toISOString(), backupSha256: project.backupSha256 });
- save();
- }
- result.completedAt = new Date().toISOString(); result.pass = true; save();
- console.log('Deleted unreferenced projects:', result.deleted.length);
- } catch (error) {
- result.pass = false; result.error = error.message; save(); throw error;
- }
- }
|