chore: 初始化仓库

中华文明全图鉴——文物全图系统(PC Web 地图 + NestJS API + 管理后台)。
含三大 IP(文物南迁北归 / 国宝海外回归 / 博物馆手艺人)、AI 文物对话、
文物地图与详情、以及 demo-video-kit 演示视频生成工具。
This commit is contained in:
selfrelease
2026-06-13 20:55:44 +08:00
commit 2d847e154f
161 changed files with 22629 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
工程结构校验脚本
用法:python3 scripts/check-structure.py
"""
from pathlib import Path
ROOT = Path(__file__).parent.parent
REQUIRED_FILES = [
# 根配置
"package.json",
"pnpm-workspace.yaml",
"tsconfig.base.json",
".gitignore",
".env.example",
"README.md",
# 文档
"1-prd.md",
"2-task.md",
"3-architecture.md",
"4-data-model.md",
"5-api.md",
"11-progress-log.md",
# 共享包
"packages/shared/package.json",
"packages/shared/tsconfig.json",
"packages/shared/src/index.ts",
"packages/shared/src/enums.ts",
"packages/shared/src/types.ts",
# 数据库包
"packages/db/package.json",
"packages/db/tsconfig.json",
"packages/db/src/index.ts",
"packages/db/src/pool.ts",
"packages/db/src/migrate.ts",
"packages/db/src/seed.ts",
"packages/db/migrations/001_init.sql",
"packages/db/seeds/001_roles.sql",
"packages/db/seeds/002_tag_categories.sql",
"packages/db/seeds/003_test_institution.sql",
"packages/db/seeds/004_test_artifacts.sql",
# API
"apps/api/package.json",
"apps/api/tsconfig.json",
"apps/api/nest-cli.json",
"apps/api/src/main.ts",
"apps/api/src/app.module.ts",
"apps/api/src/health/health.controller.ts",
# PC Web
"apps/web/package.json",
"apps/web/tsconfig.json",
"apps/web/next.config.js",
"apps/web/tailwind.config.ts",
"apps/web/src/app/layout.tsx",
"apps/web/src/app/page.tsx",
"apps/web/src/app/map/page.tsx",
# Admin
"apps/admin/package.json",
"apps/admin/tsconfig.json",
"apps/admin/vite.config.ts",
"apps/admin/index.html",
"apps/admin/src/main.tsx",
"apps/admin/src/App.tsx",
# Infra
"infra/docker-compose.yml",
# Scripts
"scripts/check-structure.py",
]
KEYWORD_CHECKS = {
"3-architecture.md": ["Next.js", "NestJS", "PostGIS", "测试策略"],
"4-data-model.md": ["artifacts", "artifact_locations", "PostGIS", "operation_logs"],
"5-api.md": ["/api/v1", "Map API", "Artifact API", "权限矩阵"],
"packages/db/migrations/001_init.sql": ["PostGIS", "artifacts", "artifact_locations", "operation_logs"],
"packages/shared/src/enums.ts": ["ArtifactCategory", "ArtifactLevel", "LocationPrecision"],
}
failed: list[str] = []
print("=== 工程结构校验 ===\n")
# 1. 文件存在性检查
print("1. 文件存在性检查")
for rel in REQUIRED_FILES:
path = ROOT / rel
if path.exists():
print(f"{rel}")
else:
print(f"{rel} <-- 缺失")
failed.append(f"缺失文件: {rel}")
# 2. 关键词检查
print("\n2. 关键词检查")
for rel, keywords in KEYWORD_CHECKS.items():
path = ROOT / rel
if not path.exists():
continue
text = path.read_text(encoding="utf-8")
for kw in keywords:
if kw in text:
print(f"{rel}'{kw}'")
else:
msg = f"{rel} 缺少关键词: '{kw}'"
print(f"{msg}")
failed.append(msg)
print()
if failed:
print(f"FAILED — {len(failed)} 项未通过:")
for item in failed:
print(f" - {item}")
raise SystemExit(1)
else:
total = len(REQUIRED_FILES)
print(f"PASSED — 共检查 {total} 个文件,所有关键词验证通过")
+96
View File
@@ -0,0 +1,96 @@
import { chromium } from "playwright";
import fs from "fs";
import path from "path";
const API = process.env.API_URL ?? "http://localhost:3002";
const OUT = path.resolve("apps/web/public/artifacts");
fs.mkdirSync(OUT, { recursive: true });
const toHd = (url, w = 1600) =>
/\?width=\d+/.test(url)
? url.replace(/\?width=\d+/, `?width=${w}`)
: `${url}${url.includes("?") ? "&" : "?"}width=${w}`;
const cleanName = (n) => n.replace(/[^]*/g, "").replace(/\([^)]*\)/g, "").trim();
// 通过 Commons MediaWiki API 按名称搜图,返回 1600px 缩略图直链
async function resolveFromCommons(page, name) {
const q = cleanName(name);
const api =
"https://commons.wikimedia.org/w/api.php?action=query&format=json&origin=*" +
"&generator=search&gsrnamespace=6&gsrlimit=6" +
`&gsrsearch=${encodeURIComponent(q)}` +
"&prop=imageinfo&iiprop=url|mime&iiurlwidth=1600";
try {
const data = await page.evaluate((u) => fetch(u).then((r) => r.json()), api);
const pages = data?.query?.pages ? Object.values(data.query.pages) : [];
for (const pg of pages) {
const info = pg.imageinfo?.[0];
if (info && /^image\/(jpeg|png)$/.test(info.mime) && info.thumburl) {
return info.thumburl;
}
}
} catch {
/* ignore */
}
return null;
}
const main = async () => {
// 从运行中的 API 取得带图文物列表
const points = await fetch(`${API}/api/v1/map/points`).then((r) => r.json());
const withImg = points.filter((p) => p.image_url);
console.log(`待下载文物图片:${withImg.length}`);
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const saveFrom = async (url, dest) => {
const resp = await page.goto(url, { timeout: 30000, waitUntil: "load" });
const ct = resp?.headers()["content-type"] ?? "";
if (resp && resp.ok() && ct.startsWith("image/")) {
const buf = await resp.body();
fs.writeFileSync(dest, buf);
return buf.length;
}
return 0;
};
let ok = 0;
let fail = 0;
for (const p of withImg) {
const dest = path.join(OUT, `${p.id}.jpg`);
if (fs.existsSync(dest)) {
ok++;
console.log(`· 跳过(已存在) ${p.name}`);
continue;
}
try {
// 1) 先用 seed 里的直链
let size = await saveFrom(toHd(p.image_url), dest);
// 2) 失败则用 Commons 搜索兜底
if (!size) {
const alt = await resolveFromCommons(page, p.name);
if (alt) size = await saveFrom(alt, dest);
}
if (size) {
ok++;
console.log(`${p.name} (${(size / 1024).toFixed(0)} KB)`);
} else {
fail++;
console.log(`${p.name} 未找到可用图片`);
}
} catch (e) {
fail++;
console.log(`${p.name} ${e.message.split("\n")[0]}`);
}
}
await browser.close();
console.log(`\n完成:成功 ${ok},失败 ${fail},输出目录 ${OUT}`);
};
main().catch((e) => {
console.error(e);
process.exit(1);
});