#!/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())