arm 候選 batch(Arcrun@341bcb1):step3 誠實化+step4 意圖替換+step6 統計回寫+t158 mode 分流+t159 daemon 登記+t160 現行 ui
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@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"
|
||||
|
||||
+260
-90
@@ -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: <canonical_id> \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 () => {
|
||||
|
||||
+111
-13
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user