附錄 C

底層架構好奇者向

平常用 Pi Agent 你不需要看這篇——這裡講的是「掀開引擎蓋看裡面在跑什麼」。當 第 22 章的 Logs 出現看不懂的錯、當你想自己 debug 而不是等下一版、當你想知道為什麼一個對話介面要跑那麼多背景服務——這篇一次講清楚。技術詞第一次會標「中文(English)」,但不會要你寫程式。看完你會知道:s6-overlay 是什麼、nginx 為什麼要擋在前面、/data/pi-agent/ 底下每個資料夾裝什麼、Watchdog 探針怎麼探、Skill 是怎麼被「發現」的。像看家裡的水電配置圖——不用你動手,但知道總開關在哪就是安心。

本文引用的原始碼(都是公開 repo):add-on 打包 WOOWTECH/Woow_ha_pi_agent_add_onconfig.yamlDockerfilerootfs/etc/nginx/nginx.confCHANGELOG.mdDOCS.md),pi-web 上游 agegr/pi-web(HA add-on 把版本鎖在 0.8.4,可看該 tag 的 package.json),agent SDK earendil-works/pi。若下文哪一句你覺得可疑,直接去那三個 repo 對原始檔——比這裡的敘述準。

為什麼要看這篇(不看也行)

先講清楚:99% 時候你不需要這篇。你按著第 2 章裝好、第 3 章從側邊欄打開、用得順順的——不用知道背後跑了什麼。就像你開冷氣不用知道壓縮機的原理。

但有那 1% 的時候會派上用場:

  • Logs 出現看不懂的錯——你在 add-on 頁面切到 Log 分頁看到「nginx: [emerg] host not found in upstream」、「s6-rc: fatal: unable to start service pi-web」、「video-tools-init exited with code 1」,這些訊息你查 Google 也查不到 Pi Agent 專屬解法。看完這附錄你會知道每個服務是幹嘛的、失敗代表什麼。
  • 想自己 debug 不想等下一版——某個功能怪怪的,你想 SSH 進 add-on 容器看看到底哪出問題。你得先知道有哪些目錄、哪些 process、log 存哪。
  • 想改進或客製——你想 fork Pi Agent、想幫上游貢獻、想寫一個 companion add-on。這時你必須理解整條 pipeline。
  • 純粹好奇——你是那種買了新設備會拆開看的人,那繼續往下讀就對了。
誠實提醒:這附錄的內容會隨版本改變。作者寫的時候 Pi Agent 是 v0.13 系列;如果你看的時候版本已經跳很遠(例如 v1.x),檔案路徑跟服務名稱可能不同。以 /etc/services.d/ 底下實際的資料夾為準,附錄只是給你一個「大約長這樣」的地圖。

整體架構:大樓管理員比喻

用一個具體比喻先把層次講清楚,之後每一節再展開。把整套系統想像成一棟大樓:

比喻對應的東西做什麼
大樓本身 Home Assistant(HA)作業系統 提供地基、水電、網路——住戶(add-on)才有得住
門房(Supervisor) HA Supervisor 負責裝新住戶、啟動住戶、每分鐘去看住戶還好嗎、住戶掛了幫他重開
接待櫃檯(Ingress) HA Ingress 系統 外面的訪客(你的瀏覽器)想找住戶都得經過櫃檯登記,櫃檯負責認證與轉介
某戶租戶(我們家) Woow HA Pi Agent add-on 這個容器 就是這篇要拆解的主角
戶內的家人 容器內的多個 process nginx、pi-web、video-tools-init 各自負責家裡的一部分工作
戶內的家管系統 s6-overlay(process supervisor) 負責喚醒家人、按順序上班、家人偷懶就叫醒他
戶內的儲藏室 /data/pi-agent/ 目錄 存所有值錢的東西——對話、金鑰、Skill、影片專案

兩個關鍵設計你要抓住:

  1. Ingress 是唯一入口

    你的瀏覽器不能直接連 Pi Agent 的內部服務。所有請求都得經過 HA 的接待櫃檯(Ingress),櫃檯先確認你是登入過的 HA admin,再放行到 Pi Agent 內部。這是為什麼第 3 章強調「側邊欄按鈕自動出現」——那個按鈕背後就是走 Ingress。

  2. 戶內是多層次的

    Pi Agent 這個 add-on 容器內部不是只跑一個 process,而是一整組——nginx 站在門口收轉發、pi-web 是主應用、video-tools-init 是首次啟動才會跑的一次性 worker,加上 s6-overlay 這個「家管系統」在背後協調。

觀念:HA add-on 本質上就是一個 Docker container。Pi Agent 這個 container 內部同時跑多個 process 不太符合「一個 container 一個 process」的教科書慣例——但這是 HA add-on 的常見做法,s6-overlay 就是為了在單一容器內優雅管理多 process 而存在的工具。

s6-overlay 開機順序:家人幾點上班

