import { Hono } from 'hono'; import type { Bindings } from '../types'; import { handleCypherSearch, handleCypherExecute } from '../actions/cypher-handlers'; import { searchByTarget } from '../actions/target-search'; export const cypherRouter = new Hono<{ Bindings: Bindings }>(); const VALID_TARGETS = new Set(['component', 'recipe', 'workflow']); // POST /cypher/search — 三元組 → 解析節點 → 語意搜尋零件 → 回傳 Cypher JSON (開發友善格式) // // t159(leo 07-31):加 `target` 指定搜尋對象(component/recipe/workflow)+`query` 名字搜尋。 // - triplets(不給 target)=混搜兩庫+意圖節點替換(步驟 4) // - triplets + target=component|recipe=只查該庫 // - query + target=名字搜尋,各自走**既有**機制(registry search/私庫 RECIPES/workflows/search) cypherRouter.post('/cypher/search', async (c) => { const body = await c.req.json() as { triplets?: unknown; mode?: unknown; target?: unknown; query?: unknown }; const rawTriplets = body?.triplets; // ── target 驗證(component / recipe / workflow)───────────────────────────── const target = typeof body?.target === 'string' ? body.target : undefined; if (target !== undefined && !VALID_TARGETS.has(target)) { return c.json({ error: `target 只接受 component/recipe/workflow,收到「${target}」` }, 400); } // ── query 名字搜尋分支(需 target)────────────────────────────────────────── const query = typeof body?.query === 'string' ? body.query.trim() : ''; if (query) { if (!target) { return c.json({ error: '給 query 必須同時給 target(component/recipe/workflow),指明要搜哪個庫' }, 400); } const apiKey = c.req.header('X-Arcrun-API-Key') ?? undefined; const r = await searchByTarget(target as 'component' | 'recipe' | 'workflow', query, c.env, apiKey); if (!r.ok) return c.json({ error: r.error }, r.status); return c.json(r.body); } if (!Array.isArray(rawTriplets) || rawTriplets.length === 0) { return c.json({ error: 'triplets 必須為非空字串陣列(或給 query + target 做名字搜尋)' }, 400); } // t158「部署≠發現」:mode=compile=純編圖(安裝器/acr push 的複製路徑,零存在性查詢); // 預設 discover=誠實查詢(AI 問「有沒有」的既有契約,not_found+指路照舊)。 const mode = body?.mode === 'compile' ? 'compile' : 'discover'; // target 限庫只屬於 discover(compile=純複製,不查任何庫,target 無意義) if (target && mode === 'compile') { return c.json({ error: 'mode=compile(複製路徑)不查庫,不接受 target;要指定搜尋對象請用 discover(預設)' }, 400); } // workflow 是名字搜尋,不參與三元組編圖——請帶 query if (target === 'workflow') { return c.json({ error: 'target=workflow 是名字搜尋,請改帶 { target: "workflow", query: "..." }(不吃 triplets)' }, 400); } try { const now = new Date(); const timestamp = now.toISOString(); const versionId = `search-v1-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`; const result = await handleCypherSearch(rawTriplets, c.env, mode, target as 'component' | 'recipe' | undefined); const response = { version: versionId, timestamp, triplets: rawTriplets, nodes: result.nodes, cypher: result.cypher, missing: result.missing, }; return c.json(response); } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); return c.json({ error: errMsg }, 400); } }); // POST /cypher/execute — 三元組 → 一步執行(search + execute 合一) cypherRouter.post('/cypher/execute', async (c) => { const body = await c.req.json() as { triplets?: unknown; context?: Record; config?: Record>; // node_name → {component, ...params} graph_id?: string; graph_name?: string; }; if (!Array.isArray(body?.triplets) || body.triplets.length === 0) { return c.json({ error: 'triplets 必須為非空字串陣列' }, 400); } const graphId = typeof body.graph_id === 'string' ? body.graph_id : `triplet-exec-${Date.now()}`; const graphName = typeof body.graph_name === 'string' ? body.graph_name : 'Triplet Execution'; const now = new Date(); const timestamp = now.toISOString(); // 版本號格式:execute-v1-20260327-143022 const versionId = `execute-v1-${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`; const apiKey = c.req.header('X-Arcrun-API-Key') ?? undefined; try { const result = await handleCypherExecute( body.triplets as unknown[], body.context, graphId, graphName, body.config, c.env, (p) => c.executionCtx.waitUntil(p), apiKey, ); // 包裝成開發友善格式(execute 成功時) const response = { version: versionId, timestamp, ...result, }; return c.json(response); } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); try { const parsed = JSON.parse(errMsg); const response = { version: versionId, timestamp, ...parsed, }; return c.json(response, 500); } catch { return c.json({ version: versionId, timestamp, success: false, error: errMsg, duration_ms: 0 }, 500); } } });