install.sh 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/bin/sh
  2. # 观澜·如东样板 v2 — Linux/macOS 离线安装 (支持 银河麒麟 V10 / 统信 UOS 20 / Ubuntu / CentOS / macOS)
  3. # 用法: sh install.sh (自动选 Python; 无合适系统 Python 时用包内便携运行时)
  4. # GUANLAN_PY=/path/python3 sh install.sh (指定 Python, 多版本机器用)
  5. # ★P13:按模块安装/卸载 —— `--module <名[,名]>` 时逐模块调用该模块自带的同名脚本;
  6. # 其余参数原样透传;不带 --module 时**保持原行为**。
  7. MODULES_ARG=""
  8. _prev=""
  9. for _a in "$@"; do
  10. if [ "$_prev" = "--module" ]; then MODULES_ARG="$_a"; fi
  11. _prev="$_a"
  12. done
  13. if [ -n "$MODULES_ARG" ]; then
  14. _status=0
  15. _ifs=$IFS; IFS=','
  16. for _m in $MODULES_ARG; do
  17. if [ -x "$_m/install.sh" ]; then
  18. sh "$_m/install.sh" "$@" || _status=$?
  19. else
  20. echo "[X] 未知模块: $_m" >&2; _status=2
  21. fi
  22. done
  23. IFS=$_ifs
  24. exit $_status
  25. fi
  26. set -e
  27. # 选项 (用户令 2026-09-17): --force 跳过"已安装?"的确认; --service 装成 systemd 服务(需 root)
  28. FORCE=0; DO_SERVICE=0
  29. for arg in "$@"; do
  30. case "$arg" in
  31. --force|-f) FORCE=1 ;;
  32. --service|-s) DO_SERVICE=1 ;;
  33. esac
  34. done
  35. # 同理: 全局 PYTHONPATH 会让 pip 误判依赖已满足而不装进 .venv (见 install.ps1 注释)
  36. unset PYTHONPATH
  37. cd "$(dirname "$0")"; ROOT=$(pwd); export PYTHONUTF8=1 LC_ALL=${LC_ALL:-C.UTF-8}
  38. say(){ printf '%s\n' "$*"; }
  39. ARCH=$(uname -m); case "$ARCH" in x86_64|amd64) A=x86_64;; aarch64|arm64) A=aarch64;; *) A=$ARCH;; esac
  40. OS=$(uname -s)
  41. DISTRO=$( [ -f /etc/os-release ] && . /etc/os-release && echo "${PRETTY_NAME:-$NAME}" || echo "$OS" )
  42. say "== 观澜 v2 安装 · $DISTRO · $ARCH"
  43. # 0) 安装前检查: 是否已安装及版本 (用户令 2026-09-17)
  44. say "== 0/7 安装前检查: 是否已安装及版本差异"
  45. # 版本单一真源 = 公共层的 version.py(模块化重构后位于 app_common/app_common_guanlan/);
  46. # ★2026-09-22 布局无关化: 新路径优先, src/version.py 现在只是兼容转发壳(无 VERSION 行)。
  47. PKG_VER=""
  48. for _vf in app_common/app_common_guanlan/version.py src/version.py; do
  49. [ -f "$_vf" ] || continue
  50. PKG_VER=$(sed -n "s/^VERSION *= *'\([^']*\)'.*/\1/p" "$_vf" 2>/dev/null | head -1)
  51. [ -n "$PKG_VER" ] && break
  52. done
  53. [ -n "$PKG_VER" ] || PKG_VER=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' dist-manifest.json 2>/dev/null | head -1)
  54. PREV_VER=""; PREV_AT=""; PREV_MODE=""
  55. if [ -f install-info.json ]; then
  56. PREV_VER=$("$ROOT/.venv/bin/python" -c "import json;print(json.load(open('install-info.json', encoding='utf-8')).get('version',''))" 2>/dev/null \
  57. || sed -n 's/.*"version": *"\([^"]*\).*/\1/p' install-info.json | head -1)
  58. PREV_AT=$(sed -n 's/.*"installed_at": *"\([^"]*\).*/\1/p' install-info.json | head -1)
  59. PREV_MODE=$(sed -n 's/.*"mode": *"\([^"]*\).*/\1/p' install-info.json | head -1)
  60. fi
  61. if [ -z "$PREV_VER" ] && [ ! -f install-info.json ]; then
  62. say " 未检测到已安装的观澜(将全新安装 v$PKG_VER)"
  63. else
  64. say " 已安装: v${PREV_VER:-(无版本记录)}(装于 ${PREV_AT:-?}; 运行方式 ${PREV_MODE:-?})"
  65. if [ "$PREV_VER" = "$PKG_VER" ]; then
  66. say " [i] 版本相同(v$PKG_VER): 重装会重建 .venv、覆盖程序文件; data/ 与 outputs/ 不受影响"
  67. elif [ -z "$PREV_VER" ]; then
  68. say " [!] 旧安装没有版本记录 —— 本包 v$PKG_VER 将按'重装/升级'处理"
  69. else
  70. say " [!] 版本差异: v$PREV_VER -> v$PKG_VER"
  71. case "$(printf '%s\n%s\n' "$PREV_VER" "$PKG_VER" | sort -V | tail -1)" in
  72. "$PREV_VER") say " [X] 已装版本比本包更新 —— 用旧包覆盖新装通常不是你想要的, 请确认";;
  73. esac
  74. fi
  75. if [ "$FORCE" = "1" ]; then say " (--force: 跳过询问, 直接继续)"
  76. elif [ ! -t 0 ]; then say " (非交互 stdin: 自动继续安装)"
  77. else
  78. printf " 是否重新安装/覆盖? 输入 y 继续, 其它键退出: "
  79. read -r ANS
  80. case "$ANS" in y|Y|yes|YES) ;; *) say " 已取消(未改任何文件)"; exit 0;; esac
  81. fi
  82. fi
  83. # 1) 选 Python: 显式指定 > 系统 3.11/3.12 > 包内便携运行时 (麒麟/统信自带多为 3.7/3.9, 跑不了本项目依赖)
  84. PY=""
  85. if [ -n "$GUANLAN_PY" ]; then PY="$GUANLAN_PY"
  86. else
  87. for c in python3.12 python3.13 python3.11 python3; do
  88. p=$(command -v $c 2>/dev/null) || continue
  89. "$p" -c 'import sys; sys.exit(0 if sys.version_info[:2] >= (3,11) else 1)' 2>/dev/null && { PY="$p"; break; }
  90. done
  91. fi
  92. if [ -z "$PY" ]; then
  93. RT=$(ls vendor/python/cpython-3.12*-$A-*linux-gnu-install_only*.tar.gz 2>/dev/null | head -1)
  94. [ "$OS" = Darwin ] && RT=""
  95. if [ -n "$RT" ]; then
  96. say " 系统无 Python 3.11+, 用包内便携运行时: $(basename "$RT")"
  97. rm -rf runtime && mkdir -p runtime && tar -xzf "$RT" -C runtime
  98. PY="$ROOT/runtime/python/bin/python3"
  99. else
  100. say " ✗ 未找到 Python 3.11+ , 包内也没有本架构 ($A) 的便携运行时。"
  101. say " 办法一: 装系统 Python 3.11+ ; 办法二: GUANLAN_PY=/路径/python3 sh install.sh"
  102. exit 2
  103. fi
  104. fi
  105. say " Python: $PY ($("$PY" -V 2>&1))"
  106. # 2) 虚拟环境 + 离线依赖 (轮子在 wheels/linux_<arch> 或 wheels/macos_arm64)
  107. "$PY" -m venv .venv 2>/dev/null || "$PY" -m venv --without-pip .venv
  108. VPY=$ROOT/.venv/bin/python
  109. "$VPY" -m ensurepip --upgrade >/dev/null 2>&1 || true
  110. "$VPY" -m pip install --upgrade pip --quiet 2>/dev/null || true
  111. case "$OS" in Darwin) W=wheels/macos_arm64;; *) W=wheels/linux_$A;; esac
  112. if [ -d "$W" ] && [ "$(ls -A "$W" 2>/dev/null | wc -l)" -gt 0 ]; then
  113. say "== 离线安装依赖 ($W)"; "$VPY" -m pip install --no-index --find-links "$W" -r requirements.txt --quiet
  114. else
  115. say "== 包内无本平台轮子 ($W), 改为联网安装"; "$VPY" -m pip install -r requirements.txt --quiet
  116. fi
  117. # 3) 写配置 (python 指向本 venv)
  118. "$VPY" - <<'PYEOF'
  119. import json, pathlib, sys
  120. p = pathlib.Path('configs/serve.json') # portability-allow: 安装脚本已 cd 到安装根
  121. c = json.loads(p.read_text(encoding='utf-8'))
  122. # 写**相对**路径 (相对安装根): 整个目录拷到别的电脑/别的盘后仍然有效 (绝对路径会在换机后失效)
  123. exe = pathlib.Path(sys.executable).resolve(); root = pathlib.Path('.').resolve()
  124. try:
  125. c['python'] = exe.relative_to(root).as_posix()
  126. except ValueError:
  127. c['python'] = exe.as_posix()
  128. p.write_text(json.dumps(c, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  129. print(' configs/serve.json -> ' + c['python'] + ' (相对安装根)')
  130. PYEOF
  131. # 4) Ollama (可选; 只影响问答与本地审核)
  132. if command -v ollama >/dev/null 2>&1; then say "== Ollama 已安装: $(ollama -v 2>&1 | head -1)"
  133. elif [ -f vendor/ollama/ollama-linux-$( [ "$A" = x86_64 ] && echo amd64 || echo arm64 ).tar.zst ]; then
  134. say "== 检测到包内 Ollama 离线包; 安装需 root:"
  135. say " sudo tar -I zstd -C /usr -xf vendor/ollama/ollama-linux-*.tar.zst && sudo useradd -r -s /bin/false -m -d /usr/share/ollama ollama 2>/dev/null; ollama serve &"
  136. say " (麒麟/统信若无 zstd: sudo yum install zstd 或 sudo apt install zstd)"
  137. else say "== 未装 Ollama: 问答与本地审核不可用, 其余页面正常 (装法见 说明书 §5)"; fi
  138. say "== 自检"; "$VPY" guanlan.py check || true
  139. say ""
  140. # 服务化 (可选; 用户令 2026-09-17): systemd 单元 guanlan.service, 需 root
  141. if [ "$DO_SERVICE" = "1" ]; then
  142. say "== 服务化 (systemd, 需 root)"
  143. "$VPY" scripts/service_ctl.py install || say " [!] 服务注册没成功 —— 不影响使用: 仍可用 guanlan.py serve 手工启动"
  144. else
  145. say "== 服务化 (未指定 --service, 跳过)"
  146. say " 装成开机自启: sudo $VPY scripts/service_ctl.py install (先看计划加 --dry-run)"
  147. fi
  148. # 安装记录 (版本单一真源 src/version.py): 安装检查与 guanlan.py check 都读它
  149. MODE=manual; [ "$DO_SERVICE" = "1" ] && MODE=systemd
  150. "$VPY" -c "import sys,pathlib; sys.path.insert(0,str(pathlib.Path('.').resolve())); from src import version as V; print(' 安装记录: ' + str(V.write_installed(pathlib.Path('.').resolve(), python=sys.executable, mode='$MODE')))"
  151. say ""
  152. say "安装完成. 服务: $VPY scripts/service_ctl.py status|start|stop"
  153. say " 手工: $VPY guanlan.py serve 停止: $VPY guanlan.py stop 入口: http://127.0.0.1:28084/"