HelloSun commited on
Commit
c074d5e
·
verified ·
1 Parent(s): 0c87290

Update installpi.sh

Browse files
Files changed (1) hide show
  1. installpi.sh +159 -291
installpi.sh CHANGED
@@ -2,92 +2,89 @@
2
  set -euo pipefail
3
 
4
  # ==============================================================================
5
- # installpi.sh - 一鍵全自動安裝 pi + llama.cpp + Qwen3.6 35B A3B MTP 模型
6
- # 特性:無 sudo、無互動、全自動、可斷點續傳、安裝後自動驗證
7
- # 模型:unsloth/Qwen3.6-35B-A3B-MTP-GGUF (優先 UD-Q4_K_M, 啟用 draft-mtp 加速)
8
- # 參考: https://huggingface.co/unsloth/Qwen3.6-35B-A3B-MTP-GGUF
9
  # ==============================================================================
10
 
11
  HOME_DIR="$HOME"
12
- LLAMA_DIR="$HOME_DIR/llama-cpp"
13
- LLAMA_BIN="$LLAMA_DIR/bin"
14
  MODEL_DIR="$HOME_DIR/models"
15
  PI_MODELS_JSON="$HOME_DIR/.pi/agent/models.json"
16
- LLAMA_PORT=8081
17
- REPO="unsloth/Qwen3.6-35B-A3B-MTP-GGUF"
18
- MODEL_ID="qwen3.6-35b-a3b"
 
19
  PROVIDER_ID="llama-cpp-local"
 
20
 
21
- # 基礎輸出函數
22
  info() { echo -e "\033[36m[INFO]\033[0m $*"; }
23
  ok() { echo -e "\033[32m[OK]\033[0m $*"; }
24
  warn() { echo -e "\033[33m[WARN]\033[0m $*" >&2; }
25
  error() { echo -e "\033[31m[ERROR]\033[0m $*" >&2; exit 1; }
26
 
27
- # 確保必要目錄
28
- mkdir -p "$LLAMA_BIN" "$MODEL_DIR" "$(dirname "$PI_MODELS_JSON")" "$HOME_DIR/.local/bin"
29
 
30
- # 檢查必要工具
31
- for cmd in curl python3 tar; do
32
- command -v "$cmd" >/dev/null 2>&1 || error "缺少必要工具: $cmd,請先安裝"
33
  done
34
 
35
  # ==============================================================================
36
- # 0. 確保 Node.js >=22.19.0 (pi 依賴,避免 pi 安裝腳本進入互動模式)
37
  # ==============================================================================
38
  ensure_node() {
39
  if command -v node >/dev/null 2>&1; then
40
  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
41
- ok "Node.js 已滿足: $(node --version)"
42
  return 0
43
  else
44
- warn "Node.js 版本過舊: $(node --version),將自動安裝新版..."
45
  fi
46
  else
47
- info "未檢測到 Node.js,將自動安裝..."
48
  fi
49
 
50
- # 採用 standalone 方式安裝 Node 22.x 到 ~/.local/share/pi-node (無 sudo)
51
- info "自動安裝 Node.js 22.x (standalone 模式)..."
52
  NODE_DIST_BASE="https://nodejs.org/dist/latest-v22.x"
53
  NODE_BASE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/pi-node"
54
  NODE_TMP_DIR="/tmp/pi-node-$$"
55
  mkdir -p "$NODE_TMP_DIR" "$NODE_BASE_DIR"
56
 
57
- # 判斷平台 / 架構
58
  OS="$(uname -s)"
59
  ARCH="$(uname -m)"
60
- case "$OS" in Linux) PLATFORM="linux" ;; Darwin) PLATFORM="darwin" ;; *) error "不支援的 OS: $OS" ;; esac
61
- case "$ARCH" in x86_64|amd64) NODE_ARCH="x64" ;; aarch64|arm64) NODE_ARCH="arm64" ;; *) error "不支援的架構: $ARCH" ;; esac
62
 
63
- info "解析 Node.js 下載連結 ($PLATFORM-$NODE_ARCH)..."
64
- curl -fsSL --retry 3 --retry-delay 5 "$NODE_DIST_BASE/SHASUMS256.txt" -o "$NODE_TMP_DIR/SHASUMS256.txt" || error "下載 SHASUMS 失敗"
65
 
66
  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")
67
- [[ -n "$NODE_FILE" ]] || error "找不到對應平台的 Node.js 二進位"
68
 
69
- info "下載 Node.js $NODE_FILE ..."
70
- curl -fsSL --retry 3 --retry-delay 5 "$NODE_DIST_BASE/$NODE_FILE" -o "$NODE_TMP_DIR/$NODE_FILE" || error "Node.js 下載失敗"
71
 
72
- # 校驗
73
  if command -v sha256sum >/dev/null 2>&1; then
74
- (cd "$NODE_TMP_DIR" && grep -F "$NODE_FILE" SHASUMS256.txt > SHASUMS256.selected && sha256sum -c SHASUMS256.selected) || warn "SHA256 校驗失敗,繼續安裝..."
75
  fi
76
 
77
- # 需要 xz 解壓
78
  if ! command -v xz >/dev/null 2>&1; then
79
- warn "未找到 xz,將嘗試使用 tar 自動解壓 (若失敗請手動安裝 xz-utils)"
80
  fi
81
 
82
  NODE_EXTRACT_DIR="$NODE_BASE_DIR/${NODE_FILE%.tar.xz}"
83
  rm -rf "$NODE_EXTRACT_DIR"
84
- tar -xf "$NODE_TMP_DIR/$NODE_FILE" -C "$NODE_BASE_DIR" || error "解壓 Node.js 失敗"
85
  rm -f "$NODE_BASE_DIR/current"
86
  ln -s "$NODE_EXTRACT_DIR" "$NODE_BASE_DIR/current"
87
  rm -rf "$NODE_TMP_DIR"
88
 
89
  export PATH="$NODE_BASE_DIR/current/bin:$PATH"
90
- # 寫入 bashrc / zshrc
91
  for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
92
  if [[ -f "$rc" ]] || [[ "$rc" == "$HOME/.bashrc" ]]; then
93
  grep -q "pi-node/current/bin" "$rc" 2>/dev/null || echo "export PATH=\"$NODE_BASE_DIR/current/bin:\$PATH\"" >> "$rc"
@@ -95,159 +92,92 @@ ensure_node() {
95
  done
96
 
97
  if command -v node >/dev/null 2>&1; then
98
- ok "Node.js 安裝完成: $(node --version) at $(command -v node)"
99
  else
100
- error "Node.js 安裝後仍無法找到,請檢查 PATH"
101
  fi
102
  }
103
  ensure_node
104
 
105
- # 確保 npm 可用
106
- command -v npm >/dev/null 2>&1 || error "npm 仍不可用,Node 安裝可能失敗"
107
  info "npm 版本: $(npm --version)"
108
 
109
-
110
  # ==============================================================================
111
- # 1. 下載 llama.cpp 預編譯二進位
112
  # ==============================================================================
113
- info "1) 下載 llama.cpp 預編譯二進位檔..."
114
- LLAMA_TAG="b10545"
115
- LLAMA_URL="https://github.com/ggml-org/llama.cpp/releases/download/${LLAMA_TAG}/llama-${LLAMA_TAG}-bin-ubuntu-x64.tar.gz"
116
  if [[ ! -x "$LLAMA_BIN/llama-server" ]]; then
