Initial commit: RHINE LAB · ANALYSIS OS:三维界面与动效实验(本仓库不含个人归档索引数据)

This commit is contained in:
WpyQwq
2026-09-19 13:04:56 +08:00
commit 3f422218a5
1098 changed files with 39410 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
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`;
}