當 HA Supervisor 按下 Pi Agent 的「啟動」,容器內部發生的事其實有明確順序。s6-overlay(有時寫 s6)是這棟家管系統的核心——它是一套「行程監督工具(process supervisor)」,負責把家人一個個依序叫起來。

階段(見下方細節)服務名稱類型做的事典型耗時
1 s6-rc-init 系統啟動 s6 自己的「家管系統」先開機,把之後要用的內部訊號通道準備好 < 1 秒
2 video-tools-init 一次性任務(oneshot) 檢查儲藏室有沒有 .video-tools-installed sentinel 檔 venv 裡的 python3 真的存在——兩個都通過就跳過;否則下載 Playwright、edge-tts、pyyaml、mutagen 加上 Chromium(~720 MB 合計),裝到 /data/pi-agent/venv//data/pi-agent/playwright-cache/;裝完放 sentinel 說「裝好了下次別再裝」。失敗是 non-fatal——退出時對話介面還能用 首次 3-8 分鐘,之後 < 1 秒
3 nginx 長駐服務(long-running) 啟動 nginx(反向代理,reverse proxy)監聽容器內部 30142 埠——所有 HA Ingress 轉進來的請求都會先撞到它 < 1 秒
4 pi-web 長駐服務 啟動 Node.js Next.js 主應用(就是你在瀏覽器看到的介面);監聽容器內部 30141 埠;nginx 會把請求轉給它 3-15 秒

這裡有一件看原始碼才發現的細節:實際上 video-tools-init oneshot 沒有被列進 nginx/pi-web 的 dependencies.d——換句話說,s6-overlay 開機時把三個服務平行啟動:video-tools-init 一邊下 720MB Chromium,nginx 跟 pi-web 一邊已經在收請求。所以裝機那 3-8 分鐘的等待時,你其實已經可以打開對話介面聊天,只是影片管線暫時不能用。這是作者刻意的設計取捨(看 rootfs/etc/s6-overlay/scripts/video-tools-init 開頭的註解「Parallel with pi-web」),為了「chat UI 立刻能用、影片工具慢慢裝」,非「等一切齊全才給你進門」。

另外,Supervisor 從外面也會定期打 Watchdog 探針(下一節詳說)——如果 pi-web 卡住、hang 住、或應答變得很慢,Supervisor 會下令重啟整個容器。s6-overlay 這時會按同樣順序再走一次,只不過第 2 步會秒退(sentinel 存在+${VENV}/bin/python3 可執行都通過才會 skip,這是雙重檢查,避免第一次裝到一半掉線的殘骸誤判成裝好)。

對照日常經驗:你可能會問「為什麼要這麼麻煩,不能一個服務搞定嗎?」——因為每個 process 有各自的職責:video-tools-init 是只跑一次的(不是常駐)、nginx 是擋在前面的(跟後端不同崗位)、pi-web 是本體。混在一起寫會變成一個超大程式很難維護。s6-overlay 就是幫你把這些「不同壽命的角色」分開管理。

nginx 的角色:HA Ingress 的翻譯官

你可能會覺得「Pi Agent 是個網頁應用,為什麼中間要多一層 nginx?多此一舉」。實際上 nginx 是整個容器裡最微妙、最關鍵的一個服務——它是翻譯官,把 HA Ingress 講的話翻譯給 pi-web 聽。

先看問題:HA 給每個 add-on 分配的 URL 長這樣——

https://homeassistant.local:8123/hassio/ingress/woow_ha_pi_agent/

而 pi-web 這個應用內部只認得「根路徑」(/),它不知道自己被藏在 /hassio/ingress/woow_ha_pi_agent/ 底下。如果不處理,pi-web 產生的 HTML 裡的連結會寫成 /settings 之類的相對路徑,瀏覽器一點就跳到 https://homeassistant.local:8123/settings——404,因為那不在 HA 的路由裡。

