做完模板、讲义或网页素材后,客户说“少了一个文件”,你却记不清发的是哪一版;同名文件反复发送,后来谁改了什么也难追溯。这类问题不一定需要复杂系统,先准备清楚的交接说明、固定的文件清单和一套复核动作,就能减少不少沟通成本。
本文围绕一个可落地的小交付:用AI辅助制作数字资料的文件交接模板,再用本地Python脚本生成和检查文件清单。适合已有模板、培训资料或设计素材服务的人,把它作为现有交付的补充;本文不虚构订单和收益,也不承诺做一份清单就能卖出去。
最终交付包括README、一个资料目录、记录文件大小和SHA256的JSON清单,以及接收方能跟做的验收步骤。示例脚本由本站编写并实际做了篡改、缺失与新增文件测试,它只读取文件字节,不上传内容,也不运行交付资料。
一、先限定服务范围与客户需求
先问客户:要接收哪些文件、用什么软件打开、谁负责最终确认,以及需要保留多少个版本。如果交付的是设计源文件,是否包含字体和素材授权;如果是讲义,是否包含可编辑版本与阅读版,都应在制作前确认。
本篇解决的是“交付包里有哪些文件、传输后是否与确认版一致”。它不检查文字事实是否正确,不证明素材可以商用,也不代替网页功能测试。文件校验通过,只能说明相对于这份清单,所检查的文件路径和内容一致。
对于只需要一份普通文档的客户,没有必要强加Python步骤。可以先交付人能读懂的文件清单,再为需要多文件、重复更新或交接留痕的项目增加校验。需求比技术名词更值得放在介绍页前面。
二、准备一份小型演示资料
建立handoff-demo文件夹,里面放delivery目录。delivery内放README.txt、使用说明.txt,以及一个包含虚构数据的“资料”子目录。不要拿客户真实资料做公开演示,也不要把密码、密钥或内部账号信息写进README。
README至少讲清这些内容:
- 本次交付的目标和版本标识。
- 哪些文件可编辑,哪些只是预览或示例。
- 每个文件的用途、打开方式和必要依赖。
- 已检查项目、明确未包含的服务以及待客户确认项。
- 问题反馈需要提供什么,例如文件名、软件版本与具体报错。
目录名称与文件名建议简短稳定,不要一层层嵌套“最终版”“最终修改版”。版本可用v1、v2配合变更说明;它是本次交付标识,不应伪装成所引用第三方软件的正式版本号。
三、让AI起草说明,文件事实由你确认
如果不希望上传资料,可以只把人工整理的文件名与用途交给AI。让它完成组织和改写,不要求它猜测未查看的文件内容。
请把下面已确认的文件用途整理成一份交接README。
受众是不熟悉项目的新接收人。
只用我提供的事实,不补软件兼容性、授权范围或未做的测试。
结构为:交付目标、文件用途、使用步骤、已检查项目、未包含内容、反馈方式。
缺少的信息写“待确认”,不要自动补全。
请区分可编辑文件、预览文件和虚构示例数据。拿到草稿后逐项核对。AI写出“支持所有系统”或“素材均可商用”时,如果你没有依据,就删除或改为待确认。README不能替代授权证明,交接时应另保留来源记录和客户已经确认的范围。
AI服务费用、人工梳理时间、软件兼容性检查、返工轮次和售后说明,都是成本组成。报价应围绕文件数量、整理难度和验收范围说明,不应按“用了AI所以一定很便宜”或不存在的行业收益数据定价。
四、配置本地校验脚本
下面脚本需要Python 3.9或以上,只用标准库。先运行python --version确认环境;Windows如果使用py启动器,把命令前缀替换为实际可用方式。没有Python的接收方仍可以阅读README,不要求为了打开文档强制安装运行环境。
把下面完整代码保存为handoff-demo中的delivery_check.py。它与delivery目录同级,清单也保存到同级,不要把清单放进待扫描目录,让它把自己计入。
import argparse
import hashlib
import json
from pathlib import Path, PurePosixPath
def scan(root):
root = Path(root)
if root.is_symlink() or not root.is_dir():
raise ValueError("交付目录不存在或是符号链接")
files = {}
for path in sorted(root.rglob("*")):
if path.is_symlink():
raise ValueError("请先处理符号链接:" + str(path))
if not path.is_file():
continue
digest = hashlib.sha256()
size = 0
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
size += len(block)
files[path.relative_to(root).as_posix()] = {
"bytes": size, "sha256": digest.hexdigest()
}
return files
def load_manifest(path):
data = json.loads(Path(path).read_text(encoding="utf-8"))
if data.get("schema_version") != 1 or not isinstance(data.get("files"), dict):
raise ValueError("清单格式或版本不正确")
for name, info in data["files"].items():
parts = PurePosixPath(name).parts
if not name or "\\" in name or ":" in name or name.startswith("/"):
raise ValueError("清单路径不正确")
if ".." in parts or PurePosixPath(name).as_posix() != name:
raise ValueError("清单路径不规范")
if not isinstance(info, dict) or type(info.get("bytes")) is not int:
raise ValueError("文件大小格式不正确")
digest = info.get("sha256", "")
if info["bytes"] < 0 or not isinstance(digest, str) or len(digest) != 64:
raise ValueError("文件校验值格式不正确")
if any(c not in "0123456789abcdef" for c in digest):
raise ValueError("文件校验值格式不正确")
return data["files"]
def main():
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=["build", "verify"])
parser.add_argument("root")
parser.add_argument("manifest")
args = parser.parse_args()
root = Path(args.root).resolve()
manifest = Path(args.manifest).resolve()
if manifest.is_relative_to(root):
raise ValueError("清单请放在交付目录外,避免把自己计入")
actual = scan(args.root)
if args.action == "build":
if manifest.exists():
raise ValueError("清单已存在,请使用新的版本文件名")
data = {"schema_version": 1, "files": actual}
manifest.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print("已生成清单,文件数:", len(actual))
return 0
expected = load_manifest(manifest)
missing = sorted(expected.keys() - actual.keys())
extra = sorted(actual.keys() - expected.keys())
changed = sorted(name for name in expected.keys() & actual.keys() if expected[name] != actual[name])
print(json.dumps({"missing": missing, "extra": extra, "changed": changed}, ensure_ascii=False))
return 1 if missing or extra or changed else 0
if __name__ == "__main__":
raise SystemExit(main())文件扫描采用分块读取,记录的是相对路径、字节数和SHA256。脚本拒绝符号链接,也拒绝把清单写进delivery内部,避免练习中出现不明确的扫描范围。它没有处理所有特殊文件系统情形,正式服务应使用普通、稳定且不再编辑的交付目录。
五、生成清单并做一次正确验收
在handoff-demo目录执行:
python delivery_check.py build delivery manifest-v1.json
python delivery_check.py verify delivery manifest-v1.json第一次命令创建清单。如果同名清单已经存在,脚本会拒绝覆盖,提醒你使用新版本文件名。不要为了消除差异,直接重建同名清单抹掉旧的交接依据。
第二次命令会输出missing、extra、changed三个列表。全部为空且命令退出码为0,表示当前目录与这份清单一致。任何列表有内容,退出码为1;格式错误或路径不合要求也会产生错误,应先排查,不要把它称为通过。
清单应在README和其他文件完成后再生成。生成后又修改说明文字,即使只改一个标点,也会改变该文件的校验值,这属于正常差异,不是脚本误报。
六、故意制造三类问题,确认工具真的能发现
只在演示副本操作。先改动README中的一行,再运行verify,changed应出现README.txt。恢复原始内容后重新检查,差异应消失。
再移走一个文件,missing应出现原路径。把一个额外文件加入delivery,extra应列出新增路径。重命名会同时表现为旧路径缺失和新路径新增,因为本脚本没有试图推断“这两个文件其实是同一个”。
本站实际完成了13项检查,包含正常生成、原样通过、内容变化、缺失、新增、重命名、拒绝覆盖清单、拒绝目录内清单、四类不规范路径以及恢复后通过。这些结果验证的是示例程序,不是你的客户交付包已经验收完成。
不要在客户原始资料上做删除或改名演示。使用副本、保留原清单,才能把验证动作与真实交付分开。
七、把它整理成客户愿意使用的交接包
推荐同级放置delivery、manifest-v1.json、delivery_check.py和一份简短验收说明。压缩前先验证一次;接收方解压后在本地再验证。不要只比较ZIP文件名,因为同名压缩包也可能包含不同内容。
清单是普通JSON,不等于数字签名。如果有人同时修改文件和清单,校验也可能通过。需要更强来源保证时,应另行设计可信的交付渠道、签名与权限管理,不能把SHA256文件清单宣传成防篡改认证或版权证明。
建议由双方确认版本名、清单文件和交付时间,并在原有沟通渠道保留记录。清单里仍有文件名等信息,对敏感项目也应按客户要求保存和分享;“没有上传正文”不代表文件名就一定可以公开。
对经常增补的资料,把变更说明写成“新增什么、修改什么、移除什么、接收方需要做什么”。新版本使用新清单,与旧包并存留底;不要自动替客户删掉历史包,也不要把文件更名当作内容没有变化。
八、接单介绍、验收与售后怎么写
服务介绍可以强调具体交付:“整理一份项目文件目录和可读README,提供文件完整性清单与接收检查步骤。”先用虚构资料演示一次缺失文件能被定位,再让客户判断是否适合自己的交接流程。
验收至少包含三方面:约定文件齐全且能打开,README用途和依赖准确,清单验证结果与实际版本一致。前两项需要人工检查,不能因为脚本显示空列表就略过。
售后约定应写清修改轮次、新增资料是否重新计费、兼容性问题如何复现,以及接收方自行修改后差异如何处理。不提供未经授权的软件、字体或素材,不承诺替客户承担资料本身的正确性责任。
开始时选择一个已经存在的多文件交付需求,做完“需求确认—说明起草—人工核对—清单生成—解压复核”这条完整链路。能清楚说明范围和结果的交付,比一串AI功能名称更容易让接收方理解价值。
交付前再检查一次目录边界
脚本会扫描目录内的普通文件,包括你可能忘记清理的草稿、临时导出和隐藏文件。正式生成前先人工浏览目录,只放本次约定资料;不要把整个工作盘当作扫描对象。扫描期间也不要继续编辑文件,否则生成清单时与校验时可能读到不同内容。
如果接收方的软件自动生成了缓存文件,先确认它是否属于约定交付物,再用清理后的副本复查。不要看到extra就自动删除文件,也不要为了得到全绿结果,把有争议的项目从清单里静默移除。差异需要解释和确认,校验程序只是帮助双方找到差异的位置。

还没有公开评论,欢迎分享你的想法。