ea1c0571c1
- registry 新增 POST /analytics/record:ANALYTICS_KV 計數器(stats:{hash_id}:{version})
為唯一真相源,衍生值(success_rate/avg_duration_ms/call_count)回填 comp: 記錄
——查詢讀取端讀哪就寫哪,不開第二真相源。KV 無 CAS,誠實標註非原子。
- cypher-executor execution-evaluator 從 stub 改真實作:執行收尾(/cypher/execute
成功與 ExecutionError 路徑+webhook 路徑)對 trace 裡每顆 Component 節點
fire-and-forget 回寫,waitUntil 包、不增加執行同步延遲(仿 recordRecipeStats 慣例)。
成敗判定=trace error 或 output.success===false(makeHttpRunner 非 2xx 不 throw)。
- registry 位置沿 search-nodes 慣例:REGISTRY_BASE_URL 覆蓋,未設走 wasmWorkerUrl。
- 新增單測 13 個全綠;本地雙 wrangler dev 端到端實測:http_request 跑 5 次
(3 成功+2 失敗)→ success_rate 1→0.6、call_count 0→5,/cypher/search 同步可見。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
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 { recordComponentStats } 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('無法解析任何節點');
|
||
}
|
||
|
||
// 2026-07-30:查 registry 判真實存在(workflow-discovery)。
|
||
// `missing` 以前寫死 [],等於告訴 AI「什麼都有」——那是「腹語術」的入口。
|
||
const { nodeResults, missingNodes } = await searchNodes(parsed, undefined, env);
|
||
|
||
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<string, unknown> | undefined,
|
||
graphId: string,
|
||
graphName: string,
|
||
config: Record<string, Record<string, unknown>> | undefined,
|
||
env: Bindings,
|
||
waitUntil: (promise: Promise<void>) => 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('無法解析任何節點');
|
||
}
|
||
|
||
const { nodeResults } = await searchNodes(parsed, config, env);
|
||
|
||
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;
|
||
|
||
// 非同步回寫每顆零件的執行統計(design.md「執行統計設計」;fire-and-forget 不阻擋回應)
|
||
waitUntil(recordComponentStats(env, graph.nodes, result.trace));
|
||
|
||
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);
|
||
// 失敗路徑同樣回寫每顆零件統計:ExecutionError 帶完整 trace(失敗節點有 error、
|
||
// 之前成功的節點照記成功);非 ExecutionError 無 trace 可歸因 → 不記(誠實:不瞎猜)。
|
||
if (err instanceof ExecutionError) {
|
||
waitUntil(recordComponentStats(env, graph.nodes, err.trace));
|
||
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;
|
||
}
|
||
}
|