洛克王国又上热搜了,但这次不是因为版本内容,而是客户端里被扒出了开发目录。热搜词叫“工程师偷懒,文件夹没删”,开发圈的人看到这种标题,多半会心一笑:这种事太熟了。
开发机上的工程目录直接被打进了发布包,测试脚本、内部文档、配置文件、日志一起发出去,用户下载几百 MB 的包体,里面却藏着开发阶段才能看到的东西。这类事故在游戏、App、前端、桌面端都反复出现过,几乎每个团队迟早都会踩一次。本文不讨论游戏包的拆包和逆向,只从工程侧谈一个更通用的问题:如何通过构建流程、目录规范和自动化检查,让“忘删文件夹”这种低级失误在发布之前就被拦住。
文中的脚本、规则和排查命令,可以直接复制到自己的项目里调整使用。你不需要是游戏团队,凡是做客户端、App、前端、桌面端打包的同学,都能从这里带走一套防残留的检查思路。
1. “文件夹没删”事件说明与技术解读
1.1 发生了什么
从传播信息看,这次讨论集中在“客户端里出现了不该出现的工程文件夹”。玩家和开发者的关注点不一样:玩家看到的是内部结构暴露,开发看到的是发布流程失守。
这类事故通常有几个共同表现:
- 发布包里混入
.git、.svn、.idea、.vscode等版本控制或编辑器目录; - 混入
test、tests、dev、debug、scripts等开发阶段目录; - 混入内部配置文件、密钥文件、日志文件、服务器地址;
- 混入未参与打包的中间产物、缓存目录、临时文件。
从工程角度看,这些内容会让包体膨胀,可能泄露内部路径和敏感配置,还会直接影响用户对产品质量的信任。更麻烦的是,一旦包体已经分发出去,撤回和更新都要走完整发版流程,成本非常高。
1.2 常见残留目录清单
下面这份清单可以当成项目自查的起点,按实际技术栈增删即可:
| 类型 | 目录 / 文件示例 | 风险等级 |
|---|---|---|
| 版本控制 | .git、.svn、.hg |
高,可能包含提交历史和内部地址 |
| 编辑器配置 | .idea、.vscode、.settings |
低,但属于明显残留 |
| 测试代码 | test、tests、__tests__ |
中,可能暴露测试账号和测试逻辑 |
| 构建产物 | dist、build、out、output |
中,重复打包会污染资源 |
| 临时文件 | tmp、temp、.cache、node_modules |
高,体积大且无意义 |
| 内部文档 | docs、internal、dev_docs |
中,可能包含未公开设计说明 |
| 配置文件 | .env、config.prod.yml、settings.local.json |
高,可能包含密钥和地址 |
| 日志文件 | logs、*.log、*.trace |
中,可能包含运行环境信息 |
| 脚本文件 | scripts、tools、deploy |
中,可能暴露部署方式和内网路径 |
这里不针对具体游戏做拆包分析,只想说明一个规律:凡是开发阶段需要的目录,如果发布流程没有做白名单过滤,就有可能在某个版本里被顺手打进去。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 这类事故的工程原因分析
2.1 手工打包是最大的风险源
很多中小团队还在用手工方式制作发布包:右键压缩、拖拽目录、从旧包基础上覆盖更新。这个流程完全依赖操作者当时的记忆和细心程度,一旦某天开发环境里多了一个新目录,或者压缩工具默认把隐藏目录也带上了,残留就发生了。
手工打包的另一个问题是不可复现。今天打包的人记得删掉 test 目录,明天换一个人可能就忘了。发布包的内容无法追溯到某个固定的构建命令,出了问题也没法快速复现和定位。
2.2 清理靠自觉,等于没有底线
有些团队会在打包前发一个“发布检查清单”,让执行者逐项确认已经删除了哪些目录。这种做法本身就有问题:人的注意力会疲劳,清单条目多了之后,确认动作会变成机械打勾,真正漏掉的反而是最显眼的东西。
更隐蔽的情况是,某个目录平时不存在,只在特定分支、特定功能开发期间出现。开发者在自己的环境里新增了一个 debug_tool/ 目录用于调试,做完功能后忘了删除,而打包的人根本不知道这个目录存在,也就谈不上主动清理。
2.3 目录权限和流程缺失
权限问题同样常见:同一台构建机上,所有人都有完全读写权限,开发目录和发布目录没有隔离。任何人在开发过程中都可能往资源目录里放文件,发布时又被整个目录打包带走。
流程缺失则体现在“没有校验产物”这一环。很多团队检查的是“功能是否正常”,很少有人检查“包里是否多了不该有的东西”。等到产物已经分发出去了,才发现里面混了一堆内部文件夹,这时候只能紧急发补丁。
3. 工程环境准备:目录规范与资产清单
3.1 目录结构约定
要根治这个问题,先要把“哪些目录能进包”定义清楚。一个推荐的约定是先建立发布根目录,只允许白名单目录进入最终产物。示例结构如下:
text复制stage/ # 打包暂存目录,发布会从这里取内容
├── assets/ # 游戏 / 应用资源
├── ui/ # 界面相关
├── audio/ # 音频
├── data/ # 数据文件
├── docs/ # 禁止进入产物
├── scripts/ # 禁止进入产物
├── tools/ # 禁止进入产物
├── .git/ # 禁止进入产物
└── build.sh # 构建脚本,不入包
这个约定的核心思想是:不靠“删除”来避免残留,靠“只要不在白名单里就不拷贝”来避免残留。开发目录和发布目录从一开始就物理隔离,比事后清理可靠得多。
3.2 用资产清单锁死发布内容
除了目录白名单,还可以维护一份资产清单(manifest),把版本、路径、校验值写清楚。发布时逐项校验,确保包里的内容完全可控。
json复制{
"name": "client_assets",
"version": "1.0.0",
"entries": [
{"path": "assets/ui/main_button.png", "sha256": "..."},
{"path": "assets/audio/bgm.mp3", "sha256": "..."},
{"path": "assets/data/config.json", "sha256": "..."}
]
}
这里的 sha256 字段可以理解为占位,实际使用时要由构建脚本计算并写入。资产清单的价值在于:如果某个文件被误删、被替换、被新增,校验阶段就能发现。
3.3 校验脚本示例
下面是一个通用的清单校验脚本,可以放到项目的 scripts/ 目录里,发布前执行。它接受两个参数:清单文件路径和待校验资源目录。
python复制import hashlib
import json
import pathlib
import sys
def sha256_of(path: pathlib.Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
return h.hexdigest()
def verify_manifest(manifest_path: str, root: str) -> int:
with open(manifest_path, "r", encoding="utf-8") as f:
manifest = json.load(f)
errors = []
for entry in manifest.get("entries", []):
rel = entry.get("path")
sha = entry.get("sha256")
file_path = pathlib.Path(root) / rel
if not file_path.exists():
errors.append(f"missing: {rel}")
continue
if sha256_of(file_path) != sha:
errors.append(f"hash mismatch: {rel}")
if errors:
print("\n".join(errors))
return 1
print("manifest verify ok")
return 0
if __name__ == "__main__":
sys.exit(verify_manifest(sys.argv[1], sys.argv[2]))
这个脚本不依赖额外第三方库,直接用 Python 标准库就能跑。实际项目里可以把生成的 hash 自动回填到 JSON 中,也可以把 sha256 换成构建 ID 或版本号,按需调整即可。
4. 构建流程治理:从源头拦截残留目录
4.1 用脚本打包,而不是手工拖拽
脚本化打包是防残留的第一道防线。脚本要做三件事:
- 清空旧产物目录;
- 只从白名单目录拷贝文件;
- 拷贝完成后自动执行敏感目录扫描。
只要是固定流程,就应该写成脚本。手工永远不会比脚本稳定,人也永远不可能记住所有细节。
4.2 黑名单 + 白名单过滤脚本示例
下面是一个 Python 打包脚本示例。它用“白名单目录 + 黑名单正则”双重过滤,保证只有 assets、ui、audio、data 目录下的文件能进入发布包,同时任何路径中命中了 .git、test、internal 等敏感关键字都会被排除。
python复制import os
import re
import shutil
SRC_ROOT = "./stage"
BUILD_OUT = "./dist/client"
BLACKLIST = re.compile(
r"(^|/)(\.git|\.svn|\.idea|\.vscode|node_modules|"
r"test|tests|dev|debug|docs?|scripts?|internal|"
r"private|server|config|log|logs)(/|$)",
re.IGNORECASE,
)
WHITELIST_DIRS = {"assets", "ui", "audio", "data"}
def should_include(rel_path: str) -> bool:
parts = rel_path.replace("\\", "/").split("/")
if parts and parts[0] not in WHITELIST_DIRS:
return False
if BLACKLIST.search(rel_path.replace("\\", "/")):
return False
return True
def copy_assets():
if os.path.exists(BUILD_OUT):
shutil.rmtree(BUILD_OUT)
os.makedirs(BUILD_OUT, exist_ok=True)
for root, dirs, files in os.walk(SRC_ROOT):
dirs[:] = [
d for d in dirs
if not BLACKLIST.search(os.path.join(root, d).replace(SRC_ROOT, "").lstrip("\\/"))
]
for name in files:
src_path = os.path.join(root, name)
rel_path = os.path.relpath(src_path, SRC_ROOT)
if not should_include(rel_path):
continue
dst_path = os.path.join(BUILD_OUT, rel_path)
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
shutil.copy2(src_path, dst_path)
print("pack:", rel_path)
if __name__ == "__main__":
copy_assets()
使用前把 SRC_ROOT 和 BUILD_OUT 改成实际路径。这个脚本的意义在于:即使开发目录里新增了一个 debug_tool/ 文件夹,只要它不在白名单里,就永远不会被拷贝到发布包中。
4.3 打包后的自检
打包完成后,不要立刻去上传分发。先做三件事:
- 列出压缩包内容,检查是否有可疑目录;
- 搜索常见敏感关键字,如内部域名、密钥前缀、内网 IP;
- 对比资产清单,确认文件数量没有异常增长。
这三步可以放在脚本末尾自动执行,也可以写成一个独立的 post-build-check.sh,在 CI 里调用。
5. 自动化门禁:让 CI 替你搜文件夹
5.1 构建产物扫描
最直接的门禁方式,是在 CI 里扫描构建完成后压缩包中所有文件名。一旦发现敏感目录,直接让任务失败。下面是一个基于 unzip -l 的扫描示例,适合 Linux / macOS 环境:
bash复制#!/usr/bin/env bash
# 构建产物敏感文件扫描示例
ARCHIVE="$1"
unzip -l "$ARCHIVE" | awk '{print $4}' | grep -E \
"(^|/)(\.git|\.svn|__pycache__|node_modules)(/|$)|\.pem$|\.key$|\.env$" \
&& { echo "检测到可疑文件,构建阻断"; exit 1; } || echo "扫描通过"
这段命令的核心是先列出压缩包内所有文件路径,再用正则过滤出敏感条目。如果命中,命令返回非零退出码,CI 任务随即失败。
5.2 通用 CI 门禁配置示例
下面是一个基于 GitHub Actions 的通用流程示例,只保留关键步骤:构建、扫描、清单校验。实际项目里可以把脚本路径和命令改成自己的。
yaml复制name: build-client
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: build
run: ./build.sh
- name: scan forbidden files
run: |
if unzip -l dist/client.zip | awk '{print $4}' | \
grep -E "(^|/)(\.git|\.svn|test|dev|internal)(/|$)" ; then
echo "release package contains forbidden directories"
exit 1
fi
- name: verify manifest
run: python scripts/verify_manifest.py dist/manifest.json dist/client
注意这里的 dist/client.zip 和 scripts/verify_manifest.py 都需要按实际项目调整。Jenkins、GitLab CI、TeamCity 的思路完全一致:先构建,再扫描,再校验,任何一步失败都不能继续发版。
5.3 CI 门禁的力度选择
第一次接入扫描时,阈值不要设得太高。建议分三步推进:
- 第一步只扫描高危目录:
.git、.svn、node_modules、__pycache__; - 第二步加入测试目录和文档目录:
test、tests、docs、internal; - 第三步加入配置文件扫描和密钥格式扫描。
一步步加规则,可以避免因为规则太严导致合法文件被频繁误杀,也能让团队逐步接受自动化门禁。
6. 敏感信息与版权合规边界
6.1 哪些内容绝对不能进发布包
和“目录残留”相比,敏感信息进包才是真正的高风险事故。下面这些内容一旦随发布包分发出去,影响面会迅速扩大:
- 数据库连接串、Redis 地址、内部 API 域名;
- 各种密钥、令牌、证书私钥;
- 未公开的策划文档、设计文档、内部会议记录;
- 未授权的图片、音频、视频素材;
- 第三方版权素材以及无法确认来源的字体、模型、关卡数据。
这类内容不适合写在“清理清单”里靠人删,而应该写进“禁止进包”的硬性规则,在构建脚本里做强制过滤。
6.2 发生泄漏后的应急流程
如果发现自己负责的项目出现了“文件夹没删”问题,不要只顾着复盘追责,先按顺序处理:
- 评估影响面:泄露的是普通目录还是敏感配置;
- 回收分发入口:暂停下载链接、下线问题版本;
- 重打发布包:修复后按完整流程重新构建;
- 通知相关方:如果涉及用户数据或合作方信息,需要按合规要求及时告知;
- 做完整复检:确认清点后的新产物无残留。
这里要特别注意,讨论自家项目的治理和整改没问题,但对第三方客户端进行拆包、逆向、提取资源的行为,超出了正常工程讨论范围,也不符合版权和平台规则。写作和实操时都应只围绕自己的工程流程展开。
6.3 合规提醒
凡是涉及素材、代码、配置、用户数据的项目,发布前都要确认授权边界。图片、字体、音频等资源要有版权记录;代码复用要符合开源协议;涉及用户数据时必须经过授权和脱敏。发布流程里加入“版权来源检查”和“敏感信息扫描”,本质上是一种保护,而不是额外负担。
7. 常见问题与排查清单
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
发布包出现 .git 文件夹 |
直接复制整个工程目录打包 | 用压缩工具列出包内容 | 改为白名单拷贝脚本 |
压缩包里有 test、dev 目录 |
过滤规则遗漏或手工打包 | 扫描产物文件名 | 补黑名单并在 CI 中阻断 |
| 配置文件被打包 | 资源目录与配置目录混放 | 搜索 .env、config.* |
目录严格分层,使用 manifest |
| 包体突然变大 | 混入了缓存或中间产物 | 按目录大小排序检查 | 清理输出目录并加自动扫描 |
| 密钥、内部域名泄漏 | 环境变量写死在代码里 | 扫描常见密钥格式和内网域名 | 密钥迁移到配置中心,禁止入库 |
| 打包脚本误删了合法文件 | 路径写错或黑名单过严 | 对比构建前后目录内容 | 输出构建日志并做差异审计 |
| CI 扫描太慢,影响发版节奏 | 规则过宽且未做预处理 | 查看扫描耗时和命中分布 | 先过滤扩展名和文件类型,再全文检索 |
| 清点完又重新出现残留 | 手工流程没有被脚本替代 | 检查发布记录 | 把检查写入固定 pipeline |
排查时不要只看最终压缩包,还要看构建日志。很多残留问题的线索在日志里非常明显:某个目录被重复拷贝、某个文件被覆盖、某个临时目录没有被清理。顺着日志走一遍,往往比盯着文件列表猜更快。
8. 工程化最佳实践
8.1 发布前固定检查动作
建议每个团队维护一份“发布门禁清单”,并且把它固化到脚本或 pipeline 里,而不是写成文档让人对照执行。至少包含以下项目:
- [ ] 从固定分支按脚本构建;
- [ ] 产物目录为全新空目录;
- [ ] 压缩包内无
.git、.svn、node_modules、__pycache__; - [ ] 压缩包内无
test、dev、internal、docs目录; - [ ] 不存在
.env、.pem、.key等敏感文件; - [ ] manifest 校验通过;
- [ ] 构建日志已留存,可复现。
这些检查项可能不够覆盖所有项目,但可以作为起步模板。
8.2 用权限划分代替自律
给不同岗位分配不同目录权限,比要求所有人“自觉”更现实:
- 策划和美术只写自己的资源目录;
- 客户端代码写入代码库目录;
- 发布目录只允许构建账号访问;
- 敏感配置单独放在配置管理服务中,不进入代码目录。
这样即使某个开发者在资源目录里放了一个临时文件夹,发布脚本的白名单也能拦住它。
8.3 定期做构建审计
每季度或每个大版本节点,做一次构建审计:
- 抽查一个发布包的完整目录清单;
- 对比 asset 数量和预期数量;
- 检查是否有异常新增文件;
- 检查敏感文件扫描日志是否被完全执行。
审计不需要很重,重点是让团队意识到“发布包是要检查的”,而不是默认它是干净的。
8.4 新成员上线前把流程讲清楚
新人最容易踩的坑,是复制一份完整工程目录,在开发机上随手新建文件夹用来放调试工具,然后打包时把这个文件夹一起带上。上线前培训只需要强调一句话:发布内容只从暂存目录取,开发目录里的任何东西都不会自动进包。这句话比讲一整节打包原理有效得多。
9. 总结:别把发布质量交给人的记忆力
“洛克王国工程师偷懒,文件夹没删”这个热搜标题看起来是在调侃某一个工程师,但落到工程层面,真正的问题不是某个人的记忆力,而是发布流程缺少兜底。
一个健康的发布流程,应该让“少删一个文件夹”变成不可能,而不是靠某个节点上的人足够细心。白名单目录、敏感目录扫描、资产清单校验、CI 门禁,这四层里面只要做成两层,绝大多数残留事故都会被拦住。
最先应该做的,是把手工打包改成脚本打包;第二件要做的事,是在 CI 里加一个目录扫描;第三件才是逐步上资产清单。这三件事都不需要花太多成本,但能实打实地把低级事故的触发概率压到接近零。
下次再看到类似的“工程师偷懒”热搜,开发者的正确反应不是跟着笑,而是回去看一眼自己的发布流程:如果今天我也漏了一个文件夹,脚本和 CI 会拦住吗?如果拦不住,那就去补一个。
