feat(cypher): 3-node wiki workflow end-to-end (FOREACH + nested interp + unified parsing)

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>
This commit is contained in:
2026-05-07 16:23:02 +08:00
parent 497f92a268
commit e8fca33f80
4 changed files with 115 additions and 40 deletions
+19 -5
View File
@@ -32,11 +32,25 @@ export function buildExecutionGraph(
return { id, type: nr.type, componentId, label: name, data };
});
const edges = parsed.edges.map(e => ({
from: e.from.toLowerCase().replace(/\s+/g, '-'),
to: e.to.toLowerCase().replace(/\s+/g, '-'),
type: toEdgeType(e.label),
}));
const edges = parsed.edges.map(e => {
// 「對每個 X」label 抽 iteratorcypher 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 };
}