arcrun — AI workflow execution engine (clean history)

Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。

此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在
richblack/arcrun 與本地 backup 分支)。含:
- acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe)
- recipe push 把關(資料外流提醒 + 打通檢查)
- 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1)
- CLI / cypher-executor / registry / 完整 SDD

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-03 15:52:38 +08:00
commit 922a57fe34
485 changed files with 89356 additions and 0 deletions
+462
View File
@@ -0,0 +1,462 @@
/**
* acr parts — 列出所有可用零件(內建清單,不依賴 registry.arcrun.dev
* acr parts scaffold <component> — 輸出 config 範本(可直接貼入 workflow.yaml
* acr parts publish <component> — 提交零件至公眾 registryPhase 5,封測後)
*/
import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import chalk from 'chalk';
import { loadConfig, getCypherExecutorUrl } from '../lib/config.js';
// ── 內建零件定義 ────────────────────────────────────────────────────────────────
interface CredentialRequirement {
key: string;
type: string;
inject_as: string;
}
interface ComponentDef {
canonical_id: string;
display_name: string;
category: 'logic' | 'data' | 'api' | 'ai';
description: string;
config_example: string;
credentials_required?: CredentialRequirement[];
}
const BUILTIN_COMPONENTS: ComponentDef[] = [
// ── 控制類(Logic) ────────────────────────────────────────────────────────
{
canonical_id: 'if_control',
display_name: 'If Control',
category: 'logic',
description: '條件分支:condition 為 true 走 ON_SUCCESS,否則走 ON_FAIL',
config_example:
` if_node:
component: if_control
condition: "status === active"`,
},
{
canonical_id: 'switch',
display_name: 'Switch',
category: 'logic',
description: '多分支條件:根據 value 欄位選擇對應分支',
config_example:
` switch_node:
component: switch
key: status
cases:
active: branch_a
inactive: branch_b`,
},
{
canonical_id: 'foreach_control',
display_name: 'Foreach',
category: 'logic',
description: '迭代:對陣列每個元素執行下游節點',
config_example:
` loop_node:
component: foreach_control
iterator: item`,
},
{
canonical_id: 'filter',
display_name: 'Filter',
category: 'logic',
description: '過濾陣列:保留符合 condition 的元素',
config_example:
` filter_node:
component: filter
key: items
condition: "status === active"`,
},
{
canonical_id: 'merge',
display_name: 'Merge',
category: 'logic',
description: '合併多個上游節點的輸出(Fan-in)',
config_example:
` merge_node:
component: merge`,
},
{
canonical_id: 'try_catch',
display_name: 'Try Catch',
category: 'logic',
description: '錯誤捕捉:下游失敗時執行 catch 分支',
config_example:
` safe_node:
component: try_catch`,
},
{
canonical_id: 'wait',
display_name: 'Wait',
category: 'logic',
description: '延遲執行:等待指定毫秒後繼續',
config_example:
` delay_node:
component: wait
ms: 1000`,
},
// ── 資料類(Data) ─────────────────────────────────────────────────────────
{
canonical_id: 'set',
display_name: 'Set',
category: 'data',
description: '設定欄位:將靜態值寫入 context',
config_example:
` set_node:
component: set
values:
status: active
source: webhook`,
},
{
canonical_id: 'array_ops',
display_name: 'Array Ops',
category: 'data',
description: '陣列操作:push / pop / slice / length',
config_example:
` arr_node:
component: array_ops
operation: push
key: items
value: "{{new_item}}"`,
},
{
canonical_id: 'string_ops',
display_name: 'String Ops',
category: 'data',
description: '字串操作:upper / lower / trim / replace / split / join / length',
config_example:
` str_node:
component: string_ops
operation: upper
input: "{{text}}"`,
},
{
canonical_id: 'number_ops',
display_name: 'Number Ops',
category: 'data',
description: '數字操作:add / sub / mul / div / round / floor / ceil / abs',
config_example:
` num_node:
component: number_ops
operation: add
a: "{{price}}"
b: 10`,
},
{
canonical_id: 'date_ops',
display_name: 'Date Ops',
category: 'data',
description: '日期操作:now / format / diff / add_days',
config_example:
` date_node:
component: date_ops
operation: now
format: "2006-01-02 15:04:05"`,
},
{
canonical_id: 'validate_json',
display_name: 'Validate JSON',
category: 'data',
description: '驗證 context 欄位是否符合 JSON Schema',
config_example:
` validate_node:
component: validate_json
schema:
type: object
required: [email, name]
properties:
email:
type: string
format: email`,
},
// ── AI 類 ──────────────────────────────────────────────────────────────────
{
canonical_id: 'ai_transform_compile',
display_name: 'AI Transform Compile',
category: 'ai',
description: '將自然語言規則編譯成可執行轉換程式',
config_example:
` compile_node:
component: ai_transform_compile
rule: "把 name 轉成大寫,並在前面加上 Hello "`,
},
{
canonical_id: 'ai_transform_run',
display_name: 'AI Transform Run',
category: 'ai',
description: '執行 ai_transform_compile 產生的轉換程式',
config_example:
` run_node:
component: ai_transform_run
program: "{{compiled_program}}"`,
},
// ── API 整合類(Recipe 型,不需 deploy Worker) ────────────────────────────
{
canonical_id: 'http_request',
display_name: 'HTTP Request',
category: 'api',
description: '通用 HTTP 請求:支援任意 method / headers / body',
config_example:
` api_node:
component: http_request
url: "https://api.example.com/data"
method: POST
headers:
Content-Type: application/json
body:
key: "{{value}}"`,
},
{
canonical_id: 'gmail',
display_name: 'Gmail',
category: 'api',
description: '寄送 Gmail(需要 gmail_token credential',
config_example:
` mail_node:
component: gmail
to: "recipient@example.com"
subject: "來自 arcrun 的通知"
body: "{{message}}"`,
credentials_required: [
{ key: 'gmail_token', type: 'OAuth2 access token', inject_as: 'access_token' },
],
},
{
canonical_id: 'google_sheets',
display_name: 'Google Sheets',
category: 'api',
description: 'Google Sheets 讀寫(需要 google_oauth credential',
config_example:
` sheet_node:
component: google_sheets
spreadsheet_id: "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms"
range: "Sheet1!A:B"
operation: append
values:
- ["{{name}}", "{{email}}"]`,
credentials_required: [
{ key: 'google_oauth', type: 'OAuth2 access token', inject_as: 'access_token' },
],
},
{
canonical_id: 'telegram',
display_name: 'Telegram',
category: 'api',
description: '發送 Telegram 訊息(需要 telegram_bot_token credential',
config_example:
` tg_node:
component: telegram
chat_id: "123456789"
text: "{{message}}"`,
credentials_required: [
{ key: 'telegram_bot_token', type: 'Bot token', inject_as: 'bot_token' },
],
},
{
canonical_id: 'line_notify',
display_name: 'LINE Notify',
category: 'api',
description: '發送 LINE Notify 通知(需要 line_token credential',
config_example:
` line_node:
component: line_notify
message: "{{notification}}"`,
credentials_required: [
{ key: 'line_token', type: 'LINE Notify token', inject_as: 'token' },
],
},
{
canonical_id: 'notion',
display_name: 'Notion',
category: 'api',
description: '透過 recipe 操作 Notion API(需先 acr recipe push',
config_example:
` # 先上傳 recipeacr recipe push notion.yaml
notion_node:
component: rec_xxxxxxxx # acr recipe push 後得到的 hash
database_id: "{{db_id}}"
properties:
Name: "{{title}}"`,
},
];
// ── 指令實作 ──────────────────────────────────────────────────────────────────
export async function cmdParts(): Promise<void> {
const categoryLabels: Record<string, string> = {
logic: '控制類(Control Flow',
data: '資料類(Data',
api: '整合類(API / Integration',
ai: 'AI 類',
};
const grouped: Record<string, ComponentDef[]> = {};
for (const comp of BUILTIN_COMPONENTS) {
if (!grouped[comp.category]) grouped[comp.category] = [];
grouped[comp.category].push(comp);
}
console.log(chalk.bold(`\n arcrun 零件庫(${BUILTIN_COMPONENTS.length} 個內建零件)\n`));
for (const cat of ['logic', 'data', 'ai', 'api']) {
const comps = grouped[cat];
if (!comps?.length) continue;
console.log(chalk.bold.underline(` ${categoryLabels[cat]}`));
for (const comp of comps) {
const credStr = comp.credentials_required?.length
? chalk.yellow(` (需要 ${comp.credentials_required.map(c => c.key).join(', ')}`)
: '';
console.log(`${chalk.cyan(comp.canonical_id.padEnd(22))}${comp.display_name}${credStr}`);
console.log(chalk.gray(` ${comp.description}`));
}
console.log('');
}
console.log(chalk.gray(' 使用 acr parts scaffold <component> 取得 config 範本'));
console.log(chalk.gray(' 第三方服務整合(Notion/Slack/GitHub 等):acr auth-recipe list'));
console.log(chalk.gray(' API 整合類若需打自訂服務,請用 acr recipe push 建立 recipe\n'));
}
export async function cmdPartsScaffold(componentId: string): Promise<void> {
const comp = BUILTIN_COMPONENTS.find(c => c.canonical_id === componentId);
if (!comp) {
// 找不到內建零件 → 嘗試 auth recipe
const config = loadConfig();
const baseUrl = getCypherExecutorUrl(config);
try {
const res = await fetch(`${baseUrl}/auth-recipes/${componentId}`);
if (res.ok) {
const data = await res.json() as { recipe: { display_name?: string; description?: string; required_secrets: Array<{ key: string; label: string; type?: string; help?: string; help_url?: string }> } };
const recipe = data.recipe;
console.log(chalk.bold(`\n ${componentId}${recipe.display_name ?? componentId}\n`));
if (recipe.description) console.log(chalk.gray(` ${recipe.description}\n`));
console.log(chalk.cyan(' # credentials.yaml 範本(填入後執行 acr creds push\n'));
for (const s of recipe.required_secrets) {
if (s.help) console.log(chalk.gray(` # ${s.label}`));
if (s.help) console.log(chalk.gray(` # ${s.help}`));
if (s.help_url) console.log(chalk.gray(` # 說明文件:${s.help_url}`));
if (s.type === 'json_blob') {
console.log(` ${s.key}: |`);
console.log(` {`);
console.log(` "type": "service_account",`);
console.log(` ...`);
console.log(` }`);
} else {
console.log(` ${s.key}: ""`);
}
console.log('');
}
console.log(chalk.cyan(' # workflow.yaml config 範例\n'));
console.log(` ${componentId}_node:`);
console.log(` component: ${componentId}`);
console.log(` method: POST`);
console.log(` _path: /your-endpoint-path`);
console.log('');
console.log(chalk.gray(` 完整說明:acr auth-recipe info ${componentId}\n`));
return;
}
} catch {
// 離線或服務不可用,繼續顯示錯誤
}
console.error(chalk.red(`找不到零件 "${componentId}"。`));
console.log(chalk.gray('執行 acr parts 查看內建零件。'));
console.log(chalk.gray('執行 acr auth-recipe list 查看第三方服務整合。'));
process.exit(1);
}
console.log(chalk.bold(`\n ${comp.canonical_id}${comp.display_name}\n`));
console.log(chalk.gray(` ${comp.description}\n`));
console.log(chalk.cyan(' # 貼入 workflow.yaml 的 config: 區塊'));
console.log(comp.config_example.split('\n').map(l => ` ${l}`).join('\n'));
if (comp.credentials_required?.length) {
console.log(chalk.bold('\n credentials.yaml 範本(填入後執行 acr creds push\n'));
for (const cred of comp.credentials_required) {
console.log(chalk.cyan(` # ${cred.type}(執行時自動注入為 ${cred.inject_as} 欄位)`));
console.log(` ${cred.key}: "your-token-here"\n`);
}
}
console.log('');
}
export async function cmdPartsPublish(componentDir: string, options: { status?: string }): Promise<void> {
const REGISTRY_URL = 'https://registry.arcrun.dev';
if (options.status) {
try {
const res = await fetch(`${REGISTRY_URL}/submit/status/${options.status}`);
const data = await res.json() as { status: string; visibility?: string; failed_step?: string; reason?: string; approved_at?: string };
console.log(chalk.bold(`\n 提交狀態:${options.status}\n`));
console.log(` 狀態:${data.status}`);
if (data.visibility) console.log(` Visibility${data.visibility}`);
if (data.failed_step) console.log(chalk.red(` 失敗步驟:${data.failed_step}`));
if (data.reason) console.log(chalk.red(` 原因:${data.reason}`));
if (data.approved_at) console.log(chalk.green(` 核准時間:${data.approved_at}`));
} catch (e) {
console.error(chalk.red(`查詢失敗:${e instanceof Error ? e.message : e}`));
}
return;
}
const config = loadConfig();
if (!config.api_key) {
console.error(chalk.red('缺少 API Key,請執行 acr init'));
process.exit(1);
}
const contractPath = join(componentDir, 'component.contract.yaml');
const mainGoPath = join(componentDir, 'main.go');
const wasmName = componentDir.split('/').pop() ?? componentDir;
const wasmPath = join(componentDir, `${wasmName}.wasm`);
if (!existsSync(contractPath)) {
console.error(chalk.red(`找不到 ${contractPath}`));
process.exit(1);
}
if (!existsSync(wasmPath)) {
console.error(chalk.red(`找不到 ${wasmPath}(請先編譯:tinygo build -o ${wasmName}.wasm -target wasi .`));
process.exit(1);
}
console.log(chalk.bold('\n 提交零件至 registry.arcrun.dev...\n'));
const formData = new FormData();
formData.append('contract', new Blob([readFileSync(contractPath)], { type: 'application/yaml' }), 'component.contract.yaml');
if (existsSync(mainGoPath)) {
formData.append('source', new Blob([readFileSync(mainGoPath)], { type: 'text/plain' }), 'main.go');
}
formData.append('wasm', new Blob([readFileSync(wasmPath)], { type: 'application/wasm' }), `${wasmName}.wasm`);
try {
const res = await fetch(`${REGISTRY_URL}/submit`, {
method: 'POST',
headers: { 'X-Arcrun-API-Key': config.api_key },
body: formData,
});
if (!res.ok) {
const err = await res.text();
console.error(chalk.red(`提交失敗(${res.status}):${err.slice(0, 200)}`));
process.exit(1);
}
const data = await res.json() as { submission_id: string; status: string; visibility?: string };
console.log(chalk.green(`✓ 提交成功`));
console.log(`\n Submission ID${chalk.cyan(data.submission_id)}`);
console.log(` 狀態:${data.status}`);
if (data.visibility) console.log(` Visibility${data.visibility}`);
console.log(chalk.gray(`\n 查詢進度:acr parts publish --status ${data.submission_id}\n`));
} catch (e) {
console.error(chalk.red(`提交失敗:${e instanceof Error ? e.message : e}`));
process.exit(1);
}
}