| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- 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,
- ),
- );
|