import fs from "node:fs/promises"; const requiredFields = [ "id", "title", "en", "department", "category", "date", "lead", "clearance", "abstract", "source", ]; const isText = (value) => typeof value === "string" && value.trim().length > 0; export function validateContent(content) { const errors = []; if (!content || typeof content !== "object" || Array.isArray(content)) { throw new Error("档案数据必须是 JSON 对象。"); } for (const key of ["categories", "columns"]) { const names = content[key]; if (!Array.isArray(names) || names.length !== 5 || !names.every(isText)) { errors.push(`${key}:必须包含五个非空分类名称`); } else if (new Set(names).size !== 5 || names.includes("全部档案")) { errors.push(`${key}:分类名称不能重复,也不能使用“全部档案”`); } } const categories = Array.isArray(content.categories) ? content.categories : []; const columns = Array.isArray(content.columns) ? content.columns : []; if ( categories.some((name) => !columns.includes(name)) || columns.some((name) => !categories.includes(name)) ) { errors.push("categories 与 columns 必须包含相同的五个分类(顺序可以不同)"); } const records = Array.isArray(content.records) ? content.records : []; // 每列条数不设上限:列内不足八条时阵列会循环重复,超过八条就向外扩充。 // 唯一的下限是每列至少一条,否则那一列无可显示。 if (records.length < 5) errors.push("records:至少需要每个分类一条档案"); // 编号宽度随总量自适应(≤999 时仍为三位,与原版一致)。 const idWidth = Math.max(3, String(records.length).length); const ids = new Set(); records.forEach((record, index) => { const label = `records[${index}]`; if (!record || typeof record !== "object" || Array.isArray(record)) { errors.push(`${label}:必须是档案对象`); return; } for (const key of requiredFields) { if (!isText(record[key])) errors.push(`${label}.${key}:必须是非空文本`); } const expectedId = `X-${String(index + 1).padStart(idWidth, "0")}`; if (record.id !== expectedId) errors.push(`${label}.id:应为 ${expectedId},编号须按顺序保持稳定`); if (ids.has(record.id)) errors.push(`${label}.id:重复编号 ${record.id}`); ids.add(record.id); if (!categories.includes(record.category)) errors.push(`${label}.category:未知分类 ${record.category}`); if ( !Array.isArray(record.findings) || record.findings.length === 0 || !record.findings.every(isText) ) { errors.push(`${label}.findings:必须包含至少一条非空研究记录`); } try { const url = new URL(record.source); // 本地归档条目用 file: 指向真实目录;保留 http(s) 以兼容公开设定类条目。 if (!["https:", "http:", "file:"].includes(url.protocol)) throw new Error(); } catch { errors.push(`${label}.source:必须是有效的 HTTP、HTTPS 或 file 链接`); } }); for (const name of columns) { const count = records.filter((record) => record?.category === name).length; if (count < 1) errors.push(`分类“${name}”:至少要有一条档案`); } if (errors.length) throw new Error(`档案数据校验失败:\n- ${errors.join("\n- ")}`); return content; } export async function loadContent() { return validateContent( JSON.parse( await fs.readFile( new URL("../content/archives.json", import.meta.url), "utf8", ), ), ); } export function archiveText(r) { return `\uFEFFRHINE LAB · LOCAL ARCHIVE INDEX\nFILE ${r.id} / ${r.title}\n${r.en}\n\n归档位置:${r.department}\n编目范围:${r.date}\n来源:${r.lead}\n访问范围:${r.clearance}\n\n${r.abstract}\n\n研究记录\n${r.findings.map((f, i) => `${i + 1}. ${f}`).join("\n")}\n\n归档参考:${r.source}\n本文件是本地归档的编目摘要,由 scripts/archive-source.mjs 依据实际目录生成。\n`; }