7b18387113
- /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>
155 lines
5.4 KiB
TypeScript
155 lines
5.4 KiB
TypeScript
/**
|
||
* acr run <workflow_name> [--input key=value...]
|
||
*
|
||
* 玩法一(Standard / Local):
|
||
* 在本機找 <workflow_name>.yaml,解析 triplets + config,
|
||
* 直接 POST /cypher/execute 給 cypher.arcrun.dev 執行。
|
||
* YAML 不存在 KV,每次執行都帶著走。
|
||
*
|
||
* 玩法二(Self-hosted,workflow 已 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-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<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}` : ''}`);
|
||
}
|
||
}
|
||
}
|
||
}
|