Files
Leo c2638668e3 ISEP 0.1.0:環境設定收成一個 plugin,本機與雲端共用一份
leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
              「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
                你自己可以 dogfooding」

搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。

51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。

尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:41:46 +08:00

596 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: ship-check
description: |
改完任何會影響用戶的東西之後、說「做完了」之前必讀(改雲端 workerportaldaemon
workflowinstaller 都算)。也在下列時機自動載入:要打包 App、要出貨、要推 bundle、
要送 MS Store、leo 問「可以測了嗎」「版本為什麼沒變」「更新了嗎」「封測者拿得到嗎」。
核心判準:**版本號是 leo 唯一的驗收介面**——portal 版本卡看雲端、daemon 檢查更新看桌面;
版本沒動=他無從判斷你做了什麼=等於沒交付,而「我在某台實例 wrangler deploy 過了」不算。
收齊:兩條版本線的差別/重打 bundle(最常漏,要 grep 複驗改動真的進去)/
改 workflow 要重編預編圖/三支機械閘+把 DMG/zip 真的打開檢查/寫 changelog(用戶語言)/
D20 開閘出貨/purge jsDelivr/從 leo 會看的那兩處抓實際畫面複驗。
附「常見漏掉的」實撞表與收工前五問。
---
# /ship-check — 改完東西後,讓 leo 看得到版本變了
> **這支解什麼病**leo 2026-08-05 原話):
> 「對人來說,**我雲端看 portal 有沒有更新,本地看 daemon 有沒有更新**,
> 這個更新機制都寫好了,然後你說你改了這麼多居然版本一樣,還要去找怎麼做,
> **這個太危險了**。」
>
> 🔴 **核心判準:版本號是 leo 唯一的驗收介面。**
> 版本沒動 = 他無從判斷你做了什麼 = 你等於沒交付。
> 「我在某台實例 `wrangler deploy` 過了」**不算**——那只改了那一台。
---
## ⚠️ 這支 skill 自己的失效模式(先讀這段)
leo 2026-08-05:「**你寫完一個 skill 然後每個我要提醒你,表示這個 skill 無效**」
**根因**:我寫這支時是**憑印象列步驟**,沒有真的走一遍使用者的路
⇒ 於是「DMG 打開長什麼樣」「Info.plist 版本對不對」這些**只有真的打開才看得到**的東西全漏了,
每一條都要 leo 問「你檢查了嗎」才補。
**因此本 skill 的鐵律**
1. **每一條檢查都要有可貼的實測輸出**——寫不出指令的條目就是還沒想清楚,不要列。
2. **凡是「使用者會看到的東西」,一律真的打開來看**(掛載 DMG、解開 zip、抓網頁內容),
不是確認檔案存在、不是看腳本說成功。
3. **被 leo 問出來的缺口,當場補進這支 skill**——否則下次還是靠他記得。
(本檔的 3.5、2.9 兩段都是這樣補進來的,日期都記著。)
---
## 什麼時候跑
**改完任何會影響用戶的東西之後**(雲端 workerportaldaemonworkflow),
在說「做完了」之前。不是收工才跑。
---
## 🚚 雲端線出貨=**一個指令**,不要照下面的步驟手工做(2026-08-08 起)
> leo 2026-08-08:「**每次做一樣的事,結果會打錯實例,就是你的出貨閘是錯的。
> 寫對的應該每次都機械式的做同一件事,寫錯位置也會被它修正。**」
> 「這個出貨閘門就是廢的,它應該要像 GitHub Actions 一樣 CI/CD。」
```bash
cd products/arcrun-rag
node installer/scripts/ship.mjs --list # 有哪些目標
node installer/scripts/ship.mjs --target stage # 預演:建+算版本+報線上差距
node installer/scripts/ship.mjs --target stage --confirm # 真的走完 stage
node installer/scripts/ship.mjs --target prod --confirm # prod(先 stage,且需 leo 開閘)
```
**九個步驟固定不變**`preflight → build → version → commit → push → pin → deploy → purge → verify`
每一步只有「執行/跳過(附機械理由)」兩種結果,**斷了後面一步都不跑**。
它已經替你做掉的事(**所以下面 §2〜§6 的手工步驟不要再照著做一遍**):
| 以前靠人記得 | 現在 |
|---|---|
| 記得重打 bundle | `build` 步驟**每次都重打**(含 `build-ui-bundle.mjs`——它跟 `build-bundles.mjs` 是**兩支**,只跑一支就是 portal 改動送不出去) |
| 記得換釘子、且**兩處都要換** | `pin` 步驟同時寫 `wrangler.toml` 的 vars 與 `worker.js` 常數 |
| 記得 purge jsDelivr | `purge` 步驟(prod 才需要),驗到收斂為止 |
| 記得複驗線上 | `verify` 步驟**真的把 daemon 下載下來算 sha256** |
| 手打 `--bundles <路徑>`(會打錯) | 🔴 **已移除**。目標只能 `--target`,座標全來自 `installer/ship.targets.json` |
🔴 **目標打錯打不進去**:本機 clone 的 `origin` 與登錄簿不符 ⇒ 當場擋;
`CLOUDFLARE_ACCOUNT_ID` 一律由登錄簿覆蓋,不吃環境裡飄來的值;
`prod` 沒有 leo 的 `.github-armed` ⇒ preflight 就斷。
📌 **stage 驗證章由管線自己蓋**`--target stage --confirm` 成功才寫 `/tmp/.stage-verified`)。
**不要用手 `touch`**——手蓋的章證明不了任何事,那正是這道閘想擋的東西。
⚠️ 桌面線(打包 App/DMG/exe)**還沒併進管線**,仍照下面 §3、§3.5 手工做,
做完把產物放進 bundles repo 的 `daemon/`,再跑 `ship.mjs`(它會驗版本與 sha 對不對得上)。
---
## 兩條版本線(先分清楚在講哪一條)
| 線 | 版本號 | leo 從哪看 | 出貨鏈 |
|---|---|---|---|
| **雲端** | `manifest.release`(如 `1.4.11` | **portal 設定頁的版本卡** | 重打 bundle → 推 bundles repo → release 自動 bump → 換安裝器釘子 → 部署安裝器 |
| **桌面** | `manifest.daemon.version`(如 `v0.18.4` | **daemon 的「檢查更新」** | 打包 App → 放進 bundles `daemon/` → 改 `manifest.daemon` → purge jsDelivr |
⚠️ 兩條**各自獨立**。改 kbdb 不會讓 daemon 版本動,反之亦然。
---
## 步驟
### 0. 先查記憶(別重蹈覆轍)
```bash
grep -rn "版本號\|出貨\|release" system-dev/wiki/decisions-summary.md | head
```
必讀 **D39「版本號由內容算出來,不由人宣告」**
- `MAJOR.MINOR``RELEASE_LINE` 檔(人只在大改版動)
- **`PATCH` 由機器決定**:內容指紋一變 +1、沒變不動(重跑不虛增)
- **真相源只有 `manifest.release` 一處**installerlandingportal 全是讀者
> ⇒ **「改了 code 但 release 沒 bump」= bundle 沒重打包**,不是版本機制壞了。
### 1. 判斷這次改動影響哪條線
```bash
git -C matrix/arcrun log --oneline -5 # 雲端 workercypher/kbdb/portal…)
git -C products/arcrun-rag log --oneline -5 # daemon/安裝器/workflow
```
- 動到 `matrix/arcrun` 的 worker 或 `console-ui/public/portal/`**雲端線**
- 動到 `collector/`(含 `cmd/arcrun-app/`**桌面線**
- 動到 `workflows/*.yaml`**要重編 workflows.json**(見步驟 2.5
### 2. 雲端線:重打 bundle**這步最常漏**
```bash
cd products/arcrun-rag
ARCRUN_REPO_ROOT=../../matrix/arcrun node installer/scripts/build-bundles.mjs --out <bundles repo>/
```
**複驗你的改動真的進去了**(不要只看腳本說成功):
```bash
grep -c "<你改的關鍵字>" <bundles repo>/tier2/<worker>/index.js
# 例:改 embed 模型 → grep -c "bge-m3" 應 > 0,且舊模型應為 0
```
> 🔴 實撞(2026-08-05):我改了 kbdb 的 embed 模型並 `wrangler deploy` 到 youlin
> 但**沒重打 bundle** ⇒ bundles repo 裡仍是 `bge-base-en`
> ⇒ **新用戶安裝/既有用戶重裝都拿到舊的**,而 leo 看到的 release 仍是 1.4.11。
### ⛔ 2.9 bundle 沒重打之前,**要主動叫 leo 別按「立即更新」**
**這是最危險的狀態**,比「沒出貨」更糟:
- 你直接 `wrangler deploy` 到某台實例 ⇒ **那台**是新的
- 但 bundle 還是舊的 ⇒ leo 按 portal 的「**立即更新**」=**從舊 bundle 重裝**
-**他的實例會被你剛修好的東西「降級」回舊版**
實例(2026-08-05,leo 說「我要去更新實例驗證」時攔下):
kbdb 已直推 `bge-m3`(1024 維)+已建 1024 維 Vectorize index
但 bundle 裡仍是 `bge-base-en`(768) ⇒ 一按更新,kbdb 退回舊模型,
**與 1024 維 index 對不上 ⇒ 中文語意搜尋直接壞掉**
📌 **判準**:只要你「直推過實例」但「還沒重打 bundle」,
**主動說一句「先別按立即更新,會裝回舊的」**——不要等 leo 自己踩到。
兩者狀態不一致的期間,**降級風險是你造成的,說清楚是你的責任**。
### 🔁 2.7 動到安裝/更新/認證的話:**stage 要走三段,不准只測 update**
leo 2026-08-14 立,全文見頂層 `decisions-summary.md` **D84 之二**
> leo 原話:「**以後你的 stage 測試除了測 update 還要用 uninstaller 刪除後再模擬第一次安裝。**」
```
① 測 update(既有 stage 實例)
② 用 uninstaller 拆掉
③ 從乾淨狀態模擬第一次安裝
```
🔴 **只做 ① 不算測過。** stage 實例是長期存在的,它身上有所有舊 binding 與舊 secret
**它天生就是「既有實例」,天生看不到全新用戶會撞的坑。**
**這條的代價是實際發生過的**2026-08-14 一天三個坑,全是只驗 update 會漏掉的):
- 安裝器從沒種過 `CF_SECRETS_API_TOKEN`**全新用戶建不出第一個帳號**`Arcrun#119`
- 中心 KV 說「你裝過了」而帳號上什麼都沒有 ⇒ **重裝死結**`Arcrun#120`
- 撞牆訊息叫用戶去跑一件做不到的事(`Arcrun#121`
**每一個都在既有實例上測不出來**,因為既有實例會沿用舊資源把缺口蓋住。
⚠️ **uninstaller 還沒做出來之前**,這條走不完 ⇒ 出貨時要**明講「③ 沒驗」**,
不准因為「工具還沒有」就跳過不提。
### 2.5 改過 workflow 的話
```bash
CYPHER_BASE=<實例 cypher URL> CYPHER_NS=<namespace> \
node installer/scripts/compile-workflows.mjs
```
⚠️ **flow 變了就不能沿用舊的預編圖**,腳本會 `exit 1` 擋住(這個閘是對的)。
不帶 `CYPHER_BASE` 就跑=直接失敗,別繞過它。
### 3. 桌面線:打包 App
```bash
cd products/arcrun-rag/collector/cmd/arcrun-app
VERSION=vX.Y.Z bash build-mac.sh && bash build-dmg.sh # Mac
VERSION=vX.Y.Z bash build-win.sh # WindowsMac 上可交叉編譯)
```
**三支機械閘必須全過**(交貨前):
```bash
for s in check-cis.sh check-render.sh check-tray.sh; do bash "$s" || echo "❌ $s"; done
```
**實跑驗證產物**(不是看檔案存在):Mac 用 `open` 真的跑一次;
或用同綑的 collector 跑 `direct --once` 確認你的修復在裡面。
#### 3.5 🔴 **把 DMG/zip 真的打開,用「使用者第一次看到的樣子」檢查**
> leo 2026-08-05 連問三次才問出來的東西——**列步驟不算驗,要真的開起來看**。
> 「我問你 Mac 打包是不是一個 folder 打開可以把 dmg 拖到 application 去?**你檢查了嗎?**」
```bash
# Mac:掛載 DMG,看使用者會看到什麼
hdiutil attach dist/Arcrun-vX.Y.Z.dmg -nobrowse -quiet -mountpoint /tmp/dmgchk
ls /tmp/dmgchk/ # 應**剛好兩項**Arcrun.app Applications 捷徑
ls /tmp/dmgchk/Arcrun.app/Contents/MacOS/ # ⚠️ 2026-08-14 訂正:現在是**單一二進位**,只有 arcrun-app
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" \
/tmp/dmgchk/Arcrun.app/Contents/Info.plist # ← 必須是本次版本,**不是 1.0.0**
/usr/libexec/PlistBuddy -c "Print :LSUIElement" \
/tmp/dmgchk/Arcrun.app/Contents/Info.plist # 應為 true(不佔 Dock
codesign -v /tmp/dmgchk/Arcrun.app # 無輸出=簽章有效
hdiutil detach /tmp/dmgchk -quiet
# Windows:解開 zip 看內容
unzip -l dist/ArcrunRAG-win-unsigned-vX.Y.Z.zip # 應有 Arcrun.exe arcrun-collector.exe
```
**逐項判準**
| 檢查 | 為什麼 | 漏掉的後果 |
|---|---|---|
| DMG 內剛好兩項 | 這就是「拖進 Applications」的標準畫面 | 沒有捷徑 ⇒ 使用者在下載資料夾直接開 ⇒ **更新會蓋錯位置**t184 |
| ~~`arcrun-collector` 同綑~~ **已過期(2026-08-14 訂正)** | 以前 daemon 是獨立二進位;**現在 collector 內建在 `arcrun-app` 裡**v0.18.27 實測:`strings` 抓得到 `LoadDirectConfig``saveDirectConfig`direct 模式符號 18 處) | 🔴 **照舊版檢查會發出「你的 App 缺件」的假警報**——總管 08-14 差點對 leo 發出來。要驗「同步能力在不在」改用 `strings` 抓 direct 模式符號 |
| `CFBundleShortVersionString` | Finder/「關於」顯示的版本 | **永遠顯示 1.0.0** ⇒ 用戶無從判斷自己是不是新版 |
| `LSUIElement=true` | 常駐小工具不該佔 Dock | 行為與設計不符 |
| `codesign -v` | 改過 bundle 內容必重簽 | launchd 拒開 |
> 🔴 實撞(2026-08-05):`CFBundleShortVersionString` 一直是 **`1.0.0`**——
> `wails.json` 沒有 `info.productVersion`,而 `wails build` **沒有 CLI 旗標**可指定。
> 解法已寫進 `build-mac.sh``build-win.sh`:建置前把版本寫進 `wails.json` 的 `info`、
> 建完用 `trap` 還原(不污染版控)。
### 4. 寫 changelog**leo 點名要的**
`products/arcrun-rag/docs-site/src/content/docs/help/changelog.md`
格式照既有的(**寫給用戶看,不是寫 commit message**):
```markdown
## 📋 出貨對照表:**改了什麼 → 要動什麼**(leo 2026-08-08:「寫清楚不要再改錯」)
> 這張表是**查的**,不是用判斷的。08-08 一整天的錯全是「憑印象決定要重打什麼」。
> **每次出貨逐列對,有碰到就一定要做。**
| 你改了什麼 | 要重打/重部署 | 版本線 | 要一起改的文件 | 最後落在哪 |
|---|---|---|---|---|
| **daemon Go 程式** | 三平台打包(mac/win/msix | **daemon 版本線** | `changelog.md` **必寫** | 用戶自己的電腦 |
| **portalconsole-ui 前端** | `build-ui-bundle.mjs` | **雲端 release** | 有畫面變化就要更 docs | 每個實例的 `arcrun-rag-ui` |
| **cypher / kbdb / registry / mcp** | `build-bundles.mjs` | **雲端 release** | — | 每個實例的對應 worker |
| **安裝器 `worker.js`** | 部署安裝器(帶 `--config` | 安裝器自己 | — | `install.arcrun.dev` |
| **landing** | 部署 landing | — | — | `rag.arcrun.dev` |
| **說明文件** | `npm run build`**`dist``deploy/docs`** → 部署 | — | — | `rag.arcrun.dev/docs` |
| **錯誤分類/新的失敗原因** | — | — | 🔴 **FAQ 一定要同步** | docs 站 |
🔴 **兩個最常漏的(都有 08-08 實據)**
- `build-bundles.mjs` **不會重建 `arcrun-rag-ui`**(它在「沿用非本腳本產」清單裡)
⇒ 只跑一支=**靜默出半套**。portal 文案傳不出去整整一天就是這個。
- docs 的 `dist → deploy/docs` **沒有腳本**,漏了它 `wrangler deploy` 會「成功」但只上傳 0.35 KiB。
### 版本修改內容(`changelog.md`)怎麼寫
`changelog.md` 是**版本號的單一真相源**,也是使用者在「版本與更新」畫面看到的內容。
- **一行一句,講「你會看到什麼不一樣」**,不是 commit 訊息翻譯
- 🔴 **要短。細節去 docs 讀**leo 08-08 看到 v0.18.24 的更新說明是一整面文字牆:
「**不要這麼長的散文,簡短講改了什麼,細節去 docs 讀**」)
- **不要帶 markdown 粗體**——那個欄位是純文字,`**` 會原樣顯示給使用者看
- 沒有對應版本的段落就**不准打包**(`changelog-section.sh --check` 會擋)
### FAQ 什麼時候一定要動
- **新增/改變任何「使用者會看到的失敗原因分類」** ⇒ FAQ 必須同步
(分類收斂成有限幾類,畫面只出分類與份數,**細節全在 FAQ**——見 t214
- **改了安裝或更新的做法** ⇒ 對應的說明頁必須同步
08-08 實據:Windows 說明還寫「解壓縮 zip、裡面兩個 exe」,而我們早就只出單一 `.exe`
📌 **判準:使用者會因為這次改動而問出新問題嗎?會 → FAQ 要有答案。**
### 什麼地方 → 修哪個實例(別再搞混)
> 🔴 **AI 的工作迴圈只有兩件事**leo 2026-08-08 原話):
> 「**實際上你是在 uncle6 修改 bundle,然後你自己在 youlin stage 安裝新版 bundle。**」
> ⇒ **改 staging bundleuncle6)→ 裝進 youlin 的 stage 環境 → 自己測**。這一圈可以無限重複。
> **「uncle6 不碰」指的是 prod 那一側,不是 staging bundle**——別把自己的工作區也擋掉。
| 這個東西 | 住在誰的帳號 | AI 能不能動 |
|---|---|---|
| **staging bundle repo** | uncle6Gitea | ✅ **這是 AI 的工作區**,隨便改隨便推 |
| **youlin 的 stage 環境** | youlin | ✅ **AI 的測試場**,隨便裝、壞了自己修 |
| **prod bundle repo** | uncle6GitHub | 🔴 要 leo arm,且只准「**原封提升**」不准重打 |
| **prod 的安裝器/landingdocs 部署** | uncle6 | 🔴 部署=出貨動作,要 leo 開閘 |
| **youlin 的 prod 環境** | youlin | 👤 leo 用來驗「用戶裝 prod 拿到什麼」 |
| **geek6688(出貨機/第一個顧客)** | geek6688 | 🔴 **不准拿它當測試場**;但**發佈後總管要手動把它更新到最新**——見 `ops-facts.md` §出貨線。**出貨機比貨舊是錯的**(08-12 實撞:停在 1.4.29、落後 12 版) |
| `rag-demo.arcrun.dev` | uncle6 | 🪦 **已退場**,任何文件/腳本不得再導向 |
📌 **一句話界線****staging 側全是我的、prod 側全是 leo 的閘。**
### 🖥️ 三台機器各自幹嘛(leo 2026-08-12 定義,08-14 再次口頭確認)
> **youlin =測試機|geek6688 =出貨機|uncle6 =總部機**
- **youlin =測試場**:**它就是拿來拆的**。裝壞了、要清空重來,**不必問、不必猶豫**
——把它當成珍貴資產而不敢動,等於讓「只有全新用戶會撞的坑」永遠測不到(D84 之二)。
唯一要守的是**時序**:確認沒有別條線正踩在上面。
- **geek6688 =出貨機/第一個顧客**:**不當測試場**,但**發佈後要更新到最新**(總管手動)。
- **uncle6 =總部機**bundle 的家。staging 側是總管的工作區,**prod 側是 leo 的閘**。
📌 **總管 2026-08-14 實錯**leo 交辦拆 youlin 時,總管去問「有沒有資料是重裝也長不回來的」
——**而這個答案 `ops-facts.md:29` 早就寫著了**。leo 當場點破:「**stage 不就是用來測的?**」
**對測試機的謹慎不是美德,是把該做的驗證擋在門外。**
## 🗺️ 出貨流程(leo 2026-08-08 深夜定死,不得再自創路徑)
> leo 原話:「**uncle6 的 bundle 要裝在 youlin 的 stage 環境,沒問題後才推 prod 改裝在
> youlin 的 prod 環境,所以 uncle6 的 bundle 要 2 套,youlin 也要兩套,
> 就像把同樣程式碼打到不同分支一樣。**」
> 「**CF 可以這樣建立分離的 stage,不是在 prod 環境測 stage。**」
### 矩陣(兩軸,四格,缺一格就會像 08-08 那樣炸)
```
產物來源(uncle6) 安裝目標(youlin
stage → staging bundle repo → youlin 的 stage 環境
prod → prod bundle repo → youlin 的 prod 環境
```
**同一份程式碼打到兩條線,像 git 的兩個分支——彼此不共用任何會互相污染的東西。**
### 六步,順序不准跳
```
① 改 code → commit + push gitea
② 打 bundle → 版本號由內容指紋算出;內容變了版本一定變
⚠️ 要跑兩支:build-bundles.mjs build-ui-bundle.mjs
(前者不會重建 arcrun-rag-ui,漏跑=靜默出半套)
③ 推 staging bundle → Gitea(不碰 GitHub,無 D20 閘)
④ 裝進 youlin 的 stage 環境 → AI 自己測:登入、首頁、匯出診斷檔(瀏覽器實載)
⑤ 👤 **leo 去 youlin 的 stage 環境**,看修復版**是不是真的修好了**
→ 他說可以,跑 scripts/stage-ok.sh
⑥ 👤 leo 跑 scripts/github-arm.sh
**把 staging bundle「原封提升」成 prod bundle**uncle6 內部:staging repo → prod repo
⑦ 👤 leo 在 **youlin 的 prod 環境**裝一次新版 prod 內容
→ 驗的是「**用戶從 prod 安裝器裝,會拿到什麼**」這條路本身
⑧ 之後任何人從 prod 安裝器裝就拿到新版;leo 想在哪抽查都行(geek6688 或任何實例)
```
🔴 **youlin 為什麼一定要兩套環境**leo 2026-08-08:「**然後我可以在 youlin 測新版的
prod 內容安裝**」):
- **stage 環境** → 驗「修復到底修好了沒」(發佈前的閘,第 ⑤ 步)
- **prod 環境** → 驗「用戶裝 prod 會拿到什麼」(發佈後的第一手驗證,第 ⑦ 步)
⇒ 兩件事驗的東西不同,**不能共用同一套環境**——
共用就會變成 2026-08-08 那樣:為了測而動到正在用的那套,把要驗的東西弄壞。
🔴 **⑥ 是「提升」不是「重打」**leo 2026-08-08 原話:
「**arm 推的是 uncle6 把 stage 的 bundle 推到 prod 的 bundle**」)
⇒ **prod 的產物必須與 stage 上被驗過的那一份逐位元相同**。
一旦重跑一次 build 才推 prod**stage 驗過的東西就不是 prod 上的東西**,
⑤ 那一關的意義當場歸零。
⇒ 驗收方式:提升後比對兩邊的**指紋/sha**,不同就是做錯了。
這正是 leo 的比喻「**就像把同樣程式碼打到不同分支一樣**」——搬,不是再做一次。
🔴 **stage 永遠在 youlin,這是固定的**leo 2026-08-08 原話:
「**5 是我去 youlin stage 環境看你的修復版是否真的修復,就可以發佈,發佈以後,
我可以在任意地方試 prod,但 stage 一定在 youlin。**」)
⇒ **核實點固定在 stage**,不是「找另一台乾淨的機器來驗」。
prod 發佈後在哪裡試都行,那是**發佈後的抽查**,不是發佈前的閘。
**兩把鑰匙都在 leo 手上(⑤⑥),AI 造不出來,這是刻意的。**
### CF 官方機制(查證出處,不是憑記憶)
`https://developers.cloudflare.com/workers/wrangler/environments/`
- 用 `[env.NAME]` 宣告環境;部署 `npx wrangler deploy --env NAME`
- **Worker 名字自動變成 `<top-level-name>-<env>`**(例:`arcrun-cypher-executor-stage`
- 🔴 官方原文:**「Non-inheritable keys are configurable at the top-level, but cannot be
inherited by environments and must be specified for each environment.」**
⇒ **KV/D1/vars 一律不繼承,每個環境必須各自宣告**——這正是環境真正分離的地方,
也是「漏宣告就會共用到 prod 資源」的風險點。
### 🔴 為什麼要把流程寫死(08-08 一整天的代價)
那天出貨是**用手拼的**:手動 cp 產物 → 手動改 manifest → 手動 sed 換兩處釘子 → 手動 deploy。
後果四連發,全部有實據:
- 改了 portal 文案,**bundle 版本沒動** ⇒ 改動永遠送不出去
- 版本沒動 ⇒ 安裝器判「同版整批跳過」⇒ **重裝也修不好**(leo 白重裝一次)
- 兩台都宣稱 `1.4.22` **程式碼卻不同**(一台有診斷端點、一台 404)
- 為了測 stage 而手動部署,**繞過安裝器注入** ⇒ leo 的 portal 畫面壞、登入斷
⇒ **他晚上要驗收時,發現要驗的東西被驗收流程本身弄壞了**
⇒ leo:「**每次做一樣的事,結果會打錯實例⋯⋯寫對的應該每次都機械式的做同一件事,
寫錯位置也會被它修正。**」**這條流程就是那個「機械式」的定義,不准再自創。**
## 🚦 第 0 步:**先上 stage,不准直達 prod**leo 2026-08-08 立,封測期起)
> leo:「現在因為**開始封測**,不直接打到 prod,而是先打到昨天建的 stage⋯⋯
> 因為**推 prod 就發佈了**,雖然現在人不多,但**要謹慎**。」
🔴 **這一段是補進來的,因為本 skill 原本從頭到尾寫的是 prod**
github-arm → GitHub `arcrun-rag-bundles` → jsDelivr/raw 驗證),
**沒有任何一步是「先上 stage 驗過再上 prod」**;而 D20 那道閘擋的是「寫 GitHub」,
不是「未經 stage 就發佈」⇒ 光「知道有 stage」對行為零作用。
順序(**不准跳**):
1. 打 **staging** bundle → 推 Gitea `arcrun-rag-bundles-staging`
(不碰 GitHub**無 D20 閘**,不需要 leo
2. 用 staging 安裝器實裝到測試實例
`https://arcrun-rag-installer-staging.uncle6-me.workers.dev`
3. 走一次**封測者真的會走的那條路**,貼實測輸出(不是 HTTP 200)
4. 過了才做下面的 prod 步驟
⚠️ **身分**leo 2026-07-25 令「測試一律用 youlin,別拿 leo21c 當探針(會製造假信號)」
⇒ 部署前先 `acr whoami`。
⚠️ **stage 與 prod 內容範圍不一樣**daemon 安裝檔、README、core 顆數)
⇒ 見下面「別再整包蓋」那段,**不要拿 staging manifest 整包覆蓋 prod**。
📌 stage 判準(leo 08-07):stage **不需要 custom domain**`*.workers.dev` 就好——
「僅預設網域不准標 ✅」的目的是「用戶拿不到=沒交付」,而 stage 的用戶就是 leo 與總管。
不掛 custom domain 反而是優點(沒人會誤入、不被索引、不會被當正式網址傳出去)。
🔧 機械閘:`.claude/hooks/stage-before-prod-guard.sh`
(6 小時內沒 stage 驗證紀錄 ⇒ 擋掉 prod bundle repogithub-armpublish-github)。
📖 stage 救過一次的實例:見頂層 `system-dev/wiki/status.md`
「同一次測試照出一個會炸掉所有用戶的回歸(stage 第一次真的救了我們)」。
## vX.Y.ZYYYY-MM-DD
**建議更新**——一句話說「這版解決你什麼問題」。
- 🔴 **最重要那項**:用戶語言描述症狀與結果
- 其他項…
```
判準:**用戶讀得懂「這對我有什麼差別」**。
不要寫「修 t195 的 401」,要寫「修好『一個檔失敗就卡住整個資料夾』」。
### 5. 出貨(**需 leo 開 D20 閘**
```bash
# leo 親跑(AI 不得代跑)
scripts/github-arm.sh "出貨 <說明>" 30
```
開閘後:
```bash
cd products/arcrun-rag
node installer/scripts/ship.mjs --bundles <bundles repo> # 先 dry-run 看待辦
node installer/scripts/ship.mjs --bundles <bundles repo> --confirm # 真出貨
```
`ship.mjs` 會依序做:推 bundle → 換安裝器釘子 → 部署安裝器/landing → **purge jsDelivr**
⚠️ **purge 一次可能不夠**,要驗到收斂(腳本自己會重試,別提前宣稱完成)。
### 5.5 說明文件站(**2026-08-08 補:它不在任何腳本裡,最容易整批過期**)
🔴 **`ship.mjs` 完全不管 docs**`grep docs` = 0 命中)。docs 是獨立的 `arcrun-docs` worker
沒人手動部署它就會**一直停在上一次**——leo 2026-08-08 抓到:Windows 安裝說明還寫著
「解壓縮 zip、裡面有兩個 exe」和「MSIX 要開開發人員模式」,而**我們早就只出單一 `.exe`**。
```bash
cd docs-site
npm run build # → dist/
rm -rf deploy && mkdir -p deploy/docs && cp -R dist/. deploy/docs/ # ⚠️ 見下
npx wrangler deploy
```
⚠️ **`dist → deploy/docs` 這一步沒有腳本,只活在人的記憶裡**(08-08 實撞):
`wrangler.toml``[assets] directory = "./deploy"`,而 Astro 建到 `dist`
**漏了它,`wrangler deploy` 會「成功」但只上傳 0.35 KiB(等於什麼都沒換)。**
**複驗要抓畫面內容,不是看部署訊息**
```bash
curl -s "https://rag.arcrun.dev/docs/start/install-windows/?cb=$RANDOM" \
| grep -oE "<這次該出現的字串>"
```
📌 **判準:凡是改動會讓說明文件過期的出貨,docs 就是出貨的一部分。**
版本、安裝方式、畫面長相變了 ⇒ 一起改、一起部署、一起複驗。
### 5.8 🔴 驗前端=**用瀏覽器真的載一次**,`curl | grep` 不算(leo 2026-08-08 立)
> leo 原話:「**你的環境有 web,你應該用 web 驗,你已經開啓了卻沒有完成,你要把這個列入規定。**」
**為什麼 `curl | grep` 是假驗證**(08-08 實撞,leo 抓到而不是我發現):
`curl` 拿到的是 **HTML 原始碼**——它**不執行 JS、不載入 `config.js`、不發 API 請求**。
所以我 grep 到文案就宣稱「前端驗過」,而使用者實際打開看到的是整條紅色錯誤:
「設定檔沒載入(config.js),這個頁面連不到你的服務」。**那個畫面我一次都沒看到。**
**判準升級**
`HTTP 200 不算驗過``grep 到字串也不算驗過`**只有「瀏覽器載入後看起來能用」才算**
```
mcp__Claude_Browser__preview_start {url: "<用戶會走的網址>"}
mcp__Claude_Browser__computer {action: "screenshot"} ← 看畫面,不是看原始碼
mcp__Claude_Browser__read_console_messages {onlyErrors: true} ← JS 有沒有炸
```
**要看的是**:有沒有錯誤橫幅/該有的資料是不是還停在「載入中…」「查詢中…」/console 有沒有紅字。
⚠️ **順帶一個會騙人的坑(同日實撞)**`curl``?cb=$RANDOM` **繞不掉 Cloudflare 邊緣快取**——
`/config.js` 一直回舊的空值,直到加 `-H "Cache-Control: no-cache"` 才看到真值。
⇒ 用 curl 查線上狀態時,**沒加 no-cache 就可能是在驗快取,不是在驗線上**。
### 6. 複驗:**從 leo 會看的那兩個地方**
```bash
# ① 雲端線:portal 版本卡讀的是安裝器 /api/latest
curl -s https://install.arcrun.dev/api/latest
# → "release" 應等於 manifest.release
# ② 桌面線:daemon「檢查更新」讀的是 jsDelivr**不是 raw**
curl -s "https://cdn.jsdelivr.net/gh/youlinhsieh/arcrun-rag-bundles@main/manifest.json" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['daemon']['version'])"
# ③ 用戶下載的固定檔名真的抓得到、且 sha 與 manifest 相符
curl -sI "https://raw.githubusercontent.com/youlinhsieh/arcrun-rag-bundles/main/daemon/ArcrunRAG-mac.dmg"
```
🔴 **`raw` 是新的不代表 `jsDelivr` 是新的**——daemon 讀 jsDelivr
2026-08-05 實撞:push 完 raw 立刻新版、jsDelivr 仍吐 v0.15.7purge 後第 2 次才收斂。
---
## 收工前自問(答不出來就是還沒做完)
1. **leo 打開 portal,版本卡會顯示新號碼嗎?** 不會 → 雲端線沒出貨完
2. **leo 按 daemon「檢查更新」,會看到新版嗎?** 不會 → 桌面線沒出貨完
3. **新用戶現在安裝,拿到的是我改的那份嗎?** 不確定 → 回步驟 2 複驗 bundle 內容
4. **changelog 有這一版嗎?** 沒有 → 用戶不知道你改了什麼
5. 以上任一是「否」→ **不准說「完成」,要說「已改,未送達」**
---
## 常見漏掉的(實撞記錄)
| 漏掉的 | 後果 | 日期 |
|---|---|---|
| 沒重打 bundle | release 不 bump、新用戶拿舊版 | 08-05 |
| 沒 purge jsDelivr | 用戶按「檢查更新」像沒反應 | 08-02 |
| 沒改 `manifest.daemon` | 「檢查更新」回「manifest 沒有 daemon 版本欄位」全體失效 | 08-02 |
| 只推 bundle 沒換釘子 | 安裝器仍裝舊 bundle | 08-01 |
| 🔴 **釘子只改了常數,沒改 `[vars]`** | **部署成功但釘子沒換**——`api/latest` 與 install 頁全是舊版,白部署一次 | 08-07 |
| 用 `rsync --delete` 覆蓋 prod bundle repo | **砍掉 prod 才有的 `daemon/`20 個安裝檔)與 `README.md`**,封測者下載不到 daemon | 08-07(推之前 `git status` 抓到) |
| 用 staging 的 manifest 整包蓋 prod | **core 從 5 顆變 24 顆**——prod 只裝 5 顆是懶載設計,會讓每個新用戶多裝 19 顆 worker | 08-07(同一個坑 `bbb433e` 有前科) |
| 用 `git worktree` 打 bundle | `0/4 bundled`——worktree 沒有 `.component-builds/` 的預編 wasmgitignore 產物) | 08-07 |
### 🔴 換釘子:真身是 `[vars]`,不是常數(08-07 實撞,白部署一次)
```js
// installer/oauth-prototype/worker.js:74
bundleBase(env) = env.BUNDLE_BASE ? env.BUNDLE_BASE : DEFAULT_BUNDLE_BASE
```
**`wrangler.toml``[vars] BUNDLE_BASE` 優先於 `worker.js` 的常數。**
- **兩處都要改**`wrangler.toml``[vars] BUNDLE_BASE``BUNDLE_BUILT`
以及 `worker.js``DEFAULT_BUNDLE_BASE`
- **為什麼會漏**:08-07 把「指向」從常數搬進設定(為了 stage/prod 用同一套機制),
**但記載該機制的文件沒跟著改** ⇒ 照舊文件做 = 改了一半 = 等於沒改
- 🔑 **可推廣**:**改了機制,就要改記載那個機制的文件**——
否則下一個人(包括你自己)照過期文件做,會得到「做完了但沒生效」
### ✅ 覆蓋 prod bundle repo 的正確動作(08-07 定,別再整包蓋)
```bash
git reset --hard HEAD && git clean -fd # 先回到 prod 原狀
cp -R <staging>/<worker>/. <worker>/ # 逐顆覆蓋,不用 rsync --delete
# manifest:只換 prod core 清單裡「原本就有」的那幾顆 + release/built/source
git add -A
git diff --cached --diff-filter=D --name-only # ← 護欄:刪除檔案數必須是 0
```
**判準:staging 與 prod 的內容範圍不一樣(daemon 安裝檔、README、core 顆數),不能整包蓋。**
| changelog 沒寫 | 用戶不知道能不能/要不要更新 | 08-05(停在 v0.15.7v0.18.x 全空) |
| portal 下載連結沒跟著換 | DMG 打好了但按鈕還給 zip | 08-05 |