Files
john 16b1a90edf fix(release): gate memind-lib DeepSeek catalog on deepseek-v4 models
Refuse memindadm production releases when bundled llm-providers.mjs still
carries the legacy deepseek-chat catalog instead of deepseek-v4-pro/flash.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 10:13:34 +08:00

380 lines
12 KiB
Bash
Executable File
Raw Permalink 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.
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
HOST="${STUDIO_HOST:-10.10.0.2}"
REMOTE_ROOT="${STUDIO_REMOTE_ROOT:-/Users/john/Project}"
APP_NAME="${STUDIO_APP_NAME:-memind_adm}"
APP_DIR="${REMOTE_ROOT}/${APP_NAME}"
INCOMING_DIR="${REMOTE_ROOT}/incoming/memind_adm"
RELEASES_DIR="${REMOTE_ROOT}/releases"
BACKUP_DIR="${REMOTE_ROOT}/backups/memind_adm"
ARCHIVE_DIR="${REMOTE_ROOT}/archives"
IGNORE_FILE="${ROOT}/scripts/.releaseignore-prod"
WEB_HEALTH_URL="${STUDIO_WEB_HEALTH_URL:-http://127.0.0.1:5174/ops/}"
API_HEALTH_URL="${STUDIO_API_HEALTH_URL:-http://127.0.0.1:8085/health}"
RELEASE_TS="$(date +%Y%m%d-%H%M%S)"
SHORT_SHA="$(git -C "${ROOT}" rev-parse --short HEAD 2>/dev/null || echo no-git)"
RELEASE_ID="${RELEASE_TS}-${SHORT_SHA}"
TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/memindadm-release.XXXXXX")"
BUNDLE_PATH="${TMP_DIR}/memind-adm-${RELEASE_ID}.tar.gz"
MANIFEST_PATH="${TMP_DIR}/memind-adm-${RELEASE_ID}.manifest.txt"
SHA_PATH="${TMP_DIR}/memind-adm-${RELEASE_ID}.sha256"
MEMIND_SRC="${TEST_MEMIND_ROOT:-${ROOT}/../Memind}"
MEMIND_LIB="${ROOT}/memind-lib"
DRY_RUN=0
SKIP_BUILD=0
AUTO_YES=0
cleanup() {
rm -rf "${TMP_DIR}" "${MEMIND_LIB}"
}
trap cleanup EXIT
usage() {
cat <<'EOF'
用法:
bash scripts/release-prod.sh [--dry-run] [--skip-build] [--yes]
说明:
1. 本地构建 dist 并打包运行产物(不含 src/ 等开发源码)
2. 上传到 103
3. 103 全量备份当前 memind_adm
4. 解包、安装依赖(不在 103 构建前端)
5. 原子切换 live 目录并重启验证
6. 旧 live 目录移入 archives,103 不再保留可编辑源码树
禁止:
- 直接 rsync 到 103
- 在 103 生产目录手改源码后继续运行
- 在 103 保留 src/、vite.config.ts 等开发源码
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--skip-build) SKIP_BUILD=1 ;;
--yes|-y) AUTO_YES=1 ;;
-h|--help)
usage
exit 0
;;
*)
echo "未知参数: $1" >&2
usage >&2
exit 1
;;
esac
shift
done
say() {
printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*"
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "缺少命令: $1" >&2
exit 1
}
}
need_cmd ssh
need_cmd scp
need_cmd rsync
need_cmd tar
need_cmd shasum
need_cmd node
if [[ -x "${ROOT}/scripts/check-release-ready.sh" ]]; then
ALLOW_MAIN_RELEASE=1 bash "${ROOT}/scripts/check-release-ready.sh"
else
echo "缺少发布前闸门: ${ROOT}/scripts/check-release-ready.sh" >&2
exit 1
fi
ROOT_GIT_BRANCH="$(git -C "${ROOT}" branch --show-current)"
ROOT_GIT_HEAD="$(git -C "${ROOT}" rev-parse HEAD)"
ROOT_ORIGIN_HEAD="$(git -C "${ROOT}" rev-parse origin/main)"
[[ "${ROOT_GIT_BRANCH}" == "main" ]] || {
echo "memindadm 生产发布必须从 main 执行,当前分支: ${ROOT_GIT_BRANCH:-detached}" >&2
exit 1
}
[[ "${ROOT_GIT_HEAD}" == "${ROOT_ORIGIN_HEAD}" ]] || {
echo "memindadm main 必须与 origin/main 完全一致" >&2
exit 1
}
[[ -f "${IGNORE_FILE}" ]] || {
echo "缺少忽略文件: ${IGNORE_FILE}" >&2
exit 1
}
say "本地预检查"
ssh -o BatchMode=yes -o ConnectTimeout=15 "${HOST}" "echo release-ssh-ok" >/dev/null
ssh -o BatchMode=yes "${HOST}" "test -d '${APP_DIR}' && test -f '${APP_DIR}/package.json'" >/dev/null
if [[ "${SKIP_BUILD}" -ne 1 ]]; then
say "本地构建 dist103 不再执行前端构建)"
(
cd "${ROOT}"
VITE_BASE_PATH="${VITE_BASE_PATH:-/ops}" npm run build
)
fi
say "验证共享模块图像运行依赖"
(
cd "${ROOT}"
node --input-type=module -e "const { default: sharp } = await import('sharp'); await sharp({ create: { width: 1, height: 1, channels: 4, background: '#000000' } }).png().toBuffer();"
)
[[ -d "${ROOT}/dist" ]] || {
echo "缺少 dist/,请先本地构建或去掉 --skip-build" >&2
exit 1
}
[[ -d "${MEMIND_SRC}" ]] || {
echo "缺少 Memind 共享模块源: ${MEMIND_SRC}" >&2
exit 1
}
git -C "${MEMIND_SRC}" fetch origin main --quiet
MEMIND_GIT_HEAD="$(git -C "${MEMIND_SRC}" rev-parse HEAD)"
MEMIND_ORIGIN_HEAD="$(git -C "${MEMIND_SRC}" rev-parse origin/main)"
MEMIND_GIT_BRANCH="$(git -C "${MEMIND_SRC}" branch --show-current)"
[[ "${MEMIND_GIT_BRANCH}" == "main" ]] || {
echo "Memind 共享模块必须从 main 打包,当前分支: ${MEMIND_GIT_BRANCH:-detached}" >&2
exit 1
}
[[ "${MEMIND_GIT_HEAD}" == "${MEMIND_ORIGIN_HEAD}" ]] || {
echo "Memind main 必须与 origin/main 完全一致" >&2
exit 1
}
[[ -z "$(git -C "${MEMIND_SRC}" status --porcelain=v1 --untracked-files=all)" ]] || {
echo "Memind 共享模块工作区必须干净" >&2
git -C "${MEMIND_SRC}" status --short --untracked-files=all >&2
exit 1
}
say "打包 Memind 共享模块到 memind-lib"
rm -rf "${MEMIND_LIB}"
mkdir -p "${MEMIND_LIB}"
rsync -a \
--prune-empty-dirs \
--exclude '.git/' \
--exclude 'node_modules/' \
--include '*/' \
--include '*.mjs' \
--include '*.json' \
--include '*.js' \
--exclude '*' \
"${MEMIND_SRC}/" "${MEMIND_LIB}/"
if [[ -d "${MEMIND_SRC}/skills" ]]; then
say "打包 Memind 平台技能目录到 memind-lib"
mkdir -p "${MEMIND_LIB}/skills"
rsync -a \
--exclude '.DS_Store' \
--exclude '__pycache__/' \
"${MEMIND_SRC}/skills/" "${MEMIND_LIB}/skills/"
fi
[[ -f "${MEMIND_LIB}/user-auth.mjs" ]] || {
echo "memind-lib 缺少 user-auth.mjs" >&2
exit 1
}
[[ -f "${MEMIND_LIB}/skills/excel-analyst/SKILL.md" ]] || {
echo "memind-lib 缺少 Excel Analyst 技能" >&2
exit 1
}
grep -q 'EXCEL_ANALYST_SKILL_NAME' "${MEMIND_LIB}/skills-registry.mjs" || {
echo "memind-lib 缺少 Excel Analyst 技能授权映射" >&2
exit 1
}
grep -q 'tkmind-excel' "${MEMIND_LIB}/capabilities.mjs" || {
echo "memind-lib 缺少 Excel Analyst runtime 扩展映射" >&2
exit 1
}
[[ -f "${MEMIND_LIB}/llm-providers.mjs" ]] || {
echo "memind-lib 缺少 llm-providers.mjs" >&2
exit 1
}
grep -q "'deepseek-v4-pro'" "${MEMIND_LIB}/llm-providers.mjs" || {
echo "memind-lib/llm-providers.mjs 缺少 deepseek-v4-pro 模型目录,拒绝发布旧 DeepSeek catalog" >&2
exit 1
}
grep -q "'deepseek-v4-flash'" "${MEMIND_LIB}/llm-providers.mjs" || {
echo "memind-lib/llm-providers.mjs 缺少 deepseek-v4-flash 模型目录" >&2
exit 1
}
if grep -q "'deepseek-chat'" "${MEMIND_LIB}/llm-providers.mjs" && ! grep -q "LEGACY_DEEPSEEK_CHAT" "${MEMIND_LIB}/llm-providers.mjs"; then
echo "memind-lib/llm-providers.mjs 仍含 legacy deepseek-chat 且无 LEGACY_DEEPSEEK_CHAT 标记,目录可能未升级" >&2
exit 1
fi
if [[ "${AUTO_YES}" -ne 1 && "${DRY_RUN}" -ne 1 ]]; then
say "发布确认"
echo "目标主机: ${HOST}"
echo "目标目录: ${APP_DIR}"
echo "发布编号: ${RELEASE_ID}"
echo "本地 HEAD: $(git -C "${ROOT}" rev-parse HEAD 2>/dev/null || echo unknown)"
echo "工作区状态:"
git -C "${ROOT}" status --short --branch | sed -n '1,120p'
read -r -p "确认继续发布到 103? [y/N] " confirm </dev/tty
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 0
fi
say "生成发布清单"
{
echo "release_id=${RELEASE_ID}"
echo "created_at=$(date '+%Y-%m-%d %H:%M:%S %z')"
echo "host=$(hostname)"
echo "root=${ROOT}"
echo "git_head=$(git -C "${ROOT}" rev-parse HEAD 2>/dev/null || echo unknown)"
echo "git_branch=$(git -C "${ROOT}" branch --show-current 2>/dev/null || echo detached)"
echo "memind_git_head=${MEMIND_GIT_HEAD}"
echo "memind_git_branch=${MEMIND_GIT_BRANCH}"
echo "git_status_begin"
git -C "${ROOT}" status --short --branch || true
echo "git_status_end"
} > "${MANIFEST_PATH}"
say "打包当前工作区"
tar \
--exclude-from="${IGNORE_FILE}" \
-czf "${BUNDLE_PATH}" \
-C "${ROOT}" \
.
checksum="$(shasum -a 256 "${BUNDLE_PATH}" | awk '{print $1}')"
printf '%s %s\n' "${checksum}" "$(basename "${BUNDLE_PATH}")" > "${SHA_PATH}"
if [[ "${DRY_RUN}" -eq 1 ]]; then
say "dry-run 完成"
ls -lh "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}"
exit 0
fi
say "上传发布产物到 103"
ssh -o BatchMode=yes "${HOST}" "mkdir -p '${INCOMING_DIR}' '${RELEASES_DIR}' '${BACKUP_DIR}' '${ARCHIVE_DIR}'"
scp -q "${BUNDLE_PATH}" "${MANIFEST_PATH}" "${SHA_PATH}" "${HOST}:${INCOMING_DIR}/"
say "在 103 备份并发布"
ssh -o BatchMode=yes "${HOST}" \
"RELEASE_ID='${RELEASE_ID}' APP_DIR='${APP_DIR}' INCOMING_DIR='${INCOMING_DIR}' RELEASES_DIR='${RELEASES_DIR}' BACKUP_DIR='${BACKUP_DIR}' ARCHIVE_DIR='${ARCHIVE_DIR}' WEB_HEALTH_URL='${WEB_HEALTH_URL}' API_HEALTH_URL='${API_HEALTH_URL}' /bin/bash" <<'REMOTE_SCRIPT'
set -euo pipefail
RELEASE_DIR="${RELEASES_DIR}/memind-adm-${RELEASE_ID}"
OLD_LIVE_DIR="${ARCHIVE_DIR}/memind_adm-source-before-${RELEASE_ID}"
BUNDLE="${INCOMING_DIR}/memind-adm-${RELEASE_ID}.tar.gz"
MANIFEST="${INCOMING_DIR}/memind-adm-${RELEASE_ID}.manifest.txt"
SHA_FILE="${INCOMING_DIR}/memind-adm-${RELEASE_ID}.sha256"
BACKUP_TAR="${BACKUP_DIR}/memind-adm-${RELEASE_ID}-before.tar.gz"
PERSISTED_ITEMS=(
".env"
"adm-preview.log"
"adm-api.log"
)
say() {
printf '\n[remote %s] %s\n' "$(date +%H:%M:%S)" "$*"
}
rollback() {
if [[ -d "${OLD_LIVE_DIR}" ]]; then
rm -rf "${APP_DIR}"
mv "${OLD_LIVE_DIR}" "${APP_DIR}"
fi
ADM_ROOT="${APP_DIR}" ADM_PORT=5174 ADM_API_PORT=8085 bash "${APP_DIR}/scripts/remote_restart.sh" >/dev/null 2>&1 || true
}
trap 'rollback' ERR
[[ -f "${BUNDLE}" ]] || { echo "missing bundle: ${BUNDLE}" >&2; exit 1; }
[[ -f "${MANIFEST}" ]] || { echo "missing manifest: ${MANIFEST}" >&2; exit 1; }
[[ -f "${SHA_FILE}" ]] || { echo "missing sha file: ${SHA_FILE}" >&2; exit 1; }
say "校验产物完整性"
cd "${INCOMING_DIR}"
shasum -a 256 -c "$(basename "${SHA_FILE}")"
say "全量备份当前 live 目录"
COPYFILE_DISABLE=1 tar -czf "${BACKUP_TAR}" -C "$(dirname "${APP_DIR}")" "$(basename "${APP_DIR}")"
say "准备 release 目录"
rm -rf "${RELEASE_DIR}"
mkdir -p "${RELEASE_DIR}"
tar -xzf "${BUNDLE}" -C "${RELEASE_DIR}"
cp "${MANIFEST}" "${RELEASE_DIR}/.release-manifest.txt"
for item in "${PERSISTED_ITEMS[@]}"; do
if [[ -e "${APP_DIR}/${item}" ]]; then
rm -rf "${RELEASE_DIR:?}/${item}"
mkdir -p "$(dirname "${RELEASE_DIR}/${item}")"
cp -a "${APP_DIR}/${item}" "$(dirname "${RELEASE_DIR}/${item}")/"
fi
done
[[ -d "${RELEASE_DIR}/dist" ]] || { echo "release bundle missing dist/" >&2; exit 1; }
[[ ! -d "${RELEASE_DIR}/src" ]] || { echo "release bundle must not contain src/" >&2; exit 1; }
[[ -f "${RELEASE_DIR}/memind-lib/user-auth.mjs" ]] || { echo "release bundle missing memind-lib/user-auth.mjs" >&2; exit 1; }
say "安装运行依赖"
cd "${RELEASE_DIR}"
export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/opt/homebrew/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin:${PATH}"
if command -v npm >/dev/null 2>&1; then
NPM_BIN="$(command -v npm)"
elif [[ -x /opt/homebrew/opt/node@24/bin/npm ]]; then
NPM_BIN="/opt/homebrew/opt/node@24/bin/npm"
else
echo "npm not found on remote" >&2
exit 1
fi
if [[ -f package-lock.json ]]; then
"${NPM_BIN}" ci --no-audit --no-fund
else
"${NPM_BIN}" install --no-audit --no-fund
fi
cat > "${RELEASE_DIR}/PRODUCTION_ARTIFACT_ONLY.md" <<'ARTIFACT'
This directory is a deployed production artifact on 103.
Do not edit source files here.
Source of truth: local repos under /Users/john/PycharmProjects/test/.
Last artifact release manifest: .release-manifest.txt
ARTIFACT
say "切换 live 目录"
rm -rf "${OLD_LIVE_DIR}"
mv "${APP_DIR}" "${OLD_LIVE_DIR}"
mv "${RELEASE_DIR}" "${APP_DIR}"
say "重启服务"
ADM_ROOT="${APP_DIR}" ADM_PORT=5174 ADM_API_PORT=8085 bash "${APP_DIR}/scripts/remote_restart.sh"
say "健康检查"
api_code="$(curl -s -o /dev/null -w '%{http_code}' "${API_HEALTH_URL}" || true)"
web_code="$(curl -s -o /dev/null -w '%{http_code}' "${WEB_HEALTH_URL}" || true)"
if [[ "${api_code}" != "200" || "${web_code}" != "200" ]]; then
echo "health check failed: api=${api_code} web=${web_code}" >&2
rm -rf "${APP_DIR}"
mv "${OLD_LIVE_DIR}" "${APP_DIR}"
ADM_ROOT="${APP_DIR}" ADM_PORT=5174 ADM_API_PORT=8085 bash "${APP_DIR}/scripts/remote_restart.sh" >/dev/null 2>&1 || true
exit 1
fi
say "检查 live 目录不再保留开发源码"
if [[ -d "${APP_DIR}/src" || -f "${APP_DIR}/vite.config.ts" ]]; then
echo "source tree still present in live dir after switch" >&2
exit 1
fi
say "发布成功"
rm -f "${BUNDLE}" "${MANIFEST}" "${SHA_FILE}"
echo "backup_tar=${BACKUP_TAR}"
echo "archived_source=${OLD_LIVE_DIR}"
echo "live_dir=${APP_DIR}"
REMOTE_SCRIPT
say "发布完成,读取线上版本摘要"
ssh -o BatchMode=yes "${HOST}" "sed -n '1,20p' '${APP_DIR}/.release-manifest.txt' && echo '---' && test ! -d '${APP_DIR}/src' && echo 'src=absent' && curl -s '${API_HEALTH_URL}' && echo && curl -sI '${WEB_HEALTH_URL}' | head -1"