diff --git a/cypher-executor/src/actions/cypher-handlers.ts b/cypher-executor/src/actions/cypher-handlers.ts index c0010ec..cc5648f 100644 --- a/cypher-executor/src/actions/cypher-handlers.ts +++ b/cypher-executor/src/actions/cypher-handlers.ts @@ -17,10 +17,12 @@ export async function handleCypherSearch( throw new Error('無法解析任何節點'); } - const { nodeResults } = searchNodes(parsed); + // 2026-07-30:查 registry 判真實存在(workflow-discovery)。 + // `missing` 以前寫死 [],等於告訴 AI「什麼都有」——那是「腹語術」的入口。 + const { nodeResults, missingNodes } = await searchNodes(parsed, undefined, env); const graph = buildExecutionGraph(parsed, nodeResults, 'cypher-search-result', 'Cypher Search Result'); - return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: [] }; + return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: missingNodes }; } export async function handleCypherExecute( @@ -50,7 +52,7 @@ export async function handleCypherExecute( throw new Error('無法解析任何節點'); } - const { nodeResults } = searchNodes(parsed, config); + const { nodeResults } = await searchNodes(parsed, config, env); const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName, config); const parseResult = graphSchema.safeParse(graph); diff --git a/cypher-executor/src/actions/search-nodes.ts b/cypher-executor/src/actions/search-nodes.ts index 0593440..a63af06 100644 --- a/cypher-executor/src/actions/search-nodes.ts +++ b/cypher-executor/src/actions/search-nodes.ts @@ -1,27 +1,53 @@ import type { ParsedTriplets, NodeRole } from './triplet-parser'; import { resolveNodeRole } from './triplet-parser'; +import { wasmWorkerUrl } from '../lib/component-loader'; + +export type NodeStatus = 'found' | 'missing' | 'unknown'; + +export type NodeInfo = { + status: NodeStatus; + componentId?: string; + type: NodeRole; + /** 零件契約(found 時附上,讓 AI 知道怎麼填 payload)。 */ + input_schema?: unknown; + /** 成功率(found 時附上,讓「被測過幾次」看得見)。 */ + success_rate?: number; + stability?: string; + /** missing 時給的相近零件建議(避免 AI 只知道「沒有」卻不知道該用什麼)。 */ + suggestions?: string[]; +}; export type SearchResult = { - nodeResults: Record; + nodeResults: Record; missingNodes: string[]; }; /** - * 對所有節點進行解析,確認每個節點對應的零件 ID。 + * 對所有節點進行解析,確認每個節點對應的零件 ID 與**是否真的存在**。 * - * 注意:此步驟只做靜態解析,不做遠端查找。 - * 零件是否真的存在由 component-loader 在執行時決定(Service Binding / KV / URL)。 + * ⚠️ 2026-07-30 改為會查 registry(workflow-discovery task 3.x)。 * - * 優先序: - * 1. Input/Output 角色:自動標記,componentId = 小寫節點名稱 - * 2. config[nodeName].component 已指定:使用 config 提供的 componentId - * 3. 其他:componentId = 節點名稱(交給 component-loader 在執行時解析) + * 改之前的行為(病灶):無條件回 `status: 'found'`、`missingNodes` 永遠是空陣列—— + * 型別雖宣告了 `'missing'` 但程式碼從不使用。實測「完全不存在的東西xyz」也回 found。 + * + * 為什麼這是嚴重問題(leo 2026-07-30 定性「腹語術」): + * AI 寫意圖 → 查詢回「都 found」(假信號)→ 實際零件不存在 + * → 部署/執行才發現 → 最快的修法是改寫成 `code` 節點自己寫 JS + * → 於是正式 workflow 只用 2 個零件、8 個 code 節點含 if×61 + * ⇒ 「零件被測過 1000 次所以 AI 只要填 payload」的價值完全落空。 + * + * 誠實限制:查不到 registry(未部署/網路失敗)時回 `'unknown'` 而不是 `'missing'`—— + * 不能因為查詢失敗就宣告零件不存在(那會讓 AI 誤判而重寫 code,正是要避免的事)。 */ -export function searchNodes( +export async function searchNodes( parsed: ParsedTriplets, config?: Record>, -): SearchResult { - const nodeResults: Record = {}; + env?: { WORKER_SUBDOMAIN?: string }, +): Promise { + const nodeResults: Record = {}; + const missingNodes: string[] = []; + + const sub = env?.WORKER_SUBDOMAIN; for (const nodeName of parsed.nodeNames) { const role = resolveNodeRole(nodeName, parsed); @@ -33,8 +59,85 @@ export function searchNodes( const configComponent = config?.[nodeName]?.component as string | undefined; const componentId = configComponent ?? nodeName; - nodeResults[nodeName] = { status: 'found', componentId, type: role }; + + // config 明確給了 component(多半是安裝器代入的 worker URL 或既有 workflow) + // → 不判 missing。這條路徑的存在性由 component-loader 在執行時決定(原行為)。 + if (configComponent) { + nodeResults[nodeName] = { status: 'found', componentId, type: role }; + continue; + } + + if (!sub) { + nodeResults[nodeName] = { status: 'unknown', componentId, type: role }; + continue; + } + + const q = await fetchComponent(sub, componentId); + if (!q.ok) { + // registry 查不通(未部署/網路失敗)⇒ 誠實回 unknown。 + // **不能誤判 missing**——那會讓 AI 以為零件不存在而重寫 code,正是要避免的事。 + nodeResults[nodeName] = { status: 'unknown', componentId, type: role }; + continue; + } + if (q.entry) { + nodeResults[nodeName] = { + status: 'found', + componentId, + type: role, + input_schema: q.entry.input_schema, + success_rate: q.entry.success_rate, + stability: q.entry.stability, + }; + continue; + } + + nodeResults[nodeName] = { status: 'missing', componentId, type: role }; + missingNodes.push(nodeName); } - return { nodeResults, missingNodes: [] }; + return { nodeResults, missingNodes }; +} + +type CatalogEntry = { + input_schema?: unknown; + success_rate?: number; + stability?: string; +}; + +/** + * 查單一零件是否存在於 registry。 + * + * ⚠️ 為什麼逐個查而非抓整份目錄:registry **沒有列表端點** + * (實測 `GET /components` → 404,只有 `GET /components/`)。 + * 這是 CP2-B 記載的缺口(「修 /components 404」)——補了列表端點後可改為抓一次。 + * 現階段逐個查:節點數通常 <10,且有 5s timeout,可接受。 + * + * 回傳 `null` 代表「查不到 registry 或該零件不存在」,由呼叫端區分: + * 整體查不通 → `unknown`;查得通但這顆沒有 → `missing`。 + */ +async function fetchComponent( + subdomain: string, + id: string, +): Promise<{ ok: boolean; entry?: CatalogEntry }> { + try { + const base = wasmWorkerUrl('registry', subdomain); + const res = await fetch(`${base}/components/${encodeURIComponent(id)}`, { + signal: AbortSignal.timeout(5000), + }); + if (res.status === 404) return { ok: true }; // registry 活著,但沒這顆 + if (!res.ok) return { ok: false }; + const body = (await res.json()) as { success?: boolean; data?: Record }; + if (body.success === false) return { ok: true }; // 同上:回「零件不存在」 + const d = body.data ?? (body as unknown as Record); + return { + ok: true, + entry: { + input_schema: d.input_schema, + success_rate: typeof d.success_rate === 'number' ? d.success_rate : undefined, + stability: typeof d.stability === 'string' ? d.stability : undefined, + }, + }; + } catch { + return { ok: false }; + } } diff --git a/cypher-executor/src/actions/webhook-graph-resolver.ts b/cypher-executor/src/actions/webhook-graph-resolver.ts index 3885a3b..37c87f7 100644 --- a/cypher-executor/src/actions/webhook-graph-resolver.ts +++ b/cypher-executor/src/actions/webhook-graph-resolver.ts @@ -14,7 +14,7 @@ export async function resolveWebhookGraph( const parsed = parseTriplets(body.triplets as unknown[]); if (!parsed) return { resolvedGraph: {}, error: '無法解析 triplets' }; - const { nodeResults } = searchNodes(parsed); + const { nodeResults } = await searchNodes(parsed); const graphId = `webhook-${Date.now()}`; const graphName = description || `Webhook ${new Date().toISOString()}`; diff --git a/system-dev/docs/3-specs/portal-auth/design.md b/system-dev/docs/3-specs/portal-auth/design.md index deaac35..93655c5 100644 --- a/system-dev/docs/3-specs/portal-auth/design.md +++ b/system-dev/docs/3-specs/portal-auth/design.md @@ -1,6 +1,8 @@ --- -status: active -superseded_by: "" +status: closed +superseded_by: "workflow-discovery" +closed_at: "2026-07-30" +closed_reason: "26/26 任務全完成、0 未完成待搬;封測 portal 多人授權已上線" --- # portal-auth — Design(RAG Portal 多人授權) diff --git a/system-dev/docs/3-specs/workflow-discovery/design.md b/system-dev/docs/3-specs/workflow-discovery/design.md index 3e2e31d..407b58d 100644 --- a/system-dev/docs/3-specs/workflow-discovery/design.md +++ b/system-dev/docs/3-specs/workflow-discovery/design.md @@ -1,5 +1,5 @@ --- -status: paused +status: active superseded_by: "" ---