一键部署:主程序+OCR同机(screen/GPU),Ollama外置局域网。

This commit is contained in:
dekun
2026-06-28 14:44:51 +08:00
parent 9713c640b4
commit 357f61c57c
13 changed files with 329 additions and 75 deletions
+2 -1
View File
@@ -17,7 +17,8 @@ CORS_ORIGINS=http://127.0.0.1:23566,http://localhost:23566
OLLAMA_BASE_URL=http://127.0.0.1:11434 OLLAMA_BASE_URL=http://127.0.0.1:11434
OLLAMA_MODEL=qwen2.5:7b OLLAMA_MODEL=qwen2.5:7b
# 局域网 GPU OCR 服务(deploy/ocr-worker),留空则本机 CPU 识别 # 局域网 GPU OCR 服务(deploy/ocr-worker),留空则本机 CPU 识别
OCR_SERVICE_URL= OCR_SERVICE_URL=http://127.0.0.1:23567
OCR_PORT=23567
OCR_API_KEY= OCR_API_KEY=
OPENAI_BASE_URL=https://api.openai.com/v1 OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o-mini OPENAI_MODEL=gpt-4o-mini
+1 -1
View File
@@ -25,7 +25,7 @@ class Settings(BaseSettings):
OCR_MAX_SIDE: int = 1280 OCR_MAX_SIDE: int = 1280
UPLOAD_MAX_SIDE: int = 2048 UPLOAD_MAX_SIDE: int = 2048
OCR_WARMUP: bool = True OCR_WARMUP: bool = True
OCR_SERVICE_URL: str = "" OCR_SERVICE_URL: str = "http://127.0.0.1:23567"
OCR_API_KEY: str = "" OCR_API_KEY: str = ""
OCR_USE_GPU: bool = False OCR_USE_GPU: bool = False
+3
View File
@@ -37,6 +37,7 @@ def seed_admin_and_settings(db: Session) -> None:
ollama_model=settings.OLLAMA_MODEL, ollama_model=settings.OLLAMA_MODEL,
openai_base_url=settings.OPENAI_BASE_URL, openai_base_url=settings.OPENAI_BASE_URL,
openai_model=settings.OPENAI_MODEL, openai_model=settings.OPENAI_MODEL,
ocr_service_url=settings.OCR_SERVICE_URL or None,
) )
) )
else: else:
@@ -48,6 +49,8 @@ def seed_admin_and_settings(db: Session) -> None:
row.openai_base_url = settings.OPENAI_BASE_URL row.openai_base_url = settings.OPENAI_BASE_URL
if not row.openai_model: if not row.openai_model:
row.openai_model = settings.OPENAI_MODEL row.openai_model = settings.OPENAI_MODEL
if not row.ocr_service_url and settings.OCR_SERVICE_URL:
row.ocr_service_url = settings.OCR_SERVICE_URL
if not row.ai_provider: if not row.ai_provider:
row.ai_provider = "ollama" row.ai_provider = "ollama"
+114 -40
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# 中学成绩档案系统 — Ubuntu 零 Node 一键部署(FastAPI 单进程 + systemd # 中学成绩档案系统 — 一键部署
# 架构:主程序 + OCR(GPU/screen 同机) | Ollama(其他电脑局域网)
# 版权所有 (c) 马建军 微信: dekun03 手机: 18364911125 # 版权所有 (c) 马建军 微信: dekun03 手机: 18364911125
# #
set -euo pipefail set -euo pipefail
@@ -8,8 +9,12 @@ set -euo pipefail
REPO_URL="${REPO_URL:-https://git.bz121.com/dekun/secondary-school-grade-archive.git}" REPO_URL="${REPO_URL:-https://git.bz121.com/dekun/secondary-school-grade-archive.git}"
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}" INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
WEB_PORT="${WEB_PORT:-23566}" WEB_PORT="${WEB_PORT:-23566}"
OCR_PORT="${OCR_PORT:-23567}"
BRANCH="${BRANCH:-main}" BRANCH="${BRANCH:-main}"
PIP_MIRROR="${PIP_MIRROR:-https://pypi.tuna.tsinghua.edu.cn/simple}" PIP_MIRROR="${PIP_MIRROR:-https://pypi.tuna.tsinghua.edu.cn/simple}"
# Ollama 在其他电脑时安装前指定,例如: OLLAMA_BASE_URL=http://192.168.8.100:11434
OLLAMA_BASE_URL="${OLLAMA_BASE_URL:-}"
OLLAMA_MODEL="${OLLAMA_MODEL:-qwen2.5:7b}"
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@@ -22,6 +27,8 @@ log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
# shellcheck source=proxy.sh # shellcheck source=proxy.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/proxy.sh" source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/proxy.sh"
# shellcheck source=ocr-common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ocr-common.sh"
require_root() { require_root() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
@@ -41,18 +48,41 @@ check_os() {
} }
check_port() { check_port() {
if command -v ss &>/dev/null && ss -tln | grep -q ":${WEB_PORT} "; then for p in "${WEB_PORT}" "${OCR_PORT}"; do
log_error "端口 ${WEB_PORT} 已被占用" if command -v ss &>/dev/null && ss -tln | grep -q ":${p} "; then
exit 1 if [[ "${p}" == "${OCR_PORT}" ]] && ocr_screen_running 2>/dev/null; then
log_warn "端口 ${OCR_PORT} 已被占用(可能已有 OCR Worker)"
elif [[ "${p}" == "${WEB_PORT}" ]]; then
log_error "端口 ${WEB_PORT} 已被占用"
exit 1
fi
fi
done
log_info "Web 端口 ${WEB_PORT} / OCR 端口 ${OCR_PORT} 检查完成"
}
prompt_ollama_url() {
if [[ -n "${OLLAMA_BASE_URL}" ]]; then
log_info "Ollama 地址: ${OLLAMA_BASE_URL}"
return
fi fi
log_info "端口 ${WEB_PORT} 可用" if [[ -t 0 ]]; then
echo ""
echo "Ollama 部署在【其他电脑】上,请填写局域网地址。"
read -rp "Ollama 地址 [http://192.168.8.100:11434]: " input
OLLAMA_BASE_URL="${input:-http://192.168.8.100:11434}"
else
OLLAMA_BASE_URL="http://127.0.0.1:11434"
log_warn "未设置 OLLAMA_BASE_URL,默认 ${OLLAMA_BASE_URL}(可在 .env 或系统设置中修改)"
fi
log_info "Ollama 地址: ${OLLAMA_BASE_URL}"
} }
install_base_packages() { install_base_packages() {
log_info "安装系统依赖…" log_info "安装系统依赖…"
apt-get update -qq apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
git curl ca-certificates openssl \ git curl ca-certificates openssl screen \
python3 python3-venv python3-pip python3-dev \ python3 python3-venv python3-pip python3-dev \
build-essential libpq-dev \ build-essential libpq-dev \
postgresql postgresql-contrib \ postgresql postgresql-contrib \
@@ -72,16 +102,27 @@ clone_or_update_repo() {
git clone --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" || git clone "${REPO_URL}" "${INSTALL_DIR}" git clone --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" || git clone "${REPO_URL}" "${INSTALL_DIR}"
fi fi
find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} + find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} +
chmod +x "${INSTALL_DIR}/deploy/start.sh" chmod +x "${INSTALL_DIR}/deploy/start.sh" \
"${INSTALL_DIR}/deploy/ocr-screen.sh" \
"${INSTALL_DIR}/deploy/ocr-worker/"*.sh 2>/dev/null || true
} }
verify_frontend_dist() { verify_frontend_dist() {
if [[ ! -f "${INSTALL_DIR}/frontend/dist/index.html" ]]; then if [[ ! -f "${INSTALL_DIR}/frontend/dist/index.html" ]]; then
log_error "未找到 frontend/dist/index.html" log_error "未找到 frontend/dist/index.html"
log_error "请先在开发机执行: cd frontend && npm run build,并将 dist 推送到仓库后再部署" log_error "仓库应已包含预构建前端;若缺失请在开发机 npm run build 后推送"
exit 1 exit 1
fi fi
log_info "前端静态资源已就绪(无需在服务器构建)" log_info "前端静态资源已就绪"
}
env_set_or_append() {
local key="$1" val="$2" file="${INSTALL_DIR}/.env"
if grep -q "^${key}=" "${file}" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${val}|" "${file}"
else
echo "${key}=${val}" >> "${file}"
fi
} }
generate_env() { generate_env() {
@@ -90,17 +131,15 @@ generate_env() {
server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') server_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
server_ip="${server_ip:-127.0.0.1}" server_ip="${server_ip:-127.0.0.1}"
if [[ -f "${env_file}" ]]; then prompt_ollama_url
log_info "保留已有 .env"
return
fi
local secret pg_pass pg_user if [[ ! -f "${env_file}" ]]; then
secret=$(openssl rand -hex 32) local secret pg_pass pg_user
pg_pass=$(openssl rand -hex 16) secret=$(openssl rand -hex 32)
pg_user="gradeapp" pg_pass=$(openssl rand -hex 16)
pg_user="gradeapp"
cat > "${env_file}" <<EOF cat > "${env_file}" <<EOF
# generated by deploy/install.sh — $(date -Iseconds) # generated by deploy/install.sh — $(date -Iseconds)
WEB_PORT=${WEB_PORT} WEB_PORT=${WEB_PORT}
FRONTEND_DIST=${INSTALL_DIR}/frontend/dist FRONTEND_DIST=${INSTALL_DIR}/frontend/dist
@@ -111,14 +150,25 @@ POSTGRES_DB=student_archive
DATABASE_URL=postgresql://${pg_user}:${pg_pass}@127.0.0.1:5432/student_archive DATABASE_URL=postgresql://${pg_user}:${pg_pass}@127.0.0.1:5432/student_archive
UPLOAD_DIR=${INSTALL_DIR}/uploads UPLOAD_DIR=${INSTALL_DIR}/uploads
CORS_ORIGINS=http://${server_ip}:${WEB_PORT},http://127.0.0.1:${WEB_PORT},http://localhost:${WEB_PORT} CORS_ORIGINS=http://${server_ip}:${WEB_PORT},http://127.0.0.1:${WEB_PORT},http://localhost:${WEB_PORT}
OLLAMA_BASE_URL=http://127.0.0.1:11434 # OCR 同机 GPU Workerscreen 常驻)
OLLAMA_MODEL=qwen2.5:7b OCR_SERVICE_URL=http://127.0.0.1:${OCR_PORT}
OCR_PORT=${OCR_PORT}
# Ollama 在其他电脑(局域网)
OLLAMA_BASE_URL=${OLLAMA_BASE_URL}
OLLAMA_MODEL=${OLLAMA_MODEL}
FLUCTUATION_THRESHOLD=0.08 FLUCTUATION_THRESHOLD=0.08
ADMIN_DEFAULT_USERNAME=admin ADMIN_DEFAULT_USERNAME=admin
ADMIN_DEFAULT_PASSWORD=admin123 ADMIN_DEFAULT_PASSWORD=admin123
EOF EOF
chmod 600 "${env_file}" chmod 600 "${env_file}"
log_info ".env 已生成(默认超级管理员 admin / admin123,请登录后修改" log_info ".env 已生成(默认 admin / admin123"
else
log_info "更新 .env 中的 OCR / Ollama 配置…"
env_set_or_append "OCR_SERVICE_URL" "http://127.0.0.1:${OCR_PORT}"
env_set_or_append "OCR_PORT" "${OCR_PORT}"
env_set_or_append "OLLAMA_BASE_URL" "${OLLAMA_BASE_URL}"
env_set_or_append "OLLAMA_MODEL" "${OLLAMA_MODEL}"
fi
} }
setup_postgresql() { setup_postgresql() {
@@ -141,9 +191,11 @@ setup_postgresql() {
} }
setup_backend() { setup_backend() {
log_info "安装 Python 依赖Paddle 包较大,约 10–30 分钟)…" log_info "安装主程序 Python 依赖…"
cd "${INSTALL_DIR}/backend" cd "${INSTALL_DIR}/backend"
python3 -m venv venv if [[ ! -d venv ]]; then
python3 -m venv venv
fi
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source venv/bin/activate source venv/bin/activate
pip install --upgrade pip --progress-bar on -i "${PIP_MIRROR}" pip install --upgrade pip --progress-bar on -i "${PIP_MIRROR}"
@@ -151,16 +203,28 @@ setup_backend() {
deactivate deactivate
} }
setup_ocr_gpu() {
if command -v nvidia-smi >/dev/null; then
log_info "检测到 NVIDIA GPUOCR 将常驻显存 (screen)"
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>/dev/null || true
else
log_warn "未检测到 NVIDIA GPUOCR 将使用 CPU(较慢)"
fi
install_ocr_worker
start_ocr_screen
wait_ocr_healthy || log_warn "OCR 仍在加载模型,稍后可执行: bash ${INSTALL_DIR}/deploy/ocr-screen.sh status"
}
stop_legacy_pm2() { stop_legacy_pm2() {
if command -v pm2 &>/dev/null; then if command -v pm2 &>/dev/null; then
pm2 delete grade-api grade-web 2>/dev/null || true pm2 delete grade-api grade-web 2>/dev/null || true
pm2 save 2>/dev/null || true pm2 save 2>/dev/null || true
log_info "已停止旧版 PM2 进程grade-api / grade-web" log_info "已停止旧版 PM2 进程"
fi fi
} }
setup_systemd() { setup_systemd() {
log_info "配置 systemd 服务…" log_info "配置 systemd 服务 grade-archive(主程序)…"
sed "s|/opt/secondary-school-grade-archive|${INSTALL_DIR}|g" \ sed "s|/opt/secondary-school-grade-archive|${INSTALL_DIR}|g" \
"${INSTALL_DIR}/deploy/grade-archive.service" > /etc/systemd/system/grade-archive.service "${INSTALL_DIR}/deploy/grade-archive.service" > /etc/systemd/system/grade-archive.service
systemctl daemon-reload systemctl daemon-reload
@@ -168,7 +232,7 @@ setup_systemd() {
} }
start_service() { start_service() {
log_info "启动服务…" log_info "启动主程序…"
cd "${INSTALL_DIR}" cd "${INSTALL_DIR}"
mkdir -p uploads backups mkdir -p uploads backups
systemctl restart grade-archive systemctl restart grade-archive
@@ -178,48 +242,58 @@ wait_healthy() {
local i local i
for i in $(seq 1 40); do for i in $(seq 1 40); do
if curl -sf "http://127.0.0.1:${WEB_PORT}/api/health" >/dev/null; then if curl -sf "http://127.0.0.1:${WEB_PORT}/api/health" >/dev/null; then
log_info "健康检查通过" log_info "主程序健康检查通过"
return 0 return 0
fi fi
sleep 3 sleep 3
done done
log_warn "健康检查超时,请查看: journalctl -u grade-archive -f" log_warn "主程序健康检查超时: journalctl -u grade-archive -f"
} }
print_summary() { print_summary() {
# shellcheck disable=SC1090
source "${INSTALL_DIR}/.env"
local ip local ip
ip=$(hostname -I 2>/dev/null | awk '{print $1}') ip=$(hostname -I 2>/dev/null | awk '{print $1}')
ip="${ip:-127.0.0.1}" ip="${ip:-127.0.0.1}"
echo "" echo ""
echo "==========================================" echo "=========================================="
echo " 中学成绩档案系统部署完成(零 Node" echo " 中学成绩档案 — 一键部署完成"
echo " 版权所有 (c) 马建军" echo " 版权所有 (c) 马建军"
echo "==========================================" echo "=========================================="
echo " 访问: http://${ip}:${WEB_PORT}" echo " 访问: http://${ip}:${WEB_PORT}"
echo " 目录: ${INSTALL_DIR}" echo " 管理员: admin / admin123(请立即修改)"
echo " 默认管理员: admin / admin123(请立即修改)"
echo "" echo ""
echo " systemctl status grade-archive" echo " 【同机 OCR — GPU 常驻 screen】"
echo " journalctl -u grade-archive -f" echo " OCR 地址: http://127.0.0.1:${OCR_PORT}"
echo " bash ${INSTALL_DIR}/deploy/update.sh" echo " 状态: bash ${INSTALL_DIR}/deploy/ocr-screen.sh status"
echo " bash ${INSTALL_DIR}/deploy/backup.sh" echo " 进入终端: screen -r ocr-worker (Ctrl+A D 退出)"
echo " 重启 OCR: bash ${INSTALL_DIR}/deploy/ocr-screen.sh restart"
echo "" echo ""
echo " 【Ollama — 其他电脑】"
echo " 地址: ${OLLAMA_BASE_URL}"
echo " 模型: ${OLLAMA_MODEL}"
echo " 可在「系统设置 → AI 模型」修改"
echo ""
echo " 主程序: systemctl status grade-archive"
echo " 更新: sudo bash ${INSTALL_DIR}/deploy/update.sh"
echo " 微信 dekun03 手机 18364911125" echo " 微信 dekun03 手机 18364911125"
echo "==========================================" echo "=========================================="
} }
main() { main() {
log_info "零 Node 一键部署开始" log_info "一键部署开始(主程序 + OCR/GPU/screen | Ollama 外置)"
require_root require_root
setup_deploy_proxy setup_deploy_proxy
check_os check_os
check_port
install_base_packages
clone_or_update_repo clone_or_update_repo
verify_frontend_dist verify_frontend_dist
check_port
install_base_packages
generate_env generate_env
setup_postgresql setup_postgresql
setup_backend setup_backend
setup_ocr_gpu
stop_legacy_pm2 stop_legacy_pm2
setup_systemd setup_systemd
start_service start_service
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# OCR Worker 共用函数:同机 GPU 识别 + screen 常驻
OCR_SCREEN_NAME="${OCR_SCREEN_NAME:-ocr-worker}"
OCR_PORT="${OCR_PORT:-23567}"
ocr_worker_dir() {
echo "${INSTALL_DIR}/deploy/ocr-worker"
}
install_ocr_worker() {
local worker_dir
worker_dir="$(ocr_worker_dir)"
if [[ ! -d "${worker_dir}" ]]; then
log_error "未找到 ${worker_dir}"
return 1
fi
log_info "安装/更新 OCR Worker (PaddleOCR GPU)…"
chmod +x "${worker_dir}"/*.sh 2>/dev/null || true
OCR_PORT="${OCR_PORT}" bash "${worker_dir}/install.sh"
}
ocr_screen_running() {
screen -list 2>/dev/null | grep -q "\.${OCR_SCREEN_NAME}[[:space:]]"
}
start_ocr_screen() {
local worker_dir
worker_dir="$(ocr_worker_dir)"
if [[ ! -x "${worker_dir}/.venv/bin/uvicorn" ]]; then
log_warn "OCR Worker 未安装,跳过 screen 启动"
return 1
fi
if ! command -v screen >/dev/null; then
log_error "未安装 screen,请 apt install screen"
return 1
fi
log_info "启动 OCR Worker → screen 会话「${OCR_SCREEN_NAME}」(GPU 常驻, 端口 ${OCR_PORT})"
if ocr_screen_running; then
screen -S "${OCR_SCREEN_NAME}" -X quit 2>/dev/null || true
sleep 1
fi
screen -dmS "${OCR_SCREEN_NAME}" bash -lc "
cd '${worker_dir}' &&
export OCR_USE_GPU=true OCR_PORT='${OCR_PORT}' OCR_HOST=0.0.0.0 &&
exec bash run.sh
"
sleep 1
}
stop_ocr_screen() {
if ocr_screen_running; then
log_info "停止 OCR screen 会话…"
screen -S "${OCR_SCREEN_NAME}" -X quit 2>/dev/null || true
fi
}
wait_ocr_healthy() {
local i
for i in $(seq 1 90); do
if curl -sf "http://127.0.0.1:${OCR_PORT}/health" >/dev/null 2>&1; then
log_info "OCR 健康检查通过 — http://127.0.0.1:${OCR_PORT}/health"
return 0
fi
sleep 2
done
log_warn "OCR 尚未就绪(模型加载可能需 1–3 分钟)"
log_warn "查看: bash ${INSTALL_DIR}/deploy/ocr-screen.sh status"
return 1
}
show_ocr_status() {
echo ""
echo "--- OCR Worker (screen: ${OCR_SCREEN_NAME}) ---"
if ocr_screen_running; then
echo "screen: 运行中"
screen -list 2>/dev/null | grep "${OCR_SCREEN_NAME}" || true
else
echo "screen: 未运行"
fi
if curl -sf "http://127.0.0.1:${OCR_PORT}/health" 2>/dev/null; then
echo ""
else
echo "health: 无响应 (http://127.0.0.1:${OCR_PORT}/health)"
fi
if command -v nvidia-smi >/dev/null; then
nvidia-smi --query-gpu=name,memory.used,memory.total,utilization.gpu --format=csv,noheader 2>/dev/null || true
fi
}
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# OCR Worker screen 管理(主程序同机部署时使用)
set -euo pipefail
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
# shellcheck source=common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
# shellcheck source=ocr-common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ocr-common.sh"
usage() {
cat <<EOF
用法: bash deploy/ocr-screen.sh <命令>
start 启动 OCR Workerscreen 后台,GPU 常驻)
stop 停止 screen 会话
restart 重启
status 查看 screen / health / GPU
attach 进入 screen 终端(Ctrl+A D 退出)
EOF
}
cmd="${1:-status}"
case "${cmd}" in
start)
start_ocr_screen
wait_ocr_healthy || true
;;
stop)
stop_ocr_screen
;;
restart)
stop_ocr_screen
sleep 1
start_ocr_screen
wait_ocr_healthy || true
;;
status)
show_ocr_status
;;
attach)
if ocr_screen_running; then
screen -r "${OCR_SCREEN_NAME}"
else
log_error "screen 未运行,请先: bash deploy/ocr-screen.sh start"
exit 1
fi
;;
*)
usage
exit 1
;;
esac
+9 -27
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 在带 NVIDIA 显卡的 Linux 机器上安装 OCR Worker # 在带 NVIDIA 显卡的 Linux 机器上安装 OCR Worker(由 deploy/install.sh 自动调用)
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$(dirname "$0")" && pwd)"
VENV="${ROOT}/.venv" VENV="${ROOT}/.venv"
PORT="${OCR_PORT:-23567}" PORT="${OCR_PORT:-23567}"
PIP_MIRROR="${PIP_MIRROR:-https://pypi.tuna.tsinghua.edu.cn/simple}"
echo "==> OCR Worker 安装目录: ${ROOT}" echo "==> OCR Worker 安装目录: ${ROOT}"
@@ -21,7 +22,7 @@ fi
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "${VENV}/bin/activate" source "${VENV}/bin/activate"
pip install -U pip wheel pip install -U pip wheel -i "${PIP_MIRROR}"
install_paddle() { install_paddle() {
if command -v nvidia-smi >/dev/null 2>&1; then if command -v nvidia-smi >/dev/null 2>&1; then
@@ -39,35 +40,16 @@ install_paddle() {
fi fi
else else
echo "==> 未检测到 GPU,安装 CPU 版 paddlepaddle…" echo "==> 未检测到 GPU,安装 CPU 版 paddlepaddle…"
pip install paddlepaddle==2.6.2 pip install paddlepaddle==2.6.2 -i "${PIP_MIRROR}"
fi fi
} }
install_paddle install_paddle
pip install -r "${ROOT}/requirements.txt" pip install -r "${ROOT}/requirements.txt" -i "${PIP_MIRROR}"
chmod +x "${ROOT}/run.sh" "${ROOT}/start.sh" 2>/dev/null || true
echo "" echo ""
echo "==> 验证依赖…"
python3 -c "import fastapi, uvicorn, paddle; print('paddle', paddle.__version__, 'OK')" python3 -c "import fastapi, uvicorn, paddle; print('paddle', paddle.__version__, 'OK')"
echo ""
cat <<EOF echo "==> OCR Worker 安装完成。由 deploy/install.sh 通过 screen 自动启动。"
echo " 手动管理: bash $(dirname "$ROOT")/ocr-screen.sh status"
安装完成。
【重要】你已经在目录 ${ROOT} 时,不要再 cd deploy/ocr-worker,直接:
cd ${ROOT}
OCR_USE_GPU=true bash start.sh
另开终端测试(服务启动后):
curl -s http://127.0.0.1:${PORT}/health
应返回: {"status":"ok","gpu":true}
注册为 systemd 服务(推荐,后台常驻):
sudo bash ${ROOT}/install-service.sh
诊断:
bash ${ROOT}/check.sh
EOF
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# 供 screen 会话调用:OCR 模型常驻 GPU,异常退出自动重启
set -uo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
VENV="${ROOT}/.venv"
PORT="${OCR_PORT:-23567}"
export OCR_USE_GPU="${OCR_USE_GPU:-true}"
export OCR_HOST="${OCR_HOST:-0.0.0.0}"
if [[ ! -d "${VENV}" ]]; then
echo "错误: 请先运行 bash install.sh"
exit 1
fi
# shellcheck disable=SC1091
source "${VENV}/bin/activate"
cd "${ROOT}"
echo "=========================================="
echo " OCR Worker | GPU=${OCR_USE_GPU} | :${PORT}"
echo " $(date -Iseconds)"
echo " 退出 screen: Ctrl+A 然后 D"
echo "=========================================="
while true; do
uvicorn app:app --host "${OCR_HOST}" --port "${PORT}" --log-level info
code=$?
echo "[$(date -Iseconds)] uvicorn 退出 code=${code}5 秒后重启…"
sleep 5
done
+6
View File
@@ -9,6 +9,8 @@ INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
# shellcheck source=common.sh # shellcheck source=common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
# shellcheck source=ocr-common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ocr-common.sh"
require_root "deploy/repair.sh" require_root "deploy/repair.sh"
@@ -19,6 +21,10 @@ log_info "开始修复 grade-archive 服务…"
stop_legacy_pm2 stop_legacy_pm2
install_ocr_deps_safe install_ocr_deps_safe
if [[ -x "${INSTALL_DIR}/deploy/ocr-worker/.venv/bin/uvicorn" ]]; then
start_ocr_screen || log_warn "OCR screen 启动失败"
wait_ocr_healthy || log_warn "OCR 未就绪"
fi
setup_systemd_service setup_systemd_service
restart_grade_service restart_grade_service
+13 -3
View File
@@ -9,12 +9,15 @@ PIP_MIRROR="${PIP_MIRROR:-https://pypi.tuna.tsinghua.edu.cn/simple}"
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh" source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
# shellcheck source=proxy.sh # shellcheck source=proxy.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/proxy.sh" source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/proxy.sh"
# shellcheck source=ocr-common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ocr-common.sh"
require_root "deploy/update.sh" require_root "deploy/update.sh"
cd "${INSTALL_DIR}" || exit 1 cd "${INSTALL_DIR}" || exit 1
find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} + find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} +
chmod +x deploy/start.sh deploy/install-ocr-deps.sh deploy/repair.sh chmod +x deploy/start.sh deploy/ocr-screen.sh deploy/install-ocr-deps.sh deploy/repair.sh
chmod +x deploy/ocr-worker/*.sh 2>/dev/null || true
setup_deploy_proxy setup_deploy_proxy
@@ -28,21 +31,28 @@ if [[ ! -f "${INSTALL_DIR}/frontend/dist/index.html" ]]; then
exit 1 exit 1
fi fi
log_info "更新后端依赖…" log_info "更新主程序依赖…"
cd backend cd backend
source venv/bin/activate source venv/bin/activate
pip install -r requirements.txt --progress-bar on -i "${PIP_MIRROR}" pip install -r requirements.txt --progress-bar on -i "${PIP_MIRROR}"
deactivate deactivate
cd "${INSTALL_DIR}" cd "${INSTALL_DIR}"
log_info "更新 OCR Worker 并重启 screen…"
install_ocr_worker
stop_ocr_screen
start_ocr_screen
wait_ocr_healthy || log_warn "OCR 加载中,稍后: bash deploy/ocr-screen.sh status"
stop_legacy_pm2 stop_legacy_pm2
install_ocr_deps_safe install_ocr_deps_safe
setup_systemd_service setup_systemd_service
restart_grade_service restart_grade_service
if ! wait_healthy; then if ! wait_healthy; then
log_error "更新后服务异常,可尝试: sudo bash ${INSTALL_DIR}/deploy/repair.sh" log_error "更新后主程序异常,可尝试: sudo bash ${INSTALL_DIR}/deploy/repair.sh"
exit 1 exit 1
fi fi
log_info "更新完成" log_info "更新完成"
show_ocr_status
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -9,7 +9,7 @@
<meta name="author" content="马建军" /> <meta name="author" content="马建军" />
<meta name="copyright" content="Copyright (c) 马建军. All rights reserved." /> <meta name="copyright" content="Copyright (c) 马建军. All rights reserved." />
<title>中学成绩档案</title> <title>中学成绩档案</title>
<script type="module" crossorigin src="/assets/index-19dlnnB9.js"></script> <script type="module" crossorigin src="/assets/index-DmCjZu_W.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-GY2etMYN.css"> <link rel="stylesheet" crossorigin href="/assets/index-GY2etMYN.css">
</head> </head>
<body> <body>
+1 -1
View File
@@ -240,7 +240,7 @@ export default function SettingsPage() {
<Form.Item <Form.Item
name="ocr_service_url" name="ocr_service_url"
label="OCR 服务地址(局域网 GPU 机器)" label="OCR 服务地址(局域网 GPU 机器)"
extra="留空则在应用服务器本机 CPU 识别。填写后类似 Ollama,例如 http://192.168.8.100:23567" extra="留空则在应用服务器本机 CPU 识别。同机部署时填 http://127.0.0.1:23567install.sh 已自动配置)"
> >
<Input placeholder="http://192.168.8.100:23567" /> <Input placeholder="http://192.168.8.100:23567" />
</Form.Item> </Form.Item>