import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { resolve } from 'node:path'; const root = process.cwd(); const catalog = [ ['api', 'API 服务开发', 16], ['database', '数据库开发', 12], ['api-database', 'API + 数据库', 12], ['tasks', '异步任务开发', 16], ['redis', 'Redis 缓存', 10], ['docker', 'Docker 容器化', 10], ['linux', 'Linux 部署', 10], ['troubleshooting', '线上问题排查', 12], ]; const errors = []; let total = 0; const report = []; mkdirSync(resolve(root, 'public/downloads'), { recursive: true }); const text = (value) => typeof value === 'string' && value.trim().length > 0; const list = (value, min = 1) => Array.isArray(value) && value.length >= min; function verify(l, slug, index) { const prefix = `${slug}/${l?.id || index}`; if (l?.id !== String(index + 1).padStart(2, '0')) errors.push(`${prefix}: invalid lesson ID`); for (const key of ['group', 'title', 'summary']) if (!text(l?.[key])) errors.push(`${prefix}: missing ${key}`); if ('minutes' in l) errors.push(`${prefix}: instructor duration metadata is not allowed`); for (const key of [ 'objectives', 'concepts', 'steps', 'checks', 'pitfalls', 'refs', ]) if (!list(l?.[key])) errors.push(`${prefix}: missing ${key}`); for (const c of l.concepts || []) if (!text(c.title) || !text(c.body)) errors.push(`${prefix}: incomplete concept`); for (const s of l.steps || []) if (!text(s.title) || !text(s.body)) errors.push(`${prefix}: incomplete step`); for (const p of l.pitfalls || []) if (!text(p.symptom) || !text(p.cause) || !text(p.fix)) errors.push(`${prefix}: incomplete troubleshooting`); for (const key of ['task', 'hint', 'answer']) if (!text(l.exercise?.[key])) errors.push(`${prefix}: missing exercise ${key}`); for (const ref of l.refs || []) { try { if (!text(ref.label) || new URL(ref.url).protocol !== 'https:') throw new Error(); } catch { errors.push(`${prefix}: invalid reference ${ref.url}`); } } if (slug !== 'api') { if ( !list(l.objectives, 3) || !list(l.steps, 4) || !list(l.concepts, 3) || !list(l.checks, 3) || !list(l.pitfalls, 2) || !list(l.refs, 2) ) errors.push(`${prefix}: insufficient practical lesson detail`); if ( !text(l.production?.scenario) || !list(l.production?.decisions) || !text(l.production?.failureDrill) || !list(l.production?.acceptance) ) errors.push(`${prefix}: incomplete production exercise`); } } const bullets = (items) => items.map((s) => `- ${s}`).join('\n'); function markdown(title, lessons) { return ( `# ${title}\n\n术核 AI · 后端工程课件\n\n` + lessons .map((l) => { let out = `## ${l.id} ${l.title}\n\n${l.summary}\n\n### 本节目标\n\n${bullets(l.objectives)}\n\n### 必要概念\n\n`; out += l.concepts .map((c) => `#### ${c.title}\n\n${c.body}\n`) .join('\n'); out += '\n### 动手实现\n\n' + l.steps .map( (s, i) => `#### ${i + 1}. ${s.title}\n\n${s.body}\n${s.code ? `\n文件:${s.filename || '终端 / 当前课时'}\n\n\`\`\`\`${s.lang || 'text'}\n${s.code}\n\`\`\`\`\n` : ''}`, ) .join('\n'); out += `\n### 验收结果\n\n${bullets(l.checks)}\n`; if (l.production) { const p = l.production; out += `\n### 生产约束与故障演练\n\n${p.scenario}\n\n#### 实现决策\n\n${bullets(p.decisions)}\n\n#### 隔离环境演练\n\n${p.failureDrill}\n\n#### 恢复验收\n\n${bullets(p.acceptance)}\n`; } out += '\n### 常见错误\n\n' + l.pitfalls .map( (p) => `#### ${p.symptom}\n\n原因:${p.cause}\n\n处理:${p.fix}\n`, ) .join('\n'); out += `\n### 扩展练习\n\n${l.exercise.task}\n\n提示:${l.exercise.hint}\n\n参考答案:\n\n${l.exercise.answer}\n\n### 技术文档\n\n${l.refs.map((r) => `- [${r.label}](${r.url})`).join('\n')}\n`; return out; }) .join('\n---\n\n') ); } for (const [slug, title, count] of catalog) { const path = resolve( root, slug === 'api' ? 'app/lessons.json' : `public/content/${slug}.json`, ); let lessons; try { lessons = JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, '')); } catch (error) { errors.push(`${slug}: ${error.message}`); continue; } if (!Array.isArray(lessons) || lessons.length !== count) { errors.push(`${slug}: expected ${count} lessons`); continue; } lessons.forEach((l, i) => verify(l, slug, i)); total += lessons.length; writeFileSync( resolve(root, `public/downloads/${slug}-lessons.md`), markdown(title, lessons), ); report.push({ slug, lessons: lessons.length, steps: lessons.reduce((n, l) => n + l.steps.length, 0), codeBlocks: lessons.reduce( (n, l) => n + l.steps.filter((s) => s.code).length, 0, ), }); } if (errors.length) { console.error(errors.join('\n')); process.exit(1); } console.log( JSON.stringify( { modules: report.length, lessons: total, details: report }, null, 2, ), );