/** * acr run [--input key=value...] * * 玩法一(Standard / Local): * 在本機找 .yaml,解析 triplets + config, * 直接 POST /cypher/execute 給 cypher.arcrun.dev 執行。 * YAML 不存在 KV,每次執行都帶著走。 * * 玩法二(Self-hosted,workflow 已 push 到 KV): * POST /webhooks/,executor 從 WEBHOOKS KV 讀取定義執行。 */ import chalk from 'chalk'; import ora from 'ora'; import { existsSync, readFileSync } from 'node:fs'; import { loadConfig, getCypherExecutorUrl } from '../lib/config.js'; import { loadWorkflowYaml, parseTriplets } from '../lib/yaml-parser.js'; interface RunOptions { input?: string[]; } export async function cmdRun(workflowName: string, options: RunOptions): Promise { const config = loadConfig(); const executorUrl = getCypherExecutorUrl(config); // 解析 --input key=value 為 JSON object const inputContext: Record = {}; for (const pair of (options.input ?? [])) { const idx = pair.indexOf('='); if (idx < 0) { console.error(chalk.red(`--input 格式錯誤:${pair}(應為 key=value)`)); process.exit(1); } inputContext[pair.slice(0, idx)] = pair.slice(idx + 1); } 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); } let workflow; try { workflow = loadWorkflowYaml(yamlPath); } catch (e) { console.error(chalk.red(`YAML 解析失敗:${e instanceof Error ? e.message : e}`)); process.exit(1); } const triplets = parseTriplets(workflow.flow); const spinner = ora(`執行 workflow "${workflow.name}"`).start(); try { const res = await fetch(`${executorUrl}/cypher/execute`, { method: 'POST', headers, body: JSON.stringify({ triplets: workflow.flow, context: { ...inputContext, ...(workflow.config ?? {}) }, graph_id: workflow.name, graph_name: workflow.name, }), }); const data = await res.json() as { success: boolean; data?: unknown; error?: string; trace?: Array<{ node: string; status: string; error?: string }>; duration_ms: number; failed_node?: string; }; printResult(spinner, data, triplets.length); } catch (e) { spinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`)); process.exit(1); } return; } // ── 玩法二:Self-hosted,workflow 已存在 KV,打 /webhooks/{name} ───────────── const spinner = ora(`執行 workflow "${workflowName}"`).start(); try { const res = await fetch(`${executorUrl}/webhooks/${workflowName}`, { method: 'POST', headers, body: JSON.stringify(inputContext), }); const data = await res.json() as { success: boolean; data?: unknown; error?: string; trace?: Array<{ node: string; status: string; error?: string }>; duration_ms: number; failed_node?: string; }; printResult(spinner, data, 0); } catch (e) { spinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`)); process.exit(1); } } // ── helpers ────────────────────────────────────────────────────────────────── function findWorkflowYaml(name: string): string | null { const candidates = [ `${name}.yaml`, `${name}.yml`, `workflows/${name}.yaml`, `workflows/${name}.yml`, ]; for (const p of candidates) { if (existsSync(p)) return p; } return null; } function printResult( spinner: ReturnType, data: { success: boolean; data?: unknown; error?: string; trace?: Array<{ node: string; status: string; error?: string }>; duration_ms: number; failed_node?: string }, _nodeCount: number, ): void { if (data.success) { spinner.succeed(chalk.green(`✓ 執行成功(${data.duration_ms}ms)`)); if (data.data !== undefined && data.data !== null) { console.log('\n 結果:'); console.log(JSON.stringify(data.data, null, 2).split('\n').map(l => ` ${l}`).join('\n')); } } else { spinner.fail(chalk.red(`✗ 執行失敗(${data.duration_ms}ms)`)); if (data.failed_node) console.log(chalk.red(`\n 失敗節點:${data.failed_node}`)); if (data.error) console.log(chalk.red(` 錯誤:${data.error}`)); if (data.trace) { console.log('\n 執行追蹤:'); for (const step of data.trace) { const icon = step.status === 'failed' ? chalk.red('✗') : chalk.green('✓'); console.log(` ${icon} ${step.node}${step.error ? ` — ${step.error}` : ''}`); } } } }