import type { Bindings, ExecutionGraph } from '../types'; import { ExecutionError, WorkflowPaused } 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, type SearchMode, type SearchTarget } from './search-nodes'; import { buildExecutionGraph } from './graph-builder'; export async function handleCypherSearch( triplets: unknown[], env: Bindings, mode: SearchMode = 'discover', target?: SearchTarget, ): Promise<{ nodes: Record; cypher: unknown; missing: string[] }> { const parsed = parseTriplets(triplets); if (!parsed) { throw new Error('無法解析任何節點'); } // 2026-07-30:查 registry 判真實存在(workflow-discovery)。 // `missing` 以前寫死 [],等於告訴 AI「什麼都有」——那是「腹語術」的入口。 // // t158(07-31 迴歸修復,leo:「這裡只是複製一些工作流的 data 過去,沒有要在這裡驗證」): // 誠實化只屬於 **discover**(AI 問「有沒有」);**compile**(部署/推送的複製路徑) // 純編圖零查詢——那本來就是既有設計(workflows.json=打包期預編的搬運), // 5cadc60 起誠實化漏進複製路徑=迴歸(冷實例 8 節點 25.7s、安裝器 timeout 炸)。 const { nodeResults, missingNodes } = await searchNodes(parsed, undefined, env, mode, target); const graph = buildExecutionGraph(parsed, nodeResults, 'cypher-search-result', 'Cypher Search Result'); return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: missingNodes }; } export async function handleCypherExecute( triplets: unknown[], context: Record | undefined, graphId: string, graphName: string, config: Record> | undefined, env: Bindings, waitUntil: (promise: Promise) => void, apiKey?: string, ): Promise<{ success: boolean; data?: unknown; error?: string; trace?: unknown; duration_ms: number; graph?: ExecutionGraph; // resumable workflow: 節點 pending 時回 paused(不算 success 也不算 fail) paused?: boolean; task_id?: string; run_id?: string; paused_node_id?: string; }> { const parsed = parseTriplets(triplets as unknown[]); if (!parsed) { throw new Error('無法解析任何節點'); } // t158:執行路徑=compile(零 discovery round-trip)——存在性由 component-loader // 在載入該節點時決定(原本的權威),查詢層不重複驗。 const { nodeResults } = await searchNodes(parsed, config, env, 'compile'); const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName, config); const parseResult = graphSchema.safeParse(graph); if (!parseResult.success) { throw new Error('圖定義產生失敗'); } const loader = createComponentLoader(env); const executor = new GraphExecutor(loader, undefined, env, apiKey); 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; // Resumable workflow: 節點回 pending → 回 paused 結構,不算成功也不算失敗 // SDD: resumable-workflow/design.md if (err instanceof WorkflowPaused) { return { success: true, paused: true, task_id: err.task_id, run_id: err.run_id, paused_node_id: err.paused_node_id, trace: err.trace_so_far, duration_ms, graph, }; } 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; } }