117
- info "下載並解壓 llama.cpp ${LLAMA_TAG}..."
118
- curl -fL --retry 3 --retry-delay 5 --progress-bar "$LLAMA_URL" -o /tmp/llama.tar.gz || error "llama.cpp 下載失敗"
119
- tar -xzf /tmp/llama.tar.gz -C /tmp || error "解壓失敗"
120
- SRC_DIR=$(find /tmp -maxdepth 1 -type d -name "llama-b*" | head -n 1)
121
- if [[ -n "$SRC_DIR" ]]; then
122
- cp -a "$SRC_DIR"/llama-* "$LLAMA_BIN/" 2>/dev/null || true
123
- cp -a "$SRC_DIR"/*.so* "$LLAMA_BIN/" 2>/dev/null || true
124
- chmod +x "$LLAMA_BIN"/llama-server "$LLAMA_BIN"/llama-cli 2>/dev/null || true
125
- rm -rf /tmp/llama.tar.gz "$SRC_DIR"
126
- [[ -x "$LLAMA_BIN/llama-server" ]] || error "llama-server 安裝失敗"
127
- ok "llama.cpp 安裝完成"
 
 
 
 
 
 
 
 
 
 
 
 
128
  else
129
- error "解壓 llama.cpp 失敗,找不到解壓目錄"
130
  fi
131
  else
132
- ok "llama.cpp 已存在,跳過下載"
133
  fi
134
 
135
  # 更新 PATH
136
  export PATH="$LLAMA_BIN:$HOME_DIR/.local/bin:$PATH"
137
  export LD_LIBRARY_PATH="$LLAMA_BIN:${LD_LIBRARY_PATH:-}"
138
- # 冪等寫入 rc
139
  for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
140
  touch "$rc" 2>/dev/null || true
141
- grep -q "llama-cpp/bin" "$rc" 2>/dev/null || echo "export PATH=\"$LLAMA_BIN:\$PATH\"" >> "$rc"
142
- grep -q "LD_LIBRARY_PATH.*llama-cpp" "$rc" 2>/dev/null || echo "export LD_LIBRARY_PATH=\"$LLAMA_BIN:\$LD_LIBRARY_PATH\"" >> "$rc"
143
  done
144
 
145
  # ==============================================================================
146
- # 2. 使用 curl 下載模型 (unsloth/Qwen3.6-35B-A3B-MTP-GGUF)
147
  # ==============================================================================
148
- info "2) 使用 curl 下載模型 ${REPO} (支援斷點續傳)..."
149
-
150
- # 使用 Python 獲取 gguf 清單,明確優先順序:針對 Qwen3.6-35B-A3B-MTP 優先 UD-Q4_K_M / UD-Q4_K_XL
151
- info "正在獲取模型檔案列表..."
152
- FILE_LIST=$(python3 -c "
153
- import urllib.request, json, sys
154
- repo = '${REPO}'
155
- url = f'https://huggingface.co/api/models/{repo}/tree/main'
156
- try:
157
- with urllib.request.urlopen(url, timeout=30) as resp:
158
- data = json.load(resp)
159
- files = [item['path'] for item in data if item.get('type')=='file' and item['path'].endswith('.gguf')]
160
- # 排除 mmproj (視覺投影) - Qwen3.6 文本模型不需要,若需要可另外處理
161
- # 優先順序:UD-Q4_K_M 為 app.py 預設,其次 UD-Q4_K_XL,再其他
162
- def score(f):
163
- fl = f.lower()
164
- if 'ud-q4_k_m' in fl: return 0
165
- if 'ud-q4_k_xl' in fl: return 1
166
- if 'q4_k_m' in fl: return 2
167
- if 'q4_k_xl' in fl: return 3
168
- if 'q4' in fl: return 4
169
- if 'q5' in fl or 'q6' in fl or 'q8' in fl: return 5
170
- if 'q3' in fl or 'iq' in fl or 'q2' in fl: return 6
171
- return 10
172
- # 對於 MTP 倉庫,主模型本身已含 MTP heads,無需單獨 mtp 檔案;僅排除 mmproj
173
- # 早期 gemma 倉庫有獨立 mtp 檔,保留相容邏輯但對 Qwen3.6 MTP 無獨立檔
174
- main_candidates = [f for f in files if 'mmproj' not in f.lower()]
175
- # 對 MTP 倉庫:優先選含 MTP 的同名檔案其實就是主模型;若倉庫名含 MTP,全部主模型皆為 MTP 版本
176
- is_mtp_repo = 'mtp' in repo.lower()
177
- if not is_mtp_repo:
178
- main_candidates = [f for f in main_candidates if 'mtp' not in f.lower()]
179
- main_candidates.sort(key=score)
180
- main = main_candidates[0] if main_candidates else ''
181
- # mtp 單獨檔 (僅非 MTP 倉庫需要)
182
- mtp = ''
183
- if not is_mtp_repo:
184
- mtp = next((f for f in files if 'mtp' in f.lower()), '')
185
- # 若是 MTP 倉庫,標記主模型即為 MTP 版本
186
- mtp_marker = 'MTP integrated' if is_mtp_repo else mtp
187
- print(f'MAIN:{main}')
188
- print(f'MTP:{mtp}')
189
- print(f'MTP_MARKER:{mtp_marker}')
190
- print(f'ALL:{ \",\".join(files)}', file=sys.stderr)
191
- except Exception as e:
192
- print(f'ERROR:{e}')
193
- sys.exit(1)
194
- " 2>/tmp/hf_file_list.log)
195
-
196
- echo "$FILE_LIST"
197
- cat /tmp/hf_file_list.log >&2 || true
198
-
199
- MAIN_FILE=$(echo "$FILE_LIST" | grep "^MAIN:" | cut -d: -f2- | xargs)
200
- MTP_FILE=$(echo "$FILE_LIST" | grep "^MTP:" | cut -d: -f2- | xargs)
201
-
202
- if [[ -z "$MAIN_FILE" ]]; then
203
- error "無法獲取主模型檔案,請檢查網路或 Repo: $REPO (詳見 /tmp/hf_file_list.log)"
204
- fi
205
 
206
- info "找到主模型: $MAIN_FILE"
207
- DOWNLOAD_URL="https://huggingface.co/${REPO}/resolve/main/${MAIN_FILE}"
208
 
209
- if [[ ! -f "$MODEL_DIR/$MAIN_FILE" ]]; then
210
- info "開始下載主模型 (支援斷點續傳,若中斷可重新執行此腳本)..."
211
- # -C - 斷點續傳, -L 跟隨重定向, --retry 自動重試
212
- curl -L -C - --retry 3 --retry-delay 5 --progress-bar "$DOWNLOAD_URL" -o "$MODEL_DIR/$MAIN_FILE" || error "主模型下載失敗"
213
- ok "主模型下載完成: $MODEL_DIR/$MAIN_FILE"
214
  else
215
- # 檢查檔案大小是否合理 (>1GB)
216
- SIZE=$(stat -c%s "$MODEL_DIR/$MAIN_FILE" 2>/dev/null || stat -f%z "$MODEL_DIR/$MAIN_FILE" 2>/dev/null || echo 0)
217
  if [[ "$SIZE" -lt 1000000000 ]]; then
218
- warn "主模型檔案過小 (${SIZE} bytes),可能下載不完整,將嘗試續傳..."
219
- curl -L -C - --retry 3 --retry-delay 5 --progress-bar "$DOWNLOAD_URL" -o "$MODEL_DIR/$MAIN_FILE" || error "續傳失敗"
220
- fi
221
- ok "主模型已存在,跳過下載: $MODEL_DIR/$MAIN_FILE ($SIZE bytes)"
222
- fi
223
- MODEL_PATH="$MODEL_DIR/$MAIN_FILE"
224
-
225
- MTP_PATH=""
226
- if [[ -n "$MTP_FILE" ]]; then
227
- info "找到 MTP 加速模型: $MTP_FILE"
228
- MTP_DOWNLOAD_URL="https://huggingface.co/${REPO}/resolve/main/${MTP_FILE}"
229
- if [[ ! -f "$MODEL_DIR/$MTP_FILE" ]]; then
230
- info "開始下載 MTP 模型..."
231
- curl -L -C - --retry 3 --retry-delay 5 --progress-bar "$MTP_DOWNLOAD_URL" -o "$MODEL_DIR/$MTP_FILE" || warn "MTP 模��下載失敗,將僅使用主模型"
232
- else
233
- ok "MTP 模型已存在,跳過下載"
234
- fi
235
- if [[ -f "$MODEL_DIR/$MTP_FILE" ]]; then
236
- MTP_PATH="$MODEL_DIR/$MTP_FILE"
237
  fi
238
- else
239
- info "未找到 MTP 模型,將僅使用主模型"
240
  fi
241
 
242
  # ==============================================================================
243
- # 3. 啟動 llama-server
244
  # ==============================================================================
245
- info "3) 啟動 llama-server (port $LLAMA_PORT)..."
246
- # 清理舊進程
 
247
  if [[ -f "$HOME_DIR/llama-server.pid" ]]; then
248
  OLD_PID=$(cat "$HOME_DIR/llama-server.pid" 2>/dev/null || echo "")
249
  if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then
250
- info "停止舊 llama-server (PID $OLD_PID)..."
251
  kill "$OLD_PID" 2>/dev/null || true
252
  sleep 2
253
  kill -9 "$OLD_PID" 2>/dev/null || true
@@ -256,118 +186,77 @@ fi
256
  pkill -f "llama-server.*${LLAMA_PORT}" 2>/dev/null || true
257
  sleep 1
258
 
259
- THREADS=$(nproc 2>/dev/null || echo 8)
260
- [[ "$THREADS" -gt 16 ]] && THREADS=16
261
-
262
- CMD=("$LLAMA_BIN/llama-server"
263
  -m "$MODEL_PATH"
264
- --host 0.0.0.0
 
 
 
 
 
265
  --port "$LLAMA_PORT"
266
- -c 131072
267
- -t "$THREADS"
268
- --flash-attn on
269
  )
270
 
271
- # MTP 加速:若為 MTP 倉庫 (unsloth/*-MTP-GGUF) 且 llama-server 支援 spec-type,則啟用 draft-mtp
272
- # Qwen3.6-35B-A3B-MTP 官方建議 --spec-draft-n-max 2 , Qwen3.5-4B-MTP 為 6
273
- IS_MTP_REPO=0
274
- if [[ "$REPO" == *"-MTP-"* ]] || [[ "$REPO" == *"-MTP"* ]]; then
275
- IS_MTP_REPO=1
276
- fi
277
- if [[ "$IS_MTP_REPO" -eq 1 ]] && "$LLAMA_BIN/llama-server" --help 2>&1 | grep -q "spec-type"; then
278
- # 根據模型大小選擇 n-max:35B 用 2,4B 用 6,向後兼容
279
- SPEC_N_MAX=2
280
- if [[ "$MODEL_PATH" == *"4B"* ]]; then SPEC_N_MAX=6; fi
281
- if [[ -n "$MTP_PATH" ]]; then
282
- # 舊 gemma 有獨立 mtp 檔,需 --model-draft
283
- CMD+=(--spec-type draft-mtp --spec-draft-n-max "$SPEC_N_MAX" --model-draft "$MTP_PATH")
284
- else
285
- # Qwen3.6 MTP 為單檔整合 MTP heads,直接啟用
286
- CMD+=(--spec-type draft-mtp --spec-draft-n-max "$SPEC_N_MAX")
287
- fi
288
- info "✅ 已啟用 MTP speculative decoding 加速 (--spec-type draft-mtp --spec-draft-n-max $SPEC_N_MAX)"
289
- elif [[ -n "$MTP_PATH" ]]; then
290
- info "MTP 模型已下載至 $MTP_PATH (預設未啟用,如需加速可手動啟用 --spec-type draft-mtp)"
291
- fi
292
-
293
- info "啟動命令: ${CMD[*]}"
294
- # 確保 log 可寫
295
  touch "$HOME_DIR/llama-server.log" 2>/dev/null || true
296
  nohup "${CMD[@]}" > "$HOME_DIR/llama-server.log" 2>&1 &
297
  echo $! > "$HOME_DIR/llama-server.pid"
298
  info "llama-server PID: $(cat "$HOME_DIR/llama-server.pid")"
299
 
300
- # 等待伺服器就緒 (最長 5 分鐘)
301
- info "等待 llama-server 就緒 (最多 5 分鐘)..."
302
  READY=0
303
  for i in {1..60}; do
304
  if curl -sf "http://127.0.0.1:${LLAMA_PORT}/health" >/dev/null 2>&1; then
305
- ok "llama-server 已就緒 (http://127.0.0.1:${LLAMA_PORT})"
306
  READY=1
307
  break
308
  fi
309
- # 也嘗試 /v1/models 作為備用健康檢查
310
  if curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/models" >/dev/null 2>&1; then
311
- ok "llama-server 已就緒 (via /v1/models)"
312
  READY=1
313
  break
314
  fi
315
  if ! kill -0 $(cat "$HOME_DIR/llama-server.pid" 2>/dev/null || echo 0) 2>/dev/null; then
316
- error "llama-server 啟動失敗,請查看 ~/llama-server.log (最後 50 行):\n$(tail -n 50 "$HOME_DIR/llama-server.log" 2>/dev/null)"
317
  fi
318
  echo -n "."
319
  sleep 5
320
  done
321
  echo ""
322
  if [[ "$READY" -ne 1 ]]; then
323
- error "llama-server 在 5 分鐘內未就緒,請查看 ~/llama-server.log"
324
- fi
325
-
326
- # 額外確認 OpenAI 相容端點
327
- if curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/models" >/dev/null 2>&1; then
328
- ok "OpenAI API 端點正常"
329
- else
330
- warn "OpenAI API 端點未回應,但 health 檢查已通過"
331
  fi
332
 
333
  # ==============================================================================
334
- # 4. 安裝 pi agent (全自動,無互動)
335
  # ==============================================================================
336
- info "4) 安裝 pi agent (全自動)..."
337
 
338
- # 確保 npm prefix 可寫,避免需要 sudo
339
  NPM_PREFIX=$(npm prefix -g 2>/dev/null || npm config get prefix 2>/dev/null || echo "$HOME/.local")
340
- info "npm 全域前綴: $NPM_PREFIX"
341
 
342
- # 如果 pi 已安裝,先檢查版本
343
  NEED_INSTALL=1
344
  if command -v pi >/dev/null 2>&1; then
345
- ok "pi 已安裝: $(pi --version 2>&1 | head -n 1)"
346
- # 嘗試更新到最新 (非強制)
347
  NEED_INSTALL=0
348
- # 若明確需要重裝,可設定 PI_FORCE_REINSTALL=1
349
  if [[ "${PI_FORCE_REINSTALL:-0}" == "1" ]]; then
350
- info "PI_FORCE_REINSTALL=1,強制重新安裝..."
351
  NEED_INSTALL=1
352
  fi
353
  fi
354
 
355
  if [[ "$NEED_INSTALL" -eq 1 ]]; then
356
- info "執行 pi 官方安裝腳本 (全自動模式)..."
357
-
358
- # 關鍵:確保無 TTY 互動,pi 安裝腳本在無 TTY 時會自動選擇 install/reinstall
359
- # 同時透過環境變數避免任何提示
360
- # 若 npm prefix 不可寫,腳本會自動使用 ~/.local
361
-
362
- # 方法1:直接透過 npm 安裝 (最可靠、全自動、無互動)
363
- # 使用與官方腳本相同的參數
364
  PI_PACKAGE="@earendil-works/pi-coding-agent"
365
- # 偵測安裝前綴
366
  if npm prefix -g 2>/dev/null | xargs test -w 2>/dev/null; then
367
  INSTALL_PREFIX=""
368
  else
369
  INSTALL_PREFIX="$HOME/.local"
370
- info "全域 prefix 不可寫,將安裝到 $INSTALL_PREFIX"
371
  fi
372
 
373
  set +e
@@ -381,29 +270,21 @@ if [[ "$NEED_INSTALL" -eq 1 ]]; then
381
  set -e
382
 
383
  if [[ "$NPM_STATUS" -ne 0 ]]; then
384
- warn "npm 安裝 pi 失敗 (code $NPM_STATUS),嘗試備用方法: curl 官方腳本..."
385
- # 備用:使用官方腳本,並透過無 TTY 自動安裝
386
- # 強制無互動:將 stdin 重定向為空,依賴腳本的「無終端時自動安裝」邏輯
387
  curl -fsSL https://pi.dev/install.sh | bash -s -- 2>&1 | tee -a /tmp/pi-install.log || true
388
  fi
389
 
390
- # 再次檢查
391
- # 更新 PATH 以包含可能的安裝位置
392
  export PATH="$HOME/.local/bin:$HOME/.pi/bin:$NPM_PREFIX/bin:$PATH"
393
  if [[ -n "${INSTALL_PREFIX:-}" ]]; then
394
  export PATH="$INSTALL_PREFIX/bin:$PATH"
395
  fi
396
- # 也包含 standalone node 的情況
397
  NODE_BIN_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/pi-node/current/bin"
398
  export PATH="$NODE_BIN_DIR:$PATH"
399
-
400
  hash -r 2>/dev/null || true
401
  fi
402
 
403
- # 最終確認 pi 可用
404
  PI_BIN=$(command -v pi 2>/dev/null || echo "")
405
  if [[ -z "$PI_BIN" ]]; then
406
- # 嘗試常見路徑
407
  for cand in "$HOME/.local/bin/pi" "$HOME/.pi/bin/pi" "$NPM_PREFIX/bin/pi" "/usr/local/bin/pi"; do
408
  if [[ -x "$cand" ]]; then
409
  PI_BIN="$cand"
@@ -414,34 +295,30 @@ if [[ -z "$PI_BIN" ]]; then
414
  fi
415
 
416
  if [[ -z "$PI_BIN" ]] || [[ ! -x "$PI_BIN" ]]; then
417
- error "pi 安裝後仍找不到執行檔,請查看 /tmp/pi-install.log"
418
  fi
419
 
420
  ok "pi 可用: $PI_BIN ($(pi --version 2>&1 | head -n 1))"
421
 
422
- # 確保 PATH 寫入 rc
423
  for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
424
  touch "$rc" 2>/dev/null || true
425
- # pi bin 目錄
426
  PI_BIN_DIR="$(dirname "$PI_BIN")"
427
  if [[ "$PI_BIN_DIR" != "" ]]; then
428
  grep -qF "$PI_BIN_DIR" "$rc" 2>/dev/null || echo "export PATH=\"$PI_BIN_DIR:\$PATH\"" >> "$rc"
429
  fi
430
- # 也確保 ~/.local/bin 在 PATH
431
  grep -qF ".local/bin" "$rc" 2>/dev/null || echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$rc"
432
  done
433
 
434
  # ==============================================================================
435
- # 5. 配置 pi 使用本地模型
436
  # ==============================================================================
437
  info "5) 配置 pi models.json ($PI_MODELS_JSON)..."
438
 
439
- # 確保 pi 設定目錄存在
440
  mkdir -p "$(dirname "$PI_MODELS_JSON")"
441
 
442
- python3 - "$PI_MODELS_JSON" "$LLAMA_PORT" "$MODEL_ID" "$PROVIDER_ID" << 'PY'
443
  import json, os, sys
444
- path, port, model_id, provider = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
445
  os.makedirs(os.path.dirname(path), exist_ok=True)
446
  cfg = {"providers": {}}
447
  if os.path.exists(path):
@@ -453,7 +330,7 @@ if os.path.exists(path):
453
  if "providers" not in cfg:
454
  cfg["providers"] = {}
455
  except Exception as e:
456
- print(f"[WARN] 無法解析舊 models.json,將覆蓋: {e}")
457
 
458
  cfg["providers"][provider] = {
459
  "baseUrl": f"http://127.0.0.1:{port}/v1",
@@ -462,23 +339,22 @@ cfg["providers"][provider] = {
462
  "compat": {"supportsDeveloperRole": True, "supportsReasoningEffort": False},
463
  "models": [{
464
  "id": model_id,
465
- "name": "Qwen3.6 35B A3B MTP (Local)",
466
  "reasoning": True,
467
  "input": ["text", "image"],
468
- "contextWindow": 66560,
469
- "maxTokens": 131072,
470
  "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}
471
  }]
472
  }
473
  with open(path, "w") as f:
474
  json.dump(cfg, f, indent=2)
475
- print(f"[OK] 已寫入 {path}")
476
  print(json.dumps(cfg, indent=2))
477
  PY
478
 
479
  ok "pi 配置完成"
480
 
481
- # 建立 alias 和 wrapper (冪等)
482
  WRAPPER="$HOME/.local/bin/pi-local"
483
  mkdir -p "$(dirname "$WRAPPER")"
484
  cat > "$WRAPPER" << EOS
@@ -488,59 +364,57 @@ EOS
488
  chmod +x "$WRAPPER"
489
  ok "已建立 pi-local wrapper: $WRAPPER"
490
 
491
- # 寫入 alias (避免重複)
492
  for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
493
  touch "$rc" 2>/dev/null || true
494
  grep -q "alias pi-local" "$rc" 2>/dev/null || echo "alias pi-local=\"pi --provider $PROVIDER_ID --model $MODEL_ID\"" >> "$rc"
495
  done
496
 
497
  # ==============================================================================
498
- # 6. 安裝後驗證 (必須通過才算安裝成功)
499
  # ==============================================================================
500
- info "6) 安裝後驗證..."
501
 
502
  VERIFICATION_FAILED=0
503
 
504
- # 6.1 檢查 llama-server
505
- info "[驗證 1/4] 檢查 llama-server 健康狀態..."
506
  if curl -sf "http://127.0.0.1:${LLAMA_PORT}/health" >/dev/null 2>&1; then
507
- ok "llama-server 健康檢查通過"
508
  elif curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/models" >/dev/null 2>&1; then
509
- ok "llama-server (via /v1/models) 健康檢查通過"
510
  else
511
- warn "llama-server 健康檢查失敗"
512
  VERIFICATION_FAILED=1
513
  fi
514
 
515
- # 6.2 檢查 OpenAI API 實際推理
516
- info "[驗證 2/4] 測試 OpenAI 相容 API 推理..."
517
  API_TEST_RESPONSE=$(curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/chat/completions" \
518
  -H "Content-Type: application/json" \
519
  -d '{
520
  "model": "'$MODEL_ID'",
521
- "messages": [{"role": "user", "content": "用一句話介紹你自己,不要超過20字"}],
522
  "max_tokens": 50,
523
  "temperature": 0.7
524
  }' 2>&1 || echo "CURL_FAILED")
525
 
526
  if echo "$API_TEST_RESPONSE" | grep -q '"choices"' && echo "$API_TEST_RESPONSE" | grep -q '"content"'; then
527
- ok "OpenAI API 推理測試通過"
528
  echo "$API_TEST_RESPONSE" | python3 -m json.tool 2>/dev/null | head -n 30 || echo "$API_TEST_RESPONSE" | head -n 20
529
  elif echo "$API_TEST_RESPONSE" | grep -q "CURL_FAILED"; then
530
- warn "API 推理 curl 失敗: $API_TEST_RESPONSE"
531
  VERIFICATION_FAILED=1
532
  else
533
- warn "API 推理回應異常,可能是模型仍在載入中..."
534
  echo "$API_TEST_RESPONSE" | head -n 50 >&2
535
- # 不立即標記失敗,等待 pi-local 測試
536
  fi
537
 
538
- # 6.3 檢查 pi 是否可用
539
- info "[驗證 3/4] 檢查 pi 指令..."
540
  if pi --version >/dev/null 2>&1; then
541
  ok "pi --version: $(pi --version 2>&1 | head -n 1)"
542
  else
543
- warn "pi --version 執行失敗"
544
  VERIFICATION_FAILED=1
545
  fi
546
 
@@ -548,56 +422,50 @@ if pi --help >/dev/null 2>&1; then
548
  ok "pi --help 正常"
549
  fi
550
 
551
- # 檢查 models.json 是否正確
552
  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
553
- ok "pi models.json 配置正確"
554
  else
555
- warn "pi models.json 配置檢查失敗"
556
- cat "$PI_MODELS_JSON" 2>/dev/null || echo "檔案不存在"
557
  VERIFICATION_FAILED=1
558
  fi
559
 
560
  if [[ -x "$WRAPPER" ]]; then
561
- ok "pi-local wrapper 存在且可執行"
562
  else
563
  warn "pi-local wrapper 缺失"
564
  VERIFICATION_FAILED=1
565
  fi
566
 
567
- # 6.4 測試 pi-local 實際可用 (最關鍵)
568
- info "[驗證 4/4] 測試 pi-local 實際推理 (超時 90 秒)..."
569
- # 先確保 PATH 包含 pi
570
  export PATH="$HOME/.local/bin:$HOME/.pi/bin:$(dirname "$PI_BIN"):$PATH"
571
 
572
- # 使用 timeout 防止卡死,並捕獲輸出
573
  set +e
574
- PI_TEST_OUTPUT=$(timeout 90 pi-local -p "用一句話介紹你自己,不超過20字" 2>&1)
575
  PI_EXIT_CODE=$?
576
  set -e
577
 
578
  if [[ $PI_EXIT_CODE -eq 0 ]] && [[ -n "$PI_TEST_OUTPUT" ]]; then
579
- ok "pi-local 推理測試通過 (exit 0)"
580
- echo "--- pi-local 輸出 ---"
581
  echo "$PI_TEST_OUTPUT" | head -n 30
582
  echo "---------------------"
583
  elif [[ $PI_EXIT_CODE -eq 124 ]]; then
584
- warn "pi-local 測試超時 (90s),可能模型載入較慢或首次啟動較久。請稍後手動重試: pi-local -p \"你好\""
585
- # 不算致命失敗,因為 API 已通過
586
  else
587
- warn "pi-local 測試失敗 (exit $PI_EXIT_CODE),輸出:"
588
  echo "$PI_TEST_OUTPUT" | head -n 100 >&2
589
- # 如果 API 測試已通過,pi-local 失敗可能是設定問題,標記警告而非致命
590
  if [[ "$VERIFICATION_FAILED" -eq 0 ]]; then
591
- # 嘗試直接用 pi 而非 wrapper 再測一次
592
- info "嘗試直接執行 pi 指令..."
593
  set +e
594
  PI_DIRECT_OUTPUT=$(timeout 90 pi --provider "$PROVIDER_ID" --model "$MODEL_ID" -p "你好" 2>&1)
595
  PI_DIRECT_CODE=$?
596
  set -e
597
  if [[ $PI_DIRECT_CODE -eq 0 ]]; then
598
- ok "pi 直接呼叫測試通過"
599
  else
600
- warn "pi 直接呼叫也失敗 (exit $PI_DIRECT_CODE): $PI_DIRECT_OUTPUT"
601
  VERIFICATION_FAILED=1
602
  fi
603
  else
@@ -606,37 +474,37 @@ else
606
  fi
607
 
608
  # ==============================================================================
609
- # 總結
610
  # ==============================================================================
611
  echo ""
612
  echo "================================================================"
613
  if [[ "$VERIFICATION_FAILED" -eq 0 ]]; then
614
- echo -e "\033[32m✅ 安裝完成且驗證通過!\033[0m"
615
  else
616
- echo -e "\033[33m⚠️ 安裝完成,但部分驗證未通過,請查看上方警告\033[0m"
617
  fi
618
  echo "================================================================"
619
  echo "- 模型: $REPO"
620
- echo " 主模型: $MODEL_PATH"
621
- if [[ -n "$MTP_PATH" ]]; then echo " MTP 模型: $MTP_PATH"; fi
622
- echo "- llama-server: http://127.0.0.1:$LLAMA_PORT (log: ~/llama-server.log, pid: ~/llama-server.pid)"
 
623
  echo "- pi: $PI_BIN ($(pi --version 2>&1 | head -n 1))"
624
- echo "- pi 配置: $PI_MODELS_JSON (provider: $PROVIDER_ID, model: $MODEL_ID)"
625
  echo "- 快速使用:"
626
  echo " source ~/.bashrc # 或 source ~/.zshrc"
627
  echo " pi-local -p \"你好\""
628
  echo " pi --provider $PROVIDER_ID --model $MODEL_ID -p \"你好\""
629
- echo "- 停止伺服器: kill \$(cat ~/llama-server.pid)"
630
- echo "- 重啟伺服器: bash ~/llama-cpp/bin/ # 或重新執行本腳本"
631
- echo "- 重新驗證: curl http://127.0.0.1:$LLAMA_PORT/v1/models && pi-local -p \"測試\""
632
  echo "================================================================"
633
 
634
  if [[ "$VERIFICATION_FAILED" -ne 0 ]]; then
635
- echo "請檢查日誌:"
636
  echo " - llama-server: ~/llama-server.log (tail -n 100 ~/llama-server.log)"
637
- echo " - pi 安裝: /tmp/pi-install.log"
638
  echo " - 模型: $MODEL_DIR/"
639
  exit 1
640
  fi
641
 
642
- exit 0
 
2
  set -euo pipefail
3
 
4
  # ==============================================================================
5
+ # installpi.sh - 一键全自动安装 pi + llama.cpp (源码编译) + Ornith 模型
6
+ # 特性:无 sudo、无互动、全自动、可断点续传、安装后自动验证
7
+ # 模型:ornith-ai/Ornith-1.5-35B-A3B-GGUF (Ornith-1.5-35B-Q4_K_M.gguf)
8
+ # 启动:taskset -c 0-14 llama-server ... -t 15 --jinja --mlock -c 131072 (极限上下文)
9
  # ==============================================================================
10
 
11
  HOME_DIR="$HOME"
12
+ LLAMA_SRC_DIR="$HOME_DIR/llama.cpp" # 源码目录
13
+ LLAMA_BIN="$LLAMA_SRC_DIR/build/bin" # 编译后二进制目录
14
  MODEL_DIR="$HOME_DIR/models"
15
  PI_MODELS_JSON="$HOME_DIR/.pi/agent/models.json"
16
+ LLAMA_PORT=8080
17
+ REPO="ornith-ai/Ornith-1.5-35B-A3B-GGUF"
18
+ MODEL_FILE="Ornith-1.5-35B-Q4_K_M.gguf"
19
+ MODEL_ID="ornith-1.5"
20
  PROVIDER_ID="llama-cpp-local"
21
+ CONTEXT_SIZE=131072 # 极限上下文 (128K)
22
 
23
+ # 基础输出函数
24
  info() { echo -e "\033[36m[INFO]\033[0m $*"; }
25
  ok() { echo -e "\033[32m[OK]\033[0m $*"; }
26
  warn() { echo -e "\033[33m[WARN]\033[0m $*" >&2; }
27
  error() { echo -e "\033[31m[ERROR]\033[0m $*" >&2; exit 1; }
28
 
29
+ # 确保必要目录
30
+ mkdir -p "$LLAMA_SRC_DIR" "$MODEL_DIR" "$(dirname "$PI_MODELS_JSON")" "$HOME_DIR/.local/bin"
31
 
32
+ # 检查必要工具(新增编译依赖)
33
+ for cmd in curl python3 tar git cmake g++; do
34
+ command -v "$cmd" >/dev/null 2>&1 || error "缺少必要工具: $cmd,请先安装"
35
  done
36
 
37
  # ==============================================================================
38
+ # 0. 确保 Node.js >=22.19.0 (pi 依赖)
39
  # ==============================================================================
40
  ensure_node() {
41
  if command -v node >/dev/null 2>&1; then
42
  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
43
+ ok "Node.js 已满足: $(node --version)"
44
  return 0
45
  else
46
+ warn "Node.js 版本过旧: $(node --version),将自动安装新版..."
47
  fi
48
  else
49
+ info "未检测到 Node.js,将自动安装..."
50
  fi
51
 
52
+ info "自动安装 Node.js 22.x (standalone 模式)..."
 
53
  NODE_DIST_BASE="https://nodejs.org/dist/latest-v22.x"
54
  NODE_BASE_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/pi-node"
55
  NODE_TMP_DIR="/tmp/pi-node-$$"
56
  mkdir -p "$NODE_TMP_DIR" "$NODE_BASE_DIR"
57
 
 
58
  OS="$(uname -s)"
59
  ARCH="$(uname -m)"
60
+ case "$OS" in Linux) PLATFORM="linux" ;; Darwin) PLATFORM="darwin" ;; *) error "不支持的 OS: $OS" ;; esac
61
+ case "$ARCH" in x86_64|amd64) NODE_ARCH="x64" ;; aarch64|arm64) NODE_ARCH="arm64" ;; *) error "不支持的架构: $ARCH" ;; esac
62
 
63
+ info "解析 Node.js 下载链接 ($PLATFORM-$NODE_ARCH)..."
64
+ curl -fsSL --retry 3 --retry-delay 5 "$NODE_DIST_BASE/SHASUMS256.txt" -o "$NODE_TMP_DIR/SHASUMS256.txt" || error "下载 SHASUMS 失败"
65
 
66
  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")
67
+ [[ -n "$NODE_FILE" ]] || error "找不到对应平台的 Node.js 二进制"
68
 
69
+ info "下载 Node.js $NODE_FILE ..."
70
+ curl -fsSL --retry 3 --retry-delay 5 "$NODE_DIST_BASE/$NODE_FILE" -o "$NODE_TMP_DIR/$NODE_FILE" || error "Node.js 下载失败"
71
 
 
72
  if command -v sha256sum >/dev/null 2>&1; then
73
+ (cd "$NODE_TMP_DIR" && grep -F "$NODE_FILE" SHASUMS256.txt > SHASUMS256.selected && sha256sum -c SHASUMS256.selected) || warn "SHA256 校验失败,继续安装..."
74
  fi
75
 
 
76
  if ! command -v xz >/dev/null 2>&1; then
77
+ warn "未找到 xz,将尝试使用 tar 自动解压 (若失败请手动安装 xz-utils)"
78
  fi
79
 
80
  NODE_EXTRACT_DIR="$NODE_BASE_DIR/${NODE_FILE%.tar.xz}"
81
  rm -rf "$NODE_EXTRACT_DIR"
82
+ tar -xf "$NODE_TMP_DIR/$NODE_FILE" -C "$NODE_BASE_DIR" || error "解压 Node.js 失败"
83
  rm -f "$NODE_BASE_DIR/current"
84
  ln -s "$NODE_EXTRACT_DIR" "$NODE_BASE_DIR/current"
85
  rm -rf "$NODE_TMP_DIR"
86
 
87
  export PATH="$NODE_BASE_DIR/current/bin:$PATH"
 
88
  for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
89
  if [[ -f "$rc" ]] || [[ "$rc" == "$HOME/.bashrc" ]]; then
90
  grep -q "pi-node/current/bin" "$rc" 2>/dev/null || echo "export PATH=\"$NODE_BASE_DIR/current/bin:\$PATH\"" >> "$rc"
 
92
  done
93
 
94
  if command -v node >/dev/null 2>&1; then
95
+ ok "Node.js 安装完成: $(node --version) at $(command -v node)"
96
  else
97
+ error "Node.js 安装后仍无法找到,请检查 PATH"
98
  fi
99
  }
100
  ensure_node
101
 
102
+ command -v npm >/dev/null 2>&1 || error "npm 仍不可用,Node 安装可能失败"
 
103
  info "npm 版本: $(npm --version)"
104
 
 
105
  # ==============================================================================
106
+ # 1. 编译 llama.cpp (从源码)
107
  # ==============================================================================
108
+ info "1) 编译 llama.cpp (源码)..."
109
+
 
110
  if [[ ! -x "$LLAMA_BIN/llama-server" ]]; then
111
+ info "克隆 llama.cpp 仓库 (深度1)..."
112
+ if [[ -d "$LLAMA_SRC_DIR/.git" ]]; then
113
+ info "源码目录已存在,更新..."
114
+ cd "$LLAMA_SRC_DIR"
115
+ git pull
116
+ else
117
+ git clone --depth 1 https://github.com/ggml-org/llama.cpp "$LLAMA_SRC_DIR"
118
+ fi
119
+
120
+ cd "$LLAMA_SRC_DIR"
121
+ info "执行 cmake 配置..."
122
+ cmake -B build \
123
+ -DGGML_NATIVE=ON \
124
+ -DGGML_BACKEND_DL=OFF \
125
+ -DLLAMA_BUILD_TESTS=OFF \
126
+ -DCMAKE_BUILD_TYPE=Release
127
+
128
+ info "开始编译 (使用 -j16)..."
129
+ cmake --build build --config Release -j 16
130
+
131
+ # 验证编译产物
132
+ if [[ -x "$LLAMA_BIN/llama-server" ]]; then
133
+ ok "llama.cpp 编译完成: $LLAMA_BIN/llama-server"
134
  else
135
+ error "编译失败,找不到 llama-server"
136
  fi
137
  else
138
+ ok "llama-server 已存在,跳过编译"
139
  fi
140
 
141
  # 更新 PATH
142
  export PATH="$LLAMA_BIN:$HOME_DIR/.local/bin:$PATH"
143
  export LD_LIBRARY_PATH="$LLAMA_BIN:${LD_LIBRARY_PATH:-}"
 
144
  for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
145
  touch "$rc" 2>/dev/null || true
146
+ grep -q "llama.cpp/build/bin" "$rc" 2>/dev/null || echo "export PATH=\"$LLAMA_BIN:\$PATH\"" >> "$rc"
147
+ grep -q "LD_LIBRARY_PATH.*llama.cpp" "$rc" 2>/dev/null || echo "export LD_LIBRARY_PATH=\"$LLAMA_BIN:\$LD_LIBRARY_PATH\"" >> "$rc"
148
  done
149
 
150
  # ==============================================================================
151
+ # 2. 下载模型 (直接使用指定 URL)
152
  # ==============================================================================
153
+ info "2) 下载模型 ${REPO} -> ${MODEL_FILE} ..."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
+ MODEL_URL="https://huggingface.co/${REPO}/resolve/main/${MODEL_FILE}"
156
+ MODEL_PATH="$MODEL_DIR/$MODEL_FILE"
157
 
158
+ if [[ ! -f "$MODEL_PATH" ]]; then
159
+ info "开始下载模型 (支持断点续传)..."
160
+ curl -L -C - --retry 3 --retry-delay 5 --progress-bar "$MODEL_URL" -o "$MODEL_PATH" || error "模型下载失败"
161
+ ok "模型下载完成: $MODEL_PATH"
 
162
  else
163
+ SIZE=$(stat -c%s "$MODEL_PATH" 2>/dev/null || stat -f%z "$MODEL_PATH" 2>/dev/null || echo 0)
 
164
  if [[ "$SIZE" -lt 1000000000 ]]; then
165
+ warn "模型文件过小 (${SIZE} bytes),可能下载不完整,将尝试续传..."
166
+ curl -L -C - --retry 3 --retry-delay 5 --progress-bar "$MODEL_URL" -o "$MODEL_PATH" || error "续传失败"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  fi
168
+ ok "模型已存在,跳过下载: $MODEL_PATH ($SIZE bytes)"
 
169
  fi
170
 
171
  # ==============================================================================
172
+ # 3. 启动 llama-server (使用 taskset 和指定参数,上下文调至极限 128K)
173
  # ==============================================================================
174
+ info "3) 启动 llama-server (port $LLAMA_PORT, taskset 0-14, context $CONTEXT_SIZE)..."
175
+
176
+ # 清理旧进程
177
  if [[ -f "$HOME_DIR/llama-server.pid" ]]; then
178
  OLD_PID=$(cat "$HOME_DIR/llama-server.pid" 2>/dev/null || echo "")
179
  if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then
180
+ info "停止旧 llama-server (PID $OLD_PID)..."
181
  kill "$OLD_PID" 2>/dev/null || true
182
  sleep 2
183
  kill -9 "$OLD_PID" 2>/dev/null || true
 
186
  pkill -f "llama-server.*${LLAMA_PORT}" 2>/dev/null || true
187
  sleep 1
188
 
189
+ # 构建命令(注意 taskset 包裹整个 llama-server)
190
+ CMD=(
191
+ taskset -c 0-14
192
+ "$LLAMA_BIN/llama-server"
193
  -m "$MODEL_PATH"
194
+ --alias ornith-1.5
195
+ -t 15
196
+ --jinja
197
+ --mlock
198
+ -c "$CONTEXT_SIZE"
199
+ --host 127.0.0.1
200
  --port "$LLAMA_PORT"
 
 
 
201
  )
202
 
203
+ info "启动命令: ${CMD[*]}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  touch "$HOME_DIR/llama-server.log" 2>/dev/null || true
205
  nohup "${CMD[@]}" > "$HOME_DIR/llama-server.log" 2>&1 &
206
  echo $! > "$HOME_DIR/llama-server.pid"
207
  info "llama-server PID: $(cat "$HOME_DIR/llama-server.pid")"
208
 
209
+ # 等待服务器就绪 (最长 5 分钟)
210
+ info "等待 llama-server 就绪 (最多 5 分钟)..."
211
  READY=0
212
  for i in {1..60}; do
213
  if curl -sf "http://127.0.0.1:${LLAMA_PORT}/health" >/dev/null 2>&1; then
214
+ ok "llama-server 已就绪 (http://127.0.0.1:${LLAMA_PORT})"
215
  READY=1
216
  break
217
  fi
 
218
  if curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/models" >/dev/null 2>&1; then
219
+ ok "llama-server 已就绪 (via /v1/models)"
220
  READY=1
221
  break
222
  fi
223
  if ! kill -0 $(cat "$HOME_DIR/llama-server.pid" 2>/dev/null || echo 0) 2>/dev/null; then
224
+ error "llama-server 启动失败,请查看 ~/llama-server.log (最后 50 行):\n$(tail -n 50 "$HOME_DIR/llama-server.log" 2>/dev/null)"
225
  fi
226
  echo -n "."
227
  sleep 5
228
  done
229
  echo ""
230
  if [[ "$READY" -ne 1 ]]; then
231
+ error "llama-server 在 5 分钟内未就绪,请查看 ~/llama-server.log"
 
 
 
 
 
 
 
232
  fi
233
 
234
  # ==============================================================================
235
+ # 4. 安装 pi agent (全自动,无互动)
236
  # ==============================================================================
237
+ info "4) 安装 pi agent (全自动)..."
238
 
 
239
  NPM_PREFIX=$(npm prefix -g 2>/dev/null || npm config get prefix 2>/dev/null || echo "$HOME/.local")
240
+ info "npm 全局前缀: $NPM_PREFIX"
241
 
 
242
  NEED_INSTALL=1
243
  if command -v pi >/dev/null 2>&1; then
244
+ ok "pi 已安装: $(pi --version 2>&1 | head -n 1)"
 
245
  NEED_INSTALL=0
 
246
  if [[ "${PI_FORCE_REINSTALL:-0}" == "1" ]]; then
247
+ info "PI_FORCE_REINSTALL=1,强制重新安装..."
248
  NEED_INSTALL=1
249
  fi
250
  fi
251
 
252
  if [[ "$NEED_INSTALL" -eq 1 ]]; then
253
+ info "执行 pi 官方安装脚本 (全自动模式)..."
 
 
 
 
 
 
 
254
  PI_PACKAGE="@earendil-works/pi-coding-agent"
 
255
  if npm prefix -g 2>/dev/null | xargs test -w 2>/dev/null; then
256
  INSTALL_PREFIX=""
257
  else
258
  INSTALL_PREFIX="$HOME/.local"
259
+ info "全局 prefix 不可写,将安装到 $INSTALL_PREFIX"
260
  fi
261
 
262
  set +e
 
270
  set -e
271
 
272
  if [[ "$NPM_STATUS" -ne 0 ]]; then
273
+ warn "npm 安装 pi 失败 (code $NPM_STATUS),尝试备用方法: curl 官方脚本..."
 
 
274
  curl -fsSL https://pi.dev/install.sh | bash -s -- 2>&1 | tee -a /tmp/pi-install.log || true
275
  fi
276
 
 
 
277
  export PATH="$HOME/.local/bin:$HOME/.pi/bin:$NPM_PREFIX/bin:$PATH"
278
  if [[ -n "${INSTALL_PREFIX:-}" ]]; then
279
  export PATH="$INSTALL_PREFIX/bin:$PATH"
280
  fi
 
281
  NODE_BIN_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/pi-node/current/bin"
282
  export PATH="$NODE_BIN_DIR:$PATH"
 
283
  hash -r 2>/dev/null || true
284
  fi
285
 
 
286
  PI_BIN=$(command -v pi 2>/dev/null || echo "")
287
  if [[ -z "$PI_BIN" ]]; then
 
288
  for cand in "$HOME/.local/bin/pi" "$HOME/.pi/bin/pi" "$NPM_PREFIX/bin/pi" "/usr/local/bin/pi"; do
289
  if [[ -x "$cand" ]]; then
290
  PI_BIN="$cand"
 
295
  fi
296
 
297
  if [[ -z "$PI_BIN" ]] || [[ ! -x "$PI_BIN" ]]; then
298
+ error "pi 安装后仍找不到执行档,请查看 /tmp/pi-install.log"
299
  fi
300
 
301
  ok "pi 可用: $PI_BIN ($(pi --version 2>&1 | head -n 1))"
302
 
 
303
  for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
304
  touch "$rc" 2>/dev/null || true
 
305
  PI_BIN_DIR="$(dirname "$PI_BIN")"
306
  if [[ "$PI_BIN_DIR" != "" ]]; then
307
  grep -qF "$PI_BIN_DIR" "$rc" 2>/dev/null || echo "export PATH=\"$PI_BIN_DIR:\$PATH\"" >> "$rc"
308
  fi
 
309
  grep -qF ".local/bin" "$rc" 2>/dev/null || echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$rc"
310
  done
311
 
312
  # ==============================================================================
313
+ # 5. 配置 pi 使用本地模型 (上下文调至极限)
314
  # ==============================================================================
315
  info "5) 配置 pi models.json ($PI_MODELS_JSON)..."
316
 
 
317
  mkdir -p "$(dirname "$PI_MODELS_JSON")"
318
 
319
+ python3 - "$PI_MODELS_JSON" "$LLAMA_PORT" "$MODEL_ID" "$PROVIDER_ID" "$CONTEXT_SIZE" << 'PY'
320
  import json, os, sys
321
+ path, port, model_id, provider, ctx_size = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], int(sys.argv[5])
322
  os.makedirs(os.path.dirname(path), exist_ok=True)
323
  cfg = {"providers": {}}
324
  if os.path.exists(path):
 
330
  if "providers" not in cfg:
331
  cfg["providers"] = {}
332
  except Exception as e:
333
+ print(f"[WARN] 无法解析旧 models.json,将覆盖: {e}")
334
 
335
  cfg["providers"][provider] = {
336
  "baseUrl": f"http://127.0.0.1:{port}/v1",
 
339
  "compat": {"supportsDeveloperRole": True, "supportsReasoningEffort": False},
340
  "models": [{
341
  "id": model_id,
342
+ "name": "Ornith 1.5 35B A3B (Local)",
343
  "reasoning": True,
344
  "input": ["text", "image"],
345
+ "contextWindow": ctx_size,
346
+ "maxTokens": ctx_size,
347
  "cost": {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}
348
  }]
349
  }
350
  with open(path, "w") as f:
351
  json.dump(cfg, f, indent=2)
352
+ print(f"[OK] 已写入 {path}")
353
  print(json.dumps(cfg, indent=2))
354
  PY
355
 
356
  ok "pi 配置完成"
357
 
 
358
  WRAPPER="$HOME/.local/bin/pi-local"
359
  mkdir -p "$(dirname "$WRAPPER")"
360
  cat > "$WRAPPER" << EOS
 
364
  chmod +x "$WRAPPER"
365
  ok "已建立 pi-local wrapper: $WRAPPER"
366
 
 
367
  for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
368
  touch "$rc" 2>/dev/null || true
369
  grep -q "alias pi-local" "$rc" 2>/dev/null || echo "alias pi-local=\"pi --provider $PROVIDER_ID --model $MODEL_ID\"" >> "$rc"
370
  done
371
 
372
  # ==============================================================================
373
+ # 6. 安装后验证
374
  # ==============================================================================
375
+ info "6) 安装后验证..."
376
 
377
  VERIFICATION_FAILED=0
378
 
379
+ # 6.1 检查 llama-server 健康
380
+ info "[验证 1/4] 检查 llama-server 健康状态..."
381
  if curl -sf "http://127.0.0.1:${LLAMA_PORT}/health" >/dev/null 2>&1; then
382
+ ok "llama-server 健康检查通过"
383
  elif curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/models" >/dev/null 2>&1; then
384
+ ok "llama-server (via /v1/models) 健康检查通过"
385
  else
386
+ warn "llama-server 健康检查失败"
387
  VERIFICATION_FAILED=1
388
  fi
389
 
390
+ # 6.2 测试 OpenAI API 推理
391
+ info "[验证 2/4] 测试 OpenAI 兼容 API 推理..."
392
  API_TEST_RESPONSE=$(curl -sf "http://127.0.0.1:${LLAMA_PORT}/v1/chat/completions" \
393
  -H "Content-Type: application/json" \
394
  -d '{
395
  "model": "'$MODEL_ID'",
396
+ "messages": [{"role": "user", "content": "用一句话介绍你自己,不要超过20字"}],
397
  "max_tokens": 50,
398
  "temperature": 0.7
399
  }' 2>&1 || echo "CURL_FAILED")
400
 
401
  if echo "$API_TEST_RESPONSE" | grep -q '"choices"' && echo "$API_TEST_RESPONSE" | grep -q '"content"'; then
402
+ ok "OpenAI API 推理测试通过"
403
  echo "$API_TEST_RESPONSE" | python3 -m json.tool 2>/dev/null | head -n 30 || echo "$API_TEST_RESPONSE" | head -n 20
404
  elif echo "$API_TEST_RESPONSE" | grep -q "CURL_FAILED"; then
405
+ warn "API 推理 curl 失败: $API_TEST_RESPONSE"
406
  VERIFICATION_FAILED=1
407
  else
408
+ warn "API 推理回应异常,可能是模型仍在加载中..."
409
  echo "$API_TEST_RESPONSE" | head -n 50 >&2
 
410
  fi
411
 
412
+ # 6.3 检查 pi 可用
413
+ info "[验证 3/4] 检查 pi 指令..."
414
  if pi --version >/dev/null 2>&1; then
415
  ok "pi --version: $(pi --version 2>&1 | head -n 1)"
416
  else
417
+ warn "pi --version 执行失败"
418
  VERIFICATION_FAILED=1
419
  fi
420
 
 
422
  ok "pi --help 正常"
423
  fi
424
 
 
425
  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
426
+ ok "pi models.json 配置正确"
427
  else
428
+ warn "pi models.json 配置检查失败"
429
+ cat "$PI_MODELS_JSON" 2>/dev/null || echo "文件不存在"
430
  VERIFICATION_FAILED=1
431
  fi
432
 
433
  if [[ -x "$WRAPPER" ]]; then
434
+ ok "pi-local wrapper 存在且可执行"
435
  else
436
  warn "pi-local wrapper 缺失"
437
  VERIFICATION_FAILED=1
438
  fi
439
 
440
+ # 6.4 测试 pi-local 实际可用
441
+ info "[验证 4/4] 测试 pi-local 实际推理 (超时 90 秒)..."
 
442
  export PATH="$HOME/.local/bin:$HOME/.pi/bin:$(dirname "$PI_BIN"):$PATH"
443
 
 
444
  set +e
445
+ PI_TEST_OUTPUT=$(timeout 90 pi-local -p "用一句话介绍你自己,不超过20字" 2>&1)
446
  PI_EXIT_CODE=$?
447
  set -e
448
 
449
  if [[ $PI_EXIT_CODE -eq 0 ]] && [[ -n "$PI_TEST_OUTPUT" ]]; then
450
+ ok "pi-local 推理测试通过 (exit 0)"
451
+ echo "--- pi-local 输出 ---"
452
  echo "$PI_TEST_OUTPUT" | head -n 30
453
  echo "---------------------"
454
  elif [[ $PI_EXIT_CODE -eq 124 ]]; then
455
+ warn "pi-local 测试超时 (90s),可能模型加载较慢或首次启动较久。请稍后手动重试: pi-local -p \"你好\""
 
456
  else
457
+ warn "pi-local 测试失败 (exit $PI_EXIT_CODE),输出:"
458
  echo "$PI_TEST_OUTPUT" | head -n 100 >&2
 
459
  if [[ "$VERIFICATION_FAILED" -eq 0 ]]; then
460
+ info "尝试直接执行 pi 指令..."
 
461
  set +e
462
  PI_DIRECT_OUTPUT=$(timeout 90 pi --provider "$PROVIDER_ID" --model "$MODEL_ID" -p "你好" 2>&1)
463
  PI_DIRECT_CODE=$?
464
  set -e
465
  if [[ $PI_DIRECT_CODE -eq 0 ]]; then
466
+ ok "pi 直接调用测试通过"
467
  else
468
+ warn "pi 直接调用也失败 (exit $PI_DIRECT_CODE): $PI_DIRECT_OUTPUT"
469
  VERIFICATION_FAILED=1
470
  fi
471
  else
 
474
  fi
475
 
476
  # ==============================================================================
477
+ # 总结
478
  # ==============================================================================
479
  echo ""
480
  echo "================================================================"
481
  if [[ "$VERIFICATION_FAILED" -eq 0 ]]; then
482
+ echo -e "\033[32m✅ 安装完成且验证通过!\033[0m"
483
  else
484
+ echo -e "\033[33m⚠️ 安装完成,但部分验证未通过,请查看上方警告\033[0m"
485
  fi
486
  echo "================================================================"
487
  echo "- 模型: $REPO"
488
+ echo " 模型文件: $MODEL_PATH"
489
+ echo "- llama-server (编译自源码): $LLAMA_BIN/llama-server"
490
+ echo " 启动参数: taskset -c 0-14 ... -t 15 --jinja --mlock -c $CONTEXT_SIZE --host 127.0.0.1 --port $LLAMA_PORT"
491
+ echo "- 服务: http://127.0.0.1:$LLAMA_PORT (log: ~/llama-server.log, pid: ~/llama-server.pid)"
492
  echo "- pi: $PI_BIN ($(pi --version 2>&1 | head -n 1))"
493
+ echo "- pi 配置: $PI_MODELS_JSON (provider: $PROVIDER_ID, model: $MODEL_ID, context: $CONTEXT_SIZE)"
494
  echo "- 快速使用:"
495
  echo " source ~/.bashrc # 或 source ~/.zshrc"
496
  echo " pi-local -p \"你好\""
497
  echo " pi --provider $PROVIDER_ID --model $MODEL_ID -p \"你好\""
498
+ echo "- 停止服务器: kill \$(cat ~/llama-server.pid)"
499
+ echo "- 重新验证: curl http://127.0.0.1:$LLAMA_PORT/v1/models && pi-local -p \"测试\""
 
500
  echo "================================================================"
501
 
502
  if [[ "$VERIFICATION_FAILED" -ne 0 ]]; then
503
+ echo "请检查日志:"
504
  echo " - llama-server: ~/llama-server.log (tail -n 100 ~/llama-server.log)"
505
+ echo " - pi 安装: /tmp/pi-install.log"
506
  echo " - 模型: $MODEL_DIR/"
507
  exit 1
508
  fi
509
 
510
+ exit 0