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:
+3
-3
@@ -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
@@ -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,
|
||||
|
||||
@@ -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() === "") {
|
||||
|
||||
Reference in New Issue
Block a user