update at 2026-07-26 10:44:05
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
198
deploy.sh
@@ -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 "$@"
|
||||||
|
|||||||
24
fonts.conf
24
fonts.conf
@@ -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/ {
|
||||||
# 预检请求:直接返回 204(CORS 头由 server 级 add_header 提供)
|
# 预检请求:直接返回 204(CORS 头由 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
20
frontend/vite.config.ts
Normal 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)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"active": "B",
|
"active": "A",
|
||||||
"cooldownMinutes": 0,
|
"cooldownMinutes": 0,
|
||||||
"servers": {
|
"servers": {
|
||||||
"A": {
|
"A": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
57
scripts/check-font-assets.js
Normal file
57
scripts/check-font-assets.js
Normal 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} 个字体文件均存在且非空`)
|
||||||
Reference in New Issue
Block a user