fixtures.mjs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. // Copyright (c) 未来飞马
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this
  5. // file, You can obtain one at https://mozilla.org/MPL/2.0/.
  6. //
  7. // Trademark Notice:
  8. // The MPL-2.0 license grants copyright permissions for source code only.
  9. // It does NOT grant any rights to use trademarks including "未来飞马",
  10. // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步".
  11. // Any use of these trademarks requires separate written permission.
  12. /**
  13. * 测试夹具生成器:现场合成 .docx / .pptx / .png,落盘到指定目录。
  14. *
  15. * 刻意不在仓库里存任何二进制素材(.gitignore 也排除了图片)——夹具全部即时生成,
  16. * 内容为合成的假数据,不含任何真实 PII。
  17. */
  18. import fs from 'node:fs';
  19. import path from 'node:path';
  20. import { spawnSync } from 'node:child_process';
  21. import { ensureDir, resolvePython } from '../lib.mjs';
  22. import { makeTinyPng } from '../case-intake.mjs';
  23. const PY_BUILD = `
  24. import sys, zipfile, os
  25. target = sys.argv[1]
  26. doc_xml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  27. <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
  28. xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
  29. xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
  30. xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><w:body>
  31. <w:p><w:pPr><w:pStyle w:val="Heading1"/></w:pPr><w:r><w:t>IC 计算机科学 考前冲刺案例</w:t></w:r></w:p>
  32. <w:p><w:r><w:t>客户在读帝国理工本科二年级,期末前两周找到我们,担心挂科。</w:t></w:r></w:p>
  33. <w:p><w:r><w:t>跟进状态:异议处理中。客户觉得贵,家长不放心老师。</w:t></w:r></w:p>
  34. <w:p><w:r><w:t>结果:期末提分 18 分,家长很满意,已续课。</w:t></w:r></w:p>
  35. <w:p><w:r><w:drawing><wp:inline><a:graphic><a:blip r:embed="rId5"/></a:graphic></wp:inline></w:drawing></w:r></w:p>
  36. <w:p><w:hyperlink r:id="rId9"><w:r><w:t>查看成绩单</w:t></w:r></w:hyperlink></w:p>
  37. </w:body></w:document>'''
  38. doc_rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  39. <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  40. <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
  41. <Relationship Id="rId9" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="https://example.invalid/score" TargetMode="External"/>
  42. </Relationships>'''
  43. pres_xml = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  44. <p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
  45. xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
  46. <p:sldIdLst><p:sldId id="256" r:id="rId1"/><p:sldId id="257" r:id="rId2"/></p:sldIdLst></p:presentation>'''
  47. pres_rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  48. <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  49. <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
  50. <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide2.xml"/>
  51. </Relationships>'''
  52. def slide(title, body, with_pic, rid):
  53. pic = ('<p:pic><p:blipFill><a:blip r:embed="%s"/></p:blipFill></p:pic>' % rid) if with_pic else ''
  54. return '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  55. <p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
  56. xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
  57. xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:cSld><p:spTree>
  58. <p:sp><p:nvSpPr><p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr><p:txBody><a:p><a:r><a:t>%s</a:t></a:r></a:p></p:txBody></p:sp>
  59. <p:sp><p:nvSpPr/><p:txBody><a:p><a:r><a:t>%s</a:t></a:r></a:p></p:txBody></p:sp>
  60. %s
  61. </p:spTree></p:cSld></p:sld>''' % (title, body, pic)
  62. slide_rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  63. <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  64. <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image1.png"/>
  65. </Relationships>'''
  66. png = bytes.fromhex(
  67. "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
  68. "0000000a49444154789c6300010000050001"
  69. "0d0a2db40000000049454e44ae426082")
  70. with zipfile.ZipFile(os.path.join(target, 'case.docx'), 'w') as zf:
  71. zf.writestr('[Content_Types].xml', '<Types/>')
  72. zf.writestr('word/document.xml', doc_xml)
  73. zf.writestr('word/_rels/document.xml.rels', doc_rels)
  74. zf.writestr('word/media/image1.png', png)
  75. with zipfile.ZipFile(os.path.join(target, 'deck.pptx'), 'w') as zf:
  76. zf.writestr('[Content_Types].xml', '<Types/>')
  77. zf.writestr('ppt/presentation.xml', pres_xml)
  78. zf.writestr('ppt/_rels/presentation.xml.rels', pres_rels)
  79. zf.writestr('ppt/slides/slide1.xml', slide('案例合集', '英国本科 方案推荐中 导师匹配', True, 'rId2'))
  80. zf.writestr('ppt/slides/slide2.xml', slide('结果与反馈', '期末提分 出分反馈 家长认可', False, 'rId2'))
  81. zf.writestr('ppt/slides/_rels/slide1.xml.rels', slide_rels)
  82. zf.writestr('ppt/media/image1.png', png)
  83. print('ok')
  84. `;
  85. /**
  86. * 生成夹具。
  87. * @param {string} dir 目标目录
  88. * @returns {{docx:string, pptx:string, images:string[]}}
  89. */
  90. export function makeFixtures(dir) {
  91. const target = ensureDir(dir);
  92. const python = resolvePython();
  93. const run = spawnSync(python, ['-c', PY_BUILD, target], { encoding: 'utf8' });
  94. if (run.status !== 0) {
  95. throw new Error(`夹具生成失败:${run.stderr || run.stdout}`);
  96. }
  97. const images = [];
  98. // 九宫格:9 张同尺寸、同一批连发(时间戳相邻)→ 应判为同一组 3x3
  99. const base = 1_700_000_000_000;
  100. for (let i = 0; i < 9; i++) {
  101. const file = path.join(target, `grid-${String(i + 1).padStart(2, '0')}.png`);
  102. fs.writeFileSync(file, makeTinyPng(400, 400));
  103. fs.utimesSync(file, new Date(base + i * 1000), new Date(base + i * 1000));
  104. images.push(file);
  105. }
  106. // 另一批:不同尺寸 + 不同时间 → 不应并进九宫格
  107. const other = path.join(target, 'other-01.png');
  108. fs.writeFileSync(other, makeTinyPng(1600, 900));
  109. fs.utimesSync(other, new Date(base - 3 * 86_400_000), new Date(base - 3 * 86_400_000));
  110. images.push(other);
  111. return { docx: path.join(target, 'case.docx'), pptx: path.join(target, 'deck.pptx'), images };
  112. }