nginx 就是負責幫忙「路徑翻譯」的中間人:

  1. 收 HA Ingress 進來的請求

    HA 把請求塞給 Pi Agent 容器的 30142 埠(ingress_port: 30142),nginx 監聽那個埠,所以第一手一定收到。

  2. 從請求頭部把 Ingress path 取出來

    HA 會在請求裡塞一個叫 X-Ingress-Path 的 HTTP header,內容像 /api/hassio_ingress/A1b2C3d4e5F6g7H8(16-128 字元的隨機字串,每次 HA 重啟會換)。

  3. 轉給 pi-web、拿到回覆

    nginx 把請求轉發到 http://127.0.0.1:30141(pi-web 內部埠),pi-web 產出 HTML 回覆。

  4. 白名單防禦:先驗證 header 合法

    為了避免有人偽造 X-Ingress-Path 亂射,nginx 用 map $http_x_ingress_path $safe_ingress_path 區塊配合 regex(正則表達式)驗證,只讓格式為 ^/api/hassio_ingress/[A-Za-z0-9_-]{16,128}$ 的過(直接從 nginx.conf 抄的,不是推測)。不符合就強制設成空字串——之後所有 body-rewrite 都變成 no-op,關閉「透過偽造 header 打 XSS」的可能。這是 v0.7.0 為了修 F-01 安全審計加的。

  5. 用 sub_filter 改寫 HTML 裡的絕對路徑

    nginx 用 sub_filter 逐字節改寫傳出的 HTML/CSS/JS:把 href="/_next/src="/_next/href="/manifesthref="/icons/、CSS 裡的 url(/_next/、還有 RSC flight payload 裡 JSON-escape 的 \"/_next/,通通加上 Ingress 前綴。

  6. 但 sub_filter 蓋不到執行期的 JS 呼叫——所以又塞了一個 client-side shim

    這才是這個檔案裡最麻煩的一段。pi-web 的前端 bundle 有 40+ 個絕對路徑的 /api/* 呼叫(fetch、EventSource、XMLHttpRequest),加上 Next.js App Router 的 RSC prefetch(?_rsc=<token>)、history.pushState/replaceState、以及 ReactDOM.preinit()next/font 執行期塞進 DOM 的 <link>——這些 sub_filter 都攔不到。所以 nginx 額外用 sub_filter</head> 前塞一段 JS,猴子補丁(monkey-patch)window.fetchEventSourceXMLHttpRequest.prototype.openhistory.pushState/replaceStateElement.prototype.setAttributeHTMLLinkElement/HTMLScriptElement/HTMLImageElementhref/src setter,全部繞去補上 window.__INGRESS_PATH__。順便把 navigator.serviceWorker.register stub 成回傳空的假 registration(HA Ingress 底下 PWA service worker 沒意義還會噴 console 錯誤)。這段補丁的迭代史看 CHANGELOG.md v0.5.0~v0.10.4 就知道有多痛。

這一層看起來很囉唆,實際上是把「pi-web 完全不需要知道 HA Ingress」這件事變成真的。pi-web 只管好好當一個 Next.js 應用;nginx 加 sub_filter 加 client-side shim 三層合力把它塞進 HA 的路由架構。這種設計叫 Ingress shim——就像插頭的轉接器,兩邊規格不同時中間放好幾片。

更正一件事:初版這篇曾說「HA 版 nginx 主要工作是 path 改寫,podman 版是 header 改寫」——這不準確。HA 版 nginx 兩件都做Host 改成 localhostOrigin 清空(v0.6.0 修 pi-web 的 isApiRequestAllowed() 403 用的,跟 podman 版同一招), 疊上 Ingress path 改寫加 client-side shim。差異不是「做的事不同」,是「HA 版比 podman 版多做兩層 Ingress 相關的東西」。

/data/pi-agent/ 目錄地圖

第 20 章已經給過一個簡表,這裡給的是完整版——你 SSH 進 add-on container 進到儲藏室之後,眼前會看到這些:

路徑裝什麼備份會不會抓
sessions/ 下的 .jsonl 每個對話 session 一個 JSONL 檔(每一行一個訊息)。DOCS.md 只寫「one per conversation」;子目錄結構(依 cwd 分資料夾或不分)是上游 pi coding agent SDK 決定的,若你要準確路徑SSH 進容器 ls /data/pi-agent/sessions/ 看實況為準,別信本文猜的樹狀圖 會(是對話存檔,per-addon 掛載、HA snapshot 涵蓋)
models.json 你在 Models 面板加的 provider、model 名稱、API key——金鑰是明文存的沒有加密,靠檔案權限 chmod 600 只讓 owner 讀寫;第 6 章提過 會(換 HA 主機這個一定要走)
auth.json 各家 OAuth provider 的 refresh token(例如 Google Drive 的授權);同樣 chmod 600
home/pi-cwd-*/ pi coding agent 的工作目錄。v0.10.0 把容器內 HOME 環境變數 pin 到 /data/pi-agent/home,所以 pi CLI 產出的 pi-cwd-* worktree 不會像 v0.9.x 以前那樣每次升級都被吃掉。命名慣例是上游決定的(DOCS.md 只寫 pi-cwd-*/,實際是 pi-cwd-<date> 或別的 suffix 以容器內為準) 會(node_modules/.cache/backup_exclude 排除以壓 snapshot 大小)
skills/<skill-name>/ 安裝的 Skill——本質上就是從 GitHub git clone 下來的資料夾;第 14 章講過概念 會(Skill 內容通常不大)
rclone/rclone.conf rclone 的雲端授權設定(包含 Google Drive access token);第 19 章設過
projects/<project>/ 影片管線的工作區。子目錄有 script/clips/segments/voice/output/——第 17 章那張中間檔表就是這裡 部分——final.mp4script.mdsubtitles.srt 會;clips/voice/ 這些大檔通常被 exclude 避免塞爆 snapshot
venv/ Python 虛擬環境(virtual environment)——edge-tts 跟其他 pip 套件裝在這裡 不會(HA snapshot exclude,因為可以重下載)
playwright-cache/ Playwright 下載的 Chromium 二進位檔(大約 200 MB) 不會(同上,可以重下載)
.video-tools-installed 一個空的 sentinel 檔——存在就代表「video-tools-init 跑過了、下次不用再跑」;第 18 章講的重跑機制就是把這個檔刪掉 會(很小,但抓不抓其實影響不大)
settings.json UI 偏好設定(主題、預設模型等)

兩個要記住的原則:

  • 值錢的(不能重生的)都在 /data/pi-agent/——sessions、models.json、skills、rclone conf。這也是 HA snapshot 抓的核心。
  • 大而可重生的(venv、playwright-cache)刻意被 exclude——如果 snapshot 抓 venv,一個 backup 檔就 500MB 起跳,實務上很痛苦。所以政策是「刪掉這些,還原後首次啟動 video-tools-init 會自動重下載」。
誠實聲明:如果你想在 HA 主機的 host 端(不是 container 內)看這個目錄,實際位置在 HAOS 大概是 /mnt/data/supervisor/addon_configs/<addon-slug>/、Supervised 大概是 /usr/share/hassio/addon_configs/<addon-slug>/——但這兩條路徑本文沒有從 add-on 原始碼裡驗證過(source 只寫「per-addon mount」,實際 host 路徑是 HA Supervisor 決定的,跨版本會變)。要動就 SSH 進 HA host 然後 ls /mnt/data/supervisor/addon_configs/ls /usr/share/hassio/addon_configs/ 兩處都試——哪個看得到就是哪個。比較穩的做法是 docker exec -it addon_<hash>_woow_ha_pi_agent bash 進到 container 內直接看 /data/pi-agent/(其中 <hash> 依 add-on 註冊 repo 而定,DOCS.md 舉的例子是 b9cf5676_woow_ha_pi_agentlocal_woow_ha_pi_agent 只有你把 repo 用 local override 加進來才會出現)。一般用戶不用碰。

Watchdog 探針:Supervisor 怎麼確認你還活著

Pi Agent 是長時間跑的服務,可能因為 memory leak、無窮迴圈、外部 API 死等而卡住。Watchdog(守望)是 HA Supervisor 用來偵測這種「還在跑但實際上死掉」情況的機制。

項目內容
探針端點(endpoint) http://[HOST]:[PORT:30142]/api/home——直接抄自 config.yamlwatchdog: 欄位。Supervisor 打 30142,經 nginx 轉給 pi-web 的 /api/home。作者選這個端點的原因:全新裝的 pi-web 在沒有 session context 下唯一會回 200 的路由就是 /api/home(CHANGELOG v0.10.0 有記錄)
啟用方式 config.yaml 直接寫成完整 URL(watchdog: "http://[HOST]:[PORT:30142]/api/home")——不是 watchdog: true 的形式(本文初版寫錯,這是 HA add-on 特有的 URL 型 watchdog 語法)
頻率 由 HA Supervisor 決定,實際數字(30 秒?60 秒?)Supervisor 沒公開,跟 HA 版本會有微調——想確定看你的 add-on Log 分頁裡「Watchdog」出現的間隔就是實測值
成功條件 HTTP 200 回應(不管 body 內容);逾時或非 200 就算失敗
失敗動作 Supervisor 呼叫 docker restart addon_<hash>_woow_ha_pi_agent——整個容器重啟;s6-overlay 從頭再走一次
對 session 的影響 不會遺失。對話檔案是 append-only 的 JSONL——每收到一句話就寫一行 flush 到磁碟,就算 restart 也只會少「還沒寫完那句」。下次開啟 session 從最後一行接續
對影片管線的副作用 如果 Watchdog 在 pitch_video 跑到一半觸發 restart,那條 pipeline 產生的 clips/ 可能有半寫入的 webm 檔——那支影片八成毀了,重跑一次。這是第 22 章「影片突然壞掉」的常見成因

Watchdog 是雙面刃:能自動撿回卡住的服務很棒,但如果你的 add-on 本身沒問題只是回應比較慢(例如你叫它處理大量對話歷史),有機會被誤判成「hang」被強制 restart。這時候要嘛加大 timeout(改 config.yaml)、要嘛暫時把 Watchdog 關掉(HA 頁面上有開關)。

debug 小技巧:你在 add-on Log 分頁看到「Watchdog missed heartbeat, restarting」之類的訊息代表 Supervisor 主動重啟;如果看到「s6-rc-init started」但沒有 crash 訊息,八成就是 Watchdog restart。這種情況通常代表 pi-web 卡住了——要不要調查取決於這是偶發還是週期性重複。

Skill 發現流程:AI 怎麼知道有哪些工具

第 14 章講過 Skill 的心智模型(給 AI 讀的工作手冊),這裡講「AI 怎麼發現」——這是很多人納悶的地方:「我 pi install 裝了新 skill,為什麼要重開 session 才生效?」

這一節是合理推論而非源碼引用:Skill 發現邏輯住在 earendil-works/pi SDK 內部(HA add-on repo 只是把它裝起來,不決定發現流程),add-on 的 DOCS.md 也沒有明文描述。本節根據 Claude/Anthropic 生態的 SKILL.md + progressive disclosure 通用模式推論——上游 SDK 有可能用不一樣的實作。「重開 session 才生效」這個現象是實測結論,但下面的六步流程可能有細節出入。要 100% 確定就去讀 @earendil-works/pi-coding-agent 的原始碼。

可能的流程是這樣,發生在每次你按「新對話」的瞬間

  1. Session 建立

    你按新對話,pi-web 建一個新的 session 檔案(sessions/.../*.jsonl)。

  2. 掃描 skills 資料夾

    pi-web 開始掃 /data/pi-agent/skills/*/SKILL.md——把每個子資料夾底下的 SKILL.md 檔案讀進來。

  3. 解析 YAML frontmatter

    每個 SKILL.md 檔案開頭有一段 YAML frontmatter,長這樣:

    ---
    name: fridge_inventory
    description: 幫使用者盤點冰箱食材、找出快過期的東西
    ---
    
    # 冰箱盤點手藝
    
    ...細節內容...

    pi-web 只讀 namedescription——整份 SKILL.md 的內文不會直接塞給 AI,只給摘要。

  4. 組成 <available_skills>

    所有 skill 的 name + description 合成一個列表,包在 XML 標籤裡。看起來像:

    <available_skills>
    <skill name="fridge_inventory">
    幫使用者盤點冰箱食材、找出快過期的東西
    </skill>
    <skill name="pitch_video">
    從腳本到 YouTube 可上傳的影片管線
    </skill>
    </available_skills>
  5. 貼到 system prompt 開頭

    這個 <available_skills> 塊被貼在 AI 的 system prompt 開頭——AI 開啟對話的第一件事就是看到「這台機器可以呼叫的手藝有這些」。

  6. AI 判斷什麼時候用哪個

    之後你講什麼 AI 都會依這份清單判斷「這個問題有沒有適合的 skill 可以叫」。要叫的時候 AI 才會去讀完整的 SKILL.md 內容(透過工具呼叫),拿到細節做事。

這就是為什麼裝新 skill 必須重開 session——已經開的 session 的 system prompt 是舊的、沒把新 skill 塞進去。也是為什麼SKILL.md 的 description 那一行超重要——寫得不好 AI 就不會挑到你這個 skill;寫得好會經常派上用場。

觀念:這種「摘要塞給 AI、細節等要用時再讀」的設計叫 progressive disclosure(漸進揭露)——不是一次把所有文件塞給 AI(會爆 token 也讓 AI 分心),而是先給選單、選了才點餐。

pi-web 的技術棧:主應用到底用什麼寫的

你在瀏覽器看到的整個介面,包括打字區、對話列表、模型下拉、Skills 面板——通通都是 pi-web 這個 Node.js 應用做出來的。它的組成:

元件用途備註
Node.js 22 JavaScript 執行環境(runtime) Dockerfile 從 nodesource 裝 node_22.x;pi-web 的 package.json 明寫 "node": ">=22.19.0"
Next.js 16.2.12 網頁框架——同時處理前端 React 與後端 API routes 抄自 pi-web 0.8.4 tag 的 package.json。App Router 架構(app/ 底下);本文驗證有沒有 legacy pages/ 混合,看 pi-web repo 為準
React ^19.2.4 前端 UI 元件框架 抄自同一份 package.json。Server Components / Suspense 都是預期有用,但實際哪個路徑用什麼本文沒進 pi-web 原始碼確認
pino 結構化 JSON logger Log 分頁看到的 JSON 行是它產出的。DOCS.md 的 log_level 欄位描述明寫「exported as LOG_LEVEL for pi-web's Next.js pino logger」,但 pino 不在 pi-web 0.8.4 package.json 直接列的 deps 裡——是透過 Next.js / 上游 SDK 引入的間接依賴
@earendil-works/pi-coding-agent coding agent 的 SDK——AI 邏輯、tool call、skill 管理都由它處理 pi-web 0.8.4 直接把 @earendil-works/[email protected] 列為 dep,in-process 呼叫(同個 Node 進程),不是另開 daemon——DOCS.md 也明寫「there is no separate agent daemon」
內部埠 30141 pi-web 監聽的 port 只在容器內開,外部經 nginx(30142)進來
對外埠 30142(Ingress) nginx 監聽的 port,HA Ingress 打進來的地方 對應 config.yaml 裡的 ingress_port: 30142

幾個技術點值得注意:

  • pi-coding-agent SDK 是 in-process 的——這代表 pi-web 崩了 agent 也跟著崩、agent 執行的 tool call(比如寫檔案)是用 pi-web 的權限做的。沒有 daemon 隔離就是為了少一層 IPC。
  • 沒有資料庫——對話存 JSONL 檔案、設定存 JSON 檔案,一切都是檔案系統。這是刻意的簡化設計(也讓備份很好處理)。
  • chat 走 SSE(Server-Sent Events)——AI 回覆邊產生邊 stream 給你看那種效果,是用 SSE 傳的。這也是為什麼 nginx 那節proxy_buffering 一定要關掉——不然回覆會被 nginx 緩到 AI 生完才一次吐出來。
對想貢獻上游的:pi-web 開源在 github.com/agegr/pi-web,pi coding agent SDK 在 github.com/earendil-works/piWoow_ha_pi_agent_add_on 這個 add-on 只是把它們打包成 HA add-on 的殼——加 nginx shim(三層:Host/Origin 改寫、sub_filter path 前綴、client-side JS 補丁)、加 s6-overlay 設定、加 video-tools-init 腳本。想改 UI 或 agent 邏輯要去 upstream 兩個 repo 提 PR。特別提醒:add-on 把 pi-web pin 在 0.8.4 不是 @latestDockerfileARG PI_WEB_VERSION=0.8.4),因為 nginx 的 sub_filter 跟 shim 綁到當時的 _next chunk 命名/RSC prefetch 行為,上游隨便一次 refactor 都可能悄悄回歸。改 pi-web 之前先看該版 nginx.conf再動。

v0.13.0 為什麼把 API key 從 add-on config 搬到 pi-web UI

這是 v0.13.0 版本最大的架構改動,你如果從舊版升上來會發現 add-on Configuration 分頁上原本的 API key 欄位不見了(第 21 章講過這個升級踩雷)。這節從架構角度講「為什麼要搬」。

舊架構(v0.12 之前)

  1. 你在 add-on Configuration 分頁填欄位

    畫面上有 anthropic_api_keyopenai_api_keyglm_api_key 一堆欄位。

  2. Supervisor 寫到 /data/options.json

    你按存檔,Supervisor 把整個表單序列化成 JSON 寫進容器內的 /data/options.json

  3. 啟動時當環境變數塞給 pi-web

    s6-overlay 開機腳本讀 /data/options.json,把每個欄位變成 ANTHROPIC_API_KEY=... 之類的 env var,然後才 exec pi-web。

  4. pi-web 從環境變數讀

    pi-web 啟動時 process.env.ANTHROPIC_API_KEY 拿金鑰。

舊架構的缺點

  • 加新家 provider 就要改 config.yaml 的 schema、發新版才行——動態不了
  • 同一家想接兩把 key(工作一把、家用一把)?做不到,一個欄位一個值
  • 沒有 Test 按鈕——你貼錯的 key,要等實際發第一封請求才知道 401 錯了
  • 動態切換慢——改欄位要 restart add-on 才吃到新值

新架構(v0.13 之後)

  1. 你打開 pi-web 的 Models 面板

    在瀏覽器介面裡,不是 add-on 那邊。

  2. 按 Add Provider、填表、按 Test

    第 6 章教過的流程。Test 按下去 pi-web 會實際發一封 API 請求驗證,綠燈才存。

  3. pi-web 存到 /data/pi-agent/models.json

    結構化的 JSON,可以裝很多 provider、每 provider 很多 key、每 key 很多 model。

  4. 不需要 restart 就生效

    下個 session 就吃到新設定,因為 pi-web 每次開 session 都會 re-read models.json

新架構的好處

  • 多家、多 key、多 model 都能動態管理
  • Test 按鈕可以事先驗證
  • 不用重啟 add-on
  • UI 上可以做「按這家的 provider 只想用其中某幾個 model」這種細節控制

遷移路徑(更正!):作者原本以為 v0.13.0 會自動幫你 seed 舊 key 進去models.json——去讀 CHANGELOG.mdDOCS.md 才發現是 hard cut,沒有 auto-migration(原文兩處講兩次「No auto-import」)。升上 v0.13.0 之後每一個 provider 都要你在 pi-web Models 面板重新貼一次 key 加按 Test——舊 options.json 裡的欄位 Supervisor 依新 schema 直接丟掉。models.json 裡舊的 $GLM_API_KEY 之類的環境變數 placeholder 會 resolve 成空字串,每個 chat 都會 401 直到你重貼——這是預期的過渡狀態。

升級前該做的準備:因為沒有 auto-migration,升級前把你在舊 Configuration 分頁的每一把 key 抄下來(貼進備忘錄)。升級完成打開 pi-web Models 面板一把一把重貼加 Test。少貼哪家哪家就 401;補上就好。對 v0.12.x → v0.13.x 升級來說,這是唯一的實質工作——第 21 章那條「升級前備份」的重點對 v0.13 就是「先備份 key 到你自己記得的地方」。

debug 進階技巧:進到容器裡面看

普通的 debug 靠 add-on Log 分頁看訊息就行。進階 debug 得進到容器內部——這裡整理幾招最常用的:

  1. 開 debug log level

    在 add-on Configuration 分頁把 log_levelinfo 改成 debug附錄 A 有詳細等級對照)。改完存檔會自動重啟,重啟後 Log 分頁會有非常詳細的訊息(有時多到眼花,看完記得改回 info)。

  2. SSH 進 HA host 再 exec 進容器

    SSH & Web Terminal add-on,或用 HA 的 Advanced SSH。連進去之後跑:

    docker ps | grep pi_agent   # 先看容器實際叫什麼
    docker exec -it addon_<hash>_woow_ha_pi_agent bash

    就進到 Pi Agent 容器內部了。可以 ls /data/pi-agentcat /etc/nginx/nginx.confps aux 看有哪些 process 在跑。注意這是 read-only 心態——別亂改容器內的檔案(重啟會消失,除非改在 /data 底下)。

  3. 看 nginx access log 抓 Ingress path

    容器內:

    tail -f /var/log/nginx/access.log

    可以看每個請求進來的 URL、狀態碼、response time。想確認「HA 有沒有真的把 X-Ingress-Path header 送進來」,看這裡最快。

  4. 手動測 edge-tts 是不是能連微軟

    容器內:

    /data/pi-agent/venv/bin/python -m edge_tts --list-voices | head

    能列出聲音就代表網路通、venv 沒壞。列不出來就是「video-tools 半殘」——沿著 第 18 章的重跑機制修復。

  5. 手動測 rclone 到 Google Drive

    容器內:

    rclone --config /data/pi-agent/rclone/rclone.conf ls gdrive: | head

    能看到 Google Drive 內容代表授權還沒過期。看到 token expired 之類就是要重新跑 rclone config reconnect第 19 章結尾提過)。

