staging 1.4.22: t213 診斷檔雲端半(新端點 GET /portal/daemon/diagnostics)+ portal 按鈕文案改導流

source Arcrun@8447165(93b1140 新端點 + 8447165 portal 文案)
InkStoneCo 總管交辦,走 stage-first 出貨鏈驗收
This commit is contained in:
2026-08-08 13:31:02 +08:00
parent e91e379530
commit 51f84bf4c3
4 changed files with 853 additions and 735 deletions
+86 -41
View File
@@ -13430,6 +13430,90 @@ portalRouter.delete(
});
})
);
async function buildDiagnostics(env, tenant2) {
const notes = [];
let embedding = { checked: false };
try {
const [statusRes, selftestRes] = await Promise.all([
kbdbFetch(env, `/embed/backfill/status?${new URLSearchParams({ owner_id: tenant2 }).toString()}`),
kbdbFetch(env, `/embed/selftest?${new URLSearchParams({ owner_id: tenant2 }).toString()}`)
]);
const statusBody = await statusRes.json().catch(() => null);
const selftestBody = await selftestRes.json().catch(() => null);
embedding = {
checked: true,
module_enabled: statusBody?.enabled ?? false,
// Vectorize+AI binding 都在,才有「index」這回事
cards_embedded: statusBody?.embedded ?? 0,
cards_pending: statusBody?.pending ?? 0,
self_test: {
ran: selftestBody?.tested ?? false,
// 三態:true=能搜到自己 false=搜不到自己(index 收錄有缺)/ null=還沒東西可測或模組未開
found_itself: selftestBody?.tested ? selftestBody?.passed ?? null : null,
note: selftestBody?.note ?? ""
}
};
} catch (e) {
notes.push(`embed \u5065\u5EB7\u72C0\u614B\u67E5\u8A62\u5931\u6557\uFF1A${e instanceof Error ? e.message : String(e)}`);
}
let library_count = 0;
let triplet_count = 0;
const ownerParam = new URLSearchParams({ owner_id: tenant2 }).toString();
try {
const [registeredLibs, autoRes, tripletRes] = await Promise.all([
listRecordsByTemplate(env, LIBRARY_TEMPLATE).catch(() => []),
kbdbFetch(env, `/entries/libraries?${ownerParam}`),
kbdbFetch(env, `/records/triplet-stats?${ownerParam}`)
]);
const knownLibs = new Set(
registeredLibs.map((r) => (r.values.name ?? "").trim()).filter((n) => !!n)
);
const autoBody = await autoRes.json().catch(() => null);
for (const name of autoBody?.libraries ?? []) {
const n = String(name ?? "").trim();
if (n && n !== "general") knownLibs.add(n);
}
library_count = knownLibs.size;
const tripletBody = await tripletRes.json().catch(() => null);
triplet_count = (tripletBody?.stats ?? []).reduce((sum, s) => sum + (Number(s.triplet_count) || 0), 0);
} catch (e) {
notes.push(`\u77E5\u8B58\u5EAB\u898F\u6A21\u67E5\u8A62\u5931\u6557\uFF1A${e instanceof Error ? e.message : String(e)}`);
}
let library_scope_check = { ran: false };
if (library_count === 0 && triplet_count === 0) {
try {
const probeRes = await kbdbFetch(env, `/entries?${new URLSearchParams({ owner_id: tenant2, limit: "1" }).toString()}`);
const probeBody = await probeRes.json().catch(() => null);
const total = probeBody?.total ?? 0;
library_scope_check = {
ran: true,
any_entries_found: total > 0,
note: total > 0 ? `\u9019\u500B\u79DF\u6236\u5E95\u4E0B\u67E5\u5F97\u5230\u5176\u4ED6\u8CC7\u6599\uFF08entries \u5171 ${total} \u7B46\uFF09\uFF0C\u4F46\u5EAB\uFF0F\u4E09\u5143\u7D44\u7D71\u8A08\u4ECD\u56DE 0\u2014\u2014\u50CF\u662F\u67E5\u8A62\u65B9\u5F0F\u6216\u79DF\u6236\u5C0D\u4E0D\u4E0A\uFF0C\u4E0D\u50CF\u771F\u7684\u6C92\u8CC7\u6599\uFF0C\u9700\u8981\u4EBA\u518D\u67E5\u4E00\u6B21` : "\u9019\u500B\u79DF\u6236\u5E95\u4E0B\u5B8C\u5168\u67E5\u4E0D\u5230\u4EFB\u4F55\u8CC7\u6599\u2014\u2014\u6BD4\u8F03\u50CF\u662F\u771F\u7684\u9084\u6C92\u6709\u8CC7\u6599\uFF0C\u4E0D\u662F\u67E5\u8A62\u65B9\u5F0F\u932F\u4E86"
};
} catch (e) {
library_scope_check = {
ran: true,
any_entries_found: null,
note: `\u81EA\u6211\u63A2\u6E2C\u67E5\u8A62\u672C\u8EAB\u5931\u6557\uFF1A${e instanceof Error ? e.message : String(e)}`
};
}
}
return { library_count, triplet_count, library_scope_check, embedding, notes };
}
portalRouter.get(
"/portal/daemon/diagnostics",
(c) => run(c, async () => {
const apiKey = (c.req.header("X-Arcrun-API-Key") ?? "").trim();
if (!apiKey) return c.json({ error: "\u7F3A\u5C11 X-Arcrun-API-Key header" }, 401);
const core = await buildDiagnostics(c.env, apiKey);
return c.json({
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
instance_url: new URL(c.req.url).origin,
bundle_version: c.env.ARCRUN_BUNDLE_VERSION ?? null,
...core
});
})
);
// ../../matrix/arcrun/cypher-executor/src/routes/init-seed.ts
var initSeedRouter = new Hono2();
@@ -14576,51 +14660,12 @@ portalDataRouter.get(
const auth = await requirePortalUser(c);
if (!auth.ok) return auth.res;
const tenant2 = portalTenant(c.env);
const notes = [];
let embedding = { checked: false };
try {
const [statusRes, selftestRes] = await Promise.all([
kbdbFetch(c.env, `/embed/backfill/status?${new URLSearchParams({ owner_id: tenant2 }).toString()}`),
kbdbFetch(c.env, `/embed/selftest?${new URLSearchParams({ owner_id: tenant2 }).toString()}`)
]);
const statusBody = await statusRes.json().catch(() => null);
const selftestBody = await selftestRes.json().catch(() => null);
embedding = {
checked: true,
module_enabled: statusBody?.enabled ?? false,
// Vectorize+AI binding 都在,才有「index」這回事
cards_embedded: statusBody?.embedded ?? 0,
cards_pending: statusBody?.pending ?? 0,
self_test: {
ran: selftestBody?.tested ?? false,
// 三態:true=能搜到自己 false=搜不到自己(index 收錄有缺)/ null=還沒東西可測或模組未開
found_itself: selftestBody?.tested ? selftestBody?.passed ?? null : null,
note: selftestBody?.note ?? ""
}
};
} catch (e) {
notes.push(`embed \u5065\u5EB7\u72C0\u614B\u67E5\u8A62\u5931\u6557\uFF1A${e instanceof Error ? e.message : String(e)}`);
}
let library_count = 0;
let triplet_count = 0;
try {
const mapRes = await kbdbFetch(c.env, `/map?${new URLSearchParams({ owner_id: tenant2 }).toString()}`);
const mapBody = await mapRes.json().catch(() => null);
const libs = Array.isArray(mapBody?.libraries) ? mapBody.libraries : [];
library_count = libs.length;
triplet_count = libs.reduce((sum, l) => sum + (Number(l.triplet_count) || 0), 0);
} catch (e) {
notes.push(`\u77E5\u8B58\u5EAB\u898F\u6A21\u67E5\u8A62\u5931\u6557\uFF1A${e instanceof Error ? e.message : String(e)}`);
}
notes.push("\u76EE\u524D\u96F2\u7AEF\u6C92\u6709\u4FDD\u5B58\u300C\u8403\u53D6/\u4E0A\u50B3\u5931\u6557\u300D\u7684\u6B77\u53F2\u7D00\u9304\uFF0C\u53EA\u80FD\u770B\u5230\u76EE\u524D\u7684\u805A\u5408\u8A08\u6578\uFF08\u4E0A\u9762 cards_pending\uFF0Fcards_embedded\uFF09\uFF1B\u82E5\u8981\u67E5\u67D0\u4E00\u6B21\u5931\u6557\u7684\u7576\u4E0B\u539F\u56E0\uFF0C\u9700\u5728\u5931\u6557\u7576\u4E0B\u7531\u5C01\u6E2C\u8005\u622A\u5716\u540C\u6B65\u5C0F\u5E6B\u624B\u8996\u7A97\u3002");
const core = await buildDiagnostics(c.env, tenant2);
return c.json({
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
instance_url: new URL(c.req.url).origin,
bundle_version: c.env.ARCRUN_BUNDLE_VERSION ?? null,
library_count,
triplet_count,
embedding,
notes
...core
});
})
);
+76 -3
View File
@@ -3115,7 +3115,11 @@ async function recomputeLibraryMap(db, input) {
bridgeMap.set(r.entity, libs);
}
const bridges = [...bridgeMap.entries()].map(([entity, libraries]) => ({ entity, libraries }));
const narrative = input.narrative?.trim() || "";
let narrative = input.narrative?.trim();
if (!narrative) {
const prev = await getLibraryMapDetail(db, library, owner);
narrative = prev?.narrative?.trim() || "";
}
const coreNames = topEntities.slice(0, 3).map((t) => t.name);
const content = `${library}\uFF1A${narrative || "\uFF08narrative \u5F85 ingest \u88DC\u5BEB\uFF09"}\u3002\u6838\u5FC3\uFF1A${coreNames.length ? coreNames.join("\u3001") : "\uFF08\u5C1A\u7121 entities\uFF09"}`;
const blockEntry = await createEntry(db, {
@@ -3171,6 +3175,68 @@ async function recomputeLibraryMap(db, input) {
triplet_library_slot_added: librarySlotAdded
};
}
async function liveTripletCountsByLibrary(db, tripletTemplateId, owner_id) {
const params = owner_id ? [tripletTemplateId, owner_id] : [tripletTemplateId];
const res = await db.prepare(
`SELECT COALESCE(NULLIF(lib_e.content, ''), 'general') AS library, COUNT(*) AS n
FROM (
SELECT DISTINCT ev.record_id
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
WHERE ev.template_id = ?${owner_id ? " AND e.owner_id = ?" : ""}
) AS tr
LEFT JOIN entry_values lev ON lev.record_id = tr.record_id AND lev.slot_name = 'library'
LEFT JOIN entries lib_e ON lib_e.id = lev.entry_id
GROUP BY COALESCE(NULLIF(lib_e.content, ''), 'general')`
).bind(...params).all();
const m = /* @__PURE__ */ new Map();
for (const r of res.results ?? []) m.set(r.library, r.n);
return m;
}
async function knownLibraryNames(db, owner_id) {
const names = /* @__PURE__ */ new Set();
const entryParams = owner_id ? [owner_id] : [];
const entryRows = await db.prepare(
`SELECT DISTINCT json_extract(metadata_json, '$.library') AS library FROM entries
WHERE ${owner_id ? "owner_id = ?" : "1=1"} AND json_extract(metadata_json, '$.library') IS NOT NULL`
).bind(...entryParams).all();
for (const r of entryRows.results ?? []) if (r.library) names.add(r.library);
const libTpl = await getTemplate(db, "portal_library");
if (libTpl) {
const libParams = owner_id ? [libTpl.id, owner_id] : [libTpl.id];
const libRows = await db.prepare(
`SELECT MAX(CASE WHEN ev.slot_name = 'name' THEN e.content END) AS name
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
WHERE ev.template_id = ?${owner_id ? " AND e.owner_id = ?" : ""}
GROUP BY ev.record_id`
).bind(...libParams).all();
for (const r of libRows.results ?? []) if (r.name) names.add(r.name);
}
return names;
}
async function ensureFreshLibraryMaps(db, owner_id, tripletTemplateName = DEFAULT_TRIPLET_TEMPLATE) {
const tripletTpl = await getTemplate(db, tripletTemplateName);
if (!tripletTpl) return;
const [liveCounts, cached, known] = await Promise.all([
liveTripletCountsByLibrary(db, tripletTpl.id, owner_id),
listLibraryMaps(db, owner_id),
knownLibraryNames(db, owner_id)
]);
const cachedByLib = new Map(cached.map((m) => [m.library, m]));
const stale = /* @__PURE__ */ new Set();
for (const [library, count] of liveCounts) {
const c = cachedByLib.get(library);
if (!c || c.triplet_count !== count) stale.add(library);
}
for (const name of known) {
if (!liveCounts.has(name) && !cachedByLib.has(name)) stale.add(name);
}
await Promise.all(
[...stale].map(
(library) => recomputeLibraryMap(db, { library, owner_id, triplet_template: tripletTemplateName }).catch(() => {
})
)
);
}
async function listLibraryMaps(db, owner_id) {
const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME);
if (!tpl) return [];
@@ -3241,11 +3307,18 @@ mapRoutes.post("/recompute", async (c) => {
}
});
mapRoutes.get("/", async (c) => {
const libraries = await listLibraryMaps(c.env.DB, c.req.query("owner_id") || void 0);
const owner = c.req.query("owner_id") || void 0;
await ensureFreshLibraryMaps(c.env.DB, owner).catch(() => {
});
const libraries = await listLibraryMaps(c.env.DB, owner);
return c.json({ success: true, libraries, count: libraries.length });
});
mapRoutes.get("/:library", async (c) => {
const map = await getLibraryMapDetail(c.env.DB, c.req.param("library"), c.req.query("owner_id") || void 0);
const owner = c.req.query("owner_id") || void 0;
const library = c.req.param("library");
await ensureFreshLibraryMaps(c.env.DB, owner).catch(() => {
});
const map = await getLibraryMapDetail(c.env.DB, library, owner);
if (!map) return c.json({ success: false, error: "not found" }, 404);
return c.json({ success: true, map });
});
+689 -689
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
File diff suppressed because one or more lines are too long