同步家纺情报 v2 数据、59 个严格权威源、趋势图库、移动端交互、Google/Yandex 翻译门控及静态 SEO/GEO 构建。 补充面向 C# 研发的 README,明确当前无 LLM、多模态与生图模型,记录翻译请求参数、缓存与失败策略、数据处理边界、前端职责和无 GitHub Pages 部署流程。 新增按日期倒序维护的 CHANGELOG,记录影响、验证与代码/数据/dist 回滚方法。 验证:11 个 Python 契约测试、13 个 Node 前端测试通过;静态构建生成 1096 个 Sitemap URL。
129 lines
5.1 KiB
Python
129 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
||
"""构建可直接部署或由本地 HTTP 服务打开的独立静态站。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from seo_config import BRAND_NAME, ENTITY_DEFINITION, KEYWORD_CLUSTERS, SECTION_INTENTS
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
PUBLISH_DATA = ("textile-intelligence.json", "textile-trends.json")
|
||
DEFAULT_SITE_URL = "http://127.0.0.1:8765"
|
||
|
||
|
||
def replace_directory(source: Path, target: Path) -> None:
|
||
previous = target.with_name(f".{target.name}-previous")
|
||
if previous.exists():
|
||
shutil.rmtree(previous)
|
||
if target.exists():
|
||
target.replace(previous)
|
||
try:
|
||
source.replace(target)
|
||
except Exception:
|
||
if previous.exists() and not target.exists():
|
||
previous.replace(target)
|
||
raise
|
||
if previous.exists():
|
||
shutil.rmtree(previous)
|
||
|
||
|
||
def write_llms_file(target: Path, payload: dict, site_url: str) -> None:
|
||
summary = payload.get("summary") or {}
|
||
lines = [
|
||
f"# {BRAND_NAME}",
|
||
"",
|
||
f"> {ENTITY_DEFINITION}",
|
||
"",
|
||
f"- 站点:{site_url}/",
|
||
f"- 权威源:{payload.get('source_success', 0)}/{payload.get('source_target', 0)}",
|
||
f"- 备货机会:{summary.get('calendar_featured', 0)}",
|
||
f"- 展会:{summary.get('exhibitions', 0)}",
|
||
f"- 政策:{summary.get('policies', 0)}",
|
||
f"- 平台:{summary.get('platforms', 0)}",
|
||
f"- 跨境活动:{summary.get('activities', 0)}",
|
||
f"- 核心词:{'、'.join((*KEYWORD_CLUSTERS['core'], *KEYWORD_CLUSTERS['task']))}",
|
||
"",
|
||
"## 可索引栏目",
|
||
"",
|
||
f"- [备货日历]({site_url}/textile/calendar/)",
|
||
f"- [展会通]({site_url}/textile/exhibitions/)",
|
||
f"- [政策预警]({site_url}/textile/policies/)",
|
||
f"- [趋势雷达]({site_url}/textile/standards/)",
|
||
f"- [平台渠道]({site_url}/textile/platforms/)",
|
||
f"- [跨境活动]({site_url}/textile/activities/)",
|
||
"",
|
||
"## 栏目回答意图",
|
||
"",
|
||
*[f"- {section}:{intent}" for section, intent in SECTION_INTENTS.items()],
|
||
"",
|
||
"官方事实、机器翻译与本站经营分析分区展示;机器翻译仅作阅读辅助,不替代报名、付款或合规提交。",
|
||
]
|
||
(target / "llms.txt").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||
|
||
|
||
def build(data_dir: Path, output_dir: Path, site_url: str) -> None:
|
||
data_dir = data_dir.resolve()
|
||
output_dir = output_dir.resolve()
|
||
staging = output_dir.with_name(f".{output_dir.name}-build")
|
||
if staging.exists():
|
||
shutil.rmtree(staging)
|
||
shutil.copytree(ROOT / "public", staging)
|
||
target_data = staging / "data"
|
||
target_data.mkdir(parents=True)
|
||
for name in PUBLISH_DATA:
|
||
source = data_dir / name
|
||
if not source.is_file():
|
||
raise FileNotFoundError(f"缺少发布数据:{source}")
|
||
shutil.copy2(source, target_data / name)
|
||
|
||
index = staging / "index.html"
|
||
html = index.read_text(encoding="utf-8")
|
||
html = html.replace('<link rel="canonical" href="./" />', f'<link rel="canonical" href="{site_url}/" />')
|
||
html = html.replace('<link rel="alternate" hreflang="zh-CN" href="./" />', f'<link rel="alternate" hreflang="zh-CN" href="{site_url}/" />')
|
||
html = html.replace('<link rel="alternate" hreflang="x-default" href="./" />', f'<link rel="alternate" hreflang="x-default" href="{site_url}/" />')
|
||
html = html.replace('<meta property="og:image" content="./assets/logo.svg" />', f'<meta property="og:image" content="{site_url}/assets/logo.svg" />')
|
||
index.write_text(html, encoding="utf-8", newline="\n")
|
||
|
||
subprocess.run([
|
||
sys.executable,
|
||
str(ROOT / "scripts" / "build_seo.py"),
|
||
"--data", str(target_data / "textile-intelligence.json"),
|
||
"--root", str(staging),
|
||
"--site-url", site_url,
|
||
], check=True, cwd=ROOT)
|
||
payload = json.loads((target_data / "textile-intelligence.json").read_text(encoding="utf-8"))
|
||
write_llms_file(staging, payload, site_url)
|
||
required = ("index.html", "robots.txt", "sitemap.xml", "llms.txt")
|
||
missing = [name for name in required if not (staging / name).is_file()]
|
||
if missing:
|
||
raise RuntimeError(f"构建产物不完整:{missing}")
|
||
replace_directory(staging, output_dir)
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="构建三笙AI情报局独立静态站")
|
||
parser.add_argument("--data-dir", default=str(ROOT / "data"))
|
||
parser.add_argument("--output-dir", default=str(ROOT / "dist"))
|
||
parser.add_argument("--site-url", default=os.environ.get("SANSHENG_SITE_URL", DEFAULT_SITE_URL))
|
||
args = parser.parse_args()
|
||
site_url = args.site_url.rstrip("/")
|
||
try:
|
||
build(Path(args.data_dir), Path(args.output_dir), site_url)
|
||
except Exception as exc:
|
||
print(f"[构建失败] {exc}", file=sys.stderr)
|
||
return 1
|
||
print(f"[构建完成] {Path(args.output_dir).resolve()}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|