合流 batch:步驟 3/4/5/6 併成一版出貨(同一顆 cypher worker,bundle 是 worker 粒度)
鐵律(08-01 血淚):CP 可分步驗收,但 bundle 是 worker 粒度——凡改同一顆 worker 的多步, 出貨前必先合流成一版再重生 bundle,否則 stage 只拿到一步(三條分支各自建 bundle 的坑)。 base=feat/step3-missing-guidance(最前緣,已含 step4+step6+t158-t162 prod 修) merge=feat/step5-conditional-edges(引擎條件邊+recipe 三層+教材世代修)
This commit is contained in:
@@ -43,12 +43,28 @@ export function buildExecutionGraph(
|
||||
iterator = foreachMatch[1];
|
||||
label = '對每個'; // 改回標準 label 走 SEMANTIC_EDGE_MAP
|
||||
}
|
||||
const edge: { from: string; to: string; type: ReturnType<typeof toEdgeType>; iterator?: string } = {
|
||||
|
||||
// 「ON_BRANCH(標籤)」抽 branch:意圖語法表達具名分支(SDD workflow-discovery 3.11)
|
||||
// 例:'my_switch >> ON_BRANCH(branch_active) >> 處理啟用' → type=ON_BRANCH, branch='branch_active'
|
||||
// 沒有這段的話,帶括號的 label 會落到 toEdgeType 的預設值 PIPE ⇒ 分支靜默失效
|
||||
// (即「教了語法但引擎不收」——比沒做更糟,故與 skill 文件同批補上)
|
||||
let branch: string | undefined;
|
||||
const branchMatch = label.match(/^(?:ON_BRANCH|分支)\s*[((]\s*([\w-]+)\s*[))]$/i);
|
||||
if (branchMatch) {
|
||||
branch = branchMatch[1];
|
||||
label = 'ON_BRANCH';
|
||||
}
|
||||
|
||||
const edge: {
|
||||
from: string; to: string; type: ReturnType<typeof toEdgeType>;
|
||||
iterator?: string; branch?: string;
|
||||
} = {
|
||||
from: e.from.toLowerCase().replace(/\s+/g, '-'),
|
||||
to: e.to.toLowerCase().replace(/\s+/g, '-'),
|
||||
type: toEdgeType(label),
|
||||
};
|
||||
if (iterator) edge.iterator = iterator;
|
||||
if (branch) edge.branch = branch;
|
||||
return edge;
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { resolveNodeRole, isVirtualIoName } from './triplet-parser';
|
||||
import { wasmWorkerUrl } from '../lib/component-loader';
|
||||
import { resolveRecipe } from '../routes/recipes';
|
||||
import type { RecipeDefinition } from '../routes/recipes';
|
||||
import { branchHintFor } from '../lib/branch-hints';
|
||||
import type { BranchHint } from '../lib/branch-hints';
|
||||
|
||||
/**
|
||||
* `not_found` 而非 `missing`:欄位契約以頂層機械考
|
||||
@@ -52,6 +54,18 @@ export type NodeInfo = {
|
||||
/** recipe found 時附上(AI 看得懂這個 recipe 在打哪個 API)。 */
|
||||
description?: string;
|
||||
endpoint?: string;
|
||||
/**
|
||||
* recipe 的 payload/回應用法自我說明(3.12,同 branch_hint 的動機):
|
||||
* 逐顆查 recipe 時光看 endpoint 不知道「payload 怎麼填、回應怎麼取值」⇒ 會退回寫 code。
|
||||
*/
|
||||
payload_hint?: {
|
||||
/** 這個 recipe 期望的 body 形狀(body_template 的欄位骨架,值是 {{var}} 佔位) */
|
||||
body_template?: unknown;
|
||||
/** 回應正規化規則存在時,說明取值路徑等 */
|
||||
response_map?: unknown;
|
||||
/** 一行說明:怎麼用這個 recipe */
|
||||
usage: string;
|
||||
};
|
||||
/**
|
||||
* not_found 時的分型指路(task 3.7):兩庫(零件 registry+recipe 庫)都查過才點名,
|
||||
* 並告訴 AI 該走哪條補件路+去哪裡看做法。欄位名 `suggestion`(單數字串)=verify.sh 03 組契約。
|
||||
@@ -63,6 +77,13 @@ export type NodeInfo = {
|
||||
similar_recipes?: string[];
|
||||
/** resolved 時的替換明細(步驟 4:意圖節點 → 真實零件/recipe)。 */
|
||||
substitution?: NodeSubstitution;
|
||||
/**
|
||||
* 分支用法自我說明(3.11):只有「本身會分岔」的零件才有
|
||||
* (if_control/switch/try_catch)。
|
||||
* 存在的理由=走 n8n 式「逐顆查、自己組圖」的 AI,光看 input_schema 不知道
|
||||
* 「判斷完之後兩條路怎麼接」⇒ 會回頭寫 code。判準:只看這一顆的回應就知道怎麼接下一步。
|
||||
*/
|
||||
branch_hint?: BranchHint;
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
@@ -216,6 +237,7 @@ export async function searchNodes(
|
||||
input_schema: hit.input_schema,
|
||||
success_rate: typeof hit.success_rate === 'number' ? hit.success_rate : undefined,
|
||||
stability: typeof hit.stability === 'string' ? hit.stability : undefined,
|
||||
branch_hint: branchHintFor(componentId),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
@@ -230,6 +252,7 @@ export async function searchNodes(
|
||||
source: 'recipe',
|
||||
description: recipe.description,
|
||||
endpoint: recipe.endpoint,
|
||||
payload_hint: buildPayloadHint(recipe),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
@@ -355,6 +378,7 @@ async function legacyPerNodeLookup(
|
||||
info: {
|
||||
status: 'found', componentId, type: role, source: 'component',
|
||||
input_schema: q.entry.input_schema, success_rate: q.entry.success_rate, stability: q.entry.stability,
|
||||
branch_hint: branchHintFor(componentId),
|
||||
},
|
||||
missing: false,
|
||||
};
|
||||
@@ -366,6 +390,7 @@ async function legacyPerNodeLookup(
|
||||
info: {
|
||||
status: 'found', componentId: recipe.canonical_id, type: role, source: 'recipe',
|
||||
description: recipe.description, endpoint: recipe.endpoint,
|
||||
payload_hint: buildPayloadHint(recipe),
|
||||
},
|
||||
missing: false,
|
||||
};
|
||||
@@ -407,7 +432,7 @@ async function legacyPerNodeLookup(
|
||||
type SubstitutionHit = Pick<
|
||||
NodeInfo,
|
||||
'status' | 'componentId' | 'source' | 'substitution' |
|
||||
'input_schema' | 'success_rate' | 'stability' | 'description' | 'endpoint'
|
||||
'input_schema' | 'success_rate' | 'stability' | 'description' | 'endpoint' | 'branch_hint'
|
||||
>;
|
||||
|
||||
function trySubstitution(
|
||||
@@ -472,6 +497,9 @@ function trySubstitution(
|
||||
input_schema: top.entry.input_schema,
|
||||
success_rate: typeof top.entry.success_rate === 'number' ? top.entry.success_rate : undefined,
|
||||
stability: typeof top.entry.stability === 'string' ? top.entry.stability : undefined,
|
||||
// 替換成分岔零件時(例「判斷有沒有新資料」→ if_control)一併附分支用法,
|
||||
// 否則 AI 換到零件卻不知道怎麼接兩條路,仍會退回寫 code。
|
||||
branch_hint: branchHintFor(top.entry.canonical_id),
|
||||
substitution: {
|
||||
from: nodeName,
|
||||
componentId: top.entry.canonical_id,
|
||||
@@ -539,6 +567,41 @@ function buildSuggestion(componentId: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* recipe 的 payload/回應用法自我說明(3.12)。
|
||||
* 動機同 branch_hint:逐顆查 recipe(n8n 式)時,光看 endpoint 不知道 payload 怎麼填、
|
||||
* 回應怎麼取值 ⇒ AI 會退回把整包寫進 workflow code。
|
||||
*/
|
||||
export function buildPayloadHint(recipe: RecipeDefinition): NodeInfo['payload_hint'] {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (recipe.body_template) {
|
||||
parts.push('payload 已收在 recipe 的 body_template 裡,你只要把 {{變數}} 對應的值放進節點 context');
|
||||
} else if (recipe.body) {
|
||||
parts.push('payload 形狀見 body 欄位({{變數}} 由節點 context 填)');
|
||||
} else {
|
||||
parts.push('未定義 body_template:節點 context 會整包當 body 送出(_ 開頭的內部欄位會被剔除)');
|
||||
}
|
||||
|
||||
if (recipe.response_map) {
|
||||
parts.push('回應已正規化:執行結果除了原始 data,另附 text(取值路徑等規則寫在 recipe 裡,換源不必改 workflow)');
|
||||
} else {
|
||||
parts.push('未定義 response_map:回應原樣放在 data,取值要自己指路徑');
|
||||
}
|
||||
|
||||
if (recipe.auth === 'binding') {
|
||||
parts.push(`認證=binding(免金鑰,用平台內建 ${recipe.binding_name ?? 'AI'})`);
|
||||
} else if (recipe.auth_service) {
|
||||
parts.push(`認證走 auth recipe「${recipe.auth_service}」(金鑰由系統在執行前注入,你不必也不該填)`);
|
||||
}
|
||||
|
||||
return {
|
||||
body_template: recipe.body_template,
|
||||
response_map: recipe.response_map,
|
||||
usage: parts.join(';') + '。',
|
||||
};
|
||||
}
|
||||
|
||||
// ── registry 查詢 ─────────────────────────────────────────────────────────────
|
||||
|
||||
type CatalogEntry = {
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
import { wasmWorkerUrl } from '../lib/component-loader';
|
||||
import { fetchTenantWorkflowSearch } from '../lib/workflow-search';
|
||||
import { listAllRecipes, type SearchNodesEnv } from './search-nodes';
|
||||
import { listAllRecipes, buildPayloadHint, type SearchNodesEnv } from './search-nodes';
|
||||
import { branchHintFor } from '../lib/branch-hints';
|
||||
|
||||
export type TargetQueryEnv = SearchNodesEnv & {
|
||||
KBDB_BASE_URL?: string;
|
||||
@@ -44,12 +45,21 @@ export async function searchByTarget(
|
||||
);
|
||||
if (!res.ok) return { ok: false, status: 502, error: `registry 搜尋失敗(HTTP ${res.status})` };
|
||||
const body = (await res.json()) as { data?: { results?: unknown[]; count?: number } };
|
||||
// 3.11:逐顆查零件(n8n 式「自己一顆一顆填」)時,會分岔的零件要自我說明分支用法。
|
||||
// leo 08-01:「它可以一一查詢自己手工填寫每個零件,就像在 n8n 那樣」——
|
||||
// 這條路徑若只回 input_schema,AI 拿到 if_control/switch 仍不知道兩條路怎麼接 ⇒ 回頭寫 code。
|
||||
const results = (body.data?.results ?? []).map(r => {
|
||||
if (!r || typeof r !== 'object') return r;
|
||||
const rec = r as Record<string, unknown>;
|
||||
const hint = branchHintFor(typeof rec.canonical_id === 'string' ? rec.canonical_id : undefined);
|
||||
return hint ? { ...rec, branch_hint: hint } : rec;
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
body: {
|
||||
target,
|
||||
query,
|
||||
results: body.data?.results ?? [],
|
||||
results,
|
||||
count: body.data?.count ?? 0,
|
||||
},
|
||||
};
|
||||
@@ -64,7 +74,10 @@ export async function searchByTarget(
|
||||
const q = query.toLowerCase();
|
||||
// 與 discover 混搜同一份庫(私庫=workflow 實際引用得到的);子字串比對、canonical 去重
|
||||
const seen = new Set<string>();
|
||||
const results: Array<{ canonical_id: string; display_name?: string; description?: string; endpoint: string }> = [];
|
||||
const results: Array<{
|
||||
canonical_id: string; display_name?: string; description?: string; endpoint: string;
|
||||
payload_hint?: unknown;
|
||||
}> = [];
|
||||
for (const r of all) {
|
||||
if (seen.has(r.canonical_id)) continue;
|
||||
const hay = `${r.canonical_id} ${r.display_name ?? ''} ${r.description ?? ''}`.toLowerCase();
|
||||
@@ -75,6 +88,8 @@ export async function searchByTarget(
|
||||
display_name: r.display_name,
|
||||
description: r.description,
|
||||
endpoint: r.endpoint,
|
||||
// 3.12:逐顆查 recipe 時也要說得出「payload 怎麼填、回應怎麼取值」
|
||||
payload_hint: buildPayloadHint(r),
|
||||
});
|
||||
}
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user