Files
Arcrun/cli/src/commands/run.ts
T
Leo 7b18387113 feat: config field in /cypher/execute — node-level component override
- /cypher/execute now accepts separate `config` field:
  {node_name: {component: "cmp_19e62efd", ...staticParams}}
- graph-builder reads config[node].component to override componentId
  (supports cmp_ hash, rec_ hash, or canonical_id)
- config[node] other fields become node.data (static params merged at runtime)
- acr run now sends workflow.config as separate `config` (not flattened into context)
- context is now only --input dynamic params

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 18:42:26 +08:00

155 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* acr run <workflow_name> [--input key=value...]
*
* 玩法一(Standard / Local):
* 在本機找 <workflow_name>.yaml,解析 triplets + config
* 直接 POST /cypher/execute 給 cypher.arcrun.dev 執行。
* YAML 不存在 KV,每次執行都帶著走。
*
* 玩法二(Self-hostedworkflow 已 push 到 KV):
* POST /webhooks/<workflow_name>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<void> {
const config = loadConfig();
const executorUrl = getCypherExecutorUrl(config);
// 解析 --input key=value 為 JSON object
const inputContext: Record<string, string> = {};
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<string, string> = { '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,
config: workflow.config ?? {}, // node_name → {component, ...params}
context: inputContext, // --input key=value 傳入的動態參數
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-hostedworkflow 已存在 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<typeof ora>,
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}` : ''}`);
}
}
}
}