Files
opendance-web/scripts/package-release.sh
T
opendance ae8a68e98f fix(thumbnail): 缩略图采样 baseColorTexture,修复 MMD 模型显示为白模
现象(用户反馈):导入 PMX 转换出的 GLB 后,列表页/审核面板里的模型是白模,
脸部颜色信息全部丢失。

定位过程(本地装真 Assimp 6.0.4 + 该模型完整复现):
1. 转换本身正常:23.7MB GLB、643 骨骼、93,350 面;
2. 18 张贴图逐字节与压缩包原图一致;用一个权威 PMX 解析器比对后确认
   39 个材质 -> 贴图的对应关系与 PMX 完全一致(不是映射错);
3. UV 采样也正确:脸部材质取到的就是皮肤色 (246,221,216);
4. 真正的问题在**我们自己的缩略图渲染器**:它刻意不解码 baseColorTexture,
   只用 baseColorFactor 上色(见文件头取舍说明);而 MMD 模型的
   baseColorFactor 基本都是白色 -> 缩略图必然全白。管理端 3D 预览有贴图,
   只有列表/审核的缩略图是白的。

修复:
1. model-thumbnail.ts 内置极简 PNG 解码器(zlib inflate + 逐行反滤波,
   8 位非隔行,colorType 0/2/3/4/6),按 baseColorFactor x 贴图逐像素采样,
   透视校正 UV;解码失败/非 PNG/隔行扫描回退纯色,绝不抛错;
2. 按 alphaMode 处理透明度:OPAQUE 忽略贴图 alpha(MMD 语义)、
   MASK 按 alphaCutoff 丢弃、BLEND 做 source-over;
3. 取景收紧 FRAMING 1.6 -> 1.3,40px 列表缩略图主体更清楚;
4. 新增 test/model-thumbnail-texture.spec.ts 6 项(含白模回归:
   baseColorFactor 全白 + 红贴图必须输出偏红,以及 MASK/OPAQUE/坏图回退);
5. 版本 0.2.4,README(.md/.txt)/DSH.md 补修订说明与 FAQ(含重新生成缩略图
   的 reanalyze-models.mjs --force 命令)。
2026-09-16 04:02:35 +08:00

