一键部署:主程序+OCR同机(screen/GPU),Ollama外置局域网。
This commit is contained in:
+2
-1
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
+104
-30
@@ -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
|
||||||
|
if command -v ss &>/dev/null && ss -tln | grep -q ":${p} "; then
|
||||||
|
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} 已被占用"
|
log_error "端口 ${WEB_PORT} 已被占用"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
log_info "端口 ${WEB_PORT} 可用"
|
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
|
||||||
|
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,11 +131,9 @@ 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
|
|
||||||
|
|
||||||
|
if [[ ! -f "${env_file}" ]]; then
|
||||||
local secret pg_pass pg_user
|
local secret pg_pass pg_user
|
||||||
secret=$(openssl rand -hex 32)
|
secret=$(openssl rand -hex 32)
|
||||||
pg_pass=$(openssl rand -hex 16)
|
pg_pass=$(openssl rand -hex 16)
|
||||||
@@ -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 Worker(screen 常驻)
|
||||||
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"
|
||||||
|
if [[ ! -d venv ]]; then
|
||||||
python3 -m venv venv
|
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 GPU,OCR 将常驻显存 (screen)"
|
||||||
|
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>/dev/null || true
|
||||||
|
else
|
||||||
|
log_warn "未检测到 NVIDIA GPU,OCR 将使用 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 Worker(screen 后台,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
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||||
|
|||||||
@@ -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:23567(install.sh 已自动配置)"
|
||||||
>
|
>
|
||||||
<Input placeholder="http://192.168.8.100:23567" />
|
<Input placeholder="http://192.168.8.100:23567" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
Reference in New Issue
Block a user