Files
Arcrun/cli/src/commands/update.ts
uncle6me-web 6846d6ddae fix(semantic): 故障照實說是故障——不再把壞掉說成「沒開通」(leo 2026-08-09 直令)
一、文案(portal/console/kbdb hint):語意搜尋是一安裝就提供的功能,
   降級=故障。橫幅改「語意搜尋目前故障/我們的問題/你不用做任何事」,
   拿掉「還沒開通、想開通請匯出診斷檔」這種要使用者申請開通的假框架。
   kbdb 降級回應加 degraded_reason(module_off / embed_query_failed)。

二、查詢向量化失敗不再偽裝成空結果(leo 點名的謊):
   semanticSearch 舊行為「AI 額度用完 → 回 []」會讓使用者以為
   自己的知識庫裡沒有這筆資料。改丟 EmbedQueryFailedError,
   route 誠實降級 keyword+照實告知是暫時故障。

三、源頭機制(裝好的實例為什麼會失去語意搜尋):
   - acr update:kbdb_embed 判斷 ===true → !==false。config 缺欄位時
     redeploy 會把 [[vectorize]]+[ai] binding 靜默剝掉(wrangler deploy
     整份覆蓋),一台正常實例就此壞掉。init 預設同步翻成 [Y/n]。
   -(另 repo)deploy-all.mjs ensureVectorizeIndex 失敗改致命中止。

四、順手自癒:孤兒向量/下架殘影搜尋時背景清除;空結果且 pending>0
   背景 backfill;no_index 拆「故障」vs「還沒有資料」兩態。

測試:kbdb 146/146(新增 degraded 6 案+selftest 1 案);cli 10/10;
瀏覽器端到端兩種故障畫面實測(local wrangler dev+portal 真登入)。
無 SDD 對應:leo 直令修故障(同 08-07 檢修孔前例的人閘直接授權路徑)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 02:05:12 +08:00

160 lines
8.6 KiB
TypeScript
Raw Permalink 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 { CfAccountClient } from '../lib/cf-api.js';
import {
wranglerAvailable,
downloadAndDeploy,
REQUIRED_KV_NAMESPACES,
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'));
// 重新解析「全部」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);
}
// D1KBDB 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'));
}
}
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,
// 語義查詢(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,
};
const result = await downloadAndDeploy(ctx, 'main', { force: opts.force });
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] ?? '';
}