ship 1.4.59:Arcrun@f87260b1234f

This commit is contained in:
ship
2026-08-27 19:50:15 +08:00
parent 472407621d
commit f54c9d8df2
9 changed files with 304 additions and 116 deletions
+111 -62
View File
@@ -3195,15 +3195,6 @@ function kbdbBase(env) {
function tenant(c) {
return c.req.header("X-Arcrun-API-Key") ?? null;
}
function graphBase(env) {
if (env.KBDB_GRAPH_URL) return env.KBDB_GRAPH_URL.replace(/\/$/, "");
return `https://kbdb-graph-plugin.${env.WORKER_SUBDOMAIN}.workers.dev`;
}
function graphHeaders(env) {
const headers = {};
if (env.KBDB_INTERNAL_TOKEN) headers["Authorization"] = `Bearer ${env.KBDB_INTERNAL_TOKEN}`;
return headers;
}
var kbdbProxyRouter, NEED_KEY;
var init_kbdb_proxy = __esm({
"cypher-executor/src/routes/kbdb-proxy.ts"() {
@@ -3359,14 +3350,23 @@ var init_kbdb_proxy = __esm({
return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } });
});
kbdbProxyRouter.get("/kbdb/graph/neighbors/:name", async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const base = graphBase(c.env);
const headers = graphHeaders(c.env);
const owner = tenant(c);
if (!owner) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const params = new URLSearchParams();
params.set("owner_id", owner);
for (const k of ["depth", "template", "directed"]) {
const v = c.req.query(k);
if (v) params.set(k, v);
}
try {
const res = await fetch(`${base}/graph/neighbors/${encodeURIComponent(c.req.param("name"))}`, { headers });
const res = await fetch(
`${base}/graph/neighbors/${encodeURIComponent(c.req.param("name"))}?${params.toString()}`,
{ headers }
);
return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } });
} catch (e) {
return c.json({ error: `kbdb-graph-plugin \u4E0D\u53EF\u9054\uFF08${base}\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502);
return c.json({ error: `KBDB \u4E0D\u53EF\u9054\uFF08${base}\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502);
}
});
kbdbProxyRouter.get("/kbdb/map", async (c) => {
@@ -15047,7 +15047,6 @@ consoleDashboardRouter.get("/console/dashboard-data", async (c) => {
const tenant2 = knowledgeOwner(c.env);
const now2 = Date.now();
const { base: kbdbUrl, headers: kbdbHeaders2 } = kbdbBase(c.env);
const graphUrl = graphBase(c.env);
const [
beatEntries,
taskEntries,
@@ -15056,7 +15055,7 @@ consoleDashboardRouter.get("/console/dashboard-data", async (c) => {
giteaSprint,
kbdbHealth,
embedStatus,
graphStats,
graphProbe,
tripletTotal,
entriesTotal,
wikiCardTotal,
@@ -15069,11 +15068,15 @@ consoleDashboardRouter.get("/console/dashboard-data", async (c) => {
cachedGiteaSprint(c.env, now2, (p) => c.executionCtx.waitUntil(p)),
fetchJson(`${kbdbUrl}/health`, kbdbHeaders2),
fetchJson(`${kbdbUrl}/embed/backfill/status`, kbdbHeaders2),
// graph-plugin 只拿來判「圖服務活著沒(燈號)——數字不從這裡拿,見 fetchTripletTotal。
// headers 一定要帶:plugin 的 /triplets 前綴掛 Bearer 閘,漏帶=永遠 401=永遠假紅燈(#100)。
// 圖服務活著沒(燈號)——數字不從這裡拿,見 fetchTripletTotal。
// 🔴 inkstone/Arcrun#168 收斂:原本探的是 kbdb-graph-plugin 的 /triplets/stats
// 但圖能力已收斂回 KBDBGET /graph/neighbors/:node)⇒ 探那顆 plugin 等於在量一個
// 沒有人走的服務:它沒裝就永遠紅燈、裝了也只證明一個不再被使用的東西活著。
// 改探 KBDB 那支端點本身——用一個不存在的節點名,它會誠實回 count:0 的 200
//graph-query.ts:查不到就是空結果,不是錯誤),正好當「這條路通不通」的探針。
fetchJson(
`${graphUrl}/triplets/stats`,
graphHeaders(c.env)
`${kbdbUrl}/graph/neighbors/${encodeURIComponent("__arcrun_graph_probe__")}?depth=1`,
kbdbHeaders2
),
fetchTripletTotal(c.env, tenant2),
// owner_id 一律鎖本租戶:原本不帶 owner 會混到別租戶(實測 459,137 vs leo 的 458,732
@@ -15183,8 +15186,8 @@ consoleDashboardRouter.get("/console/dashboard-data", async (c) => {
system: {
kbdb_ok: kbdbHealth ? kbdbHealth.ok === true : false,
embed: embedStatus ? { enabled: embedStatus.enabled === true, embedded: embedStatus.embedded ?? null, pending: embedStatus.pending ?? null } : null,
// ok = plugin 通不通(graphStats 讀得到就是通);triplets = KBDB 真 COUNT與 plugin 分頁長度無關
graph: { ok: graphStats !== null, triplets: tripletTotal },
// ok = 圖查詢通不通(探針讀得到就是通);triplets = KBDB 真 COUNT兩件事,不互相吞
graph: { ok: graphProbe !== null, triplets: tripletTotal },
workflow_total: workflowTotal
},
kb: {
@@ -15270,7 +15273,6 @@ consoleDashboardRouter.post("/console/triage-check", async (c) => {
// cypher-executor/src/routes/portal-data.ts
init_dist();
init_kbdb_proxy();
init_webhook_handlers();
// cypher-executor/src/lib/app-system.ts
@@ -17001,7 +17003,7 @@ function unwrapWorkflowData(data, key) {
}
return outer;
}
function mapGraphWorkflowOutput(data) {
function mapGraphNeighborsResponse(data) {
const layer = unwrapWorkflowData(data, "neighbors");
const neighbors = Array.isArray(layer.neighbors) ? layer.neighbors : [];
const edges = Array.isArray(layer.edges) ? layer.edges : [];
@@ -17168,6 +17170,14 @@ portalDataRouter.get(
return c.json({ success: true, entry });
})
);
async function fetchNeighborsFromKbdb(env, tenant2, node, depth) {
const qs = new URLSearchParams();
qs.set("depth", String(depth));
qs.set("template", "triplet");
const res = await kbdbFetch(env, `/graph/neighbors/${encodeURIComponent(node)}?${qs.toString()}&${ownerQuery(tenant2)}`);
const body = await res.json().catch(() => null);
return { ok: res.ok, status: res.status, body };
}
portalDataRouter.get(
"/portal/data/graph/neighbors/:name",
(c) => run(c, async () => {
@@ -17177,50 +17187,41 @@ portalDataRouter.get(
if (!await hasGraphAccess(c.env, libraries)) {
return c.json({ error: "\u7121\u77E5\u8B58\u5716\u8B5C\u6AA2\u8996\u6B0A\u9650" }, 403);
}
const nodeName = normalizeCjkQuery(c.req.param("name"));
const tenant2 = knowledgeOwner(c.env);
const wfGraph = await getTenantWorkflowGraph(c.env, "graph_neighbors");
if (wfGraph) {
const depthRaw = c.req.query("depth") ?? "";
const depth = /^\d{1,2}$/.test(depthRaw) ? Number(depthRaw) : 2;
const result = await executeWebhookGraph(
c.env,
wfGraph,
// t116: 補傳 kbdb_baset128: 補傳 templateworkflow fetch_triplets.url 用 {{input.template}}
{ node: nodeName, depth, namespace: tenant2, owner: tenant2, kbdb_base: c.env.KBDB_BASE_URL ?? "", template: "triplet" },
"graph_neighbors",
tenant2,
c.executionCtx
);
if (!result.success) {
return c.json({ error: `graph_neighbors workflow \u57F7\u884C\u5931\u6557\uFF1A${result.error ?? "\u672A\u77E5\u932F\u8AA4"}` }, 502);
}
return c.json(mapGraphWorkflowOutput(result.data));
}
const base = graphBase(c.env);
const headers = graphHeaders(c.env);
const rawName = c.req.param("name");
const depthRaw = c.req.query("depth") ?? "";
const depth = /^\d{1,2}$/.test(depthRaw) ? Number(depthRaw) : 2;
const tryNames = [rawName];
const normalized = normalizeCjkQuery(rawName);
if (normalized !== rawName) tryNames.push(normalized);
let first = null;
try {
const res = await fetch(`${base}/graph/neighbors/${encodeURIComponent(nodeName)}`, { headers });
if (!res.ok) {
return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } });
for (const name of tryNames) {
const r = await fetchNeighborsFromKbdb(c.env, tenant2, name, depth);
if (!first) first = r;
if (!r.ok) break;
const mapped = mapGraphNeighborsResponse(r.body);
if (mapped.count > 0) return c.json(mapped);
}
const resText = await res.text().catch(() => "");
let data = null;
try {
data = JSON.parse(resText);
} catch {
}
if (data && Array.isArray(data.neighbors) && data.neighbors.length === 0 && Array.isArray(data.edges) && data.edges.length === 0) {
const fallbackName = await fuzzyFindNode(c.env, tenant2, nodeName);
if (fallbackName && fallbackName !== nodeName) {
const res2 = await fetch(`${base}/graph/neighbors/${encodeURIComponent(fallbackName)}`, { headers });
return new Response(res2.body, { status: res2.status, headers: { "Content-Type": "application/json" } });
if (first?.ok) {
const fallbackName = await fuzzyFindNode(c.env, tenant2, rawName);
if (fallbackName && !tryNames.includes(fallbackName)) {
const r = await fetchNeighborsFromKbdb(c.env, tenant2, fallbackName, depth);
if (r.ok) {
const mapped = mapGraphNeighborsResponse(r.body);
if (mapped.count > 0) return c.json(mapped);
}
}
}
return new Response(resText, { status: res.status, headers: { "Content-Type": "application/json" } });
} catch (e) {
return c.json({ error: `kbdb-graph-plugin \u4E0D\u53EF\u9054\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502);
return c.json({ error: `KBDB \u5716\u8B5C\u67E5\u8A62\u4E0D\u53EF\u9054\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502);
}
if (!first) return c.json({ error: "KBDB \u5716\u8B5C\u67E5\u8A62\u6C92\u6709\u56DE\u61C9" }, 502);
if (!first.ok) {
const err = first.body && typeof first.body === "object" ? first.body : { error: `KBDB \u5716\u8B5C\u67E5\u8A62\u5931\u6557\uFF08HTTP ${first.status}\uFF09` };
return c.json(err, first.status);
}
return c.json(mapGraphNeighborsResponse(first.body));
})
);
portalDataRouter.get(
@@ -17538,6 +17539,29 @@ function canReadRecord(rec, tenant2, libraries) {
const lib = recordLibrary(rec.values);
return lib === null || canReadLibrary(libraries, lib);
}
function cardOriginalLocation(entries, library, libraryRoot) {
for (const e of entries) {
if (typeof e.metadata_json !== "string" || !e.metadata_json) continue;
let meta;
try {
meta = JSON.parse(e.metadata_json);
} catch {
continue;
}
const sourcePath = typeof meta.source_path === "string" && meta.source_path.trim() ? meta.source_path.trim() : null;
const machine = typeof meta.machine_label === "string" && meta.machine_label.trim() ? meta.machine_label.trim() : typeof meta.machine === "string" && meta.machine.trim() ? meta.machine.trim() : null;
if (!sourcePath && !machine) continue;
const root = libraryRoot && libraryRoot.trim() ? libraryRoot.trim() : null;
return {
machine,
library,
library_root: root,
source_path: sourcePath,
full_path: root && sourcePath ? `${root.replace(/\/+$/, "")}/${sourcePath.replace(/^\/+/, "")}` : null
};
}
return null;
}
portalDataRouter.get(
"/portal/data/library-cards",
(c) => run(c, async () => {
@@ -17579,7 +17603,19 @@ portalDataRouter.get(
}
const entries = filterDeprecatedEntries(body.entries);
if (entries.length === 0) return notFound(c);
return c.json({ success: true, library, page_name: pageName, entries, count: entries.length });
const libRecords = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE).catch(() => []);
const libRootRaw = libRecords.find((r) => String(r.values.name ?? "") === library)?.values.root;
const libraryRoot = typeof libRootRaw === "string" ? libRootRaw : null;
const location = cardOriginalLocation(entries, library, libraryRoot);
return c.json({
success: true,
library,
page_name: pageName,
entries,
count: entries.length,
location,
...location ? {} : { location_hint: "\u9019\u5F35\u5361\u7684\u6BB5\u843D\u6C92\u6709 machine/source_path \u4E2D\u7E7C\u8CC7\u6599\uFF0C\u7B54\u4E0D\u51FA\u539F\u6587\u7684\u5BE6\u9AD4\u4F4D\u7F6E" }
});
})
);
portalDataRouter.get(
@@ -17711,6 +17747,19 @@ portalDataRouter.get(
if (!auth.ok) return auth.res;
const libraries = parseLibraries(auth.user.values.libraries);
if (libraries.length === 0) return c.json({ success: true, records: [], count: 0, total: 0 });
const template = c.req.param("template");
if (template === LIBRARY_TEMPLATE) {
const all = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE);
const records2 = libraries.includes("*") ? all : all.filter((r) => libraries.includes(String(r.values.name ?? "")));
return c.json({
success: true,
records: records2,
count: records2.length,
page_size: all.length,
filtered_out: all.length - records2.length,
total: records2.length
});
}
const tenant2 = knowledgeOwner(c.env);
const params = new URLSearchParams(ownerQuery(tenant2));
for (const k of ["limit", "offset"]) {