建立可移交的 AI 软件交付规划文档仓库。

纳入截至 2026-10-13 会议回写后的产品基线与会议图源。
This commit is contained in:
mdszxy committed 2026-10-13 22:27:58 +08:00
commit 638d3541bf
55 files changed
+3484

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
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); });