45a546a686
病根:更新會「確保」它需要的資源存在,而它是**照名字找**的。 使用者的資源是安裝器建的(`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
157 lines
7.0 KiB
TypeScript
157 lines
7.0 KiB
TypeScript
/**
|
||
* preflight.ts — self-hosted 安裝的「偵測先於動作 + 裝完驗收」(§7.8 P0,pip 式)。
|
||
*
|
||
* 核心判準(self-hosted-init.md §7.8):
|
||
* - **偵測先於動作**:init 先檢查各前置(node / wrangler / CF 可達),缺的才裝、有的跳過。
|
||
* 不是假設齊備直接動手 → 缺一個就卡(test_arcrun/4 的 D1 大跑去讀原始碼自己想辦法)。
|
||
* - **裝完驗收**:部署後逐項確認(KV / D1 / migration / cypher 可達),缺哪項明確報哪項
|
||
* + 給一鍵補裝指令。不是靜默印灰字(原本 harness/MCP 失敗只 console.log 灰字,用戶不知道)。
|
||
* - **冪等**:重跑檢查後「什麼也沒動」。
|
||
*
|
||
* 本檔只做「偵測 + 報告」,不自己建資源(要不要建由 resource-resolver 判斷,deploy.ts 編排)。
|
||
* 🔴 Arcrun#97:報告裡的 fix 指令也算「產品的一部分」——一句「acr update(冪等重建)」
|
||
* 接在誤報的「缺 KV」後面,就是把使用者直接推去執行那個把實例洗空的動作。
|
||
*/
|
||
|
||
import { execFileSync } from 'node:child_process';
|
||
import chalk from 'chalk';
|
||
import type { CfAccountClient } from './cf-api.js';
|
||
|
||
export interface PreflightItem {
|
||
name: string;
|
||
ok: boolean;
|
||
detail?: string;
|
||
/** 缺漏時給用戶的一鍵補救指令(沒有則留空)。*/
|
||
fix?: string;
|
||
}
|
||
|
||
/** node 是否可用 + 版本(init 本身是 node 跑的,能跑到這裡 node 必在,但仍印版本供診斷)。*/
|
||
function detectNode(): PreflightItem {
|
||
try {
|
||
const v = execFileSync('node', ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] })
|
||
.toString().trim();
|
||
return { name: 'node', ok: true, detail: v };
|
||
} catch {
|
||
return { name: 'node', ok: false, fix: '安裝 Node.js 18+:https://nodejs.org' };
|
||
}
|
||
}
|
||
|
||
/** wrangler(CF CLI)是否可用 + 版本。self-hosted 部署的硬前置。*/
|
||
function detectWrangler(): PreflightItem {
|
||
try {
|
||
const v = execFileSync('wrangler', ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] })
|
||
.toString().trim();
|
||
return { name: 'wrangler', ok: true, detail: v };
|
||
} catch {
|
||
return { name: 'wrangler', ok: false, fix: 'npm i -g wrangler' };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 安裝前偵測(pip 式:先看環境有什麼)。
|
||
* CF 憑證可達由呼叫端用 CfAccountClient.verifyAccess 接著驗(需要 token,不在這層)。
|
||
* 回傳所有項目 + 是否有 fatal 缺漏(node/wrangler 缺 = 無法繼續)。
|
||
*/
|
||
export function detectEnvironment(): { items: PreflightItem[]; fatal: boolean } {
|
||
const items = [detectNode(), detectWrangler()];
|
||
const fatal = items.some((i) => !i.ok);
|
||
return { items, fatal };
|
||
}
|
||
|
||
/** 印一組偵測結果(✓/✗ + 版本 + 補救指令)。*/
|
||
export function printPreflight(title: string, items: PreflightItem[]): void {
|
||
console.log(chalk.bold(`\n ${title}`));
|
||
for (const it of items) {
|
||
if (it.ok) {
|
||
console.log(chalk.green(` ✓ ${it.name}`) + (it.detail ? chalk.gray(` ${it.detail}`) : ''));
|
||
} else {
|
||
console.log(chalk.yellow(` ✗ ${it.name}`) + (it.detail ? chalk.gray(` ${it.detail}`) : ''));
|
||
if (it.fix) console.log(chalk.gray(` → ${it.fix}`));
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 裝完驗收:逐項確認 self-hosted 環境真的就緒(§7.8 D1 根因:安裝不偵測,缺了不報)。
|
||
* 各項以「實際查 CF / 打 cypher」確認,非看 config 有沒有寫——避免假綠(mindset §7)。
|
||
*
|
||
* @returns items(每項 ok + detail/fix)。呼叫端依 allOk 決定是否 exit 非零 / 印補裝指引。
|
||
*/
|
||
export async function verifyInstall(opts: {
|
||
cf: CfAccountClient;
|
||
/** binding → KV namespace id(部署實際用上的那幾顆)。*/
|
||
kvNamespaceIds: Record<string, string>;
|
||
/** 部署實際用上的 D1 id(沒有 D1 就不傳)。*/
|
||
d1DatabaseId?: string;
|
||
cypherUrl?: string;
|
||
}): Promise<{ items: PreflightItem[]; allOk: boolean }> {
|
||
const items: PreflightItem[] = [];
|
||
|
||
// KV:核對「部署實際綁上去的那幾顆 id」在帳號上還在不在。
|
||
// 🔴 Arcrun#97:這裡**不能**用「帳號上有沒有叫 WEBHOOKS 的 namespace」來驗。
|
||
// 安裝器裝出來的實例,資源名字是 arcrun-rag-<instance>-kv-webhooks——照名字驗會誤報「缺」,
|
||
// 而那句誤報底下就寫著「fix: acr update(冪等重建)」⇒ 使用者照做,就被重建成空的。
|
||
// 驗的對象永遠是 id(我們真的綁上去的那顆),不是名字。
|
||
const kvBindings = Object.entries(opts.kvNamespaceIds);
|
||
try {
|
||
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 (${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: '檢查 CF token 的 KV 讀取權限' });
|
||
}
|
||
|
||
// D1:同理,核對實際綁上去的那顆 id 還在不在(不是核對有沒有叫 arcrun-kbdb 的庫)。
|
||
if (opts.d1DatabaseId) {
|
||
try {
|
||
const ids = new Set((await opts.cf.listD1Databases()).values());
|
||
items.push(
|
||
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)。
|
||
const m = msg(e);
|
||
const fix = /auth/i.test(m)
|
||
? 'token 缺 D1 權限:CF token 補勾「Account / D1 / Edit」→ 重產 token 填回 .env → acr update'
|
||
: '檢查 CF token 的 D1 讀取權限';
|
||
items.push({ name: `D1 ${opts.d1DatabaseId}`, ok: false, detail: m, fix });
|
||
}
|
||
}
|
||
|
||
// cypher-executor 可達(打 /health,不只看 config 有 URL)
|
||
if (opts.cypherUrl) {
|
||
try {
|
||
const res = await fetch(`${opts.cypherUrl}/health`, { method: 'GET' });
|
||
items.push(
|
||
res.ok
|
||
? { name: 'cypher-executor 可達', ok: true, detail: opts.cypherUrl }
|
||
: { name: 'cypher-executor 可達', ok: false, detail: `HTTP ${res.status} @ ${opts.cypherUrl}`, fix: 'acr update(重部署)' },
|
||
);
|
||
} catch (e) {
|
||
items.push({ name: 'cypher-executor 可達', ok: false, detail: msg(e), fix: 'acr update(重部署);或 worker 剛部署稍候再試' });
|
||
}
|
||
}
|
||
|
||
return { items, allOk: items.every((i) => i.ok) };
|
||
}
|
||
|
||
function msg(e: unknown): string {
|
||
return e instanceof Error ? e.message : String(e);
|
||
}
|