feat(onboarding+kbdb): 8.P0 cron 止血 + §7.8 onboarding + .env.example 範本

kbdb-base 8.P0:scheduled.ts cron 每分鐘 KV list → 單一 key get(lib/cron-index.ts);
  webhooks-named 維護單 key + 一次性 migrate-cron-index;acr update 自動遷移。1440 list/日 → 0。

self-hosted-init §7.8 onboarding:
  P0 init 偵測+裝完驗收(lib/preflight.ts,pip 式,冪等)
  P1 acr whoami(+--json)+ MCP arcrun_whoami(AI 不繞 CLI 猜帳號)
  P2 mcp-setup 寫完印「請重啟 client」
  P3(部分)repo .env.example 範本(每格白話說明、值留空)+ llms.txt 教 AI 幫用戶 cp 建 .env

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-09 19:15:51 +08:00
parent c5d8924fb2
commit c152f5fc1d
14 changed files with 487 additions and 48 deletions
+133
View File
@@ -0,0 +1,133 @@
/**
* 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 灰字,用戶不知道)。
* - **冪等**:重跑檢查後「什麼也沒動」(ensureKvNamespace / ensureD1Database 本就冪等)。
*
* 本檔只做「偵測 + 報告」,不自己建資源(建資源仍走 cf-api 的 ensure*,由 init 編排)。
*/
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' };
}
}
/** wranglerCF 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;
requiredKv: readonly string[];
expectD1Name?: string;
cypherUrl?: string;
}): Promise<{ items: PreflightItem[]; allOk: boolean }> {
const items: PreflightItem[] = [];
// KV:實查 CF 上現有 namespace,比對必需清單
try {
const existing = await opts.cf.listKvNamespaces();
const have = new Set(existing.keys());
const missing = opts.requiredKv.filter((t) => !have.has(t));
items.push(
missing.length === 0
? { name: `KV namespaces (${opts.requiredKv.length})`, ok: true }
: { name: 'KV namespaces', ok: false, detail: `${missing.join(', ')}`, fix: 'acr update(冪等重建)' },
);
} catch (e) {
items.push({ name: 'KV namespaces', ok: false, detail: msg(e), fix: 'acr update' });
}
// D1:實查 CF 上是否有該庫
if (opts.expectD1Name) {
try {
const dbs = await opts.cf.listD1Databases();
items.push(
dbs.has(opts.expectD1Name)
? { name: `D1 ${opts.expectD1Name}`, ok: true }
: { name: `D1 ${opts.expectD1Name}`, ok: false, detail: '不存在', fix: 'acr update(冪等重建 + 套 migration' },
);
} catch (e) {
items.push({ name: `D1 ${opts.expectD1Name}`, ok: false, detail: msg(e), fix: 'acr update' });
}
}
// 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);
}