提醒:SSH 進 HA host 那條路要小心——你有整台 HA 的 shell,動錯東西可以搞掛整個系統。如果只是為了看 Pi Agent,優先用 docker exec 進到那個容器裡動作,別在 HA host 的檔案系統亂寫。看完就 exit

架構層 troubleshoot 清單

這一節不重複第 22 章的表面症狀,只列「懂架構才能修」的那些:

  1. 想在 HA host 上直接動 /data/pi-agent/ 的檔

    症狀:你想在 HA host 用 File Editor 或 SSH 直接 vi 一份 models.json,但找不到那個路徑。
    原因:HA add-on 的 /data 在 host 端不是 /data——實際路徑本文沒從 add-on 原始碼確認(source 只寫「per-addon mount」),大概候選是 /mnt/data/supervisor/addon_configs/<slug>/(HA OS)或 /usr/share/hassio/addon_configs/<slug>/(Supervised)——但這是 Supervisor 決定的、跨版本會變。
    解法:SSH 進 HA host → 兩個路徑都 ls 一下,哪個存在就進去。更穩的做法:直接進 container docker exec -it addon_<hash>_woow_ha_pi_agent bash 然後 vi /data/pi-agent/models.json——容器內的 /data 才是 add-on API 直接綁定的位置,不用猜 host 側 mount point。改完存檔不用重啟 add-on,pi-web 下次 read 就吃到(但影響大的檔改完最好重啟保險)。

  2. Ingress 掛:nginx 一直回 403

    症狀:側邊欄按 Pi Agent 按鈕,畫面出現一片空白或 nginx 的 403 錯誤頁。
    原因:nginx 的白名單 regex 沒讓 X-Ingress-Path header 過。可能是 HA 版本更新後 header 格式微改(多了字元、少了 _),或 HA 那邊沒送這個 header 進來。
    解法:容器內 tail -f /var/log/nginx/access.log 看實際請求的 header;如果格式明顯不同(例如 slug 突然有大寫或全形),去 GitHub Woow_ha_pi_agent_add_on 開 issue 貼給作者。臨時解法:重啟 HA 本體讓 Ingress 重發新的 slug。

  3. pi-web hang:Watchdog 一直 restart

    症狀:Log 分頁每幾分鐘就跳 s6-rc-init started,代表容器不斷被 Supervisor 重啟。
    原因:pi-web 的 /api/home 端點沒在 60 秒內回 200——可能是 CPU 被吃光、記憶體不夠、Node event loop 卡在某個同步操作。
    解法:(a)先看 HA 主機 CPU/RAM 是不是滿載——Pi 3、太舊的 mini PC 很容易掛;(b)暫時關掉 Watchdog(HA add-on 頁面上有開關)看 pi-web 到底能不能自己撐;(c)開 log_level: debug 抓死前那幾秒在幹嘛;(d)真的搞不定就到 GitHub 開 issue 附上 log。

  4. video-tools 半殘:sentinel 有了但 venv 是空的

    症狀:影片管線一叫就爆錯,說找不到 edge-tts 或 Playwright。
    原因:video-tools-init 第一次跑到一半掉線但已經寫了 sentinel 就退出,之後每次都以為裝好了直接跳過。
    解法:add-on Configuration 分頁把 reset_video_tools: true 打勾存檔——這個開關會刪掉 sentinel 讓下次啟動重新跑一次 init。跑完記得回來把開關關掉(不然每次啟動都重下 720MB)。第 18 章那邊有更多這個開關的細節。

  5. Skill 裝了但 AI 看不見

    症狀pi install 說裝成功了,但你在對話裡 AI 就是不會叫。
    原因Skill 發現流程只在新 session 開始時跑一次——你的舊 session system prompt 沒有這個 skill。
    解法:按「新對話」開新 session。還是看不到就 ls /data/pi-agent/skills/ 確認資料夾真的存在、cat SKILL.md 確認 frontmatter 的 namedescription 都在。frontmatter 格式錯誤(YAML 縮排錯之類)會讓整個 skill 被跳過。

