LLM CLI & MCP Bridge Server on Synology NAS (Codex CLI, Antigravity CLI, OpenAI Tunnel)
1.4K
本專案旨在 Synology NAS 上部署一個具備 Python 環境與 OpenAI codex-cli 的 Docker 容器,並透過 GitHub Actions 實現自動化部署。
📊 用量統計已整合到 llm 函式庫 README(Llm-Cli-APIServer 段落),不再於本檔案獨立維護一份會漂移的副本。
在您的 Synology NAS 上,確保已安裝 Container Manager (Docker) 並運行了 GitHub Self-hosted Runner。
進入 GitHub Repository 的 Settings > Secrets and variables > Actions,新增以下 Secret:
SSH_ROOT_PASSWORD: 容器 SSH root 密碼(必填)。CODEX_API_KEY: 保護 /codex/exec 端點的存取金鑰(選填,留空則不驗證)。將代碼 Push 到 main 分支後,GitHub Actions 會自動同步程式碼並重新建立容器。第一次部署會自動 build image;之後一般部署會沿用既有 Docker image,不重跑 build。只有 Dockerfile、系統套件、Codex CLI、agy 或 Python dependency 變更時,手動執行 workflow 並設定 rebuild_image=true。
由於 codex-cli 需要瀏覽器登入,請依照以下步驟完成一次性授權:
codex login,日誌中會出現瀏覽器授權連結與一次性代碼。/volume1/docker/llm-cli-api-server/codex-auth,並保留 /volume1/docker/llm-cli-api-server/config 舊路徑相容。codex remote-control app-server;workflow 會輸出 codex remote-control pair 的短效配對碼,供本機 Codex 或 iOS app 遠端配對使用。/gemini/exec、/smart/exec(gemini CLI)的主要路徑已改用 agy,取代已於 2026-06-18 停用的 gemini-cli OAuth 登入。與 Codex 不同,agy 的登入是直接在 NAS container 內完成,不是在筆電登入後再複製 token:
skills/skill-llm-api-server/scripts/renew-agy-auth.sh(或手動 ssh 進 NAS 後 docker exec -it llm-cli-api-server agy)。./gemini-auth),container 重啟後仍會保留。curl http://llm-cli-api.tail28f10.ts.net:5001/gemini/status 驗證 gemini_cli 是否為 installed。本專案整合了 local-workspace-mcp 與 OpenAI 官方 tunnel-client,讓一般 ChatGPT(網頁版或 App)可以直接透過 Secure MCP Tunnel 存取 NAS 容器環境與檔案:
OPENAI_TUNNEL_ID(如 tunnel_...)。Tunnels Read + Use 權限的受限金鑰,填入 OPENAI_TUNNEL_RUNTIME_KEY(如 sk-proj-...)。OPENAI_TUNNEL_RUNTIME_KEY 與 Variable/Secret OPENAI_TUNNEL_ID。tunnel-client 與 MCP 套件,第一次啟用請在 GitHub Actions 執行 Deploy Llm-Cli-APIServer to NAS 並設定 rebuild_image=true。scripts/start-openai-tunnel.sh 在背景常駐維護 tunnel-client,並自動重連。GET /tunnel/status 檢查通道是否正常運作。codex-cli (@openai/codex)tailscaled,Tailscale hostname 為 llm-cli-api,可直連 llm-cli-api.tail28f10.ts.net:22,不需透過 NAS 主機轉發/app,所以一般程式碼更新只需 rsync + recreate,不需重建 image。/app -> NAS /volume1/docker/llm-cli-api-server (rsync 後的目前程式碼)/app/data -> NAS data/ (存放應用程式資料)/root/.codex -> NAS codex-auth/ (Codex 目前授權與 remote-control 狀態)/root/.config/codex -> NAS config/ (Codex 舊授權路徑相容)Updated: 2026-11-07 15:14 CST
| 存取方式 | 位址 | 狀態 |
|---|---|---|
| 外網 HTTPS (WAN) | http://llm-cli-api.tail28f10.ts.net:5001 | ❌ 無法連線 (Runner 限制) |
| 容器網關 (Docker) | http://172.18.0.1:5055 | ✅ 正常 |
| 組件 | 端點 | 狀態 |
|---|---|---|
| Gemini CLI | /gemini/status | ✅ 已安裝 (1.5.1) |
| Codex CLI | /codex/status | ✅ 已安裝 (codex-cli 0.167.0) |
外網統一使用 http://llm-cli-api.tail28f10.ts.net:5001;內網可使用 http://newton.tail28f10.ts.net:5055(透過 NAS 主機轉發)或 http://llm-cli-api.tail28f10.ts.net:5001(容器 Tailscale 直連,不需 NAS 防火牆放行)。
Prompt 字元限制可用 CODEX_MAX_PROMPT_LENGTH 與 GEMINI_MAX_PROMPT_LENGTH 設定;預設 0 表示 server 不設字元 guard,但 CLI、gateway 與模型本身仍可能有 context window 限制。若設定正數,超限請求會回傳 HTTP 413。
/exec 會以 CODEX_TIMEOUT 限制 codex exec 的最長執行時間,預設為 120 秒。若 Codex CLI 超過此時間仍未回應,API server 會回傳 HTTP 504,response body 會包含:
{
"error": "codex timed out after 120s"
}
若 client 端看到約 120 秒後失敗,優先檢查 container log:
docker logs llm-cli-api-server --since "2026-05-12T11:43:00Z"
相關 log 範例:
CLI start: cli=codex timeout=120s prompt_chars=...
CLI timeout: cli=codex elapsed=120.0s timeout=120s
若 container log 有 CLI timeout,代表 timeout 發生在 API server 內的 Codex CLI 執行流程;若沒有對應 log,才優先檢查 Synology reverse proxy、nginx、Cloudflare 等外層 gateway timeout。可在 GitHub repository variables 設定 CODEX_TIMEOUT 與 GEMINI_TIMEOUT,deploy workflow 會寫入 NAS 的 .env。
若在 container 內使用 Codex CLI 時看到:
bwrap: Creating new namespace failed, likely because the kernel does not support user namespaces. bwrap must be installed setuid on such systems.
這通常不是 Flask API 程式錯誤,而是 Codex sandbox 依賴的 bubblewrap 無法建立 Linux namespace。此 image 已安裝 bubblewrap 並在 build 階段執行 chmod u+s /usr/bin/bwrap;container 啟動時也會執行一次 sandbox smoke test,失敗時會在 log 中輸出 bubblewrap sandbox check failed。
可在 host/container 檢查:
bwrap --version
ls -l "$(command -v bwrap)"
sysctl kernel.unprivileged_userns_clone
sysctl user.max_user_namespaces
常見修法:
sudo sysctl -w kernel.unprivileged_userns_clone=1
sudo sysctl -w user.max_user_namespaces=15000
sudo chown root:root "$(command -v bwrap)"
sudo chmod u+s "$(command -v bwrap)"
若 host kernel 不提供這些 sysctl,或容器平台禁止 user namespace,即使 container 使用 privileged: true 仍可能失敗;這時要改 host/container runtime 設定,或讓 Codex 使用不依賴 bubblewrap 的執行模式。
容器啟動時預設會由 /app/scripts/start-codex-remote-control.sh 在背景維持 foreground codex remote-control app-server,行為等同 SSH 進容器後手動執行 codex remote-control。部署 workflow 完成授權檢查後,會輸出 codex remote-control pair 的短效配對碼;若暫時不需要遠端配對,可在 GitHub repository variables 設定 CODEX_REMOTE_CONTROL=0 停用。若 app-server 因授權尚未完成或網路暫時失敗而結束,supervisor 會依 CODEX_REMOTE_CONTROL_RESTART_DELAY 秒數重試,預設 10 秒。容器內可用 tail -f /tmp/codex_remote_control.log 檢查狀態。
GET /
{"status": "ready", "service": "LLM CLI API Server"}GET /codex/status
{"codex_cli": "installed", "version": "codex-cli 0.128.0"}(未安裝時回傳 503)GET /gemini/status(已改為 agy,見上方第 5 節)
{"gemini_cli": "installed", "tool": "agy", "auth_mode": "oauth", "version": "agy 1.1.22"}需在 Header 帶 X-API-Key(若未設定 CODEX_API_KEY 則不需要)。
此端點與 llm 函式庫的 CodexProvider 相容,設定方式:
CODEX_API_KEY=your-key
curl 範例:
curl -X POST http://llm-cli-api.tail28f10.ts.net:5001/exec \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"prompt": "請幫我寫一個 Python 的 hello world 程式"}'
Request 選填欄位:
{
"prompt": "...",
"model": "gpt-5-codex"
}
不帶 model 時,沿用 codex CLI 自己的預設 model(由該容器的 Codex 登入帳號/plan 決定,可用 codex model --help 或 /codex/help 查詢實際可用值);帶了 model 才會在指令加上 --model <model>。
回應格式:
{
"output": "print('Hello, World!')"
}
需在 Header 帶 X-API-Key(若未設定 CODEX_API_KEY 則不需要)。
curl 範例:
curl -X POST http://llm-cli-api.tail28f10.ts.net:5001/gemini/exec \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{"prompt": "請幫我寫一個 Python 的 hello world 程式", "model": "gemini-2.5-flash"}'
Request 選填欄位:
{
"prompt": "...",
"model": "gemini-2.5-flash",
"json_mode": false
}
回應格式:
{
"output": "print('Hello, World!')"
}
Python smoke test:
# 內網透過 NAS 主機轉發
python check_gemini_cli.py
# 內網容器 Tailscale 直連(推薦,不受 NAS 防火牆影響)
python check_gemini_cli.py
# 外網或需要 API key 時
CODEX_API_KEY=your-key \
GEMINI_MODEL=gemini-2.5-flash \
python check_gemini_cli.py
此腳本會依序呼叫 GET /gemini/status 與 POST /gemini/exec,可用來確認 Gemini CLI 是否已安裝、授權且可實際產生回應。
POST /smart/exec 會先執行 draft provider,再由 judge provider 判斷 draft 是否可接受。若 draft 或 judge 階段發生錯誤,API 會回傳結構化診斷欄位,方便判斷 fallback 的 root cause。
錯誤回應範例:
{
"smart_status": "error",
"fallback_reason": "auth_failure",
"failed_stage": "draft",
"provider": "gemini",
"error": "gemini exited 1: 401 Unauthorized"
}
診斷欄位:
| 欄位 | 說明 |
|---|---|
smart_status | Smart Routing 狀態;錯誤時為 error。 |
fallback_reason | 錯誤分類,例如 timeout、auth_failure、cli_not_found、nonzero_exit、unknown_error。 |
failed_stage | 失敗發生階段:draft 或 judge。 |
provider | 失敗階段使用的 provider,例如 gemini 或 codex。 |
error | 原始錯誤訊息,供 log 或人工追查使用。 |
fallback reason 分類:
| Reason | 常見原因 |
|---|---|
timeout | CLI 執行超過 CODEX_TIMEOUT 或 GEMINI_TIMEOUT。 |
auth_failure | CLI token 過期、未登入、401 Unauthorized、credential 問題。 |
cli_not_found | container 內找不到 codex 或 gemini command。 |
nonzero_exit | CLI 有執行但 return code 非 0,且錯誤不像 auth 問題。 |
unknown_error | 其他未分類例外。 |
有了這些欄位後,README 的 Smart Routing 統計不應只看 Fallback (Error) 總數;應進一步依 fallback_reason、failed_stage、provider 分組,才能知道主要 root cause 是授權、timeout、CLI 安裝,還是 provider/model 設定問題。
Dockerfile: 混合環境定義entrypoint.sh: 啟動與認證注入腳本docker-compose.yml: 容器編排與磁碟映射main.py: Flask API Server 主程式.github/workflows/deploy-synology-nas.yml: 自動化部署流程Content type
Image
Digest
sha256:7ba6390c9…
Size
1.2 GB
Last updated
5 days ago
docker pull wenchiehlee1020/llm-cli-api-server