From 5f381a44a6f28c8b26cc8bee457ee32e74975d45 Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Fri, 5 Jun 2026 07:22:37 +0800 Subject: [PATCH] =?UTF-8?q?fix(self-hosted):=20=E4=BF=AE=E5=A3=93=E6=B8=AC?= =?UTF-8?q?=E5=9B=9B=E9=98=BB=E6=96=B7=E9=A0=85=20+=20=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E5=88=86=E5=B1=A4=20+=20init=20=E9=9D=9E=E4=BA=92=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 壓測(docs/壓測報告.md)發現 acr init --self-hosted 對任何非官方 CF 帳號都裝不起來,且設定寫死全域單檔 + 強制 TTY。本次一併修: R2 dead storage 全清(#3#4,registry-canon Phase 1.5 補完): - cypher-executor wrangler.toml/test.toml/types.ts 移除 WASM_BUCKET binding - CLI deploy.ts/init.ts/cf-api.ts/config.ts 移除 R2 建立邏輯與 wasm_bucket - R2 綁信用卡違背「開源免費自架」核心;bucket 名 WASM_BUCKET 本就非法 → self-hosted 改為只需 Workers + KV(皆免費額度、不綁卡) fork 帳號部署阻斷(#1#2): - deploy.ts 新增 stripOfficialOnlyBindings(),注入暫存副本時移除 [[routes]]/zone_name/[[r2_buckets]]/[ai](fork 沒有 arcrun.dev zone) - 不刪 repo 內 toml(官方 prod CI 部署仍需 routes),只在 CLI self-hosted 路徑 strip 設定分層 + 非互動(#7#8): - config.ts loadConfig 改三層:env > 專案層 .arcrun.yaml(就近往上找)> 全域 - init 支援 --account-id/--api-token flag + CLOUDFLARE_* env,缺才互動 - 新增 acr config --where 顯示每個值的來源層(token 自動遮罩) - gitignore 一併排除 .arcrun.yaml 驗收:tsc 全綠;三層 merge 端對端測試 8/8;strip 對真實 toml 驗證 routes/R2/AI 移除而 name/workers_dev/KV 保留。 Co-Authored-By: Claude Opus 4.8 --- README.md | 4 +- cli/src/commands/config.ts | 49 ++++++++++++ cli/src/commands/init.ts | 56 ++++++++++---- cli/src/commands/validate.ts | 4 +- cli/src/index.ts | 16 +++- cli/src/lib/cf-api.ts | 18 +---- cli/src/lib/config.ts | 112 ++++++++++++++++++++++++--- cli/src/lib/deploy.ts | 55 ++++++++++++- cypher-executor/src/lib/constants.ts | 6 +- cypher-executor/src/types.ts | 2 - cypher-executor/wrangler.test.toml | 5 +- cypher-executor/wrangler.toml | 6 +- 12 files changed, 268 insertions(+), 65 deletions(-) create mode 100644 cli/src/commands/config.ts diff --git a/README.md b/README.md index c6eb500..df74b3a 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ AI 很會寫程式,就要除錯,過程浪費很多 Token 及時間,但絕 但現有工作流軟體是給人用的,對 AI 不友善。比如為了好看界面,程式碼又長又複雜,導致 AI 生成時容易出錯,甚至一個檔就灌爆 Context Window。 -所以 Arcrun 是為了 AI Friendly 的目的開發的。 +所以 Arcrun 是為了 AI Friendly 的目的開發。 -Arcrun 主要給 AI 用,因為 AI 很會寫 Code,它不是「Low code」,但更適合你用,因為只要跟 AI 說,就把後面的事做完了,比 Low code 更輕鬆。 +因為 AI 很會寫 Code,它不「Low code」,但更適合你用,因為跟 AI 說,它就把後面的事做完了,比 Low code 更簡單。 為了 AI Friendly,arcrun 內含**給 AI(Claude Code)用的 harness**:你叫 AI 用 arcrun 開發時,它知道能用什麼、不能做什麼,做錯了有機制擋住——讓 CC 順暢、且不容易做歪。 diff --git a/cli/src/commands/config.ts b/cli/src/commands/config.ts new file mode 100644 index 0000000..7d7f9c9 --- /dev/null +++ b/cli/src/commands/config.ts @@ -0,0 +1,49 @@ +/** + * acr config [--where] — 顯示目前生效的設定與每個值的來源層。 + * 解壓測 §1.2 建議 #3:讓使用者一眼確認「現在這個資料夾正用哪個帳號」,避免用錯帳號部署。 + * SDD: sdk-and-website/config-layering.md §3.1 + */ +import chalk from 'chalk'; +import { + resolveConfigSources, + activeProjectConfigPath, + type ConfigSource, +} from '../lib/config.js'; + +const SOURCE_LABEL: Record = { + env: 'env 變數', + project: '專案層 .arcrun.yaml', + global: '全域 ~/.arcrun/config.yaml', + default: '預設值', +}; + +/** 敏感欄位只印前綴,避免把 token 完整印到終端 / log。*/ +const SENSITIVE = new Set(['api_key', 'encryption_key', 'cf_api_token']); + +function mask(field: string, value: string): string { + if (SENSITIVE.has(field) && value.length > 8) return `${value.slice(0, 8)}…`; + return value; +} + +export async function cmdConfig(_options: { where?: boolean }): Promise { + const rows = resolveConfigSources(); + const projectPath = activeProjectConfigPath(); + + console.log(chalk.bold('\n arcrun 目前生效的設定\n')); + + if (projectPath) { + console.log(chalk.gray(` 專案層設定:${projectPath}(覆蓋全域)`)); + } else { + console.log(chalk.gray(' 專案層設定:無(此資料夾未放 .arcrun.yaml,使用全域)')); + } + console.log(''); + + const fieldWidth = Math.max(...rows.map(r => r.field.length), 4); + for (const { field, value, source } of rows) { + const name = field.padEnd(fieldWidth); + console.log( + ` ${chalk.cyan(name)} ${mask(field, value)} ${chalk.gray(`← ${SOURCE_LABEL[source]}`)}`, + ); + } + console.log(chalk.gray('\n 優先序:env 變數 > 專案層 .arcrun.yaml > 全域 ~/.arcrun/config.yaml\n')); +} diff --git a/cli/src/commands/init.ts b/cli/src/commands/init.ts index b1d8ba0..52f380f 100644 --- a/cli/src/commands/init.ts +++ b/cli/src/commands/init.ts @@ -11,7 +11,6 @@ import { saveConfig, type ArcrunConfig } from '../lib/config.js'; import { CfAccountClient } from '../lib/cf-api.js'; import { REQUIRED_KV_NAMESPACES, - REQUIRED_R2_BUCKET, SECRET_TARGET_WORKERS, wranglerAvailable, downloadAndDeploy, @@ -27,7 +26,14 @@ async function prompt(rl: ReturnType, question: string): return answer.trim(); } -export async function cmdInit(options: { local?: boolean; selfHosted?: boolean }): Promise { +export interface InitOptions { + local?: boolean; + selfHosted?: boolean; + accountId?: string; + apiToken?: string; +} + +export async function cmdInit(options: InitOptions): Promise { const rl = createInterface({ input: process.stdin, output: process.stdout }); console.log(chalk.bold('\n arcrun 初始化設定\n')); @@ -36,7 +42,7 @@ export async function cmdInit(options: { local?: boolean; selfHosted?: boolean } if (options.local) { await initLocal(); } else if (options.selfHosted) { - await initSelfHosted(rl); + await initSelfHosted(rl, options); } else { await initStandard(rl); } @@ -123,11 +129,14 @@ async function initStandard(rl: ReturnType): Promise): Promise { +async function initSelfHosted( + rl: ReturnType, + options: InitOptions, +): Promise { console.log(chalk.gray(' Self-hosted 模式:自動部署整套 arcrun 到你的 Cloudflare 帳號\n')); console.log(chalk.gray(' 你只需提供 CF Account ID + API Token,其餘 CLI 自動完成。\n')); @@ -138,8 +147,21 @@ async function initSelfHosted(rl: ReturnType): Promise env(CLOUDFLARE_*)> 互動問答。 + // 解壓測 #7:AI/CI 可非互動完成。帳號設定值非風險確認(mindset §7),flag/env 合法。 + // SDD: sdk-and-website/config-layering.md §2.3 + const accountId = + options.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? (await prompt(rl, '你的 Cloudflare Account ID')); + const cfApiToken = + options.apiToken ?? process.env.CLOUDFLARE_API_TOKEN + ?? (await prompt(rl, 'CF API Token(需 Workers Scripts Edit + Workers KV Storage Edit)')); + + if (!accountId || !cfApiToken) { + console.log(chalk.yellow('\n ✗ 缺少 Account ID 或 API Token。')); + console.log(chalk.yellow(' 非互動用法:acr init --self-hosted --account-id --api-token ')); + console.log(chalk.yellow(' 或設環境變數 CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN\n')); + process.exit(1); + } const cf = new CfAccountClient(accountId, cfApiToken); @@ -154,7 +176,9 @@ async function initSelfHosted(rl: ReturnType): Promise = {}; try { const existing = await cf.listKvNamespaces(); @@ -164,9 +188,6 @@ async function initSelfHosted(rl: ReturnType): Promise): Promise): Promise 0) appendFileSync(gitignorePath, '\n' + toAdd.join('\n') + '\n'); } } diff --git a/cli/src/commands/validate.ts b/cli/src/commands/validate.ts index 0fcdffe..adba53d 100644 --- a/cli/src/commands/validate.ts +++ b/cli/src/commands/validate.ts @@ -82,10 +82,10 @@ export async function cmdValidate(filePath: string, options: { offline?: boolean if (res.ok) { const data = await res.json() as { missing: string[] }; if (data.missing.length > 0) { - check('零件存在性', false, `WASM_BUCKET 中找不到:${data.missing.join(', ')}`); + check('零件存在性', false, `registry 中找不到零件:${data.missing.join(', ')}`); allPassed = false; } else { - check('零件存在性', true, '所有零件均已在 WASM_BUCKET'); + check('零件存在性', true, '所有零件均已在 registry'); } } else { check('零件存在性', false, `無法連線 ${executorUrl}(加 --offline 跳過此檢查)`); diff --git a/cli/src/index.ts b/cli/src/index.ts index 59a7ff1..1cbbb8a 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -8,6 +8,7 @@ */ import { Command } from 'commander'; import { cmdInit } from './commands/init.js'; +import { cmdConfig } from './commands/config.js'; import { cmdCredsPush } from './commands/creds.js'; import { cmdPush } from './commands/push.js'; import { cmdRun } from './commands/run.js'; @@ -27,13 +28,24 @@ program .description('arcrun — AI Workflow CLI for Cloudflare Workers + WASM') .version('1.1.0'); -// acr init [--self-hosted] +// acr init [--self-hosted] [--account-id ] [--api-token ] program .command('init') .description('互動式初始化設定(建立 ~/.arcrun/config.yaml)') .option('--local', '本機模式:不需要 Cloudflare 帳號,直接在本機測試 workflow') .option('--self-hosted', '完全 Self-hosted 模式:自行部署所有 Cloudflare Worker') - .action((options: { local?: boolean; selfHosted?: boolean }) => cmdInit(options)); + .option('--account-id ', 'self-hosted:CF Account ID(非互動;亦可用 CLOUDFLARE_ACCOUNT_ID env)') + .option('--api-token ', 'self-hosted:CF API Token(非互動;亦可用 CLOUDFLARE_API_TOKEN env)') + .action((options: { local?: boolean; selfHosted?: boolean; accountId?: string; apiToken?: string }) => + cmdInit(options), + ); + +// acr config [--where]:印出目前生效的設定與每個值的來源層(env / 專案層 / 全域) +program + .command('config') + .description('顯示目前生效的設定與來源層(避免在錯的資料夾用錯帳號)') + .option('--where', '顯示每個設定值來自哪一層(env > 專案層 .arcrun.yaml > 全域)') + .action((options: { where?: boolean }) => cmdConfig(options)); // acr creds push [credentials.yaml] const credsCmd = program.command('creds').description('Credential 管理'); diff --git a/cli/src/lib/cf-api.ts b/cli/src/lib/cf-api.ts index c576f8f..7e07529 100644 --- a/cli/src/lib/cf-api.ts +++ b/cli/src/lib/cf-api.ts @@ -78,7 +78,8 @@ export class CfKvClient { /** * Cloudflare Account-level API wrapper(self-hosted installer 用)。 * - * 負責 acr init --self-hosted 的資源建立:驗 token、建/列 KV namespace、建 R2 bucket、查 workers.dev subdomain。 + * 負責 acr init --self-hosted 的資源建立:驗 token、建/列 KV namespace、查 workers.dev subdomain。 + * (不建 R2:R2 是 dead storage 且綁卡違背開源免費;見 init.ts step 2 + registry-canon Phase 1.5。) * 與 CfKvClient(綁單一 namespace 的 KV 操作)職責不同——這個是帳號層級的資源管理。 * 對應 SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §3 step 1-2 */ @@ -138,21 +139,6 @@ export class CfAccountClient { return result.id; } - /** 建立 R2 bucket(已存在則略過,冪等)。*/ - async ensureR2Bucket(name: string): Promise { - try { - await this.cf<{ name: string }>('/r2/buckets', { - method: 'POST', - body: JSON.stringify({ name }), - }); - } catch (e) { - // bucket 已存在 → CF 回 10004 之類;視為冪等成功 - const msg = e instanceof Error ? e.message : String(e); - if (/already exists|10004/i.test(msg)) return; - throw e; - } - } - /** 查 workers.dev subdomain(cypher-executor WORKER_SUBDOMAIN 用,組對內 component URL)。*/ async getWorkersSubdomain(): Promise { const result = await this.cf<{ subdomain: string }>('/workers/subdomain'); diff --git a/cli/src/lib/config.ts b/cli/src/lib/config.ts index 4d25b36..bad2ba5 100644 --- a/cli/src/lib/config.ts +++ b/cli/src/lib/config.ts @@ -1,9 +1,11 @@ /** - * CLI 設定檔管理(~/.arcrun/config.yaml) + * CLI 設定檔管理 — 三層分層解析(SDD: sdk-and-website/config-layering.md) + * 優先序:env 變數 > 專案層 .arcrun.yaml(就近往上找)> 全域 ~/.arcrun/config.yaml + * 解壓測 #7(AI/CI 非互動)+ #8(接案多帳號),仿 git config / Claude Code MCP 模式。 */ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; import { homedir } from 'node:os'; -import { join } from 'node:path'; +import { join, dirname, parse as parsePath } from 'node:path'; import yaml from 'js-yaml'; export interface ArcrunConfig { @@ -18,7 +20,6 @@ export interface ArcrunConfig { cypher_executor_url?: string; credentials_kv_namespace_id?: string; webhooks_kv_namespace_id?: string; - wasm_bucket?: string; // 共用 multi_tenant?: boolean; // 資料外流警示:本機記住「已同意暴露 / 選擇不再警示」的資源,避免每次 push 重問(§3 首次問記住)。 @@ -30,17 +31,108 @@ export interface ArcrunConfig { const CONFIG_DIR = join(homedir(), '.arcrun'); const CONFIG_PATH = join(CONFIG_DIR, 'config.yaml'); +/** 專案層設定檔名(就近往上找)。含憑證 → 必須 gitignore(見 createCredentialsYamlIfMissing)。*/ +export const PROJECT_CONFIG_NAME = '.arcrun.yaml'; + +/** 設定來源層級(acr config --where 用,讓使用者知道每個值來自哪一層,避免用錯帳號)。*/ +export type ConfigSource = 'env' | 'project' | 'global' | 'default'; + +/** env 變數 → config 欄位映射(最高層覆蓋)。CF 兩個沿用 wrangler 慣用名,CI 設一次兩邊通用。*/ +const ENV_MAP: Record = { + ARCRUN_MODE: 'mode', + ARCRUN_API_KEY: 'api_key', + ARCRUN_ENCRYPTION_KEY: 'encryption_key', + ARCRUN_CYPHER_EXECUTOR_URL: 'cypher_executor_url', + CLOUDFLARE_ACCOUNT_ID: 'cloudflare_account_id', + CLOUDFLARE_API_TOKEN: 'cf_api_token', +}; + export function configExists(): boolean { - return existsSync(CONFIG_PATH); + return existsSync(CONFIG_PATH) || findProjectConfig() !== undefined; } -export function loadConfig(): ArcrunConfig { - if (!existsSync(CONFIG_PATH)) { - // 未初始化時回傳 local 模式預設值,讓 validate --offline 等指令能在無設定下運作 - return { mode: 'local' }; +/** 從 startDir 就近往上逐層找專案層 .arcrun.yaml,回傳第一個命中的路徑(停在檔案系統根)。*/ +export function findProjectConfig(startDir: string = process.cwd()): string | undefined { + let dir = startDir; + const root = parsePath(dir).root; + // 防呆上界:層數不會無限(root 一定到得了),但仍加保險避免異常路徑死迴圈。 + for (let i = 0; i < 256; i++) { + const candidate = join(dir, PROJECT_CONFIG_NAME); + if (existsSync(candidate)) return candidate; + if (dir === root) break; + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; } - const raw = readFileSync(CONFIG_PATH, 'utf8'); - return yaml.load(raw) as ArcrunConfig; + return undefined; +} + +/** 讀全域設定(不分層)。無檔回 undefined。*/ +function readGlobalConfig(): Partial | undefined { + if (!existsSync(CONFIG_PATH)) return undefined; + return (yaml.load(readFileSync(CONFIG_PATH, 'utf8')) as Partial) ?? undefined; +} + +/** 讀專案層設定(不分層)。無檔回 undefined。*/ +function readProjectConfig(): Partial | undefined { + const path = findProjectConfig(); + if (!path) return undefined; + return (yaml.load(readFileSync(path, 'utf8')) as Partial) ?? undefined; +} + +/** 蒐集 env 覆蓋(只取有設值的 env,欄位級)。*/ +function readEnvOverrides(): Partial { + const out: Partial = {}; + for (const [envName, field] of Object.entries(ENV_MAP)) { + const v = process.env[envName]; + if (v !== undefined && v !== '') { + // mode 需窄型別;其餘皆 string 欄位。 + (out as Record)[field] = v; + } + } + return out; +} + +/** + * 三層分層解析:全域 → 疊專案層 → 疊 env(欄位級 merge,高層只覆蓋它提供的欄位)。 + * 任一層都沒有 mode 時 fallback 'local',讓 validate --offline 等在無設定下可運作。 + */ +export function loadConfig(): ArcrunConfig { + const merged: Partial = { + ...(readGlobalConfig() ?? {}), + ...(readProjectConfig() ?? {}), + ...readEnvOverrides(), + }; + if (!merged.mode) merged.mode = 'local'; + return merged as ArcrunConfig; +} + +/** 解析每個關鍵欄位的最終值與來源層(acr config --where 用)。*/ +export function resolveConfigSources(): Array<{ field: keyof ArcrunConfig; value: string; source: ConfigSource }> { + const global = readGlobalConfig() ?? {}; + const project = readProjectConfig() ?? {}; + const env = readEnvOverrides(); + const fields: (keyof ArcrunConfig)[] = [ + 'mode', 'api_key', 'encryption_key', 'cloudflare_account_id', + 'cf_api_token', 'cypher_executor_url', + ]; + const rows: Array<{ field: keyof ArcrunConfig; value: string; source: ConfigSource }> = []; + for (const f of fields) { + let value: unknown; + let source: ConfigSource = 'default'; + if (f in env) { value = env[f]; source = 'env'; } + else if (f in project) { value = project[f]; source = 'project'; } + else if (f in global) { value = global[f]; source = 'global'; } + else if (f === 'mode') { value = 'local'; source = 'default'; } + else continue; + rows.push({ field: f, value: String(value), source }); + } + return rows; +} + +/** 回傳本次解析實際採用的專案層設定檔路徑(無則 undefined)。acr config --where 顯示用。*/ +export function activeProjectConfigPath(): string | undefined { + return findProjectConfig(); } export function saveConfig(config: ArcrunConfig): void { diff --git a/cli/src/lib/deploy.ts b/cli/src/lib/deploy.ts index 3fcf149..3ecec87 100644 --- a/cli/src/lib/deploy.ts +++ b/cli/src/lib/deploy.ts @@ -28,9 +28,6 @@ export const REQUIRED_KV_NAMESPACES = [ 'EXEC_CONTEXT', ] as const; -/** init 要建立的 R2 bucket。*/ -export const REQUIRED_R2_BUCKET = 'WASM_BUCKET'; - /** 部署後要提示用戶手動 `wrangler secret put ENCRYPTION_KEY` 的 Worker。*/ export const SECRET_TARGET_WORKERS = [ 'arcrun-cypher-executor', @@ -172,7 +169,19 @@ function discoverWorkerDirs(root: string): { tier1: string[]; tier2: string[] } return { tier1, tier2 }; } -/** 注入用戶的 KV namespace id(取代 wrangler.toml 中各 binding 的 id)+ cypher WORKER_SUBDOMAIN。*/ +/** + * 注入用戶的 KV namespace id(取代 wrangler.toml 中各 binding 的 id)+ cypher WORKER_SUBDOMAIN, + * 並 strip 掉只有 arcrun 官方帳號才有的綁定(self-hosted fork 帳號沒有)。 + * + * 為何 strip 而非刪 repo 內 toml(壓測 2026-06-04 阻斷項 #1#2#3#4): + * - repo 內各 worker toml 的 `[[routes]] zone_name="arcrun.dev"` 是**官方 prod CI 部署**需要的 + * (對外開放零件)。直接從 repo 刪會破壞官方部署。 + * - 但 fork 用戶**沒有 arcrun.dev zone** → wrangler deploy 找不到 zone 而失敗。 + * - deploy.ts 只在 self-hosted 路徑跑,且改的是「暫存目錄副本」(SDD self-hosted-init.md §3 step 4), + * 不碰用戶 repo。所以在注入時 strip 掉這些官方專屬綁定 = 對的層級。 + * - 每個 worker toml 都有 `workers_dev = true` → strip routes 後純靠 workers.dev URL,自架可達。 + * - R2(`[[r2_buckets]]`)是 dead storage(registry-canon Phase 1.5),且綁卡違背開源免費 → 一併移除。 + */ function injectWranglerConfig(tomlPath: string, ctx: DeployContext): void { if (!existsSync(tomlPath)) return; let toml = readFileSync(tomlPath, 'utf8'); @@ -196,9 +205,47 @@ function injectWranglerConfig(tomlPath: string, ctx: DeployContext): void { ); } + toml = stripOfficialOnlyBindings(toml); + writeFileSync(tomlPath, toml, 'utf8'); } +/** + * 移除 self-hosted fork 帳號沒有、會導致 wrangler deploy 失敗的官方專屬 TOML 區塊: + * - `[[routes]]`(含 pattern/zone_name):fork 沒有 arcrun.dev zone + * - `[[r2_buckets]]`:dead storage + 綁卡違背開源免費(registry-canon 1.5) + * - `[ai]`(Workers AI binding):免費帳號未必啟用,且自架預設不需要 + * 純文字行級移除(TOML table 以空行 / 下一個 `[` 區塊結束)。worker 仍靠 `workers_dev = true` 對外。 + */ +export function stripOfficialOnlyBindings(toml: string): string { + const lines = toml.split('\n'); + const out: string[] = []; + let skipping = false; + + const isBlockHeader = (l: string) => + /^\s*\[\[?(routes|r2_buckets|ai)\]?\]\s*$/.test(l); + + for (const line of lines) { + if (isBlockHeader(line)) { + skipping = true; // 進入要移除的區塊,連同 header 一起丟 + continue; + } + if (skipping) { + // 區塊結束條件:遇到下一個 table header(`[...]`)或空行 + if (/^\s*\[/.test(line)) { + skipping = false; // 這行是新區塊的開頭,保留並由下方邏輯處理 + } else if (line.trim() === '') { + skipping = false; // 空行結束區塊;空行本身丟掉避免堆疊空白 + continue; + } else { + continue; // 仍在被移除區塊內(pattern/zone_name/binding/bucket_name 等) + } + } + out.push(line); + } + return out.join('\n'); +} + /** 在 worker 目錄跑 wrangler deploy(用用戶的 CF token + account)。*/ function runWranglerDeploy(dir: string, ctx: DeployContext): void { // 先裝依賴(cypher-executor/registry 是 TS,wrangler 內建 esbuild bundle 需 node_modules) diff --git a/cypher-executor/src/lib/constants.ts b/cypher-executor/src/lib/constants.ts index 297ec44..5d4f960 100644 --- a/cypher-executor/src/lib/constants.ts +++ b/cypher-executor/src/lib/constants.ts @@ -11,7 +11,7 @@ export const VALID_EDGE_TYPES = new Set([ 'CONTAINS', 'HAS_STYLE', 'HAS_BEHAVIOR', ]); -/** 內建零件 ID 集合(不需要查 WASM_BUCKET,Worker 記憶體中已有實作)*/ +/** 內建零件 ID 集合(Worker 記憶體中已有實作)*/ export const BUILTIN_IDS = new Set([ 'webhook', 'comp_passthrough', 'comp_uppercase', 'comp_counter', ]); @@ -36,8 +36,8 @@ export const SEMANTIC_EDGE_MAP: Record = { }; /** - * 內建零件表(靜態函數,不需要 R2) - * WASM 零件從 WASM_BUCKET R2 直接讀取 + * 內建零件表(靜態函數) + * WASM 零件 = 各自獨立 Worker,cypher-executor 走 HTTP URL 呼叫(不從 R2 讀) */ export const BUILTIN_COMPONENTS = new Map([ ['comp_passthrough', (ctx) => ctx], diff --git a/cypher-executor/src/types.ts b/cypher-executor/src/types.ts index 23c13a7..698c309 100644 --- a/cypher-executor/src/types.ts +++ b/cypher-executor/src/types.ts @@ -31,8 +31,6 @@ export type Bindings = { CREDENTIALS_KV: KVNamespace; // Analytics:執行統計(fire-and-forget,key = stats:{workflowId}:{timestamp}) ANALYTICS_KV: KVNamespace; - // R2 Bucket:WASM 零件二進位 - WASM_BUCKET: R2Bucket; // Users:OAuth 登入用戶帳號(key = user:{provider}:{provider_id}) USERS_KV: KVNamespace; // Sessions:登入 session(key = sess:{session_id},TTL 7d) diff --git a/cypher-executor/wrangler.test.toml b/cypher-executor/wrangler.test.toml index 2abc260..83e17aa 100644 --- a/cypher-executor/wrangler.test.toml +++ b/cypher-executor/wrangler.test.toml @@ -5,10 +5,7 @@ compatibility_flags = ["nodejs_compat"] # 測試環境不啟用 Service Binding(Miniflare 無法解析外部服務) -# R2 mock(WASM 執行器測試用) -[[r2_buckets]] -binding = "WASM_BUCKET" -bucket_name = "arcrun-wasm" +# 2026-06-04:移除 WASM_BUCKET R2 mock。R2 wasm 路徑已 dead,不再需要測試 mock。 # KV mock(BUILD-006) [[kv_namespaces]] diff --git a/cypher-executor/wrangler.toml b/cypher-executor/wrangler.toml index f845862..b1ad7eb 100644 --- a/cypher-executor/wrangler.toml +++ b/cypher-executor/wrangler.toml @@ -32,9 +32,9 @@ id = "25bef01d079148919578894434d58c4d" binding = "SESSIONS_KV" id = "455d0505c7534883a4d4985ab8295857" -[[r2_buckets]] -binding = "WASM_BUCKET" -bucket_name = "arcrun-wasm" +# 2026-06-04:移除 WASM_BUCKET R2 binding。R2 wasm 路徑早已 dead(平台零件 = 獨立 Worker, +# 不從 R2 動態讀),保留只會誤導且 R2 需綁信用卡,與 open source 零費用核心衝突。 +# SDD: .agents/specs/component-registry-canon/tasks.md Phase 1.5(registry 已於 2026-05-07 移除,此為 cypher-executor 補清) [ai] binding = "AI"