Files
opendance-web/scripts/package-release.sh
T
zijunchiang 3738bd91b9 release(v0.7.1): 契约 websocket-keypoints 0.2.0 + 补齐场景规格与任务文档
契约变更(T-WEB-016,与 Android T-AND-011「两端一起改」配套)
- websocket-keypoints 0.1.0 -> 0.2.0,均为向后兼容(子协议名与二进制帧
  version 均不变,只新增可选控制字段与一种新控制类型):
  1.1 保活:接收端必须声明 silentTimeoutMs 与 heartbeatIntervalMs,且心跳必须
      小于静默超时的一半。根因是客户端只能用 OkHttp 的协议层 ping,而它由
      底层库自动应答、**不暴露给应用层**,接收端根本看不到,无法续命;
  3.1 hello.deviceId(该安装实例的稳定标识,不使用任何硬件序列号);
  3.2/3.3 welcome/config 携带保活字段;
  3.6 应用层 ping/pong(回显 sentAtMs,pong 另带 serverAtMs 供算 RTT);
  5   设备去重:同一 deviceId 只保留最新连接,旧连接以 1000 关闭
      (刻意用 1000:对端据 §7 不会重连,否则形成"挤掉-重连-再挤掉"循环);
  7   关闭码与重连策略:1000/1001/1002 与主动停止不重连,其余重连;
  7.1 退避参数冻结:min(500ms*2^(n-1), 15000ms) + ±20% 抖动;
  8   连接状态机(两端同名,日志可直接对照)。
- contracts/VERSION 与 src/config/contract-versions.ts 同步到 0.2.0。

补齐 rest-api.yaml 漏写的场景结构(只补规格,不改线格式,版本号不变)
- CatalogBundle 增加 scenes[](标注 0.5.0 起下发);
- 新增 Scene / SceneConfig / Vec3 schemas。catalog/export 自 v0.7.0 起就返回
  scenes[],data-models 7 也已冻结,但规格漏写会让两端对字段边界
  (如 0~1、±200)的理解不一致。

deploy/proxy/README.md 新增第九节:Gitea 站点的附件上传上限
- 与一~五节的 Web 服务反代是**两个不同站点**;实测 30.5 MB / 40 MB 上传返回
  HTTP 413,根因是 IIS 默认 maxAllowedContentLength = 30,000,000 字节;
- 给出放宽到 256 MB 的 IIS / ARR / Gitea app.ini / nginx 四处配置与复验命令;
- 这解释了 Windows 端 v0.1.0 为何被拆成 zip.part0/1/2(交付缺陷)。

文档与脚本
- 补齐 tasks/011~016 与 project.yaml 条目(T-WEB-011~015 此前已在代码中落地
  却缺任务说明书);project.yaml 的 contracts_version 由单个 0.4.0 改为六项字典;
- scripts/package-release.sh 的版本号改为从 web-server/package.json 读取
  (与 GET /health 的 version、管理端左下角展示同源),并与 project.yaml 交叉校验;
  同时补上 SHA256SUMS 与自校验(此前是手工生成的)。

验证:npm run verify 通过(contracts:check 六项一致 + 35 条路由一致、
typecheck、246 项测试、nest build);产物 opendance-web-v0.7.1.tar.gz
sha256 27f92cfb125bbcf29077d4ad90c44720cbca46d23ddef12cd7826d73bd8865e2。
2026-09-17 01:30:41 +08:00

202 lines
8.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.7.0.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"
# 版本号唯一来源:web-server/package.json 的 version(与 GET /health 的 version 同源,
# 也是管理端左下角展示的那个值)。不传参数时自动读取,并与 project.yaml 交叉校验。
PKG_VERSION="$(node -p "require('./web-server/package.json').version" 2>/dev/null || true)"
YAML_VERSION="$(sed -n 's/^version:[[:space:]]*//p' project.yaml | head -1)"
VERSION="${1:-$PKG_VERSION}"
VERSION="${VERSION:-0.7.1}"
if [ -n "$PKG_VERSION" ] && [ "$VERSION" != "$PKG_VERSION" ]; then
echo "[error] 版本号不一致:参数=$VERSION,web-server/package.json=$PKG_VERSION" >&2
exit 1
fi
if [ -n "$YAML_VERSION" ] && [ "$VERSION" != "$YAML_VERSION" ]; then
echo "[error] 版本号不一致:参数=$VERSION,project.yaml=$YAML_VERSION" >&2
exit 1
fi
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
# ---- SHA256SUMS(裸文件名,下载目录内 sha256sum -c 直接可用) ----
( cd "$OUT_DIR" && sha256sum "$NAME.tar.gz" > SHA256SUMS )
log "完成:$OUT_DIR/$NAME.tar.gz"
log "已生成 $OUT_DIR/SHA256SUMS"
if ( cd "$OUT_DIR" && sha256sum -c SHA256SUMS --status ); then
log "sha256 自校验:OK"
else
die "sha256 自校验失败"
fi
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"