#!/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('', f'') html = html.replace('', f'') html = html.replace('', f'') html = html.replace('', f'') 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())