fix(self-hosted): 修壓測四阻斷項 + 設定分層 + init 非互動

壓測(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 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-05 07:22:37 +08:00
parent 1d79ae038c
commit 5f381a44a6
12 changed files with 268 additions and 65 deletions
+2 -2
View File
@@ -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 Friendlyarcrun 內含**給 AIClaude Code)用的 harness**:你叫 AI 用 arcrun 開發時,它知道能用什麼、不能做什麼,做錯了有機制擋住——讓 CC 順暢、且不容易做歪。
+49
View File
@@ -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<ConfigSource, string> = {
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<void> {
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'));
}
+39 -17
View File
@@ -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<typeof createInterface>, question: string):
return answer.trim();
}
export async function cmdInit(options: { local?: boolean; selfHosted?: boolean }): Promise<void> {
export interface InitOptions {
local?: boolean;
selfHosted?: boolean;
accountId?: string;
apiToken?: string;
}
export async function cmdInit(options: InitOptions): Promise<void> {
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<typeof createInterface>): Promise<voi
/**
* Self-hosted installer:用戶只提供 CF Account ID + API Token,其餘自動。
* 驗 token → 建 7 KV + R2(冪等)→ 查 subdomain → 下載 release 部署 Worker
* 驗 token → 建 7 KV(冪等)→ 查 subdomain → 下載 release 部署 Worker
* → seed auth+api recipe → 寫 config → 印手動 secret 提示。
* SDD.agents/specs/arcrun/sdk-and-website/self-hosted-init.md
*/
async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<void> {
async function initSelfHosted(
rl: ReturnType<typeof createInterface>,
options: InitOptions,
): Promise<void> {
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<typeof createInterface>): Promise<v
process.exit(1);
}
const accountId = await prompt(rl, '你的 Cloudflare Account ID');
const cfApiToken = await prompt(rl, 'CF API Token(需 Workers Scripts Edit + KV Edit + R2 Edit');
// account-id / api-token 取得順序:flag > envCLOUDFLARE_*> 互動問答。
// 解壓測 #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 <id> --api-token <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<typeof createInterface>): Promise<v
process.exit(1);
}
// 2. 建 KV namespace(冪等)+ R2 bucket
// 2. 建 KV namespace(冪等)
// 不建 R2R2 是 dead storageregistry-canon Phase 1.5),且 CF R2 首次啟用強制綁信用卡,
// 違背 arcrun「開源免費自架,Workers + KV 免費額度即可運行」核心理念(壓測 2026-06-04 #3)。
const kvNamespaceIds: Record<string, string> = {};
try {
const existing = await cf.listKvNamespaces();
@@ -164,9 +188,6 @@ async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<v
kvNamespaceIds[title] = id;
console.log(chalk.green(' ✓'));
}
process.stdout.write(chalk.gray(` → R2 ${REQUIRED_R2_BUCKET}...`));
await cf.ensureR2Bucket(REQUIRED_R2_BUCKET);
console.log(chalk.green(' ✓'));
} catch (e) {
console.log(chalk.yellow(`\n ✗ 建立資源失敗:${e instanceof Error ? e.message : e}\n`));
process.exit(1);
@@ -198,7 +219,6 @@ async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<v
cypher_executor_url: cypherUrl,
webhooks_kv_namespace_id: kvNamespaceIds['WEBHOOKS'],
credentials_kv_namespace_id: kvNamespaceIds['CREDENTIALS_KV'],
wasm_bucket: REQUIRED_R2_BUCKET,
multi_tenant: false,
};
saveConfig(config);
@@ -212,7 +232,7 @@ async function initSelfHosted(rl: ReturnType<typeof createInterface>): Promise<v
}
// 結果回報(誠實:部分失敗時明說,不假綠 — mindset §7)
console.log(chalk.green('\n ✓ Cloudflare 資源就緒(7 KV + R2'));
console.log(chalk.green('\n ✓ Cloudflare 資源就緒(7 KV,免費額度即可,無需綁卡'));
console.log(chalk.green(' ✓ 設定寫入 ~/.arcrun/config.yaml'));
console.log(chalk.green(' ✓ 建立 credentials.yaml'));
@@ -289,12 +309,14 @@ function createCredentialsYamlIfMissing(): void {
);
}
// 確保 .gitignore 排除 credentials.yaml
// 確保 .gitignore 排除 credentials.yaml + 專案層 .arcrun.yaml(可能含 cf_api_token
// 壓測 §1.2 安全附帶發現:憑證進版控 = 帳號控制權外洩。
const gitignorePath = join(process.cwd(), '.gitignore');
if (existsSync(gitignorePath)) {
const content = readFileSync(gitignorePath, 'utf8');
if (!content.includes('credentials.yaml')) {
appendFileSync(gitignorePath, '\ncredentials.yaml\n');
}
const toAdd: string[] = [];
if (!content.includes('credentials.yaml')) toAdd.push('credentials.yaml');
if (!content.includes('.arcrun.yaml')) toAdd.push('.arcrun.yaml');
if (toAdd.length > 0) appendFileSync(gitignorePath, '\n' + toAdd.join('\n') + '\n');
}
}
+2 -2
View File
@@ -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 跳過此檢查)`);
+14 -2
View File
@@ -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 <id>] [--api-token <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 <id>', 'self-hostedCF Account ID(非互動;亦可用 CLOUDFLARE_ACCOUNT_ID env')
.option('--api-token <token>', 'self-hostedCF 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 管理');
+2 -16
View File
@@ -78,7 +78,8 @@ export class CfKvClient {
/**
* Cloudflare Account-level API wrapperself-hosted installer 用)。
*
* 負責 acr init --self-hosted 的資源建立:驗 token、建/列 KV namespace、建 R2 bucket、查 workers.dev subdomain。
* 負責 acr init --self-hosted 的資源建立:驗 token、建/列 KV namespace、查 workers.dev subdomain。
* (不建 R2R2 是 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<void> {
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 subdomaincypher-executor WORKER_SUBDOMAIN 用,組對內 component URL)。*/
async getWorkersSubdomain(): Promise<string> {
const result = await this.cf<{ subdomain: string }>('/workers/subdomain');
+102 -10
View File
@@ -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<string, keyof ArcrunConfig> = {
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<ArcrunConfig> | undefined {
if (!existsSync(CONFIG_PATH)) return undefined;
return (yaml.load(readFileSync(CONFIG_PATH, 'utf8')) as Partial<ArcrunConfig>) ?? undefined;
}
/** 讀專案層設定(不分層)。無檔回 undefined。*/
function readProjectConfig(): Partial<ArcrunConfig> | undefined {
const path = findProjectConfig();
if (!path) return undefined;
return (yaml.load(readFileSync(path, 'utf8')) as Partial<ArcrunConfig>) ?? undefined;
}
/** 蒐集 env 覆蓋(只取有設值的 env,欄位級)。*/
function readEnvOverrides(): Partial<ArcrunConfig> {
const out: Partial<ArcrunConfig> = {};
for (const [envName, field] of Object.entries(ENV_MAP)) {
const v = process.env[envName];
if (v !== undefined && v !== '') {
// mode 需窄型別;其餘皆 string 欄位。
(out as Record<string, unknown>)[field] = v;
}
}
return out;
}
/**
* 三層分層解析:全域 → 疊專案層 → 疊 env(欄位級 merge,高層只覆蓋它提供的欄位)。
* 任一層都沒有 mode 時 fallback 'local',讓 validate --offline 等在無設定下可運作。
*/
export function loadConfig(): ArcrunConfig {
const merged: Partial<ArcrunConfig> = {
...(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 {
+51 -4
View File
@@ -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 storageregistry-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 是 TSwrangler 內建 esbuild bundle 需 node_modules
+3 -3
View File
@@ -11,7 +11,7 @@ export const VALID_EDGE_TYPES = new Set([
'CONTAINS', 'HAS_STYLE', 'HAS_BEHAVIOR',
]);
/** 內建零件 ID 集合(不需要查 WASM_BUCKETWorker 記憶體中已有實作)*/
/** 內建零件 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<string, EdgeType> = {
};
/**
* 內建零件表(靜態函數,不需要 R2
* WASM 零件從 WASM_BUCKET R2 直接讀取
* 內建零件表(靜態函數)
* WASM 零件 = 各自獨立 Workercypher-executor 走 HTTP URL 呼叫(不從 R2 讀)
*/
export const BUILTIN_COMPONENTS = new Map<string, ComponentRunner>([
['comp_passthrough', (ctx) => ctx],
-2
View File
@@ -31,8 +31,6 @@ export type Bindings = {
CREDENTIALS_KV: KVNamespace;
// Analytics:執行統計(fire-and-forgetkey = stats:{workflowId}:{timestamp}
ANALYTICS_KV: KVNamespace;
// R2 BucketWASM 零件二進位
WASM_BUCKET: R2Bucket;
// UsersOAuth 登入用戶帳號(key = user:{provider}:{provider_id}
USERS_KV: KVNamespace;
// Sessions:登入 sessionkey = sess:{session_id}TTL 7d
+1 -4
View File
@@ -5,10 +5,7 @@ compatibility_flags = ["nodejs_compat"]
# 測試環境不啟用 Service BindingMiniflare 無法解析外部服務)
# R2 mockWASM 執行器測試用)
[[r2_buckets]]
binding = "WASM_BUCKET"
bucket_name = "arcrun-wasm"
# 2026-06-04:移除 WASM_BUCKET R2 mock。R2 wasm 路徑已 dead,不再需要測試 mock。
# KV mockBUILD-006
[[kv_namespaces]]
+3 -3
View File
@@ -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.5registry 已於 2026-05-07 移除,此為 cypher-executor 補清)
[ai]
binding = "AI"