96 lines
4.4 KiB
JavaScript
96 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
||||
|
|
/**
|
|||
|
|
* 汇总 parts/00-preamble.md + parts/NN-*.md 为最终提示词文件。
|
|||
|
|
* 用法:node scripts/assemble.mjs [--split]
|
|||
|
|
* 输出:ClassIsland-给ChatGPT的完整提示词.md(工作区根目录);--split 额外输出 out/ 下的分卷。
|
|||
|
|
*/
|
|||
|
|
import { readdirSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
|
|||
|
|
import { join, dirname } from 'node:path';
|
|||
|
|
import { fileURLToPath } from 'node:url';
|
|||
|
|
|
|||
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|||
|
|
const partsDir = join(root, 'parts');
|
|||
|
|
const outFile = join(root, 'ClassIsland-给ChatGPT的完整提示词.md');
|
|||
|
|
|
|||
|
|
const files = readdirSync(partsDir)
|
|||
|
|
.filter((f) => /^\d{2}-.+\.md$/.test(f))
|
|||
|
|
.sort((a, b) => a.localeCompare(b, 'en'));
|
|||
|
|
|
|||
|
|
if (!files.some((f) => f.startsWith('00-'))) throw new Error('缺少 parts/00-preamble.md');
|
|||
|
|
if (files.length !== 20) console.warn(`警告:part 数量为 ${files.length},预期 20`);
|
|||
|
|
|
|||
|
|
const sections = [];
|
|||
|
|
const toc = [];
|
|||
|
|
const stats = [];
|
|||
|
|
let preamble = '';
|
|||
|
|
|
|||
|
|
for (const file of files) {
|
|||
|
|
const content = readFileSync(join(partsDir, file), 'utf8').replace(/\r\n/g, '\n').trimEnd();
|
|||
|
|
if (file.startsWith('00-')) {
|
|||
|
|
preamble = content;
|
|||
|
|
for (const line of content.split('\n')) {
|
|||
|
|
if (/^## /.test(line)) toc.push(`- ${line.replace(/^##\s+/, '').trim()}`);
|
|||
|
|
}
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
for (const line of content.split('\n')) {
|
|||
|
|
if (/^## /.test(line)) toc.push(`- ${line.replace(/^##\s+/, '').trim()}`);
|
|||
|
|
else if (/^### /.test(line)) toc.push(` - ${line.replace(/^###\s+/, '').trim()}`);
|
|||
|
|
}
|
|||
|
|
const chars = [...content.replace(/\s/g, '')].length;
|
|||
|
|
const cjk = (content.match(/[\u4e00-\u9fff]/g) || []).length;
|
|||
|
|
stats.push({ file, chars, cjk, lines: content.split('\n').length });
|
|||
|
|
sections.push(content);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!preamble.includes('<!-- TOC -->')) throw new Error('preamble 缺少 <!-- TOC --> 占位符');
|
|||
|
|
const tocBlock = ['## 目录', '', ...toc, ''].join('\n');
|
|||
|
|
const body = [preamble.replace('<!-- TOC -->', tocBlock), ...sections].join('\n\n---\n\n');
|
|||
|
|
|
|||
|
|
writeFileSync(outFile, body + '\n', 'utf8');
|
|||
|
|
|
|||
|
|
const totalChars = [...body.replace(/\s/g, '')].length;
|
|||
|
|
const totalCjk = (body.match(/[\u4e00-\u9fff]/g) || []).length;
|
|||
|
|
const lines = body.split('\n').length;
|
|||
|
|
console.log(`输出:${outFile}`);
|
|||
|
|
console.log(`总字符(去空白):${totalChars};汉字:${totalCjk};行数:${lines}`);
|
|||
|
|
console.log(`粗略 token 估算:${Math.round(totalCjk * 0.9 + (totalChars - totalCjk) * 0.3)}(仅参考)`);
|
|||
|
|
console.log('各 part:');
|
|||
|
|
for (const s of stats) console.log(` ${s.file.padEnd(28)} ${String(s.chars).padStart(7)} 字符 ${String(s.cjk).padStart(6)} 汉字`);
|
|||
|
|
console.log(` 00-preamble.md ${String([...preamble.replace(/\s/g, '')].length).padStart(7)} 字符 ${String((preamble.match(/[\u4e00-\u9fff]/g) || []).length).padStart(6)} 汉字`);
|
|||
|
|
|
|||
|
|
if (process.argv.includes('--split')) {
|
|||
|
|
const outDir = join(root, 'out');
|
|||
|
|
const sectionsDir = join(outDir, 'sections');
|
|||
|
|
mkdirSync(sectionsDir, { recursive: true });
|
|||
|
|
const volumes = [
|
|||
|
|
{ name: 'vol1-总纲与技术栈(00-02)', nums: [0, 1, 2] },
|
|||
|
|
{ name: 'vol2-设计系统与主界面组件(03-05)', nums: [3, 4, 5] },
|
|||
|
|
{ name: 'vol3-课表提醒自动化(06-08)', nums: [6, 7, 8] },
|
|||
|
|
{ name: 'vol4-主题设置窗口系统与数据(09-13)', nums: [9, 10, 11, 12, 13] },
|
|||
|
|
{ name: 'vol5-动画资源验收与结语(14-19)', nums: [14, 15, 16, 17, 18, 19] },
|
|||
|
|
];
|
|||
|
|
for (const v of volumes) {
|
|||
|
|
const chunks = [];
|
|||
|
|
if (v.nums.includes(0)) chunks.push(preamble.replace('<!-- TOC -->', tocBlock));
|
|||
|
|
for (const n of v.nums) {
|
|||
|
|
if (n === 0) continue;
|
|||
|
|
const f = files.find((x) => Number(x.slice(0, 2)) === n);
|
|||
|
|
if (f) chunks.push(readFileSync(join(partsDir, f), 'utf8').replace(/\r\n/g, '\n').trimEnd());
|
|||
|
|
else console.warn(`警告:缺少 part ${n}`);
|
|||
|
|
}
|
|||
|
|
const p = join(outDir, `${v.name}.md`);
|
|||
|
|
writeFileSync(p, chunks.join('\n\n---\n\n') + '\n', 'utf8');
|
|||
|
|
const c = readFileSync(p, 'utf8');
|
|||
|
|
console.log(`分卷:${p}(${[...c.replace(/\s/g, '')].length} 字符,${(c.match(/[\u4e00-\u9fff]/g) || []).length} 汉字)`);
|
|||
|
|
}
|
|||
|
|
// 逐节文件(适合按章节投喂)
|
|||
|
|
let copied = 0;
|
|||
|
|
for (const f of files) {
|
|||
|
|
if (f.startsWith('00-')) continue;
|
|||
|
|
copyFileSync(join(partsDir, f), join(sectionsDir, f));
|
|||
|
|
copied += 1;
|
|||
|
|
}
|
|||
|
|
copyFileSync(join(partsDir, '00-preamble.md'), join(sectionsDir, '00-preamble.md'));
|
|||
|
|
console.log(`逐节文件:${sectionsDir}(${copied + 1} 个)`);
|
|||
|
|
}
|