diff --git a/.gitignore b/.gitignore index 165fa27..571517b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,6 @@ node_modules *.sw? *.ttf -frontend/vite.config.ts frontend/dist/fonts.json frontend/public/fonts.json miniprogram/assets/fonts.json diff --git a/deploy.sh b/deploy.sh index 44e06cb..c613f61 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,86 +1,144 @@ #!/usr/bin/env bash -# ----------------------------------------------------------------------------- -# deploy.sh -# -# 用途 -# - 一键执行本地推送 + 远端两台服务器拉取代码(A/B)。 -# - 默认部署分支:main。 -# -# 执行流程 -# 1) 运行本地推送脚本(默认 ./gitrun.sh) -# 2) 服务器 A 执行 git fetch + checkout + pull --ff-only -# 3) 服务器 B 执行 git fetch + checkout + pull --ff-only -# -# 前置条件 -# - 本机已配置 SSH 免密登录到 A/B 两台服务器。 -# - 远端目录已存在 Git 仓库(默认 ~/font2svg)。 -# - 本地 GITRUN_SCRIPT 可执行。 -# -# 可覆盖环境变量 -# - GITRUN_SCRIPT 本地推送脚本路径(默认 ./gitrun.sh) -# - DEPLOY_BRANCH 部署分支(默认 main) -# - REMOTE_WORKDIR 远端项目目录(默认 ~/font2svg) -# - SERVER_A 服务器 A(默认 gavin@mac.biboer.cn) -# - SERVER_A_PORT 服务器 A SSH 端口(默认 22) -# - SERVER_B 服务器 B(默认 gavin@biboer.cn) -# - SERVER_B_PORT 服务器 B SSH 端口(默认 21174) -# -# 示例 -# - 使用默认配置: -# bash deploy.sh -# - 指定分支: -# DEPLOY_BRANCH=release bash deploy.sh -# - 指定远端目录: -# REMOTE_WORKDIR=~/apps/font2svg bash deploy.sh -# -# 失败策略 -# - set -euo pipefail:任一步失败立即退出,避免部分成功导致状态不一致。 -# ----------------------------------------------------------------------------- - +# Font2SVG 生产部署脚本:M4 本机构建 → myweb 资源同步 → 服务与 Nginx 验收。 set -euo pipefail -# 部署配置(可通过环境变量覆盖) -GITRUN_SCRIPT="${GITRUN_SCRIPT:-./gitrun.sh}" DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}" -REMOTE_WORKDIR="${REMOTE_WORKDIR:-~/font2svg}" -SERVER_A="${SERVER_A:-gavin@mac.biboer.cn}" -SERVER_A_PORT="${SERVER_A_PORT:-22}" -SERVER_B="${SERVER_B:-gavin@biboer.cn}" -SERVER_B_PORT="${SERVER_B_PORT:-21174}" -SSH_OPTS=( - -o BatchMode=yes - -o ConnectTimeout=8 - -o StrictHostKeyChecking=accept-new -) +GITRUN_SCRIPT="${GITRUN_SCRIPT:-./gitrun.sh}" +SERVER_WEB="${SERVER_WEB:-myweb}" +REMOTE_WORKDIR="${REMOTE_WORKDIR:-/home/gavin/font2svg}" +SKIP_PUSH="${SKIP_PUSH:-0}" +SKIP_SUDO="${SKIP_SUDO:-0}" -log_info() { - echo "[INFO] $1" -} +COLOR_RESET=$'\033[0m' +COLOR_INFO=$'\033[1;36m' +COLOR_OK=$'\033[1;32m' +COLOR_WARN=$'\033[1;33m' +COLOR_ERROR=$'\033[1;31m' +COLOR_STEP=$'\033[1;35m' -run_local_push() { - if [[ ! -x "$GITRUN_SCRIPT" ]]; then - echo "[ERROR] git 推送脚本不可执行: $GITRUN_SCRIPT" - echo "请先执行: chmod +x $GITRUN_SCRIPT" +log_step() { printf '\n%s==> %s%s\n' "$COLOR_STEP" "$1" "$COLOR_RESET"; } +log_info() { printf '%s[信息]%s %s\n' "$COLOR_INFO" "$COLOR_RESET" "$1"; } +log_ok() { printf '%s[完成]%s %s\n' "$COLOR_OK" "$COLOR_RESET" "$1"; } +log_warn() { printf '%s[注意]%s %s\n' "$COLOR_WARN" "$COLOR_RESET" "$1"; } +log_error() { printf '%s[失败]%s %s\n' "$COLOR_ERROR" "$COLOR_RESET" "$1" >&2; } + +require_command() { + command -v "$1" >/dev/null 2>&1 || { + log_error "缺少本机命令:$1" exit 1 - fi - log_info "执行本地推送脚本: $GITRUN_SCRIPT" - "$GITRUN_SCRIPT" + } } -run_remote_pull() { - local host="$1" - local port="$2" - log_info "远程拉取: $host:$port ($DEPLOY_BRANCH)" - ssh "${SSH_OPTS[@]}" -p "$port" "$host" \ - "cd $REMOTE_WORKDIR && git fetch origin && git checkout $DEPLOY_BRANCH && git pull --ff-only origin $DEPLOY_BRANCH" +ssh_web() { + ssh -o BatchMode=yes -o ConnectTimeout=12 "$SERVER_WEB" "$@" +} + +check_local_environment() { + log_step '1/8 检查 M4 本机环境' + for tool in git pnpm node python3 rsync ssh curl; do + require_command "$tool" + done + [[ -f frontend/package.json ]] || { log_error '未找到 frontend/package.json,请在仓库根目录执行。'; exit 1; } + [[ -f frontend/vite.config.ts ]] || { + log_error '缺少 frontend/vite.config.ts:该本机配置用于加载 UnoCSS,无法构建前端。' + exit 1 + } + [[ -f miniprogram/assets/fonts.json ]] || { log_error '缺少小程序字体清单。'; exit 1; } + [[ -f frontend/public/fonts.json ]] || { log_error '缺少 Web 字体清单。'; exit 1; } + [[ -d fonts ]] || { log_error '缺少 fonts/ 字体目录。'; exit 1; } + node scripts/check-font-assets.js --manifest miniprogram/assets/fonts.json --root . + node scripts/check-font-assets.js --manifest frontend/public/fonts.json --root . + log_info "本机字体文件:$(find fonts -type f | wc -l | tr -d ' ') 个" + log_ok '本机构建与同步依赖齐全' +} + +build_and_check() { + log_step '2/8 构建并校验 Web 前端' + # 单独执行应用类型检查,避免将 Vite 配置文件误作应用源码检查。 + pnpm -C frontend exec vue-tsc --noEmit -p tsconfig.app.json + pnpm -C frontend run build + [[ -f frontend/dist/index.html ]] || { log_error '前端构建未生成 index.html。'; exit 1; } + pnpm run mp:syntax + pnpm run mp:lint + pnpm run mp:test + log_ok '前端构建、类型检查和小程序校验通过' +} + +push_source() { + log_step '3/8 推送源代码到 Git' + if [[ "$SKIP_PUSH" == '1' ]]; then + log_warn 'SKIP_PUSH=1,跳过 Git 推送' + return + fi + [[ -x "$GITRUN_SCRIPT" ]] || { log_error "推送脚本不可执行:$GITRUN_SCRIPT"; exit 1; } + "$GITRUN_SCRIPT" + log_ok '源代码已推送' +} + +prepare_remote_code() { + log_step '4/8 检查 myweb 并同步代码' + ssh_web "bash -lc 'set -e; command -v git python3 node npm curl >/dev/null; test -d \"$REMOTE_WORKDIR/.git\"; cd \"$REMOTE_WORKDIR\"; git fetch origin \"$DEPLOY_BRANCH\"; git checkout \"$DEPLOY_BRANCH\"; git pull --ff-only origin \"$DEPLOY_BRANCH\"; git log -1 --oneline'" + log_ok 'myweb 代码已同步' +} + +sync_assets() { + log_step '5/8 增量同步字体、前端构建产物与图标' + ssh_web "mkdir -p '$REMOTE_WORKDIR/fonts' '$REMOTE_WORKDIR/miniprogram/assets' '$REMOTE_WORKDIR/assets/icons' '$REMOTE_WORKDIR/frontend/dist'" + rsync -az --partial fonts/ "$SERVER_WEB:$REMOTE_WORKDIR/fonts/" + rsync -az miniprogram/assets/fonts.json miniprogram/assets/default.json miniprogram/assets/route-config.json "$SERVER_WEB:$REMOTE_WORKDIR/miniprogram/assets/" + rsync -az frontend/public/fonts.json "$SERVER_WEB:$REMOTE_WORKDIR/fonts.json" + rsync -az --delete frontend/dist/ "$SERVER_WEB:$REMOTE_WORKDIR/frontend/dist/" + rsync -az miniprogram/assets/icons/ "$SERVER_WEB:$REMOTE_WORKDIR/assets/icons/" + rsync -az miniprogram/assets/icons/webicon.png "$SERVER_WEB:$REMOTE_WORKDIR/assets/webicon.png" + rsync -az fonts.conf "$SERVER_WEB:$REMOTE_WORKDIR/fonts.conf" + log_ok '静态资源和 Nginx 配置已同步' +} + +prepare_remote_runtime() { + log_step '6/8 修复 myweb Python 运行环境' + ssh_web "bash -lc 'set -e; cd \"$REMOTE_WORKDIR\"; python3 -m venv .venv; .venv/bin/python -m pip install -q -r requirements.txt; .venv/bin/python -c \"import cv2, fontTools, uharfbuzz, numpy; print(\\\"Python 依赖正常\\\")\"'" + log_ok 'Python 虚拟环境与依赖正常' +} + +apply_nginx_and_restart() { + log_step '7/8 应用 Nginx 配置并重启 API' + if [[ "$SKIP_SUDO" == '1' ]]; then + log_warn 'SKIP_SUDO=1,跳过需要 sudo 的 Nginx/API 重启' + return + fi + log_info '将要求输入 myweb 的 sudo 密码,用于安装配置、校验并重载 Nginx。' + ssh -tt -o ConnectTimeout=12 "$SERVER_WEB" "sudo install -m 644 '$REMOTE_WORKDIR/fonts.conf' /etc/nginx/conf.d/fonts.conf && sudo nginx -t && sudo systemctl reload nginx && sudo systemctl restart font2svg-api" + log_ok 'Nginx 已重载,Font2SVG API 已重启' +} + +verify_deployment() { + log_step '8/8 验收 myweb 与公网服务' + local local_font_count remote_font_count + local_font_count=$(find fonts -type f | wc -l | tr -d ' ') + remote_font_count=$(ssh_web "find '$REMOTE_WORKDIR/fonts' -type f | wc -l | tr -d ' '") + [[ "$remote_font_count" -ge "$local_font_count" ]] || { + log_error "远端字体数量不足:$remote_font_count/$local_font_count" + exit 1 + } + ssh_web "bash -lc 'cd \"$REMOTE_WORKDIR\"; node scripts/check-font-assets.js --manifest miniprogram/assets/fonts.json --root .; node scripts/check-font-assets.js --manifest fonts.json --root .; test -s frontend/dist/index.html; test -s assets/webicon.png'" + ssh_web "bash -lc 'systemctl is-active --quiet font2svg-api; curl -fsS http://127.0.0.1:9300/healthz'" + curl --noproxy '*' --connect-timeout 10 --max-time 30 -fsS https://fonts.biboer.cn/ >/dev/null + curl --noproxy '*' --connect-timeout 10 --max-time 30 -fsS https://fonts.biboer.cn/healthz >/dev/null + curl --noproxy '*' --connect-timeout 10 --max-time 30 -fsS https://fonts.biboer.cn/fonts.json >/dev/null + log_ok "部署验收通过:字体 $remote_font_count/$local_font_count,首页、API 与清单均可访问" } main() { - run_local_push - run_remote_pull "$SERVER_A" "$SERVER_A_PORT" - run_remote_pull "$SERVER_B" "$SERVER_B_PORT" - log_info "部署完成" + check_local_environment + build_and_check + push_source + prepare_remote_code + sync_assets + prepare_remote_runtime + apply_nginx_and_restart + verify_deployment + printf '\n%s部署完成:M4 → %s%s\n' "$COLOR_OK" "$SERVER_WEB" "$COLOR_RESET" } main "$@" diff --git a/fonts.conf b/fonts.conf index da1d983..8174f09 100644 --- a/fonts.conf +++ b/fonts.conf @@ -34,16 +34,6 @@ server { add_header Access-Control-Allow-Headers "Origin,Range,Accept,Content-Type,Authorization" always; add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always; - # MIME - types { - application/json json; - font/ttf ttf; - font/otf otf; - font/woff woff; - font/woff2 woff2; - application/vnd.ms-fontobject eot; - } - # SVG 渲染 API(独立 Python 服务,systemd 监听 127.0.0.1:9300) location ^~ /api/ { # 预检请求:直接返回 204(CORS 头由 server 级 add_header 提供) @@ -93,11 +83,21 @@ server { try_files $uri =404; } - # 默认仅提供静态文件 - location / { + # 小程序图标继续从项目根目录提供,避免与 Web 构建产物冲突。 + location ^~ /assets/icons/ { try_files $uri =404; } + location = /assets/webicon.png { + try_files $uri =404; + } + + # Web 应用由 Vite 构建产物提供;未知前端路由回退到首页。 + location / { + root /home/gavin/font2svg/frontend/dist; + try_files $uri /index.html; + } + # 禁止访问隐藏文件 location ~ /\. { deny all; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..c92bb2c --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,20 @@ +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import UnoCSS from 'unocss/vite' +import wasm from 'vite-plugin-wasm' + +/** + * Web 前端的通用构建配置。 + * + * 此文件必须纳入版本控制:UnoCSS 的 `virtual:uno.css` 由插件在构建期生成, + * 缺失配置会使任何新部署环境无法构建前端。 + */ +export default defineConfig({ + plugins: [vue(), UnoCSS(), wasm()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, +}) diff --git a/miniprogram/assets/route-config.json b/miniprogram/assets/route-config.json index 1568b3f..239b841 100644 --- a/miniprogram/assets/route-config.json +++ b/miniprogram/assets/route-config.json @@ -1,5 +1,5 @@ { - "active": "B", + "active": "A", "cooldownMinutes": 0, "servers": { "A": { diff --git a/package.json b/package.json index 596b727..0b4007b 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "preview": "cd frontend && pnpm run preview", "api:serve": "if [ -x .venv/bin/python ]; then .venv/bin/python apiserver/server.py; else python3 apiserver/server.py; fi", "prepare-fonts": "python3 scripts/generate-font-list.py", + "fonts:check": "node scripts/check-font-assets.js --manifest miniprogram/assets/fonts.json --root .", "mp:syntax": "find miniprogram -name '*.js' -print0 | xargs -0 -n1 node --check", "mp:lint": "node scripts/check-miniprogram-lint.js", "mp:test": "node scripts/check-miniprogram-core-test.js" diff --git a/scripts/check-font-assets.js b/scripts/check-font-assets.js new file mode 100644 index 0000000..f8ffdd9 --- /dev/null +++ b/scripts/check-font-assets.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +/** + * 校验字体清单中的每个字体文件是否存在且非空。 + * + * 用法:node scripts/check-font-assets.js --manifest <清单路径> --root <项目根目录> + */ + +const fs = require('fs') +const path = require('path') + +function readArgument(name) { + const index = process.argv.indexOf(name) + return index >= 0 ? process.argv[index + 1] : '' +} + +const manifestPath = readArgument('--manifest') +const projectRoot = readArgument('--root') || '.' + +if (!manifestPath) { + console.error('用法错误:缺少 --manifest 参数') + process.exit(1) +} + +let manifest +try { + manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) +} catch (error) { + console.error(`无法读取字体清单 ${manifestPath}:${error.message}`) + process.exit(1) +} + +const items = Array.isArray(manifest) ? manifest : (manifest.items || manifest.fonts || []) +if (!Array.isArray(items) || items.length === 0) { + console.error(`字体清单为空:${manifestPath}`) + process.exit(1) +} + +const missing = [] +for (const item of items) { + const relativePath = String(item.path || '').replace(/^\/+/, '') + const fontPath = path.resolve(projectRoot, relativePath) + if (!relativePath || !fs.existsSync(fontPath) || fs.statSync(fontPath).size === 0) { + missing.push(`${item.id || '未知ID'} ${item.name || '未知字体'} (${relativePath || '未配置路径'})`) + } +} + +if (missing.length > 0) { + console.error(`字体资源校验失败:缺少或为空 ${missing.length}/${items.length} 个`) + missing.slice(0, 20).forEach((item) => console.error(`- ${item}`)) + if (missing.length > 20) { + console.error(`- 其余 ${missing.length - 20} 个未显示`) + } + process.exit(1) +} + +console.log(`字体资源校验通过:${items.length} 个字体文件均存在且非空`)