deploy.sh 9.6 KB

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