No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

61 líneas
3.6 KiB

  1. import fs from 'node:fs';
  2. import crypto from 'node:crypto';
  3. import assert from 'node:assert/strict';
  4. import { login, call } from './role-permissions-live.mjs';
  5. // One-time cleanup of the explicitly audited local inventory. New projects are never included.
  6. const dir = 'reports/bridge-content-cleanup-20260925';
  7. const plan = JSON.parse(fs.readFileSync(dir + '/plan.json', 'utf8'));
  8. const protectedIds = new Set([...plan.keep, ...plan.blockers].map(p => p.id));
  9. for (let changed = true; changed;) {
  10. changed = false;
  11. for (const project of plan.all.filter(p => protectedIds.has(p.id))) {
  12. for (const id of project.dependencies) if (!protectedIds.has(id)) { protectedIds.add(id); changed = true; }
  13. }
  14. }
  15. const pending = plan.candidates.filter(p => !protectedIds.has(p.id));
  16. const order = [];
  17. while (pending.length) {
  18. const index = pending.findIndex(p => !pending.some(other => other.id !== p.id && other.dependencies.includes(p.id)));
  19. assert.ok(index >= 0, 'Dependency cycle: stop rather than deleting referenced content');
  20. order.push(...pending.splice(index, 1));
  21. }
  22. console.log(JSON.stringify({ removable: order.map(p => ({ id: p.id, type: p.type, name: p.name })), protectedCount: protectedIds.size }, null, 2));
  23. if (process.argv.includes('--apply-unreferenced')) {
  24. const session = await login('root');
  25. const request = (path, body, method) => call('tran/v1/' + path, session.accessToken, body, method);
  26. const resultPath = dir + '/deleted-unreferenced.json';
  27. const result = fs.existsSync(resultPath) ? JSON.parse(fs.readFileSync(resultPath, 'utf8')) : { startedAt: new Date().toISOString(), deleted: [] };
  28. const save = () => fs.writeFileSync(resultPath, JSON.stringify(result, null, 2));
  29. try {
  30. for (const project of order) {
  31. assert.ok(['MODEL', 'SCENE', 'TRAINING'].includes(project.type));
  32. assert.ok(!protectedIds.has(project.id));
  33. const backup = fs.readFileSync(plan.backupDirectory + '/project-' + project.id + '.json');
  34. assert.equal(crypto.createHash('sha256').update(backup).digest('hex'), project.backupSha256, 'Backup must match audited snapshot');
  35. const response = await request('content/projects/' + project.id);
  36. if (response.status === 404 && result.deleted.some(p => p.id === project.id)) continue;
  37. assert.equal(response.status, 200, 'Read project ' + project.id);
  38. let current = response.data.project;
  39. assert.equal(current.type, project.type);
  40. assert.equal(current.name, project.name);
  41. assert.equal(current.version, project.version, 'Concurrent change to project ' + project.id);
  42. if (current.status === 'PUBLISHED') {
  43. const revoked = await request('content/projects/' + project.id + '/review', {
  44. version: current.version, action: 'REVOKE', comment: '按用户要求清理非005~008的历史内容;已备份并核对无保留工程或教学任务引用。',
  45. });
  46. assert.equal(revoked.status, 200, 'Revoke project ' + project.id + ': ' + revoked.message);
  47. current = revoked.data.project;
  48. }
  49. const deleted = await request('content/projects/' + project.id + '?version=' + current.version, undefined, 'DELETE');
  50. assert.equal(deleted.status, 200, 'Delete project ' + project.id + ': ' + deleted.message);
  51. result.deleted.push({ id: project.id, name: project.name, type: project.type, deletedAt: new Date().toISOString(), backupSha256: project.backupSha256 });
  52. save();
  53. }
  54. result.completedAt = new Date().toISOString(); result.pass = true; save();
  55. console.log('Deleted unreferenced projects:', result.deleted.length);
  56. } catch (error) {
  57. result.pass = false; result.error = error.message; save(); throw error;
  58. }
  59. }