Initial panel project deploy skill snapshot

This commit is contained in:
depei
2026-10-10 18:50:57 +08:00
commit 5295c1cf58
11 changed files with 2286 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交付压缩包构建脚本(合同对齐)。
产物(位于 --out-dir,默认 .Codex/delivery/):
1) <zip-name> 给甲方/终端使用的交付压缩包
2) 交付清单.md 与 zip 并列的单独文件(不打入包内)
zip 内部固定结构:
README.md 包根目录唯一一份 README
docs/screenshots/ README 引用的全部配图(解压可显示)
截图/界面截图/ 当前产品界面截图(中性文件名)
录屏/
文档/环境依赖清单.md
文档/部署注意事项.md
用法见 --help。
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path
def copy_file(src: Path, dst: Path):
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def main() -> int:
ap = argparse.ArgumentParser(description="构建交付压缩包与并列交付清单")
ap.add_argument("--project-dir", required=True)
ap.add_argument("--readme", default="README.md")
ap.add_argument("--out-dir", default=".Codex/delivery")
ap.add_argument("--assets-product", default=".Codex/delivery/assets/product")
ap.add_argument("--recordings", default=".Codex/delivery/recordings")
ap.add_argument("--dep-doc", default=".Codex/delivery/环境依赖清单.md")
ap.add_argument("--notes-doc", default=".Codex/delivery/部署注意事项.md")
ap.add_argument("--manifest-src", default=".Codex/delivery/manifest.md")
ap.add_argument("--zip-name", default=None, help="默认 smart-parking-delivery-<YYYYMMDD>.zip")
ap.add_argument("--video-name", default="full-feature-demo.mp4")
args = ap.parse_args()
root = Path(args.project_dir).resolve()
out_dir = root / args.out_dir
out_dir.mkdir(parents=True, exist_ok=True)
readme_path = root / args.readme
if not readme_path.exists():
print("[FAIL] README 不存在:%s" % readme_path)
return 1
zip_name = args.zip_name or ("smart-parking-delivery-%s.zip" % _today())
zip_path = out_dir / zip_name
with tempfile.TemporaryDirectory() as td:
stage = Path(td)
# 1) README(包根唯一一份)
copy_file(readme_path, stage / "README.md")
# 2) README 引用图片 -> docs/...(保持相对路径)
readme_text = readme_path.read_text(encoding="utf-8", errors="ignore")
img_refs = re.findall(r"!\[[^\]]*\]\(([^)]+)\)", readme_text)
copied_img = set()
for ref in img_refs:
ref = ref.strip()
if "://" in ref or ref.startswith("#"):
continue
src_img = (readme_path.parent / ref).resolve()
if not src_img.exists():
print("[FAIL] README 引用图片缺失:%s" % ref)
return 1
rel = src_img.relative_to(root)
if str(rel) not in copied_img:
copy_file(src_img, stage / rel)
copied_img.add(str(rel))
# 3) 产品界面截图(中性命名,不体现版本对比或来源宣传)
product_assets = root / args.assets_product
if product_assets.is_dir():
shutil.copytree(product_assets, stage / "截图" / "界面截图", dirs_exist_ok=True)
# 4) 录屏(只取指定 mp4)
rec_dir = root / args.recordings
video = rec_dir / args.video_name
if video.exists():
copy_file(video, stage / "录屏" / args.video_name)
# 5) 文档目录:仅依赖清单 + 注意事项(无 README、无交付清单)
for d in (args.dep_doc, args.notes_doc):
p = root / d
if p.exists():
copy_file(p, stage / "文档" / p.name)
# 打包
if zip_path.exists():
zip_path.unlink()
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z:
for p in sorted(stage.rglob("*")):
if p.is_file():
z.write(p, p.relative_to(stage).as_posix())
# 交付清单:复制为与 zip 并列的单独文件
manifest_src = root / args.manifest_src
checklist_path = out_dir / "交付清单.md"
if manifest_src.exists():
shutil.copy2(manifest_src, checklist_path)
# 校验:解压核对 README 图片
bad = _verify_zip(zip_path)
if bad:
print("[FAIL] 交付包校验未通过:%s" % bad)
return 1
print("交付包:%s (%.2f MB)" % (zip_path, zip_path.stat().st_size/1024/1024))
if checklist_path.exists():
print("并列交付清单:%s" % checklist_path)
return 0
def _today() -> str:
from datetime import datetime
return datetime.now().strftime("%Y%m%d")
def _verify_zip(zip_path: Path) -> list[str]:
problems: list[str] = []
with tempfile.TemporaryDirectory() as td:
with zipfile.ZipFile(zip_path) as z:
z.extractall(td)
v = Path(td)
names = [p.relative_to(v).as_posix() for p in v.rglob("*") if p.is_file()]
readmes = [n for n in names if n.lower().endswith("readme.md")]
if readmes != ["README.md"]:
problems.append("README 必须且只能在包根一份,实际:%s" % readmes)
if any("交付清单" in n for n in names):
problems.append("交付清单不得打入包内")
if any(re.search(r"(before|after|改造前|原版)", n, re.I) for n in names):
problems.append("包内不得出现对比期/before/after 文件命名")
docs = [n for n in names if n.startswith("文档/")]
if any(n.lower().endswith("readme.md") for n in docs):
problems.append("文档目录不得放 README")
if (v / "README.md").exists():
txt = (v / "README.md").read_text(encoding="utf-8", errors="ignore")
for ref in re.findall(r"!\[[^\]]*\]\(([^)]+)\)", txt):
if "://" in ref or ref.startswith("#"):
continue
if not (v / ref).exists():
problems.append("README 图片解压后缺失:%s" % ref)
forbidden_in_readme = [
"致谢", "原始开源项目", "原项目链接", "原作者", "基于开源",
"He Rui", "CrazyHer", "campus-surveillance",
]
for term in forbidden_in_readme:
if term.lower() in txt.lower():
problems.append("README 出现不应主动展示的来源表述:%s" % term)
return problems
if __name__ == "__main__":
sys.exit(main())