sync-workshop-manual-v3.mjs 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import crypto from 'node:crypto';
  4. import vm from 'node:vm';
  5. import { fileURLToPath } from 'node:url';
  6. const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  7. const evidence = path.join(root, 'docs/workshop-v3-alignment-20261112');
  8. const target = path.join(root, 'docs/workshop-quality-r13-20261110/final-康师傅工作坊操作手册.html');
  9. const promptsFile = path.join(path.dirname(target), 'manual/workshop-dialogue-prompts.json');
  10. const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
  11. const source = fs.readFileSync(path.join(evidence, 'source-manual-v3.html'));
  12. const sourceSha256 = sha(source);
  13. if (sourceSha256 !== 'e8ed9dbd979c17f5caa88fb820f64f94a2e54518f810d7482b6b82f39d88041d') throw Error('V3_SOURCE_CHANGED');
  14. const url = 'https://s3.fmode.cn/user/K0xKEZANOG/kangshifu/manual-v3/index.html';
  15. const sourceText = source.toString('utf8');
  16. const promptBlock = sourceText.match(/window\.PROMPTS\s*=\s*(\{[\s\S]*?\n\});/);
  17. if (!promptBlock) throw Error('V3_PROMPTS_NOT_FOUND');
  18. const prompts = vm.runInNewContext('(' + promptBlock[1] + ')', {}, { timeout: 1000 });
  19. const archived = JSON.parse(fs.readFileSync(path.join(evidence, 'source-prompts-v3.json'), 'utf8'));
  20. if (JSON.stringify(prompts) !== JSON.stringify(archived.prompts) || Object.keys(prompts).length !== 13) throw Error('V3_PROMPTS_DIFFER');
  21. // Preserve in-page anchors; resolve only the overview links for the local final filename.
  22. const linked = sourceText.replaceAll('href="../overview/index.html"',
  23. 'href="https://s3.fmode.cn/user/K0xKEZANOG/kangshifu/overview/index.html"');
  24. if (linked === sourceText) throw Error('OVERVIEW_LINK_NOT_FOUND');
  25. // Some source tables lack the existing scroll wrapper; preserve their content and columns.
  26. const responsiveTables = `<script id="ksf-v3-responsive-tables">
  27. document.querySelectorAll('table.tbl').forEach(function(table) {
  28. if (table.closest('.tbl-scroll')) return;
  29. var wrapper = document.createElement('div');
  30. wrapper.className = 'tbl-scroll';
  31. wrapper.tabIndex = 0;
  32. wrapper.setAttribute('role', 'region');
  33. wrapper.setAttribute('aria-label', '表格,可横向滚动');
  34. table.before(wrapper);
  35. wrapper.appendChild(table);
  36. });
  37. </script>\n`;
  38. const html = linked.replace('</body>', responsiveTables + '</body>');
  39. const backup = path.join(evidence, 'retired-manual-before-v3');
  40. fs.mkdirSync(backup, { recursive: true });
  41. for (const file of [target, promptsFile]) {
  42. const name = path.join(backup, path.basename(file));
  43. if (!fs.existsSync(name)) fs.copyFileSync(file, name);
  44. }
  45. fs.writeFileSync(target, html);
  46. fs.writeFileSync(promptsFile, JSON.stringify({ schema: 'ksf.workshop-prompts.v3', source: url, sourceSha256, prompts }, null, 2) + '\n');
  47. const receipt = { ok: true, source: url, sourceSha256, finalManualSha256: sha(Buffer.from(html)),
  48. finalManualBytes: Buffer.byteLength(html), promptCount: 13, teachingContentChanged: false,
  49. adjustment: 'absolute overview links and scroll wrappers for unwrapped tables; all teaching text and prompts preserved',
  50. oldManualArchived: true, deployed: false };
  51. fs.writeFileSync(path.join(evidence, 'manual-sync.json'), JSON.stringify(receipt, null, 2) + '\n');
  52. console.log(JSON.stringify(receipt, null, 2));