d48f83ae6f
CP arcrun-usable 步驟 4(目的:AI 只要填 payload——系統把「傳到 telegram」
翻成 http_request+recipe telegram_send)+leo 07-31 追加:
「難道我不能指定要搜尋工作流或節點或 recipe 嗎?」
替換(只動 discover,t158「部署≠發現」邊界不碰):
- 兩庫 exact 落空後,在 t158 一次抓好的清單記憶體內媒合,零新增 round-trip
- 規則 A 服務詞→recipe:名字全部服務詞命中同一 recipe 且唯一才換
(google_slides_create 不被 google_sheets 誤吃)
- 規則 B 強欄位斷詞→零件:canonical/display/aliases 強命中×10+弱命中,
需至少一強命中且分數唯一最高(aes_encrypt 無強命中不換)
- 換到=status resolved+substitution{from,componentId,recipe,reason},
cypher 圖節點直接帶真實 componentId;換不到照舊 not_found+3.7 指路
target 參數(各走既有機制,不新造第二套搜尋):
- triplets+target=component|recipe=只查該庫
- query+target=名字搜尋:component→registry /components/search(=MCP
arcrun_search_components 同路);recipe→私庫 RECIPES KV(回應註明公庫走
arcrun_recipe_search);workflow→新抽 lib/workflow-search.ts,
GET /workflows/search 與 target=workflow 共用(=arcrun_search_workflows 同路)
- 防呆:compile+target 400/target=workflow 吃 query 不吃 triplets/非法 target 400
驗(本地 wrangler dev,registry 種 20 合約+init/seed 10 recipe):
- 「判斷有沒有新資料 >> ON_SUCCESS >> 傳到 telegram」→ if_control(resolved)
+telegram_send(substitution.componentId=http_request)=feature 06 驗法過
- 機械考 27/27 全綠(01 組×5+03 組×4 迴歸+06 組×8+target×8+compile 迴歸×2)
- 冷啟第一發 94ms、熱 8–13ms(t158 病史對照:舊 25.7s);compile 39ms unchecked 照舊
- tsc 全綠;vitest 9 failed/179 passed=t158 基線完全相同
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
4.4 KiB
TypeScript
99 lines
4.4 KiB
TypeScript
/**
|
||
* 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<string, unknown> }
|
||
| { ok: false; status: 400 | 401 | 502; error: string };
|
||
|
||
export async function searchByTarget(
|
||
target: 'component' | 'recipe' | 'workflow',
|
||
query: string,
|
||
env: TargetQueryEnv,
|
||
apiKey?: string,
|
||
): Promise<TargetQueryResult> {
|
||
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<string>();
|
||
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: <canonical_id> 引用)。公庫(多作者市場)走 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<string, unknown>;
|
||
return { ok: true, body: { target, query, ...body } };
|
||
}
|