feat: config field in /cypher/execute — node-level component override

- /cypher/execute now accepts separate `config` field:
  {node_name: {component: "cmp_19e62efd", ...staticParams}}
- graph-builder reads config[node].component to override componentId
  (supports cmp_ hash, rec_ hash, or canonical_id)
- config[node] other fields become node.data (static params merged at runtime)
- acr run now sends workflow.config as separate `config` (not flattened into context)
- context is now only --input dynamic params

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 18:42:26 +08:00
parent 60d3e41905
commit 7b18387113
4 changed files with 31 additions and 10 deletions
+2 -1
View File
@@ -64,7 +64,8 @@ export async function cmdRun(workflowName: string, options: RunOptions): Promise
headers, headers,
body: JSON.stringify({ body: JSON.stringify({
triplets: workflow.flow, triplets: workflow.flow,
context: { ...inputContext, ...(workflow.config ?? {}) }, config: workflow.config ?? {}, // node_name → {component, ...params}
context: inputContext, // --input key=value 傳入的動態參數
graph_id: workflow.name, graph_id: workflow.name,
graph_name: workflow.name, graph_name: workflow.name,
}), }),
@@ -32,6 +32,7 @@ export async function handleCypherExecute(
context: Record<string, unknown> | undefined, context: Record<string, unknown> | undefined,
graphId: string, graphId: string,
graphName: string, graphName: string,
config: Record<string, Record<string, unknown>> | undefined,
env: Bindings, env: Bindings,
waitUntil: (promise: Promise<void>) => void, waitUntil: (promise: Promise<void>) => void,
): Promise<{ success: boolean; data?: unknown; error?: string; trace?: unknown; duration_ms: number; graph?: ExecutionGraph }> { ): Promise<{ success: boolean; data?: unknown; error?: string; trace?: unknown; duration_ms: number; graph?: ExecutionGraph }> {
@@ -49,7 +50,7 @@ export async function handleCypherExecute(
); );
} }
const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName); const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName, config);
const parseResult = graphSchema.safeParse(graph); const parseResult = graphSchema.safeParse(graph);
if (!parseResult.success) { if (!parseResult.success) {
throw new Error('圖定義產生失敗'); throw new Error('圖定義產生失敗');
+19 -7
View File
@@ -2,22 +2,34 @@ import type { ParsedTriplets } from './triplet-parser';
import { toEdgeType } from './triplet-parser'; import { toEdgeType } from './triplet-parser';
import type { SearchResult } from './search-nodes'; import type { SearchResult } from './search-nodes';
/** 從 nodeResults + parsed 組成可直接送入 /execute 的 ExecutionGraph */ /** 從 nodeResults + parsed 組成可直接送入 /execute 的 ExecutionGraph
*
* config 格式(來自 workflow YAML 的 config 欄位):
* { node_name: { component: "cmp_xxxxxxxx" | "rec_xxxxxxxx" | canonical_id, ...params } }
*
* 若 config[node].component 存在,以它覆蓋 searchNodes 偵測到的 componentId。
* config[node] 的其他欄位作為節點靜態參數(node.data),合併進執行 context。
*/
export function buildExecutionGraph( export function buildExecutionGraph(
parsed: ParsedTriplets, parsed: ParsedTriplets,
nodeResults: SearchResult['nodeResults'], nodeResults: SearchResult['nodeResults'],
graphId: string, graphId: string,
graphName: string, graphName: string,
config?: Record<string, Record<string, unknown>>,
) { ) {
const nodes = [...parsed.nodeNames].map(name => { const nodes = [...parsed.nodeNames].map(name => {
const nr = nodeResults[name]!; const nr = nodeResults[name]!;
const id = name.toLowerCase().replace(/\s+/g, '-'); const id = name.toLowerCase().replace(/\s+/g, '-');
return { const nodeConfig = config?.[name] ?? {};
id,
type: nr.type, // config[name].component 可以是 hash 或 canonical_id,覆蓋自動偵測的 componentId
componentId: nr.componentId, const componentId = (nodeConfig.component as string | undefined) ?? nr.componentId;
label: name,
}; // 其他 config 欄位作為 node.data(靜態參數)
const { component: _component, ...staticParams } = nodeConfig;
const data = Object.keys(staticParams).length > 0 ? staticParams : undefined;
return { id, type: nr.type, componentId, label: name, data };
}); });
const edges = parsed.edges.map(e => ({ const edges = parsed.edges.map(e => ({
+8 -1
View File
@@ -38,7 +38,13 @@ cypherRouter.post('/cypher/search', async (c) => {
// POST /cypher/execute — 三元組 → 一步執行(search + execute 合一) // POST /cypher/execute — 三元組 → 一步執行(search + execute 合一)
cypherRouter.post('/cypher/execute', async (c) => { cypherRouter.post('/cypher/execute', async (c) => {
const body = await c.req.json() as { triplets?: unknown; context?: Record<string, unknown>; graph_id?: string; graph_name?: string }; const body = await c.req.json() as {
triplets?: unknown;
context?: Record<string, unknown>;
config?: Record<string, Record<string, unknown>>; // node_name → {component, ...params}
graph_id?: string;
graph_name?: string;
};
if (!Array.isArray(body?.triplets) || body.triplets.length === 0) { if (!Array.isArray(body?.triplets) || body.triplets.length === 0) {
return c.json({ error: 'triplets 必須為非空字串陣列' }, 400); return c.json({ error: 'triplets 必須為非空字串陣列' }, 400);
@@ -57,6 +63,7 @@ cypherRouter.post('/cypher/execute', async (c) => {
body.context, body.context,
graphId, graphId,
graphName, graphName,
body.config,
c.env, c.env,
(p) => c.executionCtx.waitUntil(p), (p) => c.executionCtx.waitUntil(p),
); );