fix(cli): 更新不再照名字找資源——已部署 worker 綁著什麼就是什麼(Arcrun#97)

病根:更新會「確保」它需要的資源存在,而它是**照名字找**的。
使用者的資源是安裝器建的(`arcrun-rag-<x>-kv-webhooks`),更新找的是 `WEBHOOKS`
⇒ 找不到 ⇒ 新建一顆空的並綁上去。

2026-08-12 實撞(leo21c):一次例行更新後
  KV 9 顆 → 18 顆、D1 1 顆 → 2 顆,worker 全綁到新建的空的
  ⇒ 工作流一支都看不到、portal 登出、總圖空的、80 把 recipe 解不出來
  leo 原話:「leo21c 是掛掉的」。資料沒掉,但從他的角度就是東西全不見了。

修法方向:**已經部署上去的 worker 綁著什麼,那就是事實**——
名字是使用者那側的事,不是更新指令可以決定的。

📍 repo:matrix/arcrun(cli/)|📍 票:Leo/Arcrun#97
This commit is contained in:
uncle6me-web
2026-08-12 13:30:17 +08:00
parent e69d6bbc03
commit 45a546a686
7 changed files with 1356 additions and 179 deletions
+38 -20
View File
@@ -6,9 +6,11 @@
* 不是假設齊備直接動手 → 缺一個就卡(test_arcrun/4 的 D1 大跑去讀原始碼自己想辦法)。
* - **裝完驗收**:部署後逐項確認(KV / D1 / migration / cypher 可達),缺哪項明確報哪項
* + 給一鍵補裝指令。不是靜默印灰字(原本 harness/MCP 失敗只 console.log 灰字,用戶不知道)。
* - **冪等**:重跑檢查後「什麼也沒動」ensureKvNamespace / ensureD1Database 本就冪等)
* - **冪等**:重跑檢查後「什麼也沒動」。
*
* 本檔只做「偵測 + 報告」,不自己建資源(建資源仍走 cf-api 的 ensure*,由 init 編排)。
* 本檔只做「偵測 + 報告」,不自己建資源(要不要建由 resource-resolver 判斷,deploy.ts 編排)。
* 🔴 Arcrun#97:報告裡的 fix 指令也算「產品的一部分」——一句「acr update(冪等重建)」
* 接在誤報的「缺 KV」後面,就是把使用者直接推去執行那個把實例洗空的動作。
*/
import { execFileSync } from 'node:child_process';
@@ -77,42 +79,58 @@ export function printPreflight(title: string, items: PreflightItem[]): void {
*/
export async function verifyInstall(opts: {
cf: CfAccountClient;
requiredKv: readonly string[];
expectD1Name?: string;
/** binding → KV namespace id(部署實際用上的那幾顆)。*/
kvNamespaceIds: Record<string, string>;
/** 部署實際用上的 D1 id(沒有 D1 就不傳)。*/
d1DatabaseId?: string;
cypherUrl?: string;
}): Promise<{ items: PreflightItem[]; allOk: boolean }> {
const items: PreflightItem[] = [];
// KV實查 CF 上現有 namespace,比對必需清單
// KV核對「部署實際綁上去的那幾顆 id」在帳號上還在不在。
// 🔴 Arcrun#97:這裡**不能**用「帳號上有沒有叫 WEBHOOKS 的 namespace」來驗。
// 安裝器裝出來的實例,資源名字是 arcrun-rag-<instance>-kv-webhooks——照名字驗會誤報「缺」,
// 而那句誤報底下就寫著「fix: acr update(冪等重建)」⇒ 使用者照做,就被重建成空的。
// 驗的對象永遠是 id(我們真的綁上去的那顆),不是名字。
const kvBindings = Object.entries(opts.kvNamespaceIds);
try {
const existing = await opts.cf.listKvNamespaces();
const have = new Set(existing.keys());
const missing = opts.requiredKv.filter((t) => !have.has(t));
const ids = new Set((await opts.cf.listKvNamespaces()).values());
const missing = kvBindings.filter(([, id]) => !ids.has(id)).map(([b]) => b);
items.push(
missing.length === 0
? { name: `KV namespaces (${opts.requiredKv.length})`, ok: true }
: { name: 'KV namespaces', ok: false, detail: `${missing.join(', ')}`, fix: 'acr update(冪等重建)' },
? { name: `KV namespaces (${kvBindings.length})`, ok: true }
: {
name: 'KV namespaces',
ok: false,
detail: `這幾個 binding 綁著的 namespace 在帳號上找不到:${missing.join(', ')}`,
fix: '先確認那幾顆是被刪了還是 token 看不到——不要直接重跑安裝(會綁到空的)',
},
);
} catch (e) {
items.push({ name: 'KV namespaces', ok: false, detail: msg(e), fix: 'acr update' });
items.push({ name: 'KV namespaces', ok: false, detail: msg(e), fix: '檢查 CF token 的 KV 讀取權限' });
}
// D1實查 CF 上是否有該庫
if (opts.expectD1Name) {
// D1同理,核對實際綁上去的那顆 id 還在不在(不是核對有沒有叫 arcrun-kbdb 的庫)。
if (opts.d1DatabaseId) {
try {
const dbs = await opts.cf.listD1Databases();
const ids = new Set((await opts.cf.listD1Databases()).values());
items.push(
dbs.has(opts.expectD1Name)
? { name: `D1 ${opts.expectD1Name}`, ok: true }
: { name: `D1 ${opts.expectD1Name}`, ok: false, detail: '不存在', fix: 'CF token 補勾「Account / D1 / Edit」權限 → 重產 token 填回 .env → acr update' },
ids.has(opts.d1DatabaseId)
? { name: `D1 ${opts.d1DatabaseId}`, ok: true }
: {
name: `D1 ${opts.d1DatabaseId}`,
ok: false,
detail: '這顆 D1 在帳號上找不到',
fix: '先確認它是被刪了還是 token 看不到——不要直接重跑安裝(會綁到空的)',
},
);
} catch (e) {
// D1 建失敗最常見根因:CF token 沒勾 D1 權限(KV/Worker 能建但 D1 報 Authentication error)。
// D1 讀不到最常見根因:CF token 沒勾 D1 權限(KV/Worker 能建但 D1 報 Authentication error)。
const m = msg(e);
const fix = /auth/i.test(m)
? 'token 缺 D1 權限:CF token 補勾「Account / D1 / Edit」→ 重產 token 填回 .env → acr update'
: 'acr update(冪等重試)';
items.push({ name: `D1 ${opts.expectD1Name}`, ok: false, detail: m, fix });
: '檢查 CF token 的 D1 讀取權限';
items.push({ name: `D1 ${opts.d1DatabaseId}`, ok: false, detail: m, fix });
}
}