t158 重打包(Arcrun@7e631a8):/cypher/search mode 分流——複製路徑回毫秒級

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 13:32:39 +08:00
parent 0bfe37cffb
commit 216d3512c6
3 changed files with 167 additions and 44 deletions
+3 -3
View File
@@ -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"
+143 -41
View File
@@ -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,
+21
View File
@@ -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() === "") {