e8fca33f80
Three platform-level improvements that together enable the full
"草稿 → LLM 整理 → KBDB" wiki ingest workflow via cypher binding:
## 1. Nested interpolation in node.data
Previously {{var}} only supported top-level keys, so {{item.content}}
literal-passed through. Now supports dot-path:
{{paragraph.content}} → ctx.paragraph.content
{{paragraphs.0.entity}} → ctx.paragraphs[0].entity
Non-string values (object/array) JSON.stringify automatically.
## 2. 對每個 X cypher binding syntax
'A >> 對每個 paragraph >> B' parses into FOREACH edge with
iterator='paragraph'. graph-builder.ts strips the iterator from label
before edge type resolution. Backwards compatible: bare '對每個' still
defaults to item.
## 3. FOREACH preserves outer context
itemContext was previously {...result, [iter]: item}, dropping
top-level api_key etc. Now {...outerCtx, ...result, [iter]: item} so
{{api_key}} interpolation works in foreach body.
## 4. Unified recipe output parsing (sync + resume)
Extracted parseRecipeOutput() helper used by both sync claude_api
result + workflow resume callback. Strips ```json fence, parses,
spreads parsed top-level fields into result so downstream FOREACH
finds 'paragraphs' (not buried in data.paragraphs).
paused state now stores recipe_output_format + required_fields so
resume route can apply same parsing as sync path.
End-to-end verified:
- input(草稿+api_key) → synth(claude_api+recipe) → 對每個 paragraph → write_wiki(kbdb_create_block)
- Real Claude synthesis on Mira daemon: 3 triplets + 2 paragraphs
- Both paragraphs written to KBDB as wiki-page blocks (verified GET)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
57 lines
2.4 KiB
TypeScript
57 lines
2.4 KiB
TypeScript
import type { ParsedTriplets } from './triplet-parser';
|
||
import { toEdgeType } from './triplet-parser';
|
||
import type { SearchResult } from './search-nodes';
|
||
|
||
/** 從 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(
|
||
parsed: ParsedTriplets,
|
||
nodeResults: SearchResult['nodeResults'],
|
||
graphId: string,
|
||
graphName: string,
|
||
config?: Record<string, Record<string, unknown>>,
|
||
) {
|
||
const nodes = [...parsed.nodeNames].map(name => {
|
||
const nr = nodeResults[name]!;
|
||
const id = name.toLowerCase().replace(/\s+/g, '-');
|
||
const nodeConfig = config?.[name] ?? {};
|
||
|
||
// config[name].component 可以是 hash 或 canonical_id,覆蓋自動偵測的 componentId
|
||
const componentId = (nodeConfig.component as string | undefined) ?? nr.componentId;
|
||
|
||
// 其他 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 => {
|
||
// 「對每個 X」label 抽 iterator:cypher binding 表達 FOREACH 的迭代變數
|
||
// 例:'A >> 對每個 paragraph >> B' → type=FOREACH, iterator='paragraph'
|
||
// getIterableFromContext 會找 ctx.paragraphs(複數)或 ctx.paragraph
|
||
let iterator: string | undefined;
|
||
let label = e.label;
|
||
const foreachMatch = label.match(/^(?:對每個|FOREACH)\s+(\w+)$/i);
|
||
if (foreachMatch) {
|
||
iterator = foreachMatch[1];
|
||
label = '對每個'; // 改回標準 label 走 SEMANTIC_EDGE_MAP
|
||
}
|
||
const edge: { from: string; to: string; type: ReturnType<typeof toEdgeType>; iterator?: string } = {
|
||
from: e.from.toLowerCase().replace(/\s+/g, '-'),
|
||
to: e.to.toLowerCase().replace(/\s+/g, '-'),
|
||
type: toEdgeType(label),
|
||
};
|
||
if (iterator) edge.iterator = iterator;
|
||
return edge;
|
||
});
|
||
|
||
return { id: graphId, name: graphName, nodes, edges };
|
||
}
|