#!/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: # before/after 需要按独立词识别,避免 beforeEach、::before、::after 等代码语法误报。 exact_keywords = [k for k in kws if k.lower() not in {"before", "after"}] exact_pat = ( re.compile("|".join(re.escape(k) for k in exact_keywords), re.I) if exact_keywords else None ) boundary_pats = { "before": re.compile(r"(?