diff --git a/manifest.json b/manifest.json index 7e253b7..8955266 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "schema": "arcrun-rag-bundles/v1", "built": "2026-07-31", - "source": "Arcrun@062757f", + "source": "Arcrun@341bcb1", "core": [ { "name": "arcrun-array-ops", @@ -503,7 +503,7 @@ "tier": 2, "main_file": "tier2/cypher/index.js", "main_module": "index.js", - "sha256": "73c50e8bc75f13f6f0adcf39d08892d78ceb0ccc34067d3c6533a09c3138b1e0", + "sha256": "0761688567af20a0ccaca1ee07258e5500514703ae1b40d74e5bf6575aad452f", "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": "657f6016b2511eeb1f06222b7f8dc40ec2f7d5d7cd608e7c57d27b5186786f4d", + "sha256": "6e7c6239e03cedf1acd56cc5ef7c30acacab021c9833aa0c87e6909569b1785d", "compat_date": "2025-02-19", "compat_flags": [ "nodejs_compat" diff --git a/tier2/cypher/index.js b/tier2/cypher/index.js index dad4bc1..24ea982 100644 --- a/tier2/cypher/index.js +++ b/tier2/cypher/index.js @@ -9447,6 +9447,61 @@ var init_schemas = __esm({ } }); +// src/actions/execution-evaluator.ts +function componentVerdictsFromTrace(nodes, trace3) { + const componentByNodeId = /* @__PURE__ */ new Map(); + for (const n of nodes) { + if (n.type === "Component" && n.componentId) componentByNodeId.set(n.id, n.componentId); + } + const verdicts = []; + for (const step of trace3) { + const componentId = componentByNodeId.get(step.nodeId); + if (!componentId) continue; + const out = step.output; + const outputSaysFailed = typeof out === "object" && out !== null && !Array.isArray(out) && out.success === false; + verdicts.push({ + component_id: componentId, + success: !step.error && !outputSaysFailed, + duration_ms: Math.max(0, Number(step.duration_ms) || 0) + }); + } + return verdicts; +} +async function recordComponentStats(env2, nodes, trace3) { + try { + const base = (env2.REGISTRY_BASE_URL ?? (env2.WORKER_SUBDOMAIN ? wasmWorkerUrl("registry", env2.WORKER_SUBDOMAIN) : void 0))?.replace(/\/$/, ""); + if (!base) return; + const verdicts = componentVerdictsFromTrace(nodes, trace3); + if (verdicts.length === 0) return; + await Promise.all( + verdicts.map( + (v) => fetch(`${base}/analytics/record`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + canonical_id: v.component_id, + success: v.success, + duration_ms: v.duration_ms + }) + }).catch(() => void 0) + // 統計失敗不影響執行 + ) + ); + } catch { + } +} +var init_execution_evaluator = __esm({ + "src/actions/execution-evaluator.ts"() { + "use strict"; + init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process(); + init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console(); + init_performance2(); + init_component_loader(); + __name(componentVerdictsFromTrace, "componentVerdictsFromTrace"); + __name(recordComponentStats, "recordComponentStats"); + } +}); + // src/actions/webhook-handlers.ts var webhook_handlers_exports = {}; __export(webhook_handlers_exports, { @@ -9504,6 +9559,15 @@ async function executeWebhookGraph(env2, graph, triggerContext, token, apiKey, c agent_user_agent: userAgent }, ctx); recordRecipeStats(env2, executor.usedRecipeKeys, true, Date.now(), ctx); + { + const statsPromise = recordComponentStats( + env2, + parsed.data.nodes, + result.trace + ); + if (ctx?.waitUntil) ctx.waitUntil(statsPromise); + else void statsPromise; + } return { success: true, data: result.data, duration_ms }; } catch (err) { const duration_ms = Date.now() - start; @@ -9519,6 +9583,15 @@ async function executeWebhookGraph(env2, graph, triggerContext, token, apiKey, c if (!isPaused) { recordRecipeStats(env2, executor.usedRecipeKeys, false, Date.now(), ctx); } + if (!isPaused && err instanceof ExecutionError) { + const statsPromise = recordComponentStats( + env2, + parsed.data.nodes, + err.trace + ); + if (ctx?.waitUntil) ctx.waitUntil(statsPromise); + else void statsPromise; + } if (err instanceof ExecutionError) { const traceFormatted = err.trace.map((s) => ({ node: s.nodeId, @@ -9546,6 +9619,7 @@ var init_webhook_handlers = __esm({ init_schemas(); init_component_loader(); init_telemetry(); + init_execution_evaluator(); __name(recordRecipeStats, "recordRecipeStats"); __name(generateToken, "generateToken"); __name(validateAndParseWebhook, "validateAndParseWebhook"); @@ -9909,17 +9983,7 @@ init_types(); init_graph_executor(); init_schemas(); init_component_loader(); - -// src/actions/execution-evaluator.ts -init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process(); -init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console(); -init_performance2(); -async function writeEvaluation(_env, _record) { -} -__name(writeEvaluation, "writeEvaluation"); -async function updateComponentStats(_env, _componentId, _verdict, _durationMs) { -} -__name(updateComponentStats, "updateComponentStats"); +init_execution_evaluator(); // src/actions/triplet-parser.ts init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process(); @@ -9975,7 +10039,7 @@ init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console(); init_performance2(); init_component_loader(); init_recipes(); -async function searchNodes(parsed, config2, env2, mode = "discover") { +async function searchNodes(parsed, config2, env2, mode = "discover", target) { const nodeResults = {}; const missingNodes = []; if (mode === "compile") { @@ -9996,8 +10060,10 @@ async function searchNodes(parsed, config2, env2, mode = "discover") { } 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 wantComponents = target !== "recipe"; + const wantRecipes = target !== "component"; + const catalog = !wantComponents ? { status: "ok", entries: [] } : registryBase ? await fetchCatalog(registryBase) : { status: "unreachable", entries: [] }; + const recipes = wantRecipes && env2?.RECIPES ? await listAllRecipes2(env2.RECIPES) : []; const byId = /* @__PURE__ */ new Map(); for (const e of catalog.entries) { const prev = byId.get(e.canonical_id); @@ -10051,6 +10117,11 @@ async function searchNodes(parsed, config2, env2, mode = "discover") { }; continue; } + const substituted = trySubstitution(nodeName, catalog.entries, recipes); + if (substituted) { + nodeResults[nodeName] = { ...substituted, type: role }; + continue; + } const similarComponents = similarFromCatalog(catalog.entries, nodeName); const similarRecipes = similarFromRecipes(recipes, nodeName); nodeResults[nodeName] = { @@ -10166,6 +10237,64 @@ async function legacyPerNodeLookup(registryBase, componentId, nodeName, role, en }; } __name(legacyPerNodeLookup, "legacyPerNodeLookup"); +function trySubstitution(nodeName, catalogEntries, recipes) { + const lower = nodeName.toLowerCase(); + const serviceHits = SERVICE_HINTS.filter((w) => lower.includes(w)); + if (serviceHits.length > 0) { + const matched = /* @__PURE__ */ new Map(); + for (const r of recipes) { + const hay = `${r.canonical_id} ${r.display_name ?? ""} ${r.description ?? ""}`.toLowerCase(); + if (serviceHits.every((h) => hay.includes(h))) matched.set(r.canonical_id, r); + } + if (matched.size !== 1) return null; + const recipe = [...matched.values()][0]; + return { + status: "resolved", + componentId: recipe.canonical_id, + source: "recipe", + description: recipe.description, + endpoint: recipe.endpoint, + substitution: { + from: nodeName, + componentId: "http_request", + // recipe=http_request+參數模板的具名封裝 + recipe: recipe.canonical_id, + reason: `\u670D\u52D9\u8A5E\u300C${serviceHits.join("\u3001")}\u300D\u552F\u4E00\u547D\u4E2D recipe\u300C${recipe.canonical_id}\u300D\uFF1Bworkflow config \u5BEB component: ${recipe.canonical_id}\uFF08\u5E95\u5C64\u96F6\u4EF6\uFF1Dhttp_request\uFF09\uFF0C\u53EA\u9700\u586B payload` + } + }; + } + const tokens = extractTokens(nodeName); + if (tokens.length === 0) return null; + const byCanonical = /* @__PURE__ */ new Map(); + for (const e of catalogEntries) { + const strongHay = [e.canonical_id, e.display_name ?? "", ...e.aliases ?? []].join(" ").toLowerCase(); + const weakHay = [e.description ?? "", ...e.tags ?? []].join(" ").toLowerCase(); + const strongHits = tokens.filter((t) => strongHay.includes(t)); + const weakCount = tokens.filter((t) => weakHay.includes(t)).length; + const score = strongHits.length * 10 + weakCount; + if (score === 0) continue; + const prev = byCanonical.get(e.canonical_id); + if (!prev || score > prev.score) byCanonical.set(e.canonical_id, { entry: e, score, strongHits }); + } + const ranked = [...byCanonical.values()].sort((a, b) => b.score - a.score); + const top = ranked[0]; + if (!top || top.strongHits.length === 0) return null; + if (ranked[1] && ranked[1].score >= top.score) return null; + return { + status: "resolved", + componentId: top.entry.canonical_id, + source: "component", + input_schema: top.entry.input_schema, + success_rate: typeof top.entry.success_rate === "number" ? top.entry.success_rate : void 0, + stability: typeof top.entry.stability === "string" ? top.entry.stability : void 0, + substitution: { + from: nodeName, + componentId: top.entry.canonical_id, + reason: `\u65B7\u8A5E\u300C${top.strongHits.join("\u3001")}\u300D\u547D\u4E2D\u96F6\u4EF6\u300C${top.entry.canonical_id}\u300D\uFF08${top.entry.display_name ?? ""}\uFF09\u5F37\u6B04\u4F4D\u4E14\u5206\u6578\u552F\u4E00\u6700\u9AD8\uFF1B\u53EA\u9700\u7167 input_schema \u586B payload` + } + }; +} +__name(trySubstitution, "trySubstitution"); var SERVICE_HINTS = [ "google", "gmail", @@ -10357,12 +10486,12 @@ function buildExecutionGraph(parsed, nodeResults, graphId, graphName, config2) { __name(buildExecutionGraph, "buildExecutionGraph"); // src/actions/cypher-handlers.ts -async function handleCypherSearch(triplets, env2, mode = "discover") { +async function handleCypherSearch(triplets, env2, mode = "discover", target) { 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, mode); + const { nodeResults, missingNodes } = await searchNodes(parsed, void 0, env2, mode, target); const graph = buildExecutionGraph(parsed, nodeResults, "cypher-search-result", "Cypher Search Result"); return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: missingNodes }; } @@ -10384,17 +10513,7 @@ async function handleCypherExecute(triplets, context2, graphId, graphName, confi try { const result = await executor.execute(parseResult.data, context2 ?? {}, env2.EXEC_CONTEXT); const duration_ms = Date.now() - start; - const componentId = graph.nodes.find((n) => n.componentId)?.componentId ?? graphId; - const runId = `${graphId}-${Date.now()}`; - waitUntil(writeEvaluation(env2, { - run_id: runId, - workflow_id: graphId, - component_id: componentId, - verdict: "success", - duration_ms, - evaluated_at: Date.now() - })); - waitUntil(updateComponentStats(env2, componentId, "success", duration_ms)); + waitUntil(recordComponentStats(env2, graph.nodes, result.trace)); return { success: true, data: result.data, trace: result.trace, duration_ms, graph }; } catch (err) { const duration_ms = Date.now() - start; @@ -10411,19 +10530,8 @@ async function handleCypherExecute(triplets, context2, graphId, graphName, confi }; } const errMsg = err instanceof Error ? err.message : String(err); - const componentId = graph.nodes.find((n) => n.componentId)?.componentId ?? graphId; - const runId = `${graphId}-${Date.now()}`; - waitUntil(writeEvaluation(env2, { - run_id: runId, - workflow_id: graphId, - component_id: componentId, - verdict: "failed", - duration_ms, - error_message: errMsg.slice(0, 200), - evaluated_at: Date.now() - })); - waitUntil(updateComponentStats(env2, componentId, "failed", duration_ms)); if (err instanceof ExecutionError) { + waitUntil(recordComponentStats(env2, graph.nodes, err.trace)); const traceFormatted = err.trace.map((s) => ({ node: s.nodeId, status: s.error ? "failed" : "success", @@ -10443,20 +10551,130 @@ async function handleCypherExecute(triplets, context2, graphId, graphName, confi } __name(handleCypherExecute, "handleCypherExecute"); +// src/actions/target-search.ts +init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process(); +init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console(); +init_performance2(); +init_component_loader(); + +// src/lib/workflow-search.ts +init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_process(); +init_virtual_unenv_global_polyfill_cloudflare_unenv_preset_node_console(); +init_performance2(); +async function fetchTenantWorkflowSearch(env2, apiKey, q, mode = "semantic") { + const base = (env2.KBDB_BASE_URL ?? "https://arcrun-kbdb.uncle6-me.workers.dev").replace(/\/$/, ""); + const headers = { "Content-Type": "application/json" }; + if (env2.KBDB_INTERNAL_TOKEN) headers["Authorization"] = `Bearer ${env2.KBDB_INTERNAL_TOKEN}`; + const params = new URLSearchParams({ + q, + owner_id: apiKey, + // 租戶隔離(只搜本租戶的 workflow) + entry_type: "workflow", + // base 通用 filter(Q4),只回 workflow entry + mode + }); + return fetch(`${base}/entries/search?${params.toString()}`, { headers }); +} +__name(fetchTenantWorkflowSearch, "fetchTenantWorkflowSearch"); + +// src/actions/target-search.ts +async function searchByTarget(target, query, env2, apiKey) { + if (target === "component") { + const sub = env2.WORKER_SUBDOMAIN; + const registryBase = env2.REGISTRY_BASE_URL ?? (sub ? wasmWorkerUrl("registry", sub) : void 0); + if (!registryBase) return { ok: false, status: 502, error: "registry \u4F4D\u7F6E\u672A\u8A2D\u5B9A\uFF08WORKER_SUBDOMAIN\uFF0FREGISTRY_BASE_URL \u7686\u7F3A\uFF09" }; + try { + const res2 = await fetch( + `${registryBase}/components/search?q=${encodeURIComponent(query)}`, + { signal: AbortSignal.timeout(1e4) } + ); + if (!res2.ok) return { ok: false, status: 502, error: `registry \u641C\u5C0B\u5931\u6557\uFF08HTTP ${res2.status}\uFF09` }; + const body2 = await res2.json(); + return { + ok: true, + body: { + target, + query, + results: body2.data?.results ?? [], + count: body2.data?.count ?? 0 + } + }; + } catch (e) { + return { ok: false, status: 502, error: `registry \u67E5\u4E0D\u901A\uFF1A${e instanceof Error ? e.message : String(e)}` }; + } + } + if (target === "recipe") { + if (!env2.RECIPES) return { ok: false, status: 502, error: "RECIPES KV \u672A\u7D81\u5B9A" }; + const all = await listAllRecipes2(env2.RECIPES); + const q = query.toLowerCase(); + const seen = /* @__PURE__ */ new Set(); + const results = []; + 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: "\u641C\u7684\u662F\u672C\u90E8\u7F72\u79C1\u5EAB\uFF08workflow \u53EF\u76F4\u63A5 component: \u5F15\u7528\uFF09\u3002\u516C\u5EAB\uFF08\u591A\u4F5C\u8005\u5E02\u5834\uFF09\u8D70 MCP arcrun_recipe_search\uFF0FGET /public-recipes\u3002" + } + }; + } + if (!apiKey) return { ok: false, status: 401, error: "target=workflow \u9700\u8981 X-Arcrun-API-Key header\uFF08workflow \u641C\u5C0B\u9650\u672C\u79DF\u6236\uFF09" }; + const res = await fetchTenantWorkflowSearch(env2, apiKey, query); + if (!res.ok) return { ok: false, status: 502, error: `workflow \u641C\u5C0B\u5931\u6557\uFF08KBDB HTTP ${res.status}\uFF09` }; + const body = await res.json(); + return { ok: true, body: { target, query, ...body } }; +} +__name(searchByTarget, "searchByTarget"); + // src/routes/cypher.ts var cypherRouter = new Hono2(); +var VALID_TARGETS = /* @__PURE__ */ new Set(["component", "recipe", "workflow"]); cypherRouter.post("/cypher/search", async (c) => { const body = await c.req.json(); const rawTriplets = body?.triplets; + const target = typeof body?.target === "string" ? body.target : void 0; + if (target !== void 0 && !VALID_TARGETS.has(target)) { + return c.json({ error: `target \u53EA\u63A5\u53D7 component\uFF0Frecipe\uFF0Fworkflow\uFF0C\u6536\u5230\u300C${target}\u300D` }, 400); + } + const query = typeof body?.query === "string" ? body.query.trim() : ""; + if (query) { + if (!target) { + return c.json({ error: "\u7D66 query \u5FC5\u9808\u540C\u6642\u7D66 target\uFF08component\uFF0Frecipe\uFF0Fworkflow\uFF09\uFF0C\u6307\u660E\u8981\u641C\u54EA\u500B\u5EAB" }, 400); + } + const apiKey = c.req.header("X-Arcrun-API-Key") ?? void 0; + const r = await searchByTarget(target, query, c.env, apiKey); + if (!r.ok) return c.json({ error: r.error }, r.status); + return c.json(r.body); + } if (!Array.isArray(rawTriplets) || rawTriplets.length === 0) { - return c.json({ error: "triplets \u5FC5\u9808\u70BA\u975E\u7A7A\u5B57\u4E32\u9663\u5217" }, 400); + return c.json({ error: "triplets \u5FC5\u9808\u70BA\u975E\u7A7A\u5B57\u4E32\u9663\u5217\uFF08\u6216\u7D66 query + target \u505A\u540D\u5B57\u641C\u5C0B\uFF09" }, 400); } const mode = body?.mode === "compile" ? "compile" : "discover"; + if (target && mode === "compile") { + return c.json({ error: "mode=compile\uFF08\u8907\u88FD\u8DEF\u5F91\uFF09\u4E0D\u67E5\u5EAB\uFF0C\u4E0D\u63A5\u53D7 target\uFF1B\u8981\u6307\u5B9A\u641C\u5C0B\u5C0D\u8C61\u8ACB\u7528 discover\uFF08\u9810\u8A2D\uFF09" }, 400); + } + if (target === "workflow") { + return c.json({ error: 'target=workflow \u662F\u540D\u5B57\u641C\u5C0B\uFF0C\u8ACB\u6539\u5E36 { target: "workflow", query: "..." }\uFF08\u4E0D\u5403 triplets\uFF09' }, 400); + } 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, mode); + const result = await handleCypherSearch(rawTriplets, c.env, mode, target); const response = { version: versionId, timestamp, @@ -11404,18 +11622,7 @@ webhooksNamedRouter.get("/workflows/search", async (c) => { const q = c.req.query("q"); if (!q) return c.json({ error: "q \u5FC5\u586B\uFF1A\u7528\u81EA\u7136\u8A9E\u8A00\u63CF\u8FF0\u8981\u627E\u7684\u5DE5\u4F5C\u6D41\uFF08\u5982\u300C\u628A\u8CC7\u6599\u5BEB\u9032 Google Sheets\u300D\uFF09" }, 400); const mode = c.req.query("mode") === "keyword" ? "keyword" : "semantic"; - const base = (c.env.KBDB_BASE_URL ?? "https://arcrun-kbdb.uncle6-me.workers.dev").replace(/\/$/, ""); - const headers = { "Content-Type": "application/json" }; - if (c.env.KBDB_INTERNAL_TOKEN) headers["Authorization"] = `Bearer ${c.env.KBDB_INTERNAL_TOKEN}`; - const params = new URLSearchParams({ - q, - owner_id: apiKey, - // 租戶隔離(只搜本租戶的 workflow) - entry_type: "workflow", - // base 通用 filter(Q4),只回 workflow entry - mode - }); - const res = await fetch(`${base}/entries/search?${params.toString()}`, { headers }); + const res = await fetchTenantWorkflowSearch(c.env, apiKey, q, mode); return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } }); }); webhooksNamedRouter.post("/workflows/backfill-search-entries", async (c) => { @@ -14003,43 +14210,6 @@ portalRouter.get( return c.json({ success: true, libraries: libs.map(toPublicLibrary), count: libs.length }); }) ); -portalRouter.post( - "/portal/admin/libraries", - (c) => run(c, async () => { - const auth = await requirePortalAdmin(c); - if (!auth.ok) return auth.res; - const body = await c.req.json().catch(() => null); - const name = String(body?.name ?? "").trim(); - if (!isValidLibraryName(name) || name === "*") { - return c.json({ error: '\u5EAB\u540D\u9650 A-Za-z0-9_-\uFF081-64 \u5B57\u5143\uFF1B"*" \u662F\u4FDD\u7559\u503C\u4E0D\u53EF\u767B\u8A18\uFF09' }, 400); - } - const seeded = await ensurePortalTemplates(c.env); - if (seeded.errors.length > 0) { - return c.json({ error: `portal templates seed \u5931\u6557\uFF1A${seeded.errors.join("; ")}` }, 502); - } - const existing = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE); - if (existing.some((l) => (l.values.name ?? "") === name)) { - return c.json({ error: `\u5EAB ${name} \u5DF2\u767B\u8A18` }, 409); - } - const ns = portalNamespace(c.env); - const res = await kbdbFetch(c.env, "/records", { - method: "POST", - body: JSON.stringify({ - template: LIBRARY_TEMPLATE, - owner_id: ns, - values: { - name, - display_name: String(body?.display_name ?? "").trim() || name, - description: String(body?.description ?? "").trim(), - status: "active" - } - }) - }); - if (!res.ok) throw new KbdbError(`POST /records\uFF08portal_library\uFF09\u2192 ${res.status}`); - const created = await res.json(); - return c.json({ success: true, library: created.record ? toPublicLibrary(created.record) : { name } }); - }) -); portalRouter.post( "/portal/daemon/libraries", (c) => run(c, async () => { diff --git a/tier2/registry/index.js b/tier2/registry/index.js index 4b9676a..1e72324 100644 --- a/tier2/registry/index.js +++ b/tier2/registry/index.js @@ -2099,14 +2099,14 @@ var Hono = class _Hono { * app.route("/api", app2) // GET /api/user * ``` */ - route(path, app7) { + route(path, app8) { const subApp = this.basePath(path); - app7.routes.map((r) => { + app8.routes.map((r) => { let handler; - if (app7.errorHandler === errorHandler) { + if (app8.errorHandler === errorHandler) { handler = r.handler; } else { - handler = /* @__PURE__ */ __name(async (c, next) => (await compose([], app7.errorHandler)(c, () => r.handler(c, next))).res, "handler"); + handler = /* @__PURE__ */ __name(async (c, next) => (await compose([], app8.errorHandler)(c, () => r.handler(c, next))).res, "handler"); handler[COMPOSED_HANDLER] = r.handler; } subApp.#addRoute(r.method, r.path, handler); @@ -8195,16 +8195,114 @@ app5.post("/", async (c) => { }); var init_default = app5; -// src/index.ts +// src/actions/recordAnalytics.ts +async function recordAnalytics(input, env2) { + const hashId = input.canonical_id.startsWith("cmp_") ? input.canonical_id : await env2.SUBMISSIONS_KV.get(`idx:${input.canonical_id}`); + if (!hashId) { + return { ok: false, error: `\u96F6\u4EF6 ${input.canonical_id} \u4E0D\u5728\u7D22\u5F15` }; + } + const list = await env2.SUBMISSIONS_KV.list({ prefix: `comp:${hashId}:` }); + let targetKey = null; + let targetRecord = null; + let bestScore = -Infinity; + for (const key of list.keys) { + const raw2 = await env2.SUBMISSIONS_KV.get(key.name); + if (!raw2) continue; + let v; + try { + v = JSON.parse(raw2); + } catch { + continue; + } + if (v.status === "tombstone") continue; + if (input.version) { + if (String(v.version) === input.version) { + targetKey = key.name; + targetRecord = v; + break; + } + continue; + } + const score = computeScore(v); + if (score > bestScore) { + bestScore = score; + targetKey = key.name; + targetRecord = v; + } + } + if (!targetKey || !targetRecord) { + return { ok: false, error: `\u96F6\u4EF6 ${input.canonical_id} \u7121\u53EF\u7528\u7248\u672C\u8A18\u9304` }; + } + const version2 = String(targetRecord.version ?? "v1"); + const canonicalId = String(targetRecord.canonical_id ?? input.canonical_id); + const statsKey = `stats:${hashId}:${version2}`; + let counters = { total_runs: 0, success_runs: 0, total_ms: 0 }; + const rawStats = await env2.ANALYTICS_KV.get(statsKey); + if (rawStats) { + try { + const parsed = JSON.parse(rawStats); + counters = { + total_runs: Number(parsed.total_runs) || 0, + success_runs: Number(parsed.success_runs) || 0, + total_ms: Number(parsed.total_ms) || 0 + }; + } catch { + } + } + counters.total_runs += 1; + counters.success_runs += input.success ? 1 : 0; + counters.total_ms += Math.max(0, Number(input.duration_ms) || 0); + await env2.ANALYTICS_KV.put(statsKey, JSON.stringify(counters)); + const successRate = counters.success_runs / counters.total_runs; + const avgDurationMs = Math.round(counters.total_ms / counters.total_runs); + targetRecord.success_rate = successRate; + targetRecord.avg_duration_ms = avgDurationMs; + targetRecord.call_count = counters.total_runs; + await env2.SUBMISSIONS_KV.put(targetKey, JSON.stringify(targetRecord)); + return { + ok: true, + canonical_id: canonicalId, + version: version2, + total_runs: counters.total_runs, + success_runs: counters.success_runs, + success_rate: successRate, + avg_duration_ms: avgDurationMs + }; +} +__name(recordAnalytics, "recordAnalytics"); + +// src/routes/analytics.ts var app6 = new Hono2(); -app6.use("*", cors()); -app6.get("/", (c) => c.json({ service: "component-registry", version: "1.0.0", status: "ok" })); -app6.route("/components/guide", guide_default); -app6.route("/components/validate-contract", validateContract_default); -app6.route("/components", query_default); -app6.route("/components", components_default); -app6.route("/init", init_default); -var index_default = app6; +app6.post("/record", async (c) => { + const body = await c.req.json().catch(() => null); + if (!body || typeof body.canonical_id !== "string" || body.canonical_id.trim() === "") { + return c.json({ ok: false, error: "canonical_id \u5FC5\u586B" }, 400); + } + if (typeof body.success !== "boolean") { + return c.json({ ok: false, error: "success \u5FC5\u9808\u70BA boolean" }, 400); + } + const result = await recordAnalytics({ + canonical_id: body.canonical_id.trim(), + version: typeof body.version === "string" && body.version !== "" ? body.version : void 0, + success: body.success, + duration_ms: typeof body.duration_ms === "number" ? body.duration_ms : 0 + }, c.env); + if (!result.ok) return c.json(result, 404); + return c.json(result); +}); +var analytics_default = app6; + +// src/index.ts +var app7 = new Hono2(); +app7.use("*", cors()); +app7.get("/", (c) => c.json({ service: "component-registry", version: "1.0.0", status: "ok" })); +app7.route("/components/guide", guide_default); +app7.route("/components/validate-contract", validateContract_default); +app7.route("/components", query_default); +app7.route("/components", components_default); +app7.route("/init", init_default); +app7.route("/analytics", analytics_default); +var index_default = app7; export { index_default as default };