| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- #!/usr/bin/env sh
- #
- # Proposa AI 服务部署脚本(Linux 服务器)
- #
- # 功能:
- # 1. 使用 ModelScope 将 gpt2-chinese-cluecorpussmall-onnx 下载到 models/ 目录
- # (模型文件已存在时自动跳过,幂等)
- # 2. 使用 uv 按 uv.lock 安装整个项目依赖(uv sync --frozen)
- # 3. 后台启动 proposa-api,写入 PID/日志文件并做 /health 健康检查
- #
- # 用法:
- # sh deploy.sh 完整部署并后台启动(可重复执行)
- # sh deploy.sh status 查看服务运行状态与健康检查
- # sh deploy.sh stop 停止后台服务
- # sh deploy.sh restart 停止后重新后台启动
- #
- # 可用环境变量:
- # MODEL_REPO ModelScope 模型仓库 ID(默认 Maiteka/gpt2-chinese-cluecorpussmall-onnx)
- # MODEL_DIR 模型保存目录(默认 <仓库根>/models/gpt2-chinese-cluecorpussmall-onnx)
- # UV_CACHE_DIR uv 缓存目录(默认 <仓库根>/.uv-cache)
- # PROPOSA_API_PORT 健康检查端口(默认读取 .env 的 PROPOSA_API_PORT,缺省 8000)
- # RESTART=1 服务已运行时强制重启(等价于 sh deploy.sh restart)
- set -eu
- case "$0" in
- */*) SCRIPT_DIR=${0%/*} ;;
- *) SCRIPT_DIR=. ;;
- esac
- REPO_ROOT=$(CDPATH= cd "$SCRIPT_DIR" && pwd)
- cd "$REPO_ROOT"
- MODEL_REPO=${MODEL_REPO:-"Maiteka/gpt2-chinese-cluecorpussmall-onnx"}
- MODEL_DIR=${MODEL_DIR:-"$REPO_ROOT/models/gpt2-chinese-cluecorpussmall-onnx"}
- UV_CACHE_DIR=${UV_CACHE_DIR:-"$REPO_ROOT/.uv-cache"}
- export UV_CACHE_DIR
- ENV_FILE="$REPO_ROOT/.env"
- PID_FILE="$REPO_ROOT/output/api.pid"
- LOG_FILE="$REPO_ROOT/output/api.log"
- usage() {
- cat <<'EOF'
- Proposa AI 服务部署脚本(Linux 服务器)
- 用法:
- sh deploy.sh 完整部署并后台启动(可重复执行)
- sh deploy.sh status 查看服务运行状态与健康检查
- sh deploy.sh stop 停止后台服务
- sh deploy.sh restart 停止后重新后台启动
- 环境变量:
- MODEL_REPO ModelScope 模型仓库 ID(默认 Maiteka/gpt2-chinese-cluecorpussmall-onnx)
- MODEL_DIR 模型保存目录(默认 <仓库根>/models/gpt2-chinese-cluecorpussmall-onnx)
- UV_CACHE_DIR uv 缓存目录(默认 <仓库根>/.uv-cache)
- PROPOSA_API_PORT 健康检查端口(默认读取 .env 的 PROPOSA_API_PORT,缺省 8000)
- RESTART=1 服务已运行时强制重启(等价于 sh deploy.sh restart)
- EOF
- }
- # 从 .env 读取 PROPOSA_API_PORT(容忍行首空格),读不到输出空串
- env_port() {
- [ -f "$ENV_FILE" ] || return 0
- line=$(grep -E '^[[:space:]]*PROPOSA_API_PORT[[:space:]]*=' "$ENV_FILE" 2>/dev/null | tail -n 1 || true)
- [ -n "$line" ] || return 0
- printf '%s\n' "$line" | sed 's/^[[:space:]]*PROPOSA_API_PORT[[:space:]]*=[[:space:]]*//' | tr -d '[:space:]"'
- }
- API_PORT=${PROPOSA_API_PORT:-$(env_port)}
- API_PORT=${API_PORT:-8000}
- case "$API_PORT" in
- ''|*[!0-9]*)
- echo "错误: 无效的 API 端口 '$API_PORT',请设置 PROPOSA_API_PORT 或检查 .env" >&2
- exit 2
- ;;
- esac
- HEALTH_URL="http://127.0.0.1:$API_PORT/health"
- require_uv() {
- if ! command -v uv >/dev/null 2>&1; then
- echo "错误: 未找到 uv。请先安装 uv(https://docs.astral.sh/uv/)并确保在 PATH 中。" >&2
- exit 127
- fi
- }
- # ---- 1. ModelScope 下载 gpt2 模型到 models/ ----
- download_model() {
- if [ -s "$MODEL_DIR/model.onnx" ] && [ -s "$MODEL_DIR/tokenizer.json" ]; then
- echo "[1/3] 模型已存在,跳过下载: $MODEL_DIR"
- return 0
- fi
- echo "[1/3] 使用 ModelScope 下载 $MODEL_REPO -> $MODEL_DIR"
- mkdir -p "$MODEL_DIR"
- # 在临时环境安装 modelscope 并执行下载,不污染项目依赖
- uv run --no-project --with modelscope python - "$MODEL_REPO" "$MODEL_DIR" <<'PY'
- import os
- import shutil
- import sys
- from modelscope import snapshot_download
- repo = sys.argv[1]
- target = sys.argv[2]
- os.makedirs(target, exist_ok=True)
- try:
- snapshot_download(repo, local_dir=target)
- except TypeError:
- # 旧版 modelscope 不支持 local_dir:下载到缓存后复制到目标目录
- cached = snapshot_download(repo)
- for name in os.listdir(cached):
- src = os.path.join(cached, name)
- dst = os.path.join(target, name)
- if os.path.isdir(src):
- shutil.copytree(src, dst, dirs_exist_ok=True)
- else:
- shutil.copy2(src, dst)
- PY
- if [ ! -s "$MODEL_DIR/model.onnx" ] || [ ! -s "$MODEL_DIR/tokenizer.json" ]; then
- echo "错误: 模型下载后缺少 model.onnx 或 tokenizer.json: $MODEL_DIR" >&2
- return 1
- fi
- echo "模型就绪: $MODEL_DIR"
- }
- # ---- 2. uv 安装项目依赖 ----
- install_deps() {
- echo "[2/3] 使用 uv 安装项目依赖(uv sync --frozen)"
- uv sync --frozen
- if [ ! -f "$ENV_FILE" ]; then
- cp "$REPO_ROOT/.env.example" "$ENV_FILE"
- echo "注意: .env 不存在,已从 .env.example 复制。"
- echo "请编辑 .env 填写 DEEPSEEK_API_KEY 等配置,然后执行: sh deploy.sh restart"
- fi
- }
- # ---- 服务进程管理 ----
- is_running() {
- [ -f "$PID_FILE" ] || return 1
- pid=$(cat "$PID_FILE" 2>/dev/null || true)
- [ -n "$pid" ] || return 1
- kill -0 "$pid" 2>/dev/null
- }
- wait_healthy() {
- tries=0
- while [ "$tries" -lt 30 ]; do
- if command -v curl >/dev/null 2>&1; then
- if curl -fsS "$HEALTH_URL" >/dev/null 2>&1; then
- return 0
- fi
- elif is_running; then
- sleep 2
- return 0
- fi
- tries=$((tries + 1))
- sleep 1
- done
- return 1
- }
- start_service() {
- if is_running; then
- echo "服务已在运行(PID $(cat "$PID_FILE")),跳过启动;如需重启请执行: sh deploy.sh restart"
- return 0
- fi
- rm -f "$PID_FILE"
- mkdir -p "$REPO_ROOT/output"
- echo "[3/3] 后台启动 proposa-api(端口 $API_PORT)"
- echo "日志: $LOG_FILE"
- nohup uv run --frozen proposa-api >>"$LOG_FILE" 2>&1 &
- echo $! > "$PID_FILE"
- if wait_healthy; then
- echo "健康检查通过: $HEALTH_URL"
- else
- echo "警告: 30 秒内健康检查未通过,请查看日志: $LOG_FILE" >&2
- tail -n 20 "$LOG_FILE" >&2 || true
- return 1
- fi
- }
- stop_service() {
- if ! is_running; then
- echo "服务未在运行"
- rm -f "$PID_FILE"
- return 0
- fi
- pid=$(cat "$PID_FILE")
- echo "停止 proposa-api(PID $pid)"
- kill "$pid" 2>/dev/null || true
- tries=0
- while [ "$tries" -lt 10 ] && is_running; do
- sleep 1
- tries=$((tries + 1))
- done
- if is_running; then
- echo "进程未在 10 秒内退出,强制终止" >&2
- kill -9 "$pid" 2>/dev/null || true
- fi
- rm -f "$PID_FILE"
- echo "已停止"
- }
- status_service() {
- if is_running; then
- echo "运行中: PID $(cat "$PID_FILE")"
- echo "日志: $LOG_FILE"
- echo "健康检查: $HEALTH_URL"
- if command -v curl >/dev/null 2>&1; then
- curl -fsS "$HEALTH_URL" 2>/dev/null || echo "(/health 暂不可达)"
- echo
- fi
- else
- echo "未运行"
- fi
- }
- deploy() {
- echo "== Proposa AI 部署 =="
- echo "仓库: $REPO_ROOT"
- echo "模型: $MODEL_REPO"
- echo "uv 缓存: $UV_CACHE_DIR"
- echo
- require_uv
- download_model
- install_deps
- if [ "${RESTART:-0}" = "1" ]; then
- stop_service
- fi
- start_service
- }
- ACTION=${1:-deploy}
- case "$ACTION" in
- deploy)
- deploy
- ;;
- status)
- status_service
- ;;
- stop)
- stop_service
- ;;
- restart)
- stop_service
- start_service
- ;;
- -h|--help|help)
- usage
- ;;
- *)
- echo "错误: 未知命令 '$ACTION'" >&2
- usage >&2
- exit 2
- ;;
- esac
|