2707fca32b
Phase 1-5 complete per .agents/specs/u6u-core-mvp/: **Phase 1 — Cherry-pick & cleanup** - Create arcrun/ from cypher-executor, credentials, builtins, registry - Remove 9 InkStone Service Bindings (KBDB, REGISTRY, CLINIC_*, AICEO, MINI_ME) - Rewrite component-loader: 3-layer (builtin → WASM_BUCKET R2 → error) - Remove autoPublishMissing.ts, proxy.ts (AICEO), execution-logger.ts (KBDB) - Clean all KV namespace IDs and InkStone internal URLs from config files **Phase 2 — contract.yaml completeness** - Add credentials_required to gmail, google_sheets, telegram, line_notify - Add config_example to all 21 components with annotated field descriptions **Phase 3 — Credential injection** - Add credential-injector.ts: AES-GCM decrypt from CREDENTIALS_KV - Integrate into GraphExecutor before WASM execution - Structured errors with repair instructions when credential missing **Phase 4 — CLI (acr)** - cli/package.json: arcrun package, bin: acr, deps: commander/js-yaml/chalk/ora - 8 commands: init, creds push, push, run, validate, parts, list, logs - Standard mode: writes directly to user's CF KV via CF REST API - acr init: interactive setup with arcrun.dev API Key registration **Phase 5 — Open source release prep** - README.md: 5-minute quickstart, component table, workflow YAML syntax - CONTRIBUTING.md: TinyGo dev env, component scaffolding, submission flow - Security audit: no InkStone internal URLs/IDs in committed files - .gitignore: exclude credentials.yaml, .wrangler, *.wasm https://claude.ai/code/session_01BnCdSLVH8tUed9VrrPavgT
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
import type { Bindings, ExecutionGraph } from '../types';
|
||
import { ExecutionError } from '../types';
|
||
import { GraphExecutor } from '../graph-executor';
|
||
import { graphSchema } from '../lib/schemas';
|
||
import { createComponentLoader } from '../lib/component-loader';
|
||
import { writeEvaluation, updateComponentStats } from './execution-evaluator';
|
||
import { parseTriplets } from './triplet-parser';
|
||
import { searchNodes } from './search-nodes';
|
||
import { buildExecutionGraph } from './graph-builder';
|
||
|
||
export async function handleCypherSearch(
|
||
triplets: unknown[],
|
||
env: Bindings,
|
||
): Promise<{ nodes: Record<string, unknown>; cypher: unknown; missing: string[] }> {
|
||
const parsed = parseTriplets(triplets);
|
||
if (!parsed) {
|
||
throw new Error('無法解析任何節點');
|
||
}
|
||
|
||
const { nodeResults, missingNodes } = await searchNodes(parsed, env.WASM_BUCKET);
|
||
|
||
if (missingNodes.length > 0) {
|
||
return { nodes: nodeResults, cypher: null, missing: missingNodes };
|
||
}
|
||
|
||
const graph = buildExecutionGraph(parsed, nodeResults, 'cypher-search-result', 'Cypher Search Result');
|
||
return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: [] };
|
||
}
|
||
|
||
export async function handleCypherExecute(
|
||
triplets: unknown[],
|
||
context: Record<string, unknown> | undefined,
|
||
graphId: string,
|
||
graphName: string,
|
||
env: Bindings,
|
||
waitUntil: (promise: Promise<void>) => void,
|
||
): Promise<{ success: boolean; data?: unknown; error?: string; trace?: unknown; duration_ms: number; graph?: ExecutionGraph }> {
|
||
const parsed = parseTriplets(triplets as unknown[]);
|
||
if (!parsed) {
|
||
throw new Error('無法解析任何節點');
|
||
}
|
||
|
||
const { nodeResults, missingNodes } = await searchNodes(parsed, env.WASM_BUCKET);
|
||
|
||
if (missingNodes.length > 0) {
|
||
throw new Error(
|
||
`以下零件不存在於 WASM_BUCKET:${missingNodes.join(', ')}\n` +
|
||
`修復:執行 acr parts 查看可用零件清單,或執行 acr validate <workflow.yaml> 進行完整驗證。`
|
||
);
|
||
}
|
||
|
||
const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName);
|
||
const parseResult = graphSchema.safeParse(graph);
|
||
if (!parseResult.success) {
|
||
throw new Error('圖定義產生失敗');
|
||
}
|
||
|
||
const loader = createComponentLoader(env);
|
||
const executor = new GraphExecutor(loader, undefined, env);
|
||
const start = Date.now();
|
||
|
||
try {
|
||
const result = await executor.execute(parseResult.data as ExecutionGraph, context ?? {}, env.EXEC_CONTEXT);
|
||
const duration_ms = Date.now() - start;
|
||
|
||
// 非同步記錄統計(Phase 7 補充 analytics,目前為 no-op)
|
||
const componentId = graph.nodes.find(n => n.componentId)?.componentId ?? graphId;
|
||
const runId = `${graphId}-${Date.now()}`;
|
||
waitUntil(writeEvaluation(env, {
|
||
run_id: runId,
|
||
workflow_id: graphId,
|
||
component_id: componentId,
|
||
verdict: 'success',
|
||
duration_ms,
|
||
evaluated_at: Date.now(),
|
||
}));
|
||
waitUntil(updateComponentStats(env, componentId, 'success', duration_ms));
|
||
|
||
return { success: true, data: result.data, trace: result.trace, duration_ms, graph };
|
||
} catch (err) {
|
||
const duration_ms = Date.now() - start;
|
||
const errMsg = err instanceof Error ? err.message : String(err);
|
||
const componentId = graph.nodes.find(n => n.componentId)?.componentId ?? graphId;
|
||
const runId = `${graphId}-${Date.now()}`;
|
||
waitUntil(writeEvaluation(env, {
|
||
run_id: runId,
|
||
workflow_id: graphId,
|
||
component_id: componentId,
|
||
verdict: 'failed',
|
||
duration_ms,
|
||
error_message: errMsg.slice(0, 200),
|
||
evaluated_at: Date.now(),
|
||
}));
|
||
waitUntil(updateComponentStats(env, componentId, 'failed', duration_ms));
|
||
if (err instanceof ExecutionError) {
|
||
const traceFormatted = err.trace.map(s => ({
|
||
node: s.nodeId,
|
||
status: s.error ? 'failed' : 'success',
|
||
...(s.error ? { error: s.error } : {}),
|
||
}));
|
||
throw new Error(JSON.stringify({
|
||
success: false,
|
||
error: errMsg,
|
||
failed_node: err.failed_node,
|
||
failed_input: err.failed_input,
|
||
trace: traceFormatted,
|
||
duration_ms,
|
||
}));
|
||
}
|
||
throw err;
|
||
}
|
||
}
|