Files
Arcrun/cli/src/commands/update.ts
T
uncle6me-web 45a546a686 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
2026-08-12 13:30:17 +08:00

141 lines
8.1 KiB
TypeScript
Raw 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.
/**
* acr update — 從 Gitea 拉最新 archive,重新部署零件/引擎到用戶自己的 Cloudflare。
*
* 與 acr init --self-hosted 走同一條「下載 archive → 注入 KV id → wrangler deploy」的路
*(同一支 downloadAndDeploy),差別只在:init 是首次(建 KV/D1 + 寫 config),
* update 是沿用既有 config 重部署變動的 Worker。因共用 downloadAndDeploy 的內容指紋 manifest
* 「新裝零件補上、內容未變者略過」對 update 天然成立 → install ≈ updateArcrun#4)。
*
* 下載源(Arcrun#42026-07-07):由 GitHub codeload 改指 Gitea archivegit.uncle6.me),
* 讓 D20 防 flag 下不能碰 GitHub 的 self-hosted 用戶也能 acr update 補裝新零件(如 code 零件)。
*
* 對應 SDD.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §3「acr update」
*/
import chalk from 'chalk';
import { loadConfig } from '../lib/config.js';
import {
wranglerAvailable,
downloadAndDeploy,
type DeployContext,
} from '../lib/deploy.js';
export async function cmdUpdate(opts: { force?: boolean } = {}): Promise<void> {
const config = loadConfig();
if (config.mode !== 'self-hosted') {
console.log(chalk.yellow('\n acr update 只用於 self-hosted 模式(部署在你自己的 Cloudflare)。'));
console.log(chalk.gray(' 目前模式:' + config.mode + '。如要 self-host,先跑 acr init --self-hosted。\n'));
process.exit(1);
}
if (!config.cloudflare_account_id || !config.cf_api_token) {
console.log(chalk.yellow('\n config 缺 cloudflare_account_id / cf_api_token,無法部署。'));
console.log(chalk.gray(' 請重新跑 acr init --self-hosted。\n'));
process.exit(1);
}
if (!wranglerAvailable()) {
console.log(chalk.yellow('\n ✗ 找不到 wranglerCloudflare CLI)。請先 npm i -g wrangler。\n'));
process.exit(1);
}
console.log(chalk.bold('\n acr update — 拉新 release 並重新部署\n'));
// 🔴 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),
// 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,
// 語義查詢(issue #7):預設**開**,只有 config 顯式寫 kbdb_embed:false 才關。
// 🔴 2026-08-09 翻轉預設(leo:「語義搜尋已經確定是一安裝就提供的功能」)。
// 舊判斷 `=== true` 的實害:config 沒這個欄位(舊 config / 一鍵安裝實例本機補跑 update)
// 時 redeploy 會把 kbdb 的 [[vectorize]]+[ai] binding 靜默剝掉——一台**原本正常**的
// 實例就這樣失去語意搜尋,畫面上還被說成「還沒開通」。wrangler deploy 是整份覆蓋,
// binding 不在 toml 裡=直接消失,這正是「裝好的實例壞掉」的機制之一。
kbdbEmbed: config.kbdb_embed !== false,
};
// 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 失敗:✗ ...」)——必須印出來,
// 否則 worker 失敗被綠勾蓋掉(假綠):cypher 沒部上 → 後面 migrate 打舊 worker 404
// 用戶重跑 N 次都不知道根因(壓測 2026-06-11 實證)。
if (result.message?.includes('失敗')) {
console.log(chalk.yellow(`\n ⚠ 部署部分失敗:`));
console.log(chalk.yellow(' ' + result.message.split('\n').join('\n ')));
} else {
console.log(chalk.green('\n ✓ 部署完成'));
}
// 重跑 seed(薄殼:呼叫 API /init/seed;冪等,覆寫既有)。
// 修壓測 §4.1.3「update 不做 seed,但 init 提示說 update 會重試 seed」的矛盾。
const cypherUrl = config.cypher_executor_url
?? result.cypherExecutorUrl
?? (ctx.workerSubdomain ? `https://arcrun-cypher-executor.${ctx.workerSubdomain}.workers.dev` : '');
if (cypherUrl) {
process.stdout.write(chalk.gray(' → 重新 seed recipeAPI + auth,由 API 灌入)...'));
try {
const res = await fetch(`${cypherUrl}/init/seed`, { method: 'POST' });
const body = await res.json().catch(() => null) as { success?: boolean; message?: string } | null;
console.log(res.ok && body?.success
? chalk.green(` ✓ ${body.message ?? ''}`)
: chalk.yellow(` ⚠ ${body?.message ?? `HTTP ${res.status}`}`));
} catch (e) {
console.log(chalk.yellow(` ⚠ seed 失敗(${e instanceof Error ? e.message : e}`));
}
// kbdb-base 8.P0:一次性把舊的 per-key cron-idx:{apiKey}:{name} 折進單一 cron-idx:_all。
// 部署 8.P0 後既有 cron workflow 若不重 push 會停擺(scheduled 只讀新集中 key)→ 這裡冪等補上。
// 冪等、不刪舊 key、失敗不致命(重跑 acr update 會再試)。
process.stdout.write(chalk.gray(' → 遷移 cron index(舊 per-key → 集中 key,冪等)...'));
try {
const res = await fetch(`${cypherUrl}/webhooks/named/migrate-cron-index`, { method: 'POST' });
const rawText = await res.text();
let body: { success?: boolean; migrated?: number; skipped?: number; errors?: string[] } | null = null;
try { body = JSON.parse(rawText); } catch { /* 非 JSON(如 CF 錯誤頁)→ 用原文 */ }
if (res.ok && body?.success) {
console.log(chalk.green(` ✓ migrated ${body.migrated ?? 0}, skipped ${body.skipped ?? 0}`));
} else {
// 印 server 回的錯誤內容(截前 200 字)——只回 HTTP status 沒人能診斷
// (壓測 2026-06-11404→500 重跑 3 次都看不到根因)。
const detail = (body?.errors?.join('; ') ?? rawText).slice(0, 200);
console.log(chalk.yellow(` ⚠ HTTP ${res.status}${detail ? `${detail}` : ''}`));
console.log(chalk.yellow(' 404 = cypher worker 還是舊版(看上方部署是否有失敗);500 = server 端錯誤(看上行錯誤內容)'));
}
} catch (e) {
console.log(chalk.yellow(` ⚠ cron index 遷移失敗(${e instanceof Error ? e.message : e}`));
}
}
console.log('');
} else {
console.log(chalk.yellow(' ⚠ 更新尚未自動化:'));
console.log(chalk.gray(' ' + result.message.split('\n').join('\n ')) + '\n');
}
}
/** 從 cypher_executor_urlhttps://arcrun-cypher-executor.<sub>.workers.dev)抽 subdomain。*/
function extractSubdomain(url?: string): string {
if (!url) return '';
const m = url.match(/arcrun-cypher-executor\.([^.]+)\.workers\.dev/);
return m?.[1] ?? '';
}