/** * target-search — POST /cypher/search 的「指定搜尋對象」名字搜尋(t159) * * leo 07-31:「search 節點名稱和 search 工作流名稱是同一個?一個死了另一個不能動? * 難道我不能指定要搜尋工作流或節點或 recipe 嗎?」 * * ⇒ discover 入口加 `target`(component/recipe/workflow)+`query`: * - target=component → 轉發 registry GET /components/search(MCP arcrun_search_components 同一條路) * - target=recipe → 掃私庫 RECIPES KV(與 discover 混搜的第二庫**同一份讀法** listAllRecipes); * 公庫(多作者市場)另有 /public-recipes=MCP arcrun_recipe_search,回應註明 * - target=workflow → lib/workflow-search.ts(GET /workflows/search=MCP arcrun_search_workflows 同一條路) * * 「外部 API 只有一條一致的路」:三個 target 各自對應**既有**搜尋機制,本檔只做轉接, * 不新造第二套搜尋。flag 安全:主動 pull,無輪詢。 */ import { wasmWorkerUrl } from '../lib/component-loader'; import { fetchTenantWorkflowSearch } from '../lib/workflow-search'; import { listAllRecipes, type SearchNodesEnv } from './search-nodes'; export type TargetQueryEnv = SearchNodesEnv & { KBDB_BASE_URL?: string; KBDB_INTERNAL_TOKEN?: string; }; export type TargetQueryResult = | { ok: true; body: Record } | { ok: false; status: 400 | 401 | 502; error: string }; export async function searchByTarget( target: 'component' | 'recipe' | 'workflow', query: string, env: TargetQueryEnv, apiKey?: string, ): Promise { if (target === 'component') { const sub = env.WORKER_SUBDOMAIN; const registryBase = env.REGISTRY_BASE_URL ?? (sub ? wasmWorkerUrl('registry', sub) : undefined); if (!registryBase) return { ok: false, status: 502, error: 'registry 位置未設定(WORKER_SUBDOMAIN/REGISTRY_BASE_URL 皆缺)' }; try { const res = await fetch( `${registryBase}/components/search?q=${encodeURIComponent(query)}`, { signal: AbortSignal.timeout(10000) }, ); if (!res.ok) return { ok: false, status: 502, error: `registry 搜尋失敗(HTTP ${res.status})` }; const body = (await res.json()) as { data?: { results?: unknown[]; count?: number } }; return { ok: true, body: { target, query, results: body.data?.results ?? [], count: body.data?.count ?? 0, }, }; } catch (e) { return { ok: false, status: 502, error: `registry 查不通:${e instanceof Error ? e.message : String(e)}` }; } } if (target === 'recipe') { if (!env.RECIPES) return { ok: false, status: 502, error: 'RECIPES KV 未綁定' }; const all = await listAllRecipes(env.RECIPES); const q = query.toLowerCase(); // 與 discover 混搜同一份庫(私庫=workflow 實際引用得到的);子字串比對、canonical 去重 const seen = new Set(); const results: Array<{ canonical_id: string; display_name?: string; description?: string; endpoint: string }> = []; for (const r of all) { if (seen.has(r.canonical_id)) continue; const hay = `${r.canonical_id} ${r.display_name ?? ''} ${r.description ?? ''}`.toLowerCase(); if (!hay.includes(q)) continue; seen.add(r.canonical_id); results.push({ canonical_id: r.canonical_id, display_name: r.display_name, description: r.description, endpoint: r.endpoint, }); } return { ok: true, body: { target, query, results, count: results.length, note: '搜的是本部署私庫(workflow 可直接 component: 引用)。公庫(多作者市場)走 MCP arcrun_recipe_search/GET /public-recipes。', }, }; } // target === 'workflow':租戶隔離,必帶 API key(同 GET /workflows/search 的既有契約) if (!apiKey) return { ok: false, status: 401, error: 'target=workflow 需要 X-Arcrun-API-Key header(workflow 搜尋限本租戶)' }; const res = await fetchTenantWorkflowSearch(env, apiKey, query); if (!res.ok) return { ok: false, status: 502, error: `workflow 搜尋失敗(KBDB HTTP ${res.status})` }; const body = (await res.json()) as Record; return { ok: true, body: { target, query, ...body } }; }