deploy.sh 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. #!/usr/bin/env sh
  2. #
  3. # Proposa AI 服务部署脚本(Linux 服务器)
  4. #
  5. # 功能:
  6. # 1. 使用 ModelScope 将 gpt2-chinese-cluecorpussmall-onnx 下载到 models/ 目录
  7. # (模型文件已存在时自动跳过,幂等)
  8. # 2. 使用 uv 按 uv.lock 安装整个项目依赖(uv sync --frozen)
  9. # 3. 后台启动 proposa-api,写入 PID/日志文件并做 /health 健康检查
  10. #
  11. # 用法:
  12. # sh deploy.sh 完整部署并后台启动(可重复执行)
  13. # sh deploy.sh status 查看服务运行状态与健康检查
  14. # sh deploy.sh stop 停止后台服务
  15. # sh deploy.sh restart 停止后重新后台启动
  16. #
  17. # 可用环境变量:
  18. # MODEL_REPO ModelScope 模型仓库 ID(默认 Maiteka/gpt2-chinese-cluecorpussmall-onnx)
  19. # MODEL_DIR 模型保存目录(默认 <仓库根>/models/gpt2-chinese-cluecorpussmall-onnx)
  20. # UV_CACHE_DIR uv 缓存目录(默认 <仓库根>/.uv-cache)
  21. # PROPOSA_API_PORT 健康检查端口(默认读取 .env 的 PROPOSA_API_PORT,缺省 8000)
  22. # RESTART=1 服务已运行时强制重启(等价于 sh deploy.sh restart)
  23. set -eu
  24. case "$0" in
  25. */*) SCRIPT_DIR=${0%/*} ;;
  26. *) SCRIPT_DIR=. ;;
  27. esac
  28. REPO_ROOT=$(CDPATH= cd "$SCRIPT_DIR" && pwd)
  29. cd "$REPO_ROOT"
  30. MODEL_REPO=${MODEL_REPO:-"Maiteka/gpt2-chinese-cluecorpussmall-onnx"}
  31. MODEL_DIR=${MODEL_DIR:-"$REPO_ROOT/models/gpt2-chinese-cluecorpussmall-onnx"}
  32. UV_CACHE_DIR=${UV_CACHE_DIR:-"$REPO_ROOT/.uv-cache"}
  33. export UV_CACHE_DIR
  34. ENV_FILE="$REPO_ROOT/.env"
  35. PID_FILE="$REPO_ROOT/output/api.pid"
  36. LOG_FILE="$REPO_ROOT/output/api.log"
  37. usage() {
  38. cat <<'EOF'
  39. Proposa AI 服务部署脚本(Linux 服务器)
  40. 用法:
  41. sh deploy.sh 完整部署并后台启动(可重复执行)
  42. sh deploy.sh status 查看服务运行状态与健康检查
  43. sh deploy.sh stop 停止后台服务
  44. sh deploy.sh restart 停止后重新后台启动
  45. 环境变量:
  46. MODEL_REPO ModelScope 模型仓库 ID(默认 Maiteka/gpt2-chinese-cluecorpussmall-onnx)
  47. MODEL_DIR 模型保存目录(默认 <仓库根>/models/gpt2-chinese-cluecorpussmall-onnx)
  48. UV_CACHE_DIR uv 缓存目录(默认 <仓库根>/.uv-cache)
  49. PROPOSA_API_PORT 健康检查端口(默认读取 .env 的 PROPOSA_API_PORT,缺省 8000)
  50. RESTART=1 服务已运行时强制重启(等价于 sh deploy.sh restart)
  51. EOF
  52. }
  53. # 从 .env 读取 PROPOSA_API_PORT(容忍行首空格),读不到输出空串
  54. env_port() {
  55. [ -f "$ENV_FILE" ] || return 0
  56. line=$(grep -E '^[[:space:]]*PROPOSA_API_PORT[[:space:]]*=' "$ENV_FILE" 2>/dev/null | tail -n 1 || true)
  57. [ -n "$line" ] || return 0
  58. printf '%s\n' "$line" | sed 's/^[[:space:]]*PROPOSA_API_PORT[[:space:]]*=[[:space:]]*//' | tr -d '[:space:]"'
  59. }
  60. API_PORT=${PROPOSA_API_PORT:-$(env_port)}
  61. API_PORT=${API_PORT:-8000}
  62. case "$API_PORT" in
  63. ''|*[!0-9]*)
  64. echo "错误: 无效的 API 端口 '$API_PORT',请设置 PROPOSA_API_PORT 或检查 .env" >&2
  65. exit 2
  66. ;;
  67. esac
  68. HEALTH_URL="http://127.0.0.1:$API_PORT/health"
  69. require_uv() {
  70. if ! command -v uv >/dev/null 2>&1; then
  71. echo "错误: 未找到 uv。请先安装 uv(https://docs.astral.sh/uv/)并确保在 PATH 中。" >&2
  72. exit 127
  73. fi
  74. }
  75. # ---- 1. ModelScope 下载 gpt2 模型到 models/ ----
  76. download_model() {
  77. if [ -s "$MODEL_DIR/model.onnx" ] && [ -s "$MODEL_DIR/tokenizer.json" ]; then
  78. echo "[1/3] 模型已存在,跳过下载: $MODEL_DIR"
  79. return 0
  80. fi
  81. echo "[1/3] 使用 ModelScope 下载 $MODEL_REPO -> $MODEL_DIR"
  82. mkdir -p "$MODEL_DIR"
  83. # 在临时环境安装 modelscope 并执行下载,不污染项目依赖
  84. uv run --no-project --with modelscope python - "$MODEL_REPO" "$MODEL_DIR" <<'PY'
  85. import os
  86. import shutil
  87. import sys
  88. from modelscope import snapshot_download
  89. repo = sys.argv[1]
  90. target = sys.argv[2]
  91. os.makedirs(target, exist_ok=True)
  92. try:
  93. snapshot_download(repo, local_dir=target)
  94. except TypeError:
  95. # 旧版 modelscope 不支持 local_dir:下载到缓存后复制到目标目录
  96. cached = snapshot_download(repo)
  97. for name in os.listdir(cached):
  98. src = os.path.join(cached, name)
  99. dst = os.path.join(target, name)
  100. if os.path.isdir(src):
  101. shutil.copytree(src, dst, dirs_exist_ok=True)
  102. else:
  103. shutil.copy2(src, dst)
  104. PY
  105. if [ ! -s "$MODEL_DIR/model.onnx" ] || [ ! -s "$MODEL_DIR/tokenizer.json" ]; then
  106. echo "错误: 模型下载后缺少 model.onnx 或 tokenizer.json: $MODEL_DIR" >&2
  107. return 1
  108. fi
  109. echo "模型就绪: $MODEL_DIR"
  110. }
  111. # ---- 2. uv 安装项目依赖 ----
  112. install_deps() {
  113. echo "[2/3] 使用 uv 安装项目依赖(uv sync --frozen)"
  114. uv sync --frozen
  115. if [ ! -f "$ENV_FILE" ]; then
  116. cp "$REPO_ROOT/.env.example" "$ENV_FILE"
  117. echo "注意: .env 不存在,已从 .env.example 复制。"
  118. echo "请编辑 .env 填写 DEEPSEEK_API_KEY 等配置,然后执行: sh deploy.sh restart"
  119. fi
  120. }
  121. # ---- 服务进程管理 ----
  122. is_running() {
  123. [ -f "$PID_FILE" ] || return 1
  124. pid=$(cat "$PID_FILE" 2>/dev/null || true)
  125. [ -n "$pid" ] || return 1
  126. kill -0 "$pid" 2>/dev/null
  127. }
  128. wait_healthy() {
  129. tries=0
  130. while [ "$tries" -lt 30 ]; do
  131. if command -v curl >/dev/null 2>&1; then
  132. if curl -fsS "$HEALTH_URL" >/dev/null 2>&1; then
  133. return 0
  134. fi
  135. elif is_running; then
  136. sleep 2
  137. return 0
  138. fi
  139. tries=$((tries + 1))
  140. sleep 1
  141. done
  142. return 1
  143. }
  144. start_service() {
  145. if is_running; then
  146. echo "服务已在运行(PID $(cat "$PID_FILE")),跳过启动;如需重启请执行: sh deploy.sh restart"
  147. return 0
  148. fi
  149. rm -f "$PID_FILE"
  150. mkdir -p "$REPO_ROOT/output"
  151. echo "[3/3] 后台启动 proposa-api(端口 $API_PORT)"
  152. echo "日志: $LOG_FILE"
  153. nohup uv run --frozen proposa-api >>"$LOG_FILE" 2>&1 &
  154. echo $! > "$PID_FILE"
  155. if wait_healthy; then
  156. echo "健康检查通过: $HEALTH_URL"
  157. else
  158. echo "警告: 30 秒内健康检查未通过,请查看日志: $LOG_FILE" >&2
  159. tail -n 20 "$LOG_FILE" >&2 || true
  160. return 1
  161. fi
  162. }
  163. stop_service() {
  164. if ! is_running; then
  165. echo "服务未在运行"
  166. rm -f "$PID_FILE"
  167. return 0
  168. fi
  169. pid=$(cat "$PID_FILE")
  170. echo "停止 proposa-api(PID $pid)"
  171. kill "$pid" 2>/dev/null || true
  172. tries=0
  173. while [ "$tries" -lt 10 ] && is_running; do
  174. sleep 1
  175. tries=$((tries + 1))
  176. done
  177. if is_running; then
  178. echo "进程未在 10 秒内退出,强制终止" >&2
  179. kill -9 "$pid" 2>/dev/null || true
  180. fi
  181. rm -f "$PID_FILE"
  182. echo "已停止"
  183. }
  184. status_service() {
  185. if is_running; then
  186. echo "运行中: PID $(cat "$PID_FILE")"
  187. echo "日志: $LOG_FILE"
  188. echo "健康检查: $HEALTH_URL"
  189. if command -v curl >/dev/null 2>&1; then
  190. curl -fsS "$HEALTH_URL" 2>/dev/null || echo "(/health 暂不可达)"
  191. echo
  192. fi
  193. else
  194. echo "未运行"
  195. fi
  196. }
  197. deploy() {
  198. echo "== Proposa AI 部署 =="
  199. echo "仓库: $REPO_ROOT"
  200. echo "模型: $MODEL_REPO"
  201. echo "uv 缓存: $UV_CACHE_DIR"
  202. echo
  203. require_uv
  204. download_model
  205. install_deps
  206. if [ "${RESTART:-0}" = "1" ]; then
  207. stop_service
  208. fi
  209. start_service
  210. }
  211. ACTION=${1:-deploy}
  212. case "$ACTION" in
  213. deploy)
  214. deploy
  215. ;;
  216. status)
  217. status_service
  218. ;;
  219. stop)
  220. stop_service
  221. ;;
  222. restart)
  223. stop_service
  224. start_service
  225. ;;
  226. -h|--help|help)
  227. usage
  228. ;;
  229. *)
  230. echo "错误: 未知命令 '$ACTION'" >&2
  231. usage >&2
  232. exit 2
  233. ;;
  234. esac