diff --git a/.component-builds/claude_api/src/index.ts b/.component-builds/claude_api/src/index.ts index 288d026..95015fb 100644 --- a/.component-builds/claude_api/src/index.ts +++ b/.component-builds/claude_api/src/index.ts @@ -72,7 +72,13 @@ async function runWasm(input: unknown): Promise { init.body = body; } const res = await fetch(url, init); - return await res.text(); + const text = await res.text(); + // 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope, + // 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。 + if (!res.ok) { + return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text }); + } + return text; }, }; diff --git a/.component-builds/http_request/src/index.ts b/.component-builds/http_request/src/index.ts index 11179a8..c350938 100644 --- a/.component-builds/http_request/src/index.ts +++ b/.component-builds/http_request/src/index.ts @@ -61,7 +61,19 @@ async function runWasm(input: unknown): Promise { init.body = body; } const res = await fetch(url, init); - return await res.text(); + const text = await res.text(); + // 修架構債(main.go:112):host function 原本只回 body,丟掉 HTTP status code, + // 導致 4xx/5xx(如 Notion 401)被零件判成 success → 引擎對失敗報告成功(系統假綠根因)。 + // 修法:非 2xx 包成帶 "error" key 的 envelope,讓所有消費零件既有的 parsed["error"] 判定 + // 鏈正確識別失敗(2xx 維持原樣回 body 原文,向後相容不破壞 happy path)。 + if (!res.ok) { + return JSON.stringify({ + error: `HTTP ${res.status}`, + status: res.status, + body: text, + }); + } + return text; }, }; diff --git a/.component-builds/kbdb_upsert_block/src/index.ts b/.component-builds/kbdb_upsert_block/src/index.ts index 4a6bc80..560e7b9 100644 --- a/.component-builds/kbdb_upsert_block/src/index.ts +++ b/.component-builds/kbdb_upsert_block/src/index.ts @@ -55,7 +55,13 @@ async function runWasm(input: unknown): Promise { init.body = body; } const res = await fetch(url, init); - return await res.text(); + const text = await res.text(); + // 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope, + // 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。 + if (!res.ok) { + return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text }); + } + return text; }, }; diff --git a/.component-builds/km_writer/src/index.ts b/.component-builds/km_writer/src/index.ts index 5f8ccf0..67a169e 100644 --- a/.component-builds/km_writer/src/index.ts +++ b/.component-builds/km_writer/src/index.ts @@ -58,7 +58,13 @@ async function runWasm(input: unknown): Promise { init.body = body; } const res = await fetch(url, init); - return await res.text(); + const text = await res.text(); + // 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope, + // 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。 + if (!res.ok) { + return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text }); + } + return text; }, }; diff --git a/.env.example b/.env.example index c4652d6..d447118 100644 --- a/.env.example +++ b/.env.example @@ -19,8 +19,12 @@ # # 2) 金鑰(API Token): # https://dash.cloudflare.com/profile/api-tokens → Create Custom Token → -# 勾兩組權限:Account / Workers Scripts / Edit 和 Account / Workers KV Storage / Edit -# → 建立後複製那串 token 貼到下面。(不需要 R2、不需要綁卡。) +# 勾三組權限(缺一不可): +# · Account / Workers Scripts / Edit +# · Account / Workers KV Storage / Edit +# · Account / D1 / Edit ← 必勾!arcrun 用 D1 存 workflow/recipe, +# 漏勾會在 acr init 建 D1 時報 Authentication error +# → 建立後複製那串 token 貼到下面。(不需要 R2、不需要綁卡,D1 也在免費額度。) # CLOUDFLARE_ACCOUNT_ID= CLOUDFLARE_API_TOKEN= diff --git a/cli/package-lock.json b/cli/package-lock.json index 6859940..ee6e93d 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "arcrun", - "version": "1.3.2", + "version": "1.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "arcrun", - "version": "1.3.2", + "version": "1.3.4", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/cli/package.json b/cli/package.json index d040e65..2e4b282 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "arcrun", - "version": "1.3.3", + "version": "1.3.4", "description": "AI Workflow CLI for arcrun — self-host WASM-based AI workflows on your own Cloudflare", "bin": { "acr": "dist/index.js" diff --git a/cli/src/commands/init.ts b/cli/src/commands/init.ts index f44717f..e0ccbd2 100644 --- a/cli/src/commands/init.ts +++ b/cli/src/commands/init.ts @@ -228,7 +228,15 @@ async function initSelfHosted( d1DatabaseId = await cf.ensureD1Database('arcrun-kbdb'); console.log(chalk.green(' ✓')); } catch (e) { - console.log(chalk.yellow(`\n ⚠ D1 build failed (${e instanceof Error ? e.message : e}); KBDB Base 暫不可用,可 acr update 重試`)); + 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 用) diff --git a/cli/src/commands/run.ts b/cli/src/commands/run.ts index 8e0d85d..8cbb900 100644 --- a/cli/src/commands/run.ts +++ b/cli/src/commands/run.ts @@ -37,14 +37,14 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise const headers: Record = { 'Content-Type': 'application/json' }; if (config.api_key) headers['X-Arcrun-API-Key'] = config.api_key; - // ── 玩法一:Standard 模式,YAML 在本機,帶著打 /cypher/execute ────────────── - if (config.mode === 'standard' || config.mode === 'local') { - const yamlPath = findWorkflowYaml(workflowName); - if (!yamlPath) { - console.error(chalk.red(`找不到 ${workflowName}.yaml(在目前目錄或子目錄尋找)`)); - console.error(chalk.gray('玩法二(已 push 到 KV)請改用 Self-hosted 模式')); - process.exit(1); - } + // ── 玩法一:本機有 YAML → 直接帶著打 /cypher/execute(不需先 push)────────────── + // 2026-06-09 修:原本只有 standard/local 走這條,self-hosted 一律走玩法二(/webhooks/, + // 需先 push 到 KV)。導致 self-hosted 用戶(如壓測 Haiku)有本機 YAML 卻 acr run 直接打 + // /webhooks/ → 沒 push = 404 純文字 → res.json() 爆「Unexpected non-whitespace...」假錯誤。 + // 正解:只要本機找得到 YAML 就走玩法一直接執行(三模式一致);找不到才退玩法二(按名字打已 push 的)。 + const localYamlPath = findWorkflowYaml(workflowName); + if (localYamlPath) { + const yamlPath = localYamlPath; let workflow; try { @@ -71,6 +71,14 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise }), }); + // 非 2xx 先擋:直接 res.json() 對 404「Not Found」這種純文字會爆出誤導的 + // 「Unexpected non-whitespace character after JSON」。看 res.ok 給人話。 + if (!res.ok) { + const body = await res.text(); + spinner.fail(chalk.red(`執行失敗(HTTP ${res.status}):${body.slice(0, 200)}`)); + process.exit(1); + } + const data = await res.json() as { success: boolean; data?: unknown; @@ -88,7 +96,7 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise return; } - // ── 玩法二:Self-hosted,workflow 已存在 KV,打 /webhooks/{name} ───────────── + // ── 玩法二:本機沒這個 YAML → 按名字打已 push 到 KV 的 workflow ─────────────── const spinner = ora(`執行 workflow "${workflowName}"`).start(); try { const res = await fetch(`${executorUrl}/webhooks/${workflowName}`, { @@ -97,6 +105,20 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise body: JSON.stringify(inputContext), }); + // 非 2xx 先擋(同玩法一):404 純文字別硬 res.json()。404 多半是「還沒 push」。 + if (!res.ok) { + if (res.status === 404) { + spinner.fail(chalk.red(`找不到已部署的 workflow "${workflowName}"`)); + console.error(chalk.gray(` 本機也沒有 ${workflowName}.yaml。請確認:`)); + console.error(chalk.gray(` ① 本機有 YAML → 在該檔所在目錄跑 acr run(會直接執行,不需先 push)`)); + console.error(chalk.gray(` ② 要跑已部署的 → 先 acr push .yaml 再 acr run `)); + } else { + const body = await res.text(); + spinner.fail(chalk.red(`執行失敗(HTTP ${res.status}):${body.slice(0, 200)}`)); + } + process.exit(1); + } + const data = await res.json() as { success: boolean; data?: unknown; @@ -116,11 +138,14 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise // ── helpers ────────────────────────────────────────────────────────────────── function findWorkflowYaml(name: string): string | null { + // 容忍使用者直接給含副檔名的檔名(acr run foo.yaml)——剝掉再補,避免找成 foo.yaml.yaml。 + const base = name.replace(/\.(ya?ml)$/i, ''); const candidates = [ - `${name}.yaml`, - `${name}.yml`, - `workflows/${name}.yaml`, - `workflows/${name}.yml`, + name, // 原樣(已含副檔名或本就是路徑) + `${base}.yaml`, + `${base}.yml`, + `workflows/${base}.yaml`, + `workflows/${base}.yml`, ]; for (const p of candidates) { if (existsSync(p)) return p; diff --git a/cli/src/commands/update.ts b/cli/src/commands/update.ts index 2c65ea6..596228b 100644 --- a/cli/src/commands/update.ts +++ b/cli/src/commands/update.ts @@ -57,11 +57,28 @@ export async function cmdUpdate(): Promise { 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')); + } + } + const ctx: DeployContext = { accountId: config.cloudflare_account_id, apiToken: config.cf_api_token, workerSubdomain: extractSubdomain(config.cypher_executor_url), kvNamespaceIds, + d1DatabaseId: d1DatabaseId || undefined, }; const result = await downloadAndDeploy(ctx); diff --git a/cli/src/lib/preflight.ts b/cli/src/lib/preflight.ts index b45b0ad..58bfb15 100644 --- a/cli/src/lib/preflight.ts +++ b/cli/src/lib/preflight.ts @@ -104,10 +104,15 @@ export async function verifyInstall(opts: { items.push( dbs.has(opts.expectD1Name) ? { name: `D1 ${opts.expectD1Name}`, ok: true } - : { name: `D1 ${opts.expectD1Name}`, ok: false, detail: '不存在', fix: 'acr update(冪等重建 + 套 migration)' }, + : { name: `D1 ${opts.expectD1Name}`, ok: false, detail: '不存在', fix: 'CF token 補勾「Account / D1 / Edit」權限 → 重產 token 填回 .env → acr update' }, ); } catch (e) { - items.push({ name: `D1 ${opts.expectD1Name}`, ok: false, detail: msg(e), fix: 'acr update' }); + // 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' + : 'acr update(冪等重試)'; + items.push({ name: `D1 ${opts.expectD1Name}`, ok: false, detail: m, fix }); } } diff --git a/llms.txt b/llms.txt index af4945a..b285d04 100644 --- a/llms.txt +++ b/llms.txt @@ -36,8 +36,11 @@ npm i -g arcrun && acr install-harness - **帶使用者拿值**——用白話照抄式引導,**不要講 KV / Worker / R2 / zone 等術語**(他聽不懂也不需懂): - 帳號代碼(`CLOUDFLARE_ACCOUNT_ID`):登入 https://dash.cloudflare.com 右側欄複製。 - 金鑰(`CLOUDFLARE_API_TOKEN`):https://dash.cloudflare.com/profile/api-tokens → Create Custom Token → - 照抄勾**兩組**權限(Account/Workers Scripts/Edit、Account/Workers KV Storage/Edit) - → 複製產生的 token。(不需要 R2、不需要綁信用卡——只用 Workers + KV 免費額度。) + 照抄勾**三組**權限: + · Account / Workers Scripts / Edit + · Account / Workers KV Storage / Edit + · Account / D1 / Edit ← **必勾**,arcrun 用 D1 存 workflow/recipe;漏勾會 init 時 D1 建失敗(Authentication error) + → 複製產生的 token。(不需要 R2、不需要綁信用卡——D1 也在免費額度,不綁卡。) - `NAMESPACE`:隨便取個英數小名(非密碼)。`ENCRYPTION_KEY`:你可幫他產 (`node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`)。 - 使用者把值貼進 .env(或貼給你、你幫他填進對應格)。**CLOUDFLARE 兩格沒填,後面什麼都跑不了。** diff --git a/registry/components/http_request/main.go b/registry/components/http_request/main.go index fff14b8..af6b978 100644 --- a/registry/components/http_request/main.go +++ b/registry/components/http_request/main.go @@ -108,10 +108,16 @@ func main() { responseStr := string(outBuf[:outLen]) - // 2026-05-14:偵測 JSON `{"error":"..."}` 模式視為 4xx 失敗 - // 理由:host function 沒回 HTTP status code(架構債),先用 body 啟發式 catch。 - // 標準 API(cypher-executor / KBDB / 多數 REST)失敗時都回 {"error":...} JSON。 - // 對應 SDD: arcrun.md 三-A P1 #4「http_request status code 缺乏 surface」。 + // 偵測 JSON `{"error":"..."}` 模式視為失敗。 + // 2026-06-09 修架構債:host function(.component-builds/http_request/src/index.ts)現在對非 2xx + // 回 envelope `{"error":"HTTP ","status":,"body":<原文>}`——故此處 parsed["error"] + // 能正確 catch 所有 4xx/5xx(含 Notion 401 那種 body 用 {"object":"error"} 不帶 error key 的)。 + // 之前 host fn 只回 body 原文丟掉 status → 401 被判 success(系統假綠根因,已修)。 + // 註:claude_api/kbdb_upsert_block/km_writer 已同樣修(非 2xx 回 error envelope)。 + // auth_service_account 不套此 envelope——它 main.go 自己解析 OAuth token 回應的 + // {access_token,error,error_description},access_token 空即視為失敗,已有自己的判定, + // 套 envelope 反而會丟掉 error_description 破壞 token exchange 錯誤處理。 + // 待辦:4 份 inline host fn 最好抽成共用 helper(dedup,目前複製貼上)。 var parsed map[string]interface{} if err := json.Unmarshal([]byte(responseStr), &parsed); err == nil { if errVal, ok := parsed["error"]; ok && errVal != nil {