d48f83ae6f
CP arcrun-usable 步驟 4(目的:AI 只要填 payload——系統把「傳到 telegram」
翻成 http_request+recipe telegram_send)+leo 07-31 追加:
「難道我不能指定要搜尋工作流或節點或 recipe 嗎?」
替換(只動 discover,t158「部署≠發現」邊界不碰):
- 兩庫 exact 落空後,在 t158 一次抓好的清單記憶體內媒合,零新增 round-trip
- 規則 A 服務詞→recipe:名字全部服務詞命中同一 recipe 且唯一才換
(google_slides_create 不被 google_sheets 誤吃)
- 規則 B 強欄位斷詞→零件:canonical/display/aliases 強命中×10+弱命中,
需至少一強命中且分數唯一最高(aes_encrypt 無強命中不換)
- 換到=status resolved+substitution{from,componentId,recipe,reason},
cypher 圖節點直接帶真實 componentId;換不到照舊 not_found+3.7 指路
target 參數(各走既有機制,不新造第二套搜尋):
- triplets+target=component|recipe=只查該庫
- query+target=名字搜尋:component→registry /components/search(=MCP
arcrun_search_components 同路);recipe→私庫 RECIPES KV(回應註明公庫走
arcrun_recipe_search);workflow→新抽 lib/workflow-search.ts,
GET /workflows/search 與 target=workflow 共用(=arcrun_search_workflows 同路)
- 防呆:compile+target 400/target=workflow 吃 query 不吃 triplets/非法 target 400
驗(本地 wrangler dev,registry 種 20 合約+init/seed 10 recipe):
- 「判斷有沒有新資料 >> ON_SUCCESS >> 傳到 telegram」→ if_control(resolved)
+telegram_send(substitution.componentId=http_request)=feature 06 驗法過
- 機械考 27/27 全綠(01 組×5+03 組×4 迴歸+06 組×8+target×8+compile 迴歸×2)
- 冷啟第一發 94ms、熱 8–13ms(t158 病史對照:舊 25.7s);compile 39ms unchecked 照舊
- tsc 全綠;vitest 9 failed/179 passed=t158 基線完全相同
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
143 lines
5.3 KiB
TypeScript
143 lines
5.3 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 { 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<string, unknown>; 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<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('無法解析任何節點');
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
}
|