chore: 初始化仓库
中华文明全图鉴——文物全图系统(PC Web 地图 + NestJS API + 管理后台)。 含三大 IP(文物南迁北归 / 国宝海外回归 / 博物馆手艺人)、AI 文物对话、 文物地图与详情、以及 demo-video-kit 演示视频生成工具。
This commit is contained in:
@@ -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} 个文件,所有关键词验证通过")
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user