常見問題

我可以自己 fork Pi Agent add-on 改嗎?授權允許嗎?
可以。Pi Agent add-on 是 MIT 授權Dockerfileorg.opencontainers.image.licenses="MIT" label 有明文,以 repo 的 LICENSE 為準),你可以 fork、改、發自己的 repo 讓別人裝。但要注意兩件事:(1)上游的 pi-web(agegr/pi-webpackage.json 寫 MIT)跟 pi-coding-agent(earendil-works/pi)是不同 repo 的授權,改它們也要看那邊的 LICENSE;(2)你 fork 後改的部分建議另外命名(例如 Woow_ha_pi_agent_myfork)不要沿用原始 slug woow_ha_pi_agent,不然升級時 HA 會混淆。
Home Assistant Container 版(不是 Supervised/HA OS)能不能跑 Pi Agent?
不能。Pi Agent 是 HA add-on,只有 HA OS 跟 HA Supervised 這兩種安裝方式有 Supervisor——沒有 Supervisor 就沒有 add-on 系統。HA Container(純 Docker 跑 homeassistant/home-assistant)只有 core,不能裝 add-on。如果你用 HA Container 想要 Pi Agent,建議直接跑 Woow_podman_pi_agent_package——那是給 podman/docker 用的等效部署,不需要 HA add-on 系統。
有 K3s 或 Podman 版本嗎?我想跑在 Kubernetes 上
有。三個平行版本: 三個版本共用同一個 pi-web + pi-coding-agent 上游,包法不同:HA 版加 Ingress shim,K3s 版加 Helm chart + Deployment,Podman 版加 Quadlet systemd units。功能大同小異,選你環境合適的。
我想幫 pi-web 或 agent 上游貢獻,要去哪個 repo?
分清楚要改的層—— 改上游要照他們的 PR 流程;改 add-on 直接在 WOOWTECH repo 開 issue/PR 就好。改之前建議先開 issue 聊過再動手,免得白工。
為什麼要 panel_admin: true?我想給家人一般 HA 帳號也能用
Pi Agent 的 config.yaml 裡設 panel_admin: true——這代表側邊欄按鈕只有 admin 權限的 HA 帳號才看得到。這是刻意的:Pi Agent 內部有 API 金鑰、有存對話紀錄、有能執行任意工具的 AI,一般家人帳號拿到會有安全與帳單風險。如果你真的要開給非 admin 帳號:改 config.yamlpanel_admin 設成 false——但你要承擔多人共用金鑰、對話混雜、家人不小心叫 AI 亂花錢的後果。折衷做法是為家人開單獨的 admin 帳號(在 HA 使用者管理裡),你比較能追蹤誰做了什麼。
Pi Agent 會不會把我家的對話送到雲端?
對話本身存本地——所有 sessions/*.jsonl 都在你 HA 硬碟上,沒有任何 telemetry 把它上傳到 WOOWTECH。但 AI 回覆需要打 provider API——你選了 OpenAI/Anthropic/GLM/DeepSeek 之類,你送出的訊息就是走那家的 API(那家的隱私政策決定他們怎麼用)。真正 100% 本地:接一家本地的 LLM(例如自己跑 Ollama 或 llama.cpp)當 provider,這樣完全不出家。這是 BYOK 架構的好處——你選誰去打交道自己決定。
能不能同時跑兩個 Pi Agent add-on(一個工作用一個家用)?
技術上做得到,實務上麻煩。HA 允許同一個 add-on 從不同 repo 各裝一份——如果你的兩份是不同來源(例如 WOOWTECH 官方 + 你自己 fork 版),可以並存。但兩者的 side panel 按鈕會撞名、slug 要獨立、儲存要分開資料夾。比較實際的做法:一個 Pi Agent + 用第 8 章的 Session 分主題(工作對話開一個 session、家用開另一個),不用真的裝兩份。真的要完全隔離就開兩台 HA 主機。
版本升級(例如從 v0.12 到 v0.14)會不會刪掉我的資料?
不會。HA add-on 升級流程只換 Docker 映像檔(程式碼),不動 /data 底下的東西——你的 sessions、models.json、skills 通通還在。第 21 章講過的 v0.13 API key 搬家是結構變了但資料沒丟,pi-web 首次啟動會自動 seed 到新位置。真正會丟的只有你自己主動刪(例如 uninstall add-on 再重裝)、或 reset_video_tools 那類明確標「reset」的開關。