181 lines
7.8 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# ============================================================
# 打包发布产物:opendance-web-v0.2.4.tar.gz
#
# 产物结构(Linux 业务服务部署,不使用 Docker、不部署反向代理):
# web-server/ NestJS 源码 + 构建产物
# admin/ React 管理端构建产物
# deploy/ install.sh / ctl.sh / uninstall.sh / .env.example
# env-lib.sh (.env 语法预检) / assimp-lib.sh (Assimp 定位安装)
# init-env.sh (.env 引导)
# systemd|openrc|initd / proxy(反代说明文档,仅文档不部署)
# contracts/ 当前版本契约快照
# THIRD_PARTY.md 开源项目声明
# README.md 使用说明(Markdown, 仓库首页展示的就是它)
# README.txt 使用说明(纯文本同内容版本)
#
# 用法:./scripts/package-release.sh [版本号]
# ============================================================
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
VERSION="${1:-0.2.4}"
NAME="opendance-web-v${VERSION}"
OUT_DIR="$REPO_ROOT/dist-release"
STAGE="$OUT_DIR/$NAME"
log() { printf '\033[1;34m[package]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
# ---------- 1. 契约一致性 ----------
log "校验契约版本一致性..."
( cd web-server && node scripts/check-contracts.mjs )
# ---------- 2. 后端构建 ----------
log "构建后端..."
( cd web-server && npm run build )
# ---------- 3. 管理端构建 ----------
if [ -d web-server/admin/node_modules ]; then
log "构建管理端..."
( cd web-server/admin && npm run build )
else
log "警告:web-server/admin/node_modules 不存在,跳过管理端构建(产物将缺少 admin/)"
fi
# ---------- 4. 组织产物目录 ----------
log "整理产物目录 $STAGE ..."
rm -rf "$STAGE"
mkdir -p "$STAGE"
# 后端源码 + 构建产物(排除依赖、测试、存储、密钥)
# 排除模式**必须锚定到 web-server 根目录**(前导 /)。
# 否则 rsync 的 'storage' 会匹配任意层级的同名目录,把源码目录
# src/storage/ 与编译产物 dist/storage/ 一并删掉——发布包会缺少整个
# storage 模块,服务启动即 MODULE_NOT_FOUND。这是本项目第二次踩到
# 「storage 误伤源码目录」这个坑(第一次是 .gitignore)。
rsync -a --delete \
--exclude 'node_modules' \
--exclude '/storage/' \
--exclude '/data/' \
--exclude '/tmp/' \
--exclude '/.env' \
--exclude '/.env.local' \
--exclude '/admin/' \
web-server/ "$STAGE/web-server/"
# 管理端构建产物(文档要求 admin/ 与 web-server/ 平级)
if [ -d web-server/admin/dist ]; then
mkdir -p "$STAGE/admin"
cp -r web-server/admin/dist/. "$STAGE/admin/"
else
log "警告:未找到 web-server/admin/dist,产物不含 admin/"
fi
cp -r deploy "$STAGE/deploy"
# 部署配置里绝不能带真实 .env(含数据库口令)
rm -f "$STAGE/deploy/.env"
rm -rf "$STAGE/deploy"/systemd/*.bak "$STAGE/deploy"/openrc/*.bak "$STAGE/deploy"/initd/*.bak
cp -r contracts "$STAGE/contracts"
cp THIRD_PARTY.md README.txt README.md project.yaml DSH.md "$STAGE/"
cp -r tasks "$STAGE/tasks"
# ---------- 5. 打包 ----------
log "生成压缩包..."
( cd "$OUT_DIR" && tar -czf "$NAME.tar.gz" "$NAME" )
# ---------- 6. 产物完整性校验 ----------
# 打包脚本本身的错误(如排除模式写错)会静默产出残缺的发布包,
# 直到目标机启动时才暴露。这里做一次硬校验:src 下每个 .ts 都必须在
# 包内有对应的 dist/*.js,且关键文件齐全。
log "校验产物完整性…"
MANIFEST_TMP=$(tar -tzf "$OUT_DIR/$NAME.tar.gz")
fail=0
# 6.1 src → dist 一一对应(能抓到整目录被误排除这类问题)
# 注意 nest build 的 sourceRoot 是 src,产物会**去掉 src/ 前缀**:
# web-server/src/storage/storage.service.ts → web-server/dist/storage/storage.service.js
while IFS= read -r tsfile; do
rel="${tsfile#src/}"
jsfile="opendance-web-v${VERSION}/web-server/dist/${rel%.ts}.js"
if ! grep -qx "$jsfile" <<< "$MANIFEST_TMP"; then
echo " ✗ 缺少编译产物:$jsfile(源:$tsfile)" >&2
fail=1
fi
done < <(cd web-server && find src -name '*.ts' ! -name '*.spec.ts')
[ "$fail" -eq 0 ] && echo " ✓ src 下所有 .ts 均有对应 dist/*.js"
# 6.2 关键文件
for required in \
"opendance-web-v${VERSION}/web-server/dist/main.js" \
"opendance-web-v${VERSION}/web-server/src/storage/storage.service.ts" \
"opendance-web-v${VERSION}/admin/index.html" \
"opendance-web-v${VERSION}/deploy/install.sh" \
"opendance-web-v${VERSION}/deploy/ctl.sh" \
"opendance-web-v${VERSION}/deploy/check-access.sh" \
"opendance-web-v${VERSION}/web-server/dist/preprocess/vmd-parser.js" \
"opendance-web-v${VERSION}/web-server/dist/preprocess/mmd-pose.js" \
"opendance-web-v${VERSION}/web-server/dist/preprocess/mmd-skeleton.js" \
"opendance-web-v${VERSION}/web-server/scripts/inspect-vmd.mjs" \
"opendance-web-v${VERSION}/web-server/scripts/import-default-model.mjs" \
"opendance-web-v${VERSION}/web-server/scripts/reanalyze-models.mjs" \
"opendance-web-v${VERSION}/web-server/dist/common/zip-reader.js" \
"opendance-web-v${VERSION}/web-server/dist/models/model-convert.js" \
"opendance-web-v${VERSION}/web-server/dist/models/glb-textures.js" \
"opendance-web-v${VERSION}/web-server/dist/migrations/1789450000000-AddModelConversionFields.js" \
"opendance-web-v${VERSION}/web-server/dist/migrations/1789460000000-AddModelStats.js" \
"opendance-web-v${VERSION}/web-server/dist/models/model-inspector.js" \
"opendance-web-v${VERSION}/web-server/dist/models/model-thumbnail.js" \
"opendance-web-v${VERSION}/web-server/dist/models/vrm-writer.js" \
"opendance-web-v${VERSION}/web-server/dist/models/gltf-packer.js" \
"opendance-web-v${VERSION}/deploy/init-env.sh" \
"opendance-web-v${VERSION}/deploy/env-lib.sh" \
"opendance-web-v${VERSION}/deploy/assimp-lib.sh" \
"opendance-web-v${VERSION}/deploy/proxy/README.md" \
"opendance-web-v${VERSION}/deploy/proxy/nginx.conf.example" \
"opendance-web-v${VERSION}/deploy/proxy/web.config.example" \
"opendance-web-v${VERSION}/deploy/systemd/opendance-api.service" \
"opendance-web-v${VERSION}/contracts/VERSION" \
"opendance-web-v${VERSION}/THIRD_PARTY.md" \
"opendance-web-v${VERSION}/README.txt" \
"opendance-web-v${VERSION}/README.md" ; do
if ! grep -qx "$required" <<< "$MANIFEST_TMP"; then
echo " ✗ 缺少关键文件:$required" >&2
fail=1
fi
done
# 6.3 不得包含的东西(模式必须精确,否则会误伤 src/storage、.env.example)
PREFIX="opendance-web-v${VERSION}"
check_forbidden() {
local label="$1" pattern="$2"
if grep -qE "$pattern" <<< "$MANIFEST_TMP"; then
echo " ✗ 产物中混入了不应包含的内容($label)" >&2
grep -E "$pattern" <<< "$MANIFEST_TMP" | head -3 | sed 's/^/ /' >&2
fail=1
fi
}
check_forbidden "node_modules" "node_modules"
check_forbidden "真实 .env" "^${PREFIX}/(deploy|web-server)/\.env$"
check_forbidden "运行时存储目录" "^${PREFIX}/web-server/storage/"
check_forbidden "keystore" "\.jks$|keystore\.properties$"
[ "$fail" -eq 0 ] && echo " ✓ 无 node_modules / 真实 .env / 运行时存储 / keystore"
if [ "$fail" -ne 0 ]; then
rm -f "$OUT_DIR/$NAME.tar.gz"
die "产物完整性校验失败,已删除有问题的压缩包"
fi
log "完成:$OUT_DIR/$NAME.tar.gz"
echo
echo "产物内容:"
# 先把清单读入变量,避免 tar 因管道提前关闭(SIGPIPE)而误报 "write error"
MANIFEST=$(tar -tzf "$OUT_DIR/$NAME.tar.gz")
echo "$MANIFEST" | sed -n '1,20p'
echo " ...(共 $(echo "$MANIFEST" | wc -l) 项,$(du -h "$OUT_DIR/$NAME.tar.gz" | cut -f1))"
echo
echo "部署方式:解压后进入 deploy/,复制 .env.example 为 .env 并填写数据库信息,执行 ./install.sh"