From 216d3512c606c3ba62041fcfb581b3c56af60a05 Mon Sep 17 00:00:00 2001 From: richblack Date: Fri, 31 Jul 2026 13:32:39 +0800 Subject: [PATCH] =?UTF-8?q?t158=20=E9=87=8D=E6=89=93=E5=8C=85=EF=BC=88Arcr?= =?UTF-8?q?un@7e631a8=EF=BC=89=EF=BC=9A/cypher/search=20mode=20=E5=88=86?= =?UTF-8?q?=E6=B5=81=E2=80=94=E2=80=94=E8=A4=87=E8=A3=BD=E8=B7=AF=E5=BE=91?= =?UTF-8?q?=E5=9B=9E=E6=AF=AB=E7=A7=92=E7=B4=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - compile=純編圖零查詢(安裝器/acr push);discover=誠實查詢預設(契約不變) - /cypher/execute 一律 compile;discover 批次化(registry 新增 /components/catalog) - 本地實測:compile 8 節點 3ms(迴歸前水準);verify 01/03 9/9 綠 Co-Authored-By: Claude Fable 5 --- manifest.json | 6 +- tier2/cypher/index.js | 184 +++++++++++++++++++++++++++++++--------- tier2/registry/index.js | 21 +++++ 3 files changed, 167 insertions(+), 44 deletions(-) diff --git a/manifest.json b/manifest.json index b65242e..f20f080 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "schema": "arcrun-rag-bundles/v1", "built": "2026-07-31", - "source": "Arcrun@7e87a33", + "source": "Arcrun@7e631a8", "core": [ { "name": "arcrun-array-ops", @@ -503,7 +503,7 @@ "tier": 2, "main_file": "tier2/cypher/index.js", "main_module": "index.js", - "sha256": "583637701eec268eb0666ecb6aa5fa45f50f942c535612053c8f5e135a69d312", + "sha256": "68f723dfe927c8a3dc016189c9f87188f806a443ae96d0632badb2b8541f970d", "compat_date": "2025-02-19", "compat_flags": [ "nodejs_compat", @@ -585,7 +585,7 @@ "tier": 2, "main_file": "tier2/registry/index.js", "main_module": "index.js", - "sha256": "002917e9695c6afbd1ae4d85b2131ea0a9b2a2fc575d2f853d746604b2deaa08", + "sha256": "657f6016b2511eeb1f06222b7f8dc40ec2f7d5d7cd608e7c57d27b5186786f4d", "compat_date": "2025-02-19", "compat_flags": [ "nodejs_compat" diff --git a/tier2/cypher/index.js b/tier2/cypher/index.js index f6b1e8f..5ac6a29 100644 --- a/tier2/cypher/index.js +++ b/tier2/cypher/index.js @@ -9975,11 +9975,35 @@ init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console(); init_performance2(); init_component_loader(); init_recipes(); -async function searchNodes(parsed, config2, env2) { +async function searchNodes(parsed, config2, env2, mode = "discover") { const nodeResults = {}; const missingNodes = []; + if (mode === "compile") { + for (const nodeName of parsed.nodeNames) { + const role = resolveNodeRole(nodeName, parsed); + if ((role === "Input" || role === "Output") && isVirtualIoName(nodeName)) { + nodeResults[nodeName] = { status: "found", componentId: nodeName.toLowerCase(), type: role }; + continue; + } + const configComponent = config2?.[nodeName]?.component; + nodeResults[nodeName] = { + status: configComponent ? "found" : "unchecked", + componentId: configComponent ?? nodeName, + type: role + }; + } + return { nodeResults, missingNodes }; + } const sub = env2?.WORKER_SUBDOMAIN; const registryBase = env2?.REGISTRY_BASE_URL ?? (sub ? wasmWorkerUrl("registry", sub) : void 0); + const catalog = registryBase ? await fetchCatalog(registryBase) : { status: "unreachable", entries: [] }; + const recipes = env2?.RECIPES ? await listAllRecipes2(env2.RECIPES) : []; + const byId = /* @__PURE__ */ new Map(); + for (const e of catalog.entries) { + const prev = byId.get(e.canonical_id); + if (!prev || (e.score ?? 0) > (prev.score ?? 0)) byId.set(e.canonical_id, e); + for (const a of e.aliases ?? []) if (!byId.has(a)) byId.set(a, e); + } for (const nodeName of parsed.nodeNames) { const role = resolveNodeRole(nodeName, parsed); if ((role === "Input" || role === "Output") && isVirtualIoName(nodeName)) { @@ -9992,28 +10016,30 @@ async function searchNodes(parsed, config2, env2) { nodeResults[nodeName] = { status: "found", componentId, type: role }; continue; } - if (!registryBase) { + if (catalog.status === "unreachable") { nodeResults[nodeName] = { status: "unknown", componentId, type: role }; continue; } - const q = await fetchComponent(registryBase, componentId); - if (!q.ok) { - nodeResults[nodeName] = { status: "unknown", componentId, type: role }; + if (catalog.status === "no_endpoint") { + const legacy = await legacyPerNodeLookup(registryBase, componentId, nodeName, role, env2, recipes); + nodeResults[nodeName] = legacy.info; + if (legacy.missing) missingNodes.push(nodeName); continue; } - if (q.entry) { + const hit = byId.get(componentId); + if (hit) { nodeResults[nodeName] = { status: "found", componentId, type: role, source: "component", - input_schema: q.entry.input_schema, - success_rate: q.entry.success_rate, - stability: q.entry.stability + input_schema: hit.input_schema, + success_rate: typeof hit.success_rate === "number" ? hit.success_rate : void 0, + stability: typeof hit.stability === "string" ? hit.stability : void 0 }; continue; } - const recipe = env2?.RECIPES ? await resolveRecipe(componentId, env2.RECIPES) : null; + const recipe = recipes.find((r) => r.canonical_id === componentId); if (recipe) { nodeResults[nodeName] = { status: "found", @@ -10025,10 +10051,8 @@ async function searchNodes(parsed, config2, env2) { }; continue; } - const [similarComponents, similarRecipes] = await Promise.all([ - searchSimilarComponents(registryBase, nodeName), - env2?.RECIPES ? searchSimilarRecipes(env2.RECIPES, nodeName) : Promise.resolve([]) - ]); + const similarComponents = similarFromCatalog(catalog.entries, nodeName); + const similarRecipes = similarFromRecipes(recipes, nodeName); nodeResults[nodeName] = { status: "not_found", componentId, @@ -10042,6 +10066,106 @@ async function searchNodes(parsed, config2, env2) { return { nodeResults, missingNodes }; } __name(searchNodes, "searchNodes"); +async function fetchCatalog(registryBase) { + try { + const res = await fetch(`${registryBase}/components/catalog`, { signal: AbortSignal.timeout(1e4) }); + if (res.status === 404) return { status: "no_endpoint", entries: [] }; + if (!res.ok) return { status: "unreachable", entries: [] }; + const body = await res.json(); + return { status: "ok", entries: body.data?.components ?? [] }; + } catch { + return { status: "unreachable", entries: [] }; + } +} +__name(fetchCatalog, "fetchCatalog"); +async function listAllRecipes2(kv) { + try { + const list = await kv.list({ prefix: "recipe:" }); + return (await Promise.all( + list.keys.map((k) => kv.get(k.name, "json")) + )).filter(Boolean); + } catch { + return []; + } +} +__name(listAllRecipes2, "listAllRecipes"); +function similarFromCatalog(entries, nodeName) { + const searchableOf = /* @__PURE__ */ __name((e) => [e.canonical_id, e.display_name ?? "", e.description ?? "", ...e.aliases ?? [], ...e.tags ?? []].join(" ").toLowerCase(), "searchableOf"); + const full = nodeName.toLowerCase(); + const direct = entries.filter((e) => searchableOf(e).includes(full)).map((e) => e.canonical_id); + if (direct.length > 0) return [...new Set(direct)].slice(0, 3); + const tokens = extractTokens(nodeName); + if (tokens.length === 0) return []; + const count3 = /* @__PURE__ */ new Map(); + for (const e of entries) { + const hay = searchableOf(e); + const hits = tokens.filter((t) => hay.includes(t)).length; + if (hits > 0) count3.set(e.canonical_id, Math.max(count3.get(e.canonical_id) ?? 0, hits)); + } + return [...count3.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([id]) => id); +} +__name(similarFromCatalog, "similarFromCatalog"); +function similarFromRecipes(recipes, nodeName) { + const tokens = [nodeName.toLowerCase(), ...extractTokens(nodeName)]; + const seen = /* @__PURE__ */ new Set(); + const matched = []; + for (const r of recipes) { + if (seen.has(r.canonical_id)) continue; + const hay = `${r.canonical_id} ${r.display_name ?? ""} ${r.description ?? ""}`.toLowerCase(); + if (tokens.some((t) => hay.includes(t))) { + seen.add(r.canonical_id); + matched.push(r.canonical_id); + } + } + return matched.slice(0, 3); +} +__name(similarFromRecipes, "similarFromRecipes"); +async function legacyPerNodeLookup(registryBase, componentId, nodeName, role, env2, recipes) { + const q = await fetchComponent(registryBase, componentId); + if (!q.ok) return { info: { status: "unknown", componentId, type: role }, missing: false }; + if (q.entry) { + return { + info: { + status: "found", + componentId, + type: role, + source: "component", + input_schema: q.entry.input_schema, + success_rate: q.entry.success_rate, + stability: q.entry.stability + }, + missing: false + }; + } + const recipe = recipes.find((r) => r.canonical_id === componentId) ?? (env2?.RECIPES ? await resolveRecipe(componentId, env2.RECIPES) : null); + if (recipe) { + return { + info: { + status: "found", + componentId: recipe.canonical_id, + type: role, + source: "recipe", + description: recipe.description, + endpoint: recipe.endpoint + }, + missing: false + }; + } + const similarComponents = await searchSimilarComponents(registryBase, nodeName); + const similarRecipes = similarFromRecipes(recipes, nodeName); + return { + info: { + status: "not_found", + componentId, + type: role, + suggestion: buildSuggestion(componentId), + ...similarComponents.length > 0 ? { similar_components: similarComponents } : {}, + ...similarRecipes.length > 0 ? { similar_recipes: similarRecipes } : {} + }, + missing: true + }; +} +__name(legacyPerNodeLookup, "legacyPerNodeLookup"); var SERVICE_HINTS = [ "google", "gmail", @@ -10197,29 +10321,6 @@ async function searchSimilarComponents(registryBase, nodeName) { return [...count3.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([id]) => id); } __name(searchSimilarComponents, "searchSimilarComponents"); -async function searchSimilarRecipes(kv, nodeName) { - try { - const list = await kv.list({ prefix: "recipe:" }); - const all = (await Promise.all( - list.keys.map((k) => kv.get(k.name, "json")) - )).filter(Boolean); - const tokens = [nodeName.toLowerCase(), ...extractTokens(nodeName)]; - const seen = /* @__PURE__ */ new Set(); - const matched = []; - for (const r of all) { - if (seen.has(r.canonical_id)) continue; - const hay = `${r.canonical_id} ${r.display_name ?? ""} ${r.description ?? ""}`.toLowerCase(); - if (tokens.some((t) => hay.includes(t))) { - seen.add(r.canonical_id); - matched.push(r.canonical_id); - } - } - return matched.slice(0, 3); - } catch { - return []; - } -} -__name(searchSimilarRecipes, "searchSimilarRecipes"); // src/actions/graph-builder.ts init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process(); @@ -10256,12 +10357,12 @@ function buildExecutionGraph(parsed, nodeResults, graphId, graphName, config2) { __name(buildExecutionGraph, "buildExecutionGraph"); // src/actions/cypher-handlers.ts -async function handleCypherSearch(triplets, env2) { +async function handleCypherSearch(triplets, env2, mode = "discover") { const parsed = parseTriplets(triplets); if (!parsed) { throw new Error("\u7121\u6CD5\u89E3\u6790\u4EFB\u4F55\u7BC0\u9EDE"); } - const { nodeResults, missingNodes } = await searchNodes(parsed, void 0, env2); + const { nodeResults, missingNodes } = await searchNodes(parsed, void 0, env2, mode); const graph = buildExecutionGraph(parsed, nodeResults, "cypher-search-result", "Cypher Search Result"); return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: missingNodes }; } @@ -10271,7 +10372,7 @@ async function handleCypherExecute(triplets, context2, graphId, graphName, confi if (!parsed) { throw new Error("\u7121\u6CD5\u89E3\u6790\u4EFB\u4F55\u7BC0\u9EDE"); } - const { nodeResults } = await searchNodes(parsed, config2, env2); + const { nodeResults } = await searchNodes(parsed, config2, env2, "compile"); const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName, config2); const parseResult = graphSchema.safeParse(graph); if (!parseResult.success) { @@ -10350,11 +10451,12 @@ cypherRouter.post("/cypher/search", async (c) => { if (!Array.isArray(rawTriplets) || rawTriplets.length === 0) { return c.json({ error: "triplets \u5FC5\u9808\u70BA\u975E\u7A7A\u5B57\u4E32\u9663\u5217" }, 400); } + const mode = body?.mode === "compile" ? "compile" : "discover"; try { const now2 = /* @__PURE__ */ new Date(); const timestamp = now2.toISOString(); const versionId = `search-v1-${now2.getFullYear()}${String(now2.getMonth() + 1).padStart(2, "0")}${String(now2.getDate()).padStart(2, "0")}-${String(now2.getHours()).padStart(2, "0")}${String(now2.getMinutes()).padStart(2, "0")}${String(now2.getSeconds()).padStart(2, "0")}`; - const result = await handleCypherSearch(rawTriplets, c.env); + const result = await handleCypherSearch(rawTriplets, c.env, mode); const response = { version: versionId, timestamp, diff --git a/tier2/registry/index.js b/tier2/registry/index.js index b107e96..4b9676a 100644 --- a/tier2/registry/index.js +++ b/tier2/registry/index.js @@ -8123,6 +8123,27 @@ __name(isPlainObject, "isPlainObject"); // src/routes/query.ts var app4 = new Hono2(); +app4.get("/catalog", async (c) => { + const list = await c.env.SUBMISSIONS_KV.list({ prefix: "comp:" }); + const seen = /* @__PURE__ */ new Set(); + const components = []; + for (const key of list.keys) { + const raw2 = await c.env.SUBMISSIONS_KV.get(key.name); + if (!raw2) continue; + let v; + try { + v = JSON.parse(raw2); + } catch { + continue; + } + if (v.status === "tombstone" || v.visibility !== "public") continue; + const dedup = `${String(v.component_hash_id ?? "")}:${String(v.version ?? "")}`; + if (seen.has(dedup)) continue; + seen.add(dedup); + components.push(toComponentRecord(v)); + } + return c.json({ success: true, data: { components, count: components.length } }); +}); app4.get("/search", async (c) => { const q = c.req.query("q"); if (!q || q.trim() === "") {