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
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交付物阻断式核查脚本(对齐软件改造部署外包交付规范)。
检查项:
A. 7 项交付物:源码 / 当前产品截图 / 全功能录屏 / 网盘归档 / README / Docker 部署 / 依赖与注意事项
B. README 12 项必备内容(按标题关键词启发式匹配)
C. 痕迹/来源清理:对用户提供的商业标识与来源关键词做全文检索(交付物中不得命中)
D. 交付清单 .Codex/delivery/manifest.md 存在且 7 项条目齐全
退出码:0 全部通过;1 存在阻断项。
用法见 --help。
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
README_KEYWORDS = {
"一句话简介": ["简介", "项目介绍"],
"详细功能描述": ["功能", "功能特性", "功能特点"],
"行业痛点": ["痛点"],
"技术优势": ["技术优势", "核心优势"],
"应用场景": ["应用场景", "使用场景", "场景"],
"目标行业与人群": ["目标行业", "适用行业", "适配行业", "目标用户", "适用人群"],
"商业应用价值": ["商业价值", "应用价值"],
"技术架构(含拓扑图)": ["技术架构", "架构"],
"环境前置依赖": ["环境要求", "前置依赖", "依赖要求", "环境依赖"],
"快速开始": ["快速开始", "快速上手", "quick start"],
"安装部署教程": ["部署", "安装"],
"版权与使用说明": ["版权", "使用说明"],
}
SCAN_SUFFIXES = {
".ts", ".tsx", ".js", ".jsx", ".vue", ".java", ".py", ".go", ".json",
".md", ".html", ".yml", ".yaml", ".less", ".css", ".scss", ".xml",
}
IMAGE_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp"}
VIDEO_SUFFIXES = {".mp4", ".mkv", ".mov", ".avi", ".webm"}
SKIP_DIRS = {
"node_modules", ".git", "dist", "build", "target", ".next", ".umi",
"__pycache__", ".venv", "venv",
}
def iter_files(root: Path):
# os.walk 自顶向下并在目录层剪枝,避免遍历 node_modules/.git 等巨型目录
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [
d for d in dirnames
if d not in SKIP_DIRS and not d.lower().startswith(".codex")
]
for name in filenames:
yield Path(dirpath) / name
def find_files(root: Path, suffixes: set[str]) -> list[Path]:
return [p for p in iter_files(root) if p.suffix.lower() in suffixes]
def main() -> int:
ap = argparse.ArgumentParser(description="开源改造项目交付物阻断式核查")
ap.add_argument("--project-dir", required=True, help="项目根目录")
ap.add_argument("--readme", default="README.md", help="README 相对路径")
ap.add_argument("--delivery-dir", default=".Codex/delivery", help="交付目录(相对项目)")
ap.add_argument("--assets-dir", default=".Codex/delivery/assets",
help="截图/录屏素材目录(相对项目)")
ap.add_argument("--trace-keywords", default="",
help="逗号分隔的商业标识与来源宣传关键词(原公司名、外部仓库、项目代号等)")
ap.add_argument("--allow-no-docker", action="store_true",
help="项目确实不适配 Docker 时,要求存在书面说明而非 Dockerfile")
ap.add_argument("--netdisk-link", default="",
help="网盘归档链接(manifest 中也可提供,二选一)")
ap.add_argument("--out", default=".Codex/reports/deliverables-check.json")
args = ap.parse_args()
root = Path(args.project_dir).resolve()
violations: list[str] = []
warnings: list[str] = []
evidence: dict = {}
# ---------- A1. 源码可构建产物线索 ----------
# 多模块项目(如 frontend/backend 分离)清单可能在子目录,递归查找
manifest_names = {
"package.json", "pom.xml", "build.gradle", "go.mod",
"requirements.txt", "pyproject.toml", "Cargo.toml",
}
manifest_hits = [p for p in iter_files(root) if p.name in manifest_names]
if not manifest_hits:
violations.append("未发现项目清单文件(package.json/pom.xml/go.mod 等),无法确认源码完整性")
else:
evidence["manifests"] = sorted(str(p.relative_to(root)) for p in manifest_hits)
# ---------- A2/A3. 截图与录屏 ----------
assets_dir = root / args.assets_dir
alt_dirs = [root / args.delivery_dir / "screenshots", root / args.delivery_dir / "recordings"]
images: list[Path] = []
videos: list[Path] = []
search_dirs = [d for d in [assets_dir, *alt_dirs] if d.exists()]
for d in search_dirs:
images.extend(find_files(d, IMAGE_SUFFIXES))
videos.extend(find_files(d, VIDEO_SUFFIXES))
# 对外只提供当前产品界面截图;交付素材目录不得含改造前/before 旧界面
leaked_before = [p for p in images if re.search(r"(before|改造前|原版)", p.name, re.I)]
if leaked_before:
violations.append("交付素材中包含改造前/原版截图(不应随包交付):%s"
% "、".join(p.name for p in leaked_before[:8]))
if not images:
violations.append("缺少产品界面截图(应放在 %s 或交付目录)" % args.assets_dir)
else:
evidence["screenshots"] = len(images)
if not videos:
violations.append("缺少全功能操作演示录屏(mp4/mov 等)")
else:
evidence["recordings"] = len(videos)
# ---------- A5. README ----------
readme_path = root / args.readme
readme_text = ""
if not readme_path.exists():
violations.append("缺少 README 文档:%s" % args.readme)
else:
readme_text = readme_path.read_text(encoding="utf-8", errors="ignore")
missing = []
low = readme_text.lower()
for item, kws in README_KEYWORDS.items():
if not any(k.lower() in low for k in kws):
missing.append(item)
if missing:
violations.append("README 缺少必备内容:%s" % "、".join(missing))
# 架构图线索:mermaid 或图片引用
if not re.search(r"```mermaid|!\[.*\]\(.*\)|架构图|拓扑图", readme_text):
warnings.append("README 未发现架构/拓扑图(mermaid 或图片)")
# 截图图文对应
if not re.search(r"!\[.*\]\(.*\)", readme_text):
warnings.append("README 无图片引用,合同要求功能逐条对应截图")
# ---------- A6. Docker / 书面说明 ----------
docker_files = [p for p in [root / "Dockerfile", root / "docker-compose.yml", root / "docker-compose.yaml"] if p.exists()]
if not docker_files:
if args.allow_no_docker:
note = root / args.delivery_dir / "no-docker说明.md"
if not note.exists():
violations.append("已声明不适配 Docker,但缺少书面说明(%s)" % note.relative_to(root))
else:
violations.append("缺少 Dockerfile / docker-compose(不适配 Docker 请加 --allow-no-docker 并提供书面说明)")
# ---------- A7. 依赖清单与部署注意事项 ----------
dep_docs = list((root / args.delivery_dir).glob("*依赖*")) + list((root / args.delivery_dir).glob("*注意事项*"))
dep_docs += [root / "依赖清单.md", root / "部署注意事项.md"]
dep_docs = [p for p in dep_docs if p.exists()]
if not dep_docs:
# 若 README 已含环境依赖/注意事项章节,降级为警告
if re.search(r"环境(要求|依赖|前置)|注意事项", readme_text):
warnings.append("未单独提供依赖清单/部署注意事项文档(README 中有相关章节,建议单独输出)")
else:
violations.append("缺少环境依赖清单与部署注意事项说明文档")
# ---------- A4. 网盘链接 + D. manifest ----------
manifest_path = root / args.delivery_dir / "manifest.md"
manifest_text = manifest_path.read_text(encoding="utf-8", errors="ignore") if manifest_path.exists() else ""
netdisk = args.netdisk_link or (re.search(r"https?://\S*(?:pan\.baidu|aliyundrive|alipan|123pan|cloud\.189)\S*", manifest_text) or [""])[0]
if not netdisk:
if not manifest_text:
violations.append("缺少网盘归档链接,且未提供交付清单 manifest.md")
else:
violations.append("未提供网盘素材归档链接(命令行 --netdisk-link 或写入 manifest)")
if not manifest_path.exists():
violations.append("缺少交付清单:%s" % manifest_path.relative_to(root))
else:
required_items = ["源码", "截图", "录屏", "网盘", "README", "Docker", "依赖"]
miss_items = [w for w in required_items if w not in manifest_text]
if miss_items:
violations.append("manifest.md 未覆盖交付物条目:%s" % "、".join(miss_items))
# ---------- C. 痕迹清理检索 ----------
kws = [k.strip() for k in args.trace_keywords.split(",") if k.strip()]
traces: list[str] = []
if kws:
pat = re.compile("|".join(re.escape(k) for k in kws), re.I)
for p in iter_files(root):
if p.suffix.lower() not in SCAN_SUFFIXES:
continue
rel = str(p.relative_to(root))
if re.search(r"license|copying", rel, re.I):
continue
try:
txt = p.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
if pat.search(txt):
traces.append(rel)
if traces:
violations.append("痕迹清理不彻底,以下文件命中原标识关键词:%s" % "、".join(sorted(set(traces))[:20]))
# ---------- 输出 ----------
result = {
"timestamp": datetime.now().isoformat(),
"project_dir": str(root),
"violations": violations,
"warnings": warnings,
"evidence": evidence,
"passed": not violations,
}
out_path = root / args.out
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
print("交付物核查:%d 个阻断项,%d 个警告" % (len(violations), len(warnings)))
for v in violations:
print(" [FAIL]", v)
for w in warnings:
print(" [WARN]", w)
print("report=%s" % out_path)
return 0 if not violations else 1
if __name__ == "__main__":
sys.exit(main())