update at 2026-07-26 10:44:05

This commit is contained in:
douboer@gmail.com
2026-07-26 10:44:05 +08:00
parent e21fa7dcdd
commit 007d635516
7 changed files with 219 additions and 84 deletions

1
.gitignore vendored
View File

@@ -23,7 +23,6 @@ node_modules
*.sw? *.sw?
*.ttf *.ttf
frontend/vite.config.ts
frontend/dist/fonts.json frontend/dist/fonts.json
frontend/public/fonts.json frontend/public/fonts.json
miniprogram/assets/fonts.json miniprogram/assets/fonts.json

198
deploy.sh
View File

@@ -1,86 +1,144 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ----------------------------------------------------------------------------- # Font2SVG 生产部署脚本M4 本机构建 → myweb 资源同步 → 服务与 Nginx 验收。
# 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任一步失败立即退出避免部分成功导致状态不一致。
# -----------------------------------------------------------------------------
set -euo pipefail set -euo pipefail
# 部署配置(可通过环境变量覆盖)
GITRUN_SCRIPT="${GITRUN_SCRIPT:-./gitrun.sh}"
DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}" DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}"
REMOTE_WORKDIR="${REMOTE_WORKDIR:-~/font2svg}" GITRUN_SCRIPT="${GITRUN_SCRIPT:-./gitrun.sh}"
SERVER_A="${SERVER_A:-gavin@mac.biboer.cn}" SERVER_WEB="${SERVER_WEB:-myweb}"
SERVER_A_PORT="${SERVER_A_PORT:-22}" REMOTE_WORKDIR="${REMOTE_WORKDIR:-/home/gavin/font2svg}"
SERVER_B="${SERVER_B:-gavin@biboer.cn}" SKIP_PUSH="${SKIP_PUSH:-0}"
SERVER_B_PORT="${SERVER_B_PORT:-21174}" SKIP_SUDO="${SKIP_SUDO:-0}"
SSH_OPTS=(
-o BatchMode=yes
-o ConnectTimeout=8
-o StrictHostKeyChecking=accept-new
)
log_info() { COLOR_RESET=$'\033[0m'
echo "[INFO] $1" 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() { log_step() { printf '\n%s==> %s%s\n' "$COLOR_STEP" "$1" "$COLOR_RESET"; }
if [[ ! -x "$GITRUN_SCRIPT" ]]; then log_info() { printf '%s[信息]%s %s\n' "$COLOR_INFO" "$COLOR_RESET" "$1"; }
echo "[ERROR] git 推送脚本不可执行: $GITRUN_SCRIPT" log_ok() { printf '%s[完成]%s %s\n' "$COLOR_OK" "$COLOR_RESET" "$1"; }
echo "请先执行: chmod +x $GITRUN_SCRIPT" 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 exit 1
fi }
log_info "执行本地推送脚本: $GITRUN_SCRIPT"
"$GITRUN_SCRIPT"
} }
run_remote_pull() { ssh_web() {
local host="$1" ssh -o BatchMode=yes -o ConnectTimeout=12 "$SERVER_WEB" "$@"
local port="$2" }
log_info "远程拉取: $host:$port ($DEPLOY_BRANCH)"
ssh "${SSH_OPTS[@]}" -p "$port" "$host" \ check_local_environment() {
"cd $REMOTE_WORKDIR && git fetch origin && git checkout $DEPLOY_BRANCH && git pull --ff-only origin $DEPLOY_BRANCH" 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() { main() {
run_local_push check_local_environment
run_remote_pull "$SERVER_A" "$SERVER_A_PORT" build_and_check
run_remote_pull "$SERVER_B" "$SERVER_B_PORT" push_source
log_info "部署完成" 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 "$@" main "$@"

View File

@@ -34,16 +34,6 @@ server {
add_header Access-Control-Allow-Headers "Origin,Range,Accept,Content-Type,Authorization" always; add_header Access-Control-Allow-Headers "Origin,Range,Accept,Content-Type,Authorization" always;
add_header Access-Control-Expose-Headers "Content-Length,Content-Range" 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 # SVG 渲染 API独立 Python 服务systemd 监听 127.0.0.1:9300
location ^~ /api/ { location ^~ /api/ {
# 预检请求:直接返回 204CORS 头由 server 级 add_header 提供) # 预检请求:直接返回 204CORS 头由 server 级 add_header 提供)
@@ -93,11 +83,21 @@ server {
try_files $uri =404; try_files $uri =404;
} }
# 默认仅提供静态文件 # 小程序图标继续从项目根目录提供,避免与 Web 构建产物冲突。
location / { location ^~ /assets/icons/ {
try_files $uri =404; 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 ~ /\. { location ~ /\. {
deny all; deny all;

20
frontend/vite.config.ts Normal file
View File

@@ -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)),
},
},
})

View File

@@ -1,5 +1,5 @@
{ {
"active": "B", "active": "A",
"cooldownMinutes": 0, "cooldownMinutes": 0,
"servers": { "servers": {
"A": { "A": {

View File

@@ -8,6 +8,7 @@
"preview": "cd frontend && pnpm run preview", "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", "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", "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:syntax": "find miniprogram -name '*.js' -print0 | xargs -0 -n1 node --check",
"mp:lint": "node scripts/check-miniprogram-lint.js", "mp:lint": "node scripts/check-miniprogram-lint.js",
"mp:test": "node scripts/check-miniprogram-core-test.js" "mp:test": "node scripts/check-miniprogram-core-test.js"

View File

@@ -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} 个字体文件均存在且非空`)