Files
Arcrun/cli/src/commands/run.ts
T
Leo 2594f8371d feat: add /register endpoint + fix acr run Mode 1 (inline YAML execution)
- POST /register on cypher.arcrun.dev: HMAC-SHA256(email, ENCRYPTION_KEY) → ak_{32hex}, no DB needed
- acr run: Mode 1 (standard/local) now finds local YAML and POSTs to /cypher/execute inline
- acr init: fix register URL → cypher.arcrun.dev/register; fix local mode description
- acr init --local: creates hello.yaml example workflow
- cli v1.0.3 published

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

154 lines
5.3 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,
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-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}` : ''}`);
}
}
}
}