Files
mdszxy 638d3541bf 建立可移交的 AI 软件交付规划文档仓库。
纳入截至 2026-10-13 会议回写后的产品基线与会议图源。
2026-10-13 22:27:58 +08:00

94 lines
9.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('node:fs');
const path = require('node:path');
// 文档是唯一编辑源;本脚本只生成图集和展示文件。
const root = __dirname;
const bundlePackages = process.env.DIAGRAM_NODE_MODULES || 'C:/Users/mk/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules';
const { chromium } = require(path.join(bundlePackages, 'playwright'));
const escapeHtml = text => text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
const files = fs.readdirSync(root).filter(name => /^G0[1-5]-.+\.md$/.test(name)).sort();
async function main() {
const cache = path.join(root, '.render-cache');
fs.mkdirSync(cache, { recursive: true });
const library = path.join(cache, 'mermaid-11.12.0.min.js');
if (!fs.existsSync(library)) {
const response = await fetch('https://cdn.jsdelivr.net/npm/[email protected]/dist/mermaid.min.js');
if (!response.ok) throw new Error(`Mermaid download: ${response.status}`);
fs.writeFileSync(library, await response.text());
}
const executablePath = process.env.DIAGRAM_BROWSER || 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe';
const browser = await chromium.launch({ executablePath, headless: true });
const page = await browser.newPage({ viewport: { width: 1800, height: 1200 }, deviceScaleFactor: 1.5 });
await page.setContent('<!doctype html><html lang="zh-CN"><meta charset="utf-8"><style>body{margin:0;font-family:"Microsoft YaHei",sans-serif}#canvas{background:white}svg{display:block}</style><div id="canvas"></div></html>');
await page.addScriptTag({ path: library });
await page.evaluate(() => mermaid.initialize({
startOnLoad: false, securityLevel: 'strict', theme: 'base',
fontFamily: 'Microsoft YaHei, Arial, sans-serif',
themeVariables: {
fontSize: '20px', primaryColor: '#eef5ff', primaryTextColor: '#17233a', primaryBorderColor: '#8198bb',
lineColor: '#60718a', secondaryColor: '#f3f8f6', tertiaryColor: '#ffffff',
clusterBkg: '#f8fafc', clusterBorder: '#cbd5e1', actorBkg: '#edf3fb', actorBorder: '#8198bb',
actorTextColor: '#17233a', signalColor: '#334155', signalTextColor: '#17233a',
labelBoxBkgColor: '#f3f6fa', labelBoxBorderColor: '#9aabc1', labelTextColor: '#17233a',
loopTextColor: '#17233a', noteBkgColor: '#fffbea', noteBorderColor: '#d4c79f', noteTextColor: '#17233a'
},
flowchart: { htmlLabels: false, useMaxWidth: false, nodeSpacing: 32, rankSpacing: 58, padding: 18, wrappingWidth: 310, curve: 'basis' },
sequence: { useMaxWidth: false, wrap: true, width: 240, actorMargin: 62, diagramMarginX: 30, diagramMarginY: 20, messageMargin: 40, boxMargin: 14, mirrorActors: false }
}));
const manifest = [];
for (const file of files) {
const md = fs.readFileSync(path.join(root, file), 'utf8');
const title = md.match(/^# (.+)$/m)[1];
const conclusion = md.match(/^\*\*(.+)\*\*$/m)[1];
const sources = [...md.matchAll(/```mermaid\r?\n([\s\S]+?)\r?\n```/g)];
const images = [...md.matchAll(/!\[([^\]]+)\]\(([^)]+)\.svg\)/g)];
if (sources.length !== images.length) throw new Error(`${file}: sources and images differ`);
const diagrams = [];
for (let index = 0; index < sources.length; index++) {
const name = images[index][2];
const source = sources[index][1];
fs.writeFileSync(path.join(root, `${name}.mmd`), source + '\n');
const rendered = await page.evaluate(async ({ source, id }) => {
const { svg } = await mermaid.render(id, source);
document.getElementById('canvas').innerHTML = svg;
const element = document.querySelector('#canvas svg');
const view = element.viewBox.baseVal;
return { svg, width: view.width, height: view.height };
}, { source, id: `diagram${file.slice(1, 3)}x${index}` });
if (!rendered.width || !rendered.height || /Syntax error/.test(rendered.svg)) throw new Error(`${name}: invalid render`);
const header = 124;
const width = Math.max(1000, Math.ceil(rendered.width + 64));
const height = Math.ceil(rendered.height + header + 48);
const wrapped = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="${name}Title ${name}Desc"><title id="${name}Title">${escapeHtml(title)} · ${escapeHtml(images[index][1])}</title><desc id="${name}Desc">${escapeHtml(conclusion)};2026-10-12,业务基线 0.14,图集版本 0.17</desc><rect width="100%" height="100%" fill="#fff"/><text x="32" y="42" font-family="Microsoft YaHei,Arial,sans-serif" font-size="28" font-weight="600" fill="#17233a">${escapeHtml(index ? `${title.slice(0, 3)} · ${images[index][1]}` : title)}</text><text x="32" y="79" font-family="Microsoft YaHei,Arial,sans-serif" font-size="18" fill="#526176">2026-10-12 · 业务基线 0.14 · 图集 0.17 · ${index ? '展开图' : '主图'} · 技术选型未定</text><g transform="translate(${Math.round((width - rendered.width) / 2)},${header})">${rendered.svg}</g></svg>`;
fs.writeFileSync(path.join(root, `${name}.svg`), wrapped);
await page.evaluate(svg => { document.getElementById('canvas').innerHTML = svg; }, wrapped);
const scale = Math.min(1, 2200 / width);
await page.evaluate(({ width, height }) => {
const svg = document.querySelector('#canvas > svg');
svg.style.width = `${width}px`;
svg.style.height = `${height}px`;
document.getElementById('canvas').style.width = `${width}px`;
}, { width: Math.ceil(width * scale), height: Math.ceil(height * scale) });
await page.locator('#canvas').screenshot({ path: path.join(root, `${name}.png`) });
diagrams.push({ name, label: images[index][1], width, height, svg: wrapped });
console.log(`${name}: ${width}x${height}`);
}
manifest.push({ file, title, conclusion, diagrams });
}
const menu = manifest.map((entry, index) => `<button type="button" data-index="${index}" aria-pressed="${index === 0}">${escapeHtml(entry.title)}</button>`).join('');
const panels = manifest.map((entry, index) => `<section id="panel${index}" ${index ? 'hidden' : ''}><h1>${escapeHtml(entry.title)}</h1><p class="lead">${escapeHtml(entry.conclusion)}</p><p class="links"><a href="${escapeHtml(entry.file)}">阅读 Markdown 与编辑源</a>${entry.diagrams.map(graph => ` · <a href="${escapeHtml(graph.name)}.svg">${escapeHtml(graph.label)} SVG</a> · <a href="${escapeHtml(graph.name)}.png">PNG</a>`).join('')}</p>${entry.diagrams.map(graph => `<article><h2>${escapeHtml(graph.label)}</h2><div class="diagram">${graph.svg}</div></article>`).join('')}</section>`).join('');
const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>AI 软件交付平台 · G01–G05 会议图集</title><style>
*{box-sizing:border-box}body{margin:0;background:#f2f5fa;color:#17233a;font-family:"Microsoft YaHei",Arial,sans-serif;font-size:16px}header{background:#fff;padding:20px 28px;border-bottom:1px solid #dce3ed}header p{margin:4px 0;color:#526176}nav{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px}button{font:inherit;border:1px solid #cbd5e1;border-radius:6px;background:#fff;padding:10px 14px;color:#17233a;cursor:pointer}button[aria-pressed=true]{background:#253f6b;color:#fff;border-color:#253f6b}main{padding:24px 28px}h1{font-size:28px;margin:0 0 12px}h2{font-size:19px;margin:0 0 12px}.lead{line-height:1.8;margin:0 0 10px}.links{font-size:14px;line-height:1.8}a{color:#245b98}article{background:white;border-radius:8px;padding:20px;margin:20px 0;border:1px solid #dce3ed}.diagram{overflow:auto}.diagram>svg{width:100%;height:auto;min-width:1000px;display:block}footer{color:#526176;font-size:14px;line-height:1.8;margin-top:20px}@media(max-width:700px){header,main{padding:16px}article{padding:12px}h1{font-size:24px}}@media print{nav,header,.links{display:none}body,main{background:white;padding:0}section[hidden]{display:block!important}article{break-inside:avoid;page-break-after:always;border:0;padding:0}.diagram{overflow:visible}.diagram>svg{min-width:0}footer{display:none}}
</style></head><body><header><strong>AI 软件交付与持续服务平台 · 会议图集</strong><p>G01–G05 · 2026-10-12 · 业务基线 0.14 · 图集 0.17</p><nav aria-label="选择图表">${menu}</nav></header><main>${panels}<footer>虚线及“后续 / 待定”文字表示目标能力或候选,不代表首期承诺。图表达业务与概念,技术架构未定案。完整规则及图内省略的分支请查看对应 Markdown;附图用于会中展开。此图集已内嵌图形,可离线打开。</footer></main><script>document.querySelectorAll('nav button').forEach(button=>button.addEventListener('click',()=>{document.querySelectorAll('nav button').forEach(peer=>peer.setAttribute('aria-pressed',String(peer===button)));document.querySelectorAll('main section').forEach((panel,index)=>panel.hidden=index!==Number(button.dataset.index));}));</script></body></html>`;
fs.writeFileSync(path.join(root, 'G01-G05-会议图集.html'), html);
fs.writeFileSync(path.join(root, 'render-manifest.json'), JSON.stringify({ date: '2026-10-12', baseline: '0.14', version: '0.17', mermaid: '11.12.0', diagrams: manifest.map(({ file, title, diagrams }) => ({ file, title, diagrams: diagrams.map(({ svg, ...graph }) => graph) })) }, null, 2) + '\n');
await page.goto('file:///' + path.join(root, 'G01-G05-会议图集.html').replaceAll('\\', '/'));
for (let index = 0; index < manifest.length; index++) {
await page.locator(`button[data-index="${index}"]`).click();
await page.screenshot({ path: path.join(cache, `review-G0${index + 1}.png`), fullPage: true });
}
await browser.close();
}
main().catch(error => { console.error(error); process.exit(1); });