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:
+23
-40
@@ -10,7 +10,6 @@ import chalk from 'chalk';
|
||||
import { saveConfig, type ArcrunConfig } from '../lib/config.js';
|
||||
import { CfAccountClient } from '../lib/cf-api.js';
|
||||
import {
|
||||
REQUIRED_KV_NAMESPACES,
|
||||
downloadAndDeploy,
|
||||
type DeployContext,
|
||||
} from '../lib/deploy.js';
|
||||
@@ -135,7 +134,7 @@ async function initStandard(rl: ReturnType<typeof createInterface>): Promise<voi
|
||||
|
||||
/**
|
||||
* Self-hosted installer:用戶只提供 CF Account ID + API Token,其餘自動。
|
||||
* 驗 token → 建 KV(冪等,數量見 REQUIRED_KV_NAMESPACES)→ 查 subdomain → 下載 release 部署 Worker
|
||||
* 驗 token → 查 subdomain → 下載部署物 → 解析資源(沿用既有/必要才新建)→ 部署 Worker
|
||||
* → seed auth+api recipe → 寫 config → 印手動 secret 提示。
|
||||
* SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md
|
||||
*/
|
||||
@@ -185,41 +184,13 @@ async function initSelfHosted(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. 建 KV namespace(冪等)
|
||||
// 2. KV / D1 / Vectorize 不在這裡預先建(Arcrun#97)。
|
||||
// 舊版在這一步「照名字 ensure」一輪再往下傳,acr update 沿用同一段程式碼
|
||||
// ⇒ 對一台安裝器裝出來的實例(資源名字不同)等於每次更新都重建一整套空的綁上去。
|
||||
// 現在資源解析統一在 downloadAndDeploy 內:**先看已部署的 worker 綁著什麼**,
|
||||
// 對得上就沿用、確定沒人綁過才建、說不準就停手。init 走 mode:'init'(允許從零建起)。
|
||||
// 不建 R2:R2 是 dead storage(registry-canon Phase 1.5),且 CF R2 首次啟用強制綁信用卡,
|
||||
// 違背 arcrun「開源免費自架,Workers + KV 免費額度即可運行」核心理念(壓測 2026-06-04 #3)。
|
||||
const kvNamespaceIds: Record<string, string> = {};
|
||||
try {
|
||||
const existing = await cf.listKvNamespaces();
|
||||
for (const title of REQUIRED_KV_NAMESPACES) {
|
||||
process.stdout.write(chalk.gray(` → KV ${title}...`));
|
||||
const id = await cf.ensureKvNamespace(title, existing);
|
||||
kvNamespaceIds[title] = id;
|
||||
console.log(chalk.green(' ✓'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(`\n ✗ 建立資源失敗:${e instanceof Error ? e.message : e}\n`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2.5 build D1 for KBDB Base (atomic universal table). Free on Workers Free, no credit card
|
||||
// (kbdb-base SDD Q4). idempotent: reuse if exists.
|
||||
let d1DatabaseId = '';
|
||||
try {
|
||||
process.stdout.write(chalk.gray(' → D1 arcrun-kbdb...'));
|
||||
d1DatabaseId = await cf.ensureD1Database('arcrun-kbdb');
|
||||
console.log(chalk.green(' ✓'));
|
||||
} catch (e) {
|
||||
const em = e instanceof Error ? e.message : String(e);
|
||||
console.log(chalk.yellow(`\n ⚠ D1 build failed (${em})`));
|
||||
if (/auth/i.test(em)) {
|
||||
// 最常見根因:CF token 沒勾 D1 權限(KV/Worker 建得起來但 D1 報 Authentication error)。
|
||||
console.log(chalk.yellow(' 多半是 CF token 缺 D1 權限 → 去 token 補勾「Account / D1 / Edit」'));
|
||||
console.log(chalk.gray(' 重產 token 填回 .env 後跑 acr update。D1 存 workflow/recipe,沒它後續會受限。'));
|
||||
} else {
|
||||
console.log(chalk.gray(' KBDB Base 暫不可用,可 acr update 重試。'));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 查 workers.dev subdomain(cypher-executor WORKER_SUBDOMAIN 用)
|
||||
let workerSubdomain = '';
|
||||
@@ -245,8 +216,20 @@ async function initSelfHosted(
|
||||
console.log(chalk.gray('\n → 下載部署物 + 部署 Worker(從 GitHub 拉預編譯 wasm,用你的 CF token 部署)...'));
|
||||
// selfHosted: true → deploy 注入 MULTI_TENANT="false"(mcp-account-source §5.5,修 MCP 401)。
|
||||
// init.ts 這條本就是 --self-hosted 分支(config.mode 稍後寫 'self-hosted')。
|
||||
const deployCtx: DeployContext = { accountId, apiToken: cfApiToken, workerSubdomain, kvNamespaceIds, d1DatabaseId, selfHosted: true, kbdbEmbed };
|
||||
const deploy = await downloadAndDeploy(deployCtx);
|
||||
const deployCtx: DeployContext = { accountId, apiToken: cfApiToken, workerSubdomain, selfHosted: true, kbdbEmbed };
|
||||
const deploy = await downloadAndDeploy(deployCtx, 'main', { mode: 'init', api: cf });
|
||||
|
||||
// 資源解析喊停(例:這台其實已經裝過、但某顆綁著的資源不見了)→ 什麼都沒建、什麼都沒部。
|
||||
if (deploy.blocked) {
|
||||
console.log(chalk.yellow('\n ⚠ 安裝沒有進行,你的 Cloudflare 帳號維持原樣。\n'));
|
||||
console.log(' ' + deploy.message.split('\n').join('\n '));
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 實際用上的資源(沿用既有的,或這次新建的)——寫 config / 驗收都以這份為準,不再自己查名字。
|
||||
const kvNamespaceIds = deployCtx.kvNamespaceIds ?? {};
|
||||
const d1DatabaseId = deployCtx.d1DatabaseId ?? '';
|
||||
const cypherUrl = deploy.cypherExecutorUrl
|
||||
?? (workerSubdomain ? `https://arcrun-cypher-executor.${workerSubdomain}.workers.dev` : '');
|
||||
// self-hosted 自己的 MCP worker URL(mcp-account-source §3:.mcp.json 指自己,不 fallback 官方)。
|
||||
@@ -290,8 +273,8 @@ async function initSelfHosted(
|
||||
// + 給一鍵補裝指令(不靜默印灰字)。假綠零容忍(mindset §7):看實際狀態,非看 config 寫了沒。
|
||||
const verify = await verifyInstall({
|
||||
cf,
|
||||
requiredKv: REQUIRED_KV_NAMESPACES,
|
||||
expectD1Name: d1DatabaseId ? 'arcrun-kbdb' : undefined,
|
||||
kvNamespaceIds,
|
||||
d1DatabaseId: d1DatabaseId || undefined,
|
||||
cypherUrl,
|
||||
});
|
||||
printPreflight('安裝驗收(裝完檢查)', verify.items);
|
||||
@@ -301,7 +284,7 @@ async function initSelfHosted(
|
||||
}
|
||||
|
||||
// 結果回報(誠實:部分失敗時明說,不假綠 — mindset §7)
|
||||
console.log(chalk.green(`\n ✓ Cloudflare 資源就緒(${REQUIRED_KV_NAMESPACES.length} KV,免費額度即可,無需綁卡)`));
|
||||
console.log(chalk.green(`\n ✓ Cloudflare 資源就緒(${Object.keys(kvNamespaceIds).length} KV,免費額度即可,無需綁卡)`));
|
||||
console.log(chalk.green(' ✓ 設定寫入 ~/.arcrun/config.yaml'));
|
||||
console.log(chalk.green(' ✓ 建立 credentials.yaml'));
|
||||
|
||||
|
||||
+17
-36
@@ -14,11 +14,9 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { loadConfig } from '../lib/config.js';
|
||||
import { CfAccountClient } from '../lib/cf-api.js';
|
||||
import {
|
||||
wranglerAvailable,
|
||||
downloadAndDeploy,
|
||||
REQUIRED_KV_NAMESPACES,
|
||||
type DeployContext,
|
||||
} from '../lib/deploy.js';
|
||||
|
||||
@@ -44,43 +42,15 @@ export async function cmdUpdate(opts: { force?: boolean } = {}): Promise<void> {
|
||||
|
||||
console.log(chalk.bold('\n acr update — 拉新 release 並重新部署\n'));
|
||||
|
||||
// 重新解析「全部」KV namespace id(冪等:已存在則重用),不只 config 存的兩個。
|
||||
// 壓測 §4.1.3:舊版 update 只注入 WEBHOOKS+CREDENTIALS_KV,其餘 6 個注入成空字串 →
|
||||
// 重部署反而可能弄壞需要 RECIPES/EXEC_CONTEXT/... 的 worker。改為與 init 同樣全建妥。
|
||||
const cf = new CfAccountClient(config.cloudflare_account_id, config.cf_api_token);
|
||||
const kvNamespaceIds: Record<string, string> = {};
|
||||
try {
|
||||
const existing = await cf.listKvNamespaces();
|
||||
for (const title of REQUIRED_KV_NAMESPACES) {
|
||||
kvNamespaceIds[title] = await cf.ensureKvNamespace(title, existing);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(`\n ✗ 解析 KV namespace 失敗:${e instanceof Error ? e.message : e}\n`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// D1(KBDB Base)冪等補建——之前只在 init 建,update 漏了,導致「init 時 D1 失敗(如 token 缺權限)
|
||||
// → 補好權限後沒有任何指令會補建 D1」(壓測 2026-06-09:D1 一直建不起來的真根因)。
|
||||
// update 既是「冪等重部署」就該與 init 一致把 D1 也 ensure 上。
|
||||
let d1DatabaseId = '';
|
||||
try {
|
||||
process.stdout.write(chalk.gray(' → D1 arcrun-kbdb(冪等)...'));
|
||||
d1DatabaseId = await cf.ensureD1Database('arcrun-kbdb');
|
||||
console.log(chalk.green(' ✓'));
|
||||
} catch (e) {
|
||||
const em = e instanceof Error ? e.message : String(e);
|
||||
console.log(chalk.yellow(` ⚠ ${em}`));
|
||||
if (/auth/i.test(em)) {
|
||||
console.log(chalk.yellow(' CF token 缺 D1 權限 → 補勾「Account / D1 / Edit」重產 token 填回 .env 再 acr update'));
|
||||
}
|
||||
}
|
||||
|
||||
// 🔴 Arcrun#97:這裡**曾經**先「照名字 ensure」一輪 KV + D1 再往下傳。
|
||||
// binding 名(WEBHOOKS)被當成 CF 上的資源標題去找,安裝器建的資源不叫那個名字
|
||||
// ⇒ 每次都對不上 ⇒ 每次都新建一顆空的綁上去 ⇒ 使用者的工作流/登入/子庫從畫面上消失。
|
||||
// 現在資源解析整段搬進 downloadAndDeploy:先讀「你已部署的 worker 現在綁著什麼」再決定,
|
||||
// 而且是**下載完、看得到這版要哪些 binding 之後**才決定,不再由這裡預先造一批。
|
||||
const ctx: DeployContext = {
|
||||
accountId: config.cloudflare_account_id,
|
||||
apiToken: config.cf_api_token,
|
||||
workerSubdomain: extractSubdomain(config.cypher_executor_url),
|
||||
kvNamespaceIds,
|
||||
d1DatabaseId: d1DatabaseId || undefined,
|
||||
// self-hosted → 注入 MULTI_TENANT="false"(mcp-account-source §5.5,修 acr update 部署的 MCP 401)。
|
||||
// config 源頭:init 寫 multi_tenant:false + mode:'self-hosted'。acr update 只在 self-hosted 跑。
|
||||
selfHosted: config.mode === 'self-hosted' || config.multi_tenant === false,
|
||||
@@ -93,7 +63,18 @@ export async function cmdUpdate(opts: { force?: boolean } = {}): Promise<void> {
|
||||
kbdbEmbed: config.kbdb_embed !== false,
|
||||
};
|
||||
|
||||
const result = await downloadAndDeploy(ctx, 'main', { force: opts.force });
|
||||
// mode:'update' → 資源解析在「一顆該更新的 worker 都找不到」時會停手而不是重建一整套
|
||||
//(Arcrun#97 的另一道門:名字對不上時別假裝這是全新安裝)。
|
||||
const result = await downloadAndDeploy(ctx, 'main', { force: opts.force, mode: 'update' });
|
||||
|
||||
// 資源解析階段喊停:什麼都沒建、什麼都沒部。原文照印,然後非零離開——
|
||||
// 不能混進「部分失敗」的黃字裡帶過(那正是使用者不會發現的那種失敗)。
|
||||
if (result.blocked) {
|
||||
console.log(chalk.yellow('\n ⚠ 更新沒有進行,你的實例維持原樣。\n'));
|
||||
console.log(' ' + result.message.split('\n').join('\n '));
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (result.implemented) {
|
||||
// message 含部分失敗清單(「部署 X/Y 成功,N 失敗:✗ ...」)——必須印出來,
|
||||
|
||||
Reference in New Issue
Block a user