#!/usr/bin/env bash set -euo pipefail # ============================================================================== # installpi.sh - 一键全自动安装 pi + llama.cpp (源码编译) + Qwen2.5 模型 # 特性:无 sudo、无互动、全自动、可断点续传、安装后自动验证 # 模型:unsloth/GLM-4.7-Flash-GGUF (GLM-4.7-Flash-Q4_K_M.gguf) # 启动:taskset -c 0-14 llama-server ... -t 15 --jinja --load-mode mlock -c 65536 (64K上下文) # ============================================================================== HOME_DIR="$HOME" # 脚本一开始就把之前安装的 node / pi / llama 路径放进 PATH,避免每次重复装 node export PATH="${XDG_DATA_HOME:-$HOME/.local/share}/pi-node/current/bin:$HOME/.local/bin:$HOME/.pi/bin:$PATH" LLAMA_SRC_DIR="$HOME_DIR/llama.cpp" # 源码目录 LLAMA_BIN="$LLAMA_SRC_DIR/build/bin" # 编译后二进制目录 MODEL_DIR="$HOME_DIR/models" PI_MODELS_JSON="$HOME_DIR/.pi/agent/models.json" LLAMA_PORT=8080 REPO="unsloth/GLM-4.7-Flash-GGUF" MODEL_FILE="GLM-4.7-Flash-Q4_K_M.gguf" MODEL_ID="glm-4.7-flash-q4_k_m" PROVIDER_ID="llama-cpp-local" CONTEXT_SIZE=65536 # 上下文 64K # 基础输出函数 info() { echo -e "\033[36m[INFO]\033[0m $*"; } ok() { echo -e "\033[32m[OK]\033[0m $*"; } warn() { echo -e "\033[33m[WARN]\033[0m $*" >&2; } error() { echo -e "\033[31m[ERROR]\033[0m $*" >&2; exit 1; } # 确保必要目录 mkdir -p "$LLAMA_SRC_DIR" "$MODEL_DIR" "$(dirname "$PI_MODELS_JSON")" "$HOME_DIR/.local/bin" # 检查必要工具(新增编译依赖) for cmd in curl python3 tar git cmake g++; do command -v "$cmd" >/dev/null 2>&1 || error "缺少必要工具: $cmd,请先安装" done # ============================================================================== # 0. 确保 Node.js >=22.19.0 (pi 依赖) # ============================================================================== ensure_node() { if command -v node >/dev/null 2>&1; then if node -e 'const [maj,min,patch]=process.versions.node.split(".").map(Number); process.exit(maj>22||(maj===22&&(min>19||(min===19&&patch>=0)))?0:1)' >/dev/null 2>&1; then ok "Node.js 已满足: $(node --version)" return 0 else warn "Node.js 版本过旧: $(node --version),将自动安装新版..." fi else info "未检测到 Node.js,将自动安装..." fi info "自动安装 Node.js 22.x (standalone 模式)..." NODE_DIST_BASE="https://nodejs.org/dist/latest-v22.x" NODE_BASE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/pi-node" NODE_TMP_DIR="/tmp/pi-node-$$" mkdir -p "$NODE_TMP_DIR" "$NODE_BASE_DIR" OS="$(uname -s)" ARCH="$(uname -m)" case "$OS" in Linux) PLATFORM="linux" ;; Darwin) PLATFORM="darwin" ;; *) error "不支持的 OS: $OS" ;; esac case "$ARCH" in x86_64|amd64) NODE_ARCH="x64" ;; aarch64|arm64) NODE_ARCH="arm64" ;; *) error "不支持的架构: $ARCH" ;; esac info "解析 Node.js 下载链接 ($PLATFORM-$NODE_ARCH)..." curl -fsSL --retry 3 --retry-delay 5 "$NODE_DIST_BASE/SHASUMS256.txt" -o "$NODE_TMP_DIR/SHASUMS256.txt" || error "下载 SHASUMS 失败" NODE_FILE=$(awk -v suffix="-$PLATFORM-$NODE_ARCH.tar.xz" 'index($2,"node-v")==1 && substr($2,length($2)-length(suffix)+1)==suffix{print $2; exit}' "$NODE_TMP_DIR/SHASUMS256.txt") [[ -n "$NODE_FILE" ]] || error "找不到对应平台的 Node.js 二进制" info "下载 Node.js $NODE_FILE ..." curl -fsSL --retry 3 --retry-delay 5 "$NODE_DIST_BASE/$NODE_FILE" -o "$NODE_TMP_DIR/$NODE_FILE" || error "Node.js 下载失败" if command -v sha256sum >/dev/null 2>&1; then (cd "$NODE_TMP_DIR" && grep -F "$NODE_FILE" SHASUMS256.txt > SHASUMS256.selected && sha256sum -c SHASUMS256.selected) || warn "SHA256 校验失败,继续安装..." fi if ! command -v xz >/dev/null 2>&1; then warn "未找到 xz,将尝试使用 tar 自动解压 (若失败请手动安装 xz-utils)" fi NODE_EXTRACT_DIR="$NODE_BASE_DIR/${NODE_FILE%.tar.xz}" rm -rf "$NODE_EXTRACT_DIR" tar -xf "$NODE_TMP_DIR/$NODE_FILE" -C "$NODE_BASE_DIR" || error "解压 Node.js 失败" rm -f "$NODE_BASE_DIR/current" ln -s "$NODE_EXTRACT_DIR" "$NODE_BASE_DIR/current" rm -rf "$NODE_TMP_DIR" export PATH="$NODE_BASE_DIR/current/bin:$PATH" for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do if [[ -f "$rc" ]] || [[ "$rc" == "$HOME/.bashrc" ]]; then grep -q "pi-node/current/bin" "$rc" 2>/dev/null || echo "export PATH=\"$NODE_BASE_DIR/current/bin:\$PATH\"" >> "$rc" fi done if command -v node >/dev/null 2>&1; then ok "Node.js 安装完成: $(node --version) at $(command -v node)" else error "Node.js 安装后仍无法找到,请检查 PATH" fi } ensure_node command -v npm >/dev/null 2>&1 || error "npm 仍不可用,Node 安装可能失败" info "npm 版本: $(npm --version)" # ============================================================================== # 1. 编译 llama.cpp (从源码) # ============================================================================== info "1) 编译 llama.cpp (源码)..." if [[ "${LLAMA_FORCE_REBUILD:-0}" == "1" ]]; then info "LLAMA_FORCE_REBUILD=1,清理旧 build 目录强制重编..." rm -rf "$LLAMA_SRC_DIR/build" fi if [[ ! -x "$LLAMA_BIN/llama-server" ]]; then info "克隆 llama.cpp 仓库 (深度1)..." if [[ -d "$LLAMA_SRC_DIR/.git" ]]; then info "源码目录已存在,更新..." cd "$LLAMA_SRC_DIR" git pull || warn "git pull 失败,继续使用现有源码" else git clone --depth 1 https://github.com/ggml-org/llama.cpp "$LLAMA_SRC_DIR" fi cd "$LLAMA_SRC_DIR" info "执行 cmake 配置 (纯 CPU,不开 NATIVE/DL,避免 MoE/多变体后端在纯 CPU 上崩)..." rm -rf build cmake -B build \ -DGGML_NATIVE=OFF \ -DGGML_CPU=ON \ -DGGML_BACKEND_DL=OFF \ -DLLAMA_BUILD_TESTS=OFF \ -DCMAKE_BUILD_TYPE=Release info "开始编译 (使用 -j16)..." cmake --build build --config Release -j 16 # 验证编译产物 if [[ -x "$LLAMA_BIN/llama-server" ]]; then ok "llama.cpp 编译完成: $LLAMA_BIN/llama-server" else error "编译失败,找不到 llama-server" fi else ok "llama-server 已存在,跳过编译 (如需重编: LLAMA_FORCE_REBUILD=1 ./installpi.sh)" fi # 更新 PATH export PATH="$LLAMA_BIN:$HOME_DIR/.local/bin:$PATH" export LD_LIBRARY_PATH="$LLAMA_BIN:${LD_LIBRARY_PATH:-}" for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do touch "$rc" 2>/dev/null || true grep -q "llama.cpp/build/bin" "$rc" 2>/dev/null || echo "export PATH=\"$LLAMA_BIN:\$PATH\"" >> "$rc" grep -q "LD_LIBRARY_PATH.*llama.cpp" "$rc" 2>/dev/null || echo "export LD_LIBRARY_PATH=\"$LLAMA_BIN:\$LD_LIBRARY_PATH\"" >> "$rc" done # ============================================================================== # 2. 下载模型 (直接使用指定 URL) # ============================================================================== info "2) 下载模型 ${REPO} -> ${MODEL_FILE} ..." MODEL_URL="https://huggingface.co/${REPO}/resolve/main/${MODEL_FILE}" MODEL_PATH="$MODEL_DIR/${MODEL_FILE}" if [[ ! -f "$MODEL_PATH" ]]; then info "开始下载模型 (支持断点续传)..." curl -L -C - --retry 3 --retry-delay 5 --progress-bar "$MODEL_URL" -o "$MODEL_PATH" || error "模型下载失败" ok "模型下载完成: $MODEL_PATH" else SIZE=$(stat -c%s "$MODEL_PATH" 2>/dev/null || stat -f%z "$MODEL_PATH" 2>/dev/null || echo 0) if [[ "$SIZE" -lt 1000000000 ]]; then warn "模型文件过小 (${SIZE} bytes),可能下载不完整,将尝试续传..." curl -L -C - --retry 3 --retry-delay 5 --progress-bar "$MODEL_URL" -o "$MODEL_PATH" || error "续传失败" fi ok "模型已存在,跳过下载: $MODEL_PATH ($SIZE bytes)" fi # ============================================================================== # 3. 启动 llama-server (使用 taskset 和指定参数,上下文 64K) # ============================================================================== info "3) 启动 llama-server (port $LLAMA_PORT, taskset 0-14, context $CONTEXT_SIZE)..." # 清理旧进程 if [[ -f "$HOME_DIR/llama-server.pid" ]]; then OLD_PID=$(cat "$HOME_DIR/llama-server.pid" 2>/dev/null || echo "") if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then info "停止旧 llama-server (PID $OLD_PID)..." kill "$OLD_PID" 2>/dev/null || true sleep 2 kill -9 "$OLD_PID" 2>/dev/null || true fi fi pkill -f "llama-server.*${LLAMA_PORT}" 2>/dev/null || true sleep 1 # 按实际 CPU 数决定 taskset 范围 / 线程数 (避免机器核数不足时 taskset 报错) NPROC=$(nproc 2>/dev/null || echo 16) if [[ "$NPROC" -ge 15 ]]; then TASKSET_ARGS=(-c 0-14) NTHREADS=15 else TASKSET_ARGS=(-c "0-$((NPROC-1))") NTHREADS="$NPROC" fi if ! taskset "${TASKSET_ARGS[@]}" true 2>/dev/null; then warn "taskset ${TASKSET_ARGS[*]} 不可用,改用不绑核启动" TASKSET_ARGS=() fi # 构建命令(prlimit 限制内存 32GB;LD_LIBRARY_PATH 已在前面 export,prlimit 会继承) CMD=( taskset "${TASKSET_ARGS[@]}" prlimit --as=34359738368 "$LLAMA_BIN/llama-server" -m "$MODEL_PATH" --alias "$MODEL_ID" -t "$NTHREADS" --jinja --load-mode mlock -c "$CONTEXT_SIZE" --host 127.0.0.1 --port "$LLAMA_PORT" ) # taskset 参数为空时去掉 taskset 本体 if [[ "${#TASKSET_ARGS[@]}" -eq 0 ]]; then CMD=(prlimit --as=34359738368 "$LLAMA_BIN/llama-server" -m "$MODEL_PATH" --alias "$MODEL_ID" -t "$NTHREADS" --jinja --load-mode mlock -c "$CONTEXT_SIZE" --host 127.0.0.1 --port "$LLAMA_PORT") fi info "启动命令: ${CMD[*]}" touch "$HOME_DIR/llama-server.log" 2>/dev/null || true nohup "${CMD[@]}" > "$HOME_DIR/llama-server.log" 2>&1 & echo $! > "$HOME_DIR/llama-server.pid" info "llama-server PID: $(cat "$HOME_DIR/llama-server.pid")" # 等待服务器就绪 (最长 5 分钟) info "等待 llama-server 就绪 (最多 5 分钟)..." READY=0 for i in {1..60}; do if curl -sf "http://127.0.0.1:${LLAMA_PORT}/health" >/dev/null 2>&1; then ok "llama-server 已就绪 (http://127.0.0.1:${LLAMA_PORT})" READY=1 break fi if curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/models" >/dev/null 2>&1; then ok "llama-server 已就绪 (via /v1/models)" READY=1 break fi if ! kill -0 $(cat "$HOME_DIR/llama-server.pid" 2>/dev/null || echo 0) 2>/dev/null; then error "llama-server 启动失败,请查看 ~/llama-server.log (最后 50 行):\n$(tail -n 50 "$HOME_DIR/llama-server.log" 2>/dev/null)" fi echo -n "." sleep 5 done echo "" if [[ "$READY" -ne 1 ]]; then error "llama-server 在 5 分钟内未就绪,请查看 ~/llama-server.log" fi # ============================================================================== # 4. 安装 pi agent (全自动,无互动) # ============================================================================== info "4) 安装 pi agent (全自动)..." NPM_PREFIX=$(npm prefix -g 2>/dev/null || npm config get prefix 2>/dev/null || echo "$HOME/.local") info "npm 全局前缀: $NPM_PREFIX" # 确保 PATH 包含本地 bin 目录 export PATH="$HOME/.local/bin:$HOME/.pi/bin:$NPM_PREFIX/bin:$PATH" NEED_INSTALL=1 if command -v pi >/dev/null 2>&1; then ok "pi 已安装: $(pi --version 2>&1 | head -n 1)" NEED_INSTALL=0 if [[ "${PI_FORCE_REINSTALL:-0}" == "1" ]]; then info "PI_FORCE_REINSTALL=1,强制重新安装..." NEED_INSTALL=1 fi fi if [[ "$NEED_INSTALL" -eq 1 ]]; then info "安装 pi (@earendil-works/pi-coding-agent)..." PI_PACKAGE="@earendil-works/pi-coding-agent" # 优先安装到 ~/.local (无需 sudo) INSTALL_PREFIX="$HOME/.local" info "安装到 $INSTALL_PREFIX (无需 sudo)" set +e npm install -g --ignore-scripts --no-fund --no-audit --progress=false --prefix "$INSTALL_PREFIX" "$PI_PACKAGE" 2>&1 | tee /tmp/pi-install.log NPM_STATUS=${PIPESTATUS[0]} set -e if [[ "$NPM_STATUS" -ne 0 ]]; then warn "npm 安装 pi 失败 (code $NPM_STATUS),尝试备用方法: curl 官方脚本..." curl -fsSL https://pi.dev/install.sh | bash -s -- 2>&1 | tee -a /tmp/pi-install.log || true fi export PATH="$HOME/.local/bin:$HOME/.pi/bin:$NPM_PREFIX/bin:$PATH" NODE_BIN_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/pi-node/current/bin" export PATH="$NODE_BIN_DIR:$PATH" hash -r 2>/dev/null || true fi PI_BIN=$(command -v pi 2>/dev/null || echo "") if [[ -z "$PI_BIN" ]]; then for cand in "$HOME/.local/bin/pi" "$HOME/.pi/bin/pi" "$NPM_PREFIX/bin/pi" "/usr/local/bin/pi"; do if [[ -x "$cand" ]]; then PI_BIN="$cand" export PATH="$(dirname "$cand"):$PATH" break fi done fi if [[ -z "$PI_BIN" ]] || [[ ! -x "$PI_BIN" ]]; then error "pi 安装后仍找不到执行档,请查看 /tmp/pi-install.log" fi ok "pi 可用: $PI_BIN ($(pi --version 2>&1 | head -n 1))" for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do touch "$rc" 2>/dev/null || true PI_BIN_DIR="$(dirname "$PI_BIN")" if [[ "$PI_BIN_DIR" != "" ]]; then grep -qF "$PI_BIN_DIR" "$rc" 2>/dev/null || echo "export PATH=\"$PI_BIN_DIR:\$PATH\"" >> "$rc" fi grep -qF ".local/bin" "$rc" 2>/dev/null || echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$rc" done # ============================================================================== # 5. 配置 pi 使用本地模型 (上下文调至极限) # ============================================================================== info "5) 配置 pi models.json ($PI_MODELS_JSON)..." mkdir -p "$(dirname "$PI_MODELS_JSON")" python3 - "$PI_MODELS_JSON" "$LLAMA_PORT" "$MODEL_ID" "$PROVIDER_ID" "$CONTEXT_SIZE" << 'PY' import json, os, sys path, port, model_id, provider, ctx_size = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], int(sys.argv[5]) os.makedirs(os.path.dirname(path), exist_ok=True) cfg = {"providers": {}} if os.path.exists(path): try: with open(path) as f: loaded = json.load(f) if isinstance(loaded, dict): cfg = loaded if "providers" not in cfg: cfg["providers"] = {} except Exception as e: print(f"[WARN] 无法解析旧 models.json,将覆盖: {e}") cfg["providers"][provider] = { "baseUrl": f"http://127.0.0.1:{port}/v1", "api": "openai-completions", "apiKey": "not-needed", "compat": {"supportsDeveloperRole": True, "supportsReasoningEffort": False}, "models": [{ "id": model_id, "name": "GLM-4.7-Flash Q4_K_M (Local)", "reasoning": True, "input": ["text"], "contextWindow": ctx_size, "maxTokens": ctx_size, "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0} }] } with open(path, "w") as f: json.dump(cfg, f, indent=2) print(f"[OK] 已写入 {path}") print(json.dumps(cfg, indent=2)) PY ok "pi 配置完成" WRAPPER="$HOME/.local/bin/pi-local" mkdir -p "$(dirname "$WRAPPER")" cat > "$WRAPPER" << EOS #!/usr/bin/env bash exec pi --provider $PROVIDER_ID --model $MODEL_ID "\$@" EOS chmod +x "$WRAPPER" ok "已建立 pi-local wrapper: $WRAPPER" for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do touch "$rc" 2>/dev/null || true grep -q "alias pi-local" "$rc" 2>/dev/null || echo "alias pi-local=\"pi --provider $PROVIDER_ID --model $MODEL_ID\"" >> "$rc" done # ============================================================================== # 6. 安装后验证 # ============================================================================== info "6) 安装后验证..." VERIFICATION_FAILED=0 # 6.1 检查 llama-server 健康 info "[验证 1/4] 检查 llama-server 健康状态..." if curl -sf "http://127.0.0.1:${LLAMA_PORT}/health" >/dev/null 2>&1; then ok "llama-server 健康检查通过" elif curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/models" >/dev/null 2>&1; then ok "llama-server (via /v1/models) 健康检查通过" else warn "llama-server 健康检查失败" VERIFICATION_FAILED=1 fi # 6.2 测试 OpenAI API 推理 info "[验证 2/4] 测试 OpenAI 兼容 API 推理..." API_TEST_RESPONSE=$(curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/chat/completions" \ -H "Content-Type: application/json" \ -d '{ "model": "'$MODEL_ID'", "messages": [{"role": "user", "content": "用一句话介绍你自己,不要超过20字"}], "max_tokens": 50, "temperature": 0.7 }' 2>&1 || echo "CURL_FAILED") if echo "$API_TEST_RESPONSE" | grep -q '"choices"' && echo "$API_TEST_RESPONSE" | grep -q '"content"'; then ok "OpenAI API 推理测试通过" echo "$API_TEST_RESPONSE" | python3 -m json.tool 2>/dev/null | head -n 30 || echo "$API_TEST_RESPONSE" | head -n 20 elif echo "$API_TEST_RESPONSE" | grep -q "CURL_FAILED"; then warn "API 推理 curl 失败: $API_TEST_RESPONSE" VERIFICATION_FAILED=1 else warn "API 推理回应异常,可能是模型仍在加载中..." echo "$API_TEST_RESPONSE" | head -n 50 >&2 fi # 6.3 检查 pi 可用 info "[验证 3/4] 检查 pi 指令..." if pi --version >/dev/null 2>&1; then ok "pi --version: $(pi --version 2>&1 | head -n 1)" else warn "pi --version 执行失败" VERIFICATION_FAILED=1 fi if pi --help >/dev/null 2>&1; then ok "pi --help 正常" fi if python3 -c "import json; d=json.load(open('$PI_MODELS_JSON')); assert '$PROVIDER_ID' in d.get('providers',{}); assert any(m['id']=='$MODEL_ID' for m in d['providers']['$PROVIDER_ID'].get('models',[]))" 2>/dev/null; then ok "pi models.json 配置正确" else warn "pi models.json 配置检查失败" cat "$PI_MODELS_JSON" 2>/dev/null || echo "文件不存在" VERIFICATION_FAILED=1 fi if [[ -x "$WRAPPER" ]]; then ok "pi-local wrapper 存在且可执行" else warn "pi-local wrapper 缺失" VERIFICATION_FAILED=1 fi # 6.4 测试 pi-local 实际可用 info "[验证 4/4] 测试 pi-local 实际推理 (超时 120 秒)..." export PATH="$HOME/.local/bin:$HOME/.pi/bin:$(dirname "$PI_BIN"):$PATH" hash -r 2>/dev/null || true set +e PI_TEST_OUTPUT=$(timeout 120 pi --provider "$PROVIDER_ID" --model "$MODEL_ID" -p "用一句话介绍你自己,不超过20字" 2>&1) PI_EXIT_CODE=$? set -e if [[ $PI_EXIT_CODE -eq 0 ]] && [[ -n "$PI_TEST_OUTPUT" ]]; then ok "pi 推理测试通过 (exit 0)" echo "--- pi 输出 ---" echo "$PI_TEST_OUTPUT" | head -n 30 echo "----------------" elif [[ $PI_EXIT_CODE -eq 124 ]]; then warn "pi 测试超时 (120s),可能模型加载较慢或首次启动较久。请稍后手动重试: pi --provider $PROVIDER_ID --model $MODEL_ID -p \"你好\"" else warn "pi 测试失败 (exit $PI_EXIT_CODE),输出:" echo "$PI_TEST_OUTPUT" | head -n 100 >&2 VERIFICATION_FAILED=1 fi # ============================================================================== # 总结 # ============================================================================== echo "" echo "================================================================" if [[ "$VERIFICATION_FAILED" -eq 0 ]]; then echo -e "\033[32m✅ 安装完成且验证通过!\033[0m" else echo -e "\033[33m⚠️ 安装完成,但部分验证未通过,请查看上方警告\033[0m" fi echo "================================================================" echo "- 模型: $REPO ($MODEL_FILE)" echo " 模型文件: $MODEL_PATH" echo "- 内存限制: 32GB" echo "- llama-server (编译自源码): $LLAMA_BIN/llama-server" echo " 启动参数: taskset -c 0-14 ... -t 15 --jinja --load-mode mlock -c $CONTEXT_SIZE --host 127.0.0.1 --port $LLAMA_PORT" echo "- 服务: http://127.0.0.1:$LLAMA_PORT (log: ~/llama-server.log, pid: ~/llama-server.pid)" echo "- pi: $PI_BIN ($(pi --version 2>&1 | head -n 1))" echo "- pi 配置: $PI_MODELS_JSON (provider: $PROVIDER_ID, model: $MODEL_ID, context: $CONTEXT_SIZE)" echo "- 快速使用:" echo " source ~/.bashrc # 或 source ~/.zshrc" echo " pi-local -p \"你好\"" echo " pi --provider $PROVIDER_ID --model $MODEL_ID -p \"你好\"" echo "- 停止服务器: kill \$(cat ~/llama-server.pid)" echo "- 重新验证: curl http://127.0.0.1:$LLAMA_PORT/v1/models && pi-local -p \"测试\"" echo "================================================================" if [[ "$VERIFICATION_FAILED" -ne 0 ]]; then echo "请检查日志:" echo " - llama-server: ~/llama-server.log (tail -n 100 ~/llama-server.log)" echo " - pi 安装: /tmp/pi-install.log" echo " - 模型: $MODEL_DIR/" exit 1 fi exit 0