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())
|