fix(portal): 讀不到就說讀不到——總圖不再把「讀不到」畫成「你沒有」(Arcrun#100)

leo 2026-08-12 打開總圖看到:「0 個實體 · 0 條關聯/知識庫還沒有任何關聯——
上傳文件後 AI 會自動織網」。**而他庫裡有 1854 條三元組。**
那句話會叫他去做一件不需要做的事。

三個獨立的洞疊起來才變成那句謊:
  ① console-dashboard.ts 打 kbdb-graph-plugin 沒帶認證(同段落打 kbdb 的兩支都有帶)
  ② 那顆 worker 不在更新的部署清單裡 ⇒ token 一換它就落單
  ③ **畫面把 null 畫成 0**——後端已經誠實回 null 了,是前端把它變成謊話

為什麼一直沒被發現:graph plugin 原本身上沒有 token ⇒ 門開著 ⇒ 沒帶也進得去。
2026-08-12 輪替後它有了 token,門關上,401 才浮出來。

📍 repo:matrix/arcrun(cypher-executor/src/routes/、console-ui/public/)
📍 票:Leo/Arcrun#100
This commit is contained in:
uncle6me-web
2026-08-12 13:33:53 +08:00
parent e69d6bbc03
commit a5e4caf5cb
9 changed files with 410 additions and 28 deletions
@@ -48,7 +48,7 @@
*/
import { Hono } from 'hono';
import type { Bindings } from '../types';
import { kbdbBase, graphBase } from './kbdb-proxy';
import { kbdbBase, graphBase, graphHeaders } from './kbdb-proxy';
import { validateConsoleSession } from './console-auth';
import {
type KbdbEntry,
@@ -104,6 +104,31 @@ async function fetchJson<T>(url: string, headers?: Record<string, string>): Prom
}
}
/**
* 本租戶三元組的**真實總數**(null = 讀不到,畫面要顯示「讀不到」而非 0)。
*
* 🔴 Arcrun#100:不可以拿 graph-plugin `/triplets/stats` 的 `total` 當數量。
* 那支的 `total` 是**分頁長度**不是 COUNT——它走 `/records/by-template/triplet`KBDB 端
* `searchByTemplate` 預設 limit=100、硬上限 500)且不帶 owner 過濾,所以 1854 條的庫
* 只會回 100。修好 401 之後若還讀它,畫面會從「0」變成「100」——一樣是假的。
* 真相源=KBDB `/records/triplet-stats`(真 SQL COUNT(*)、依 owner_id 過濾、無上限),
* 回 `{ success, stats: [{ library, triplet_count }] }`,加總即全庫條數。
*/
async function fetchTripletTotal(env: Bindings, tenant: string): Promise<number | null> {
const { base, headers } = kbdbBase(env);
const data = await fetchJson<{ stats?: { triplet_count?: unknown }[] }>(
`${base}/records/triplet-stats?owner_id=${encodeURIComponent(tenant)}`,
headers,
);
if (!data || !Array.isArray(data.stats)) return null;
let total = 0;
for (const row of data.stats) {
if (typeof row?.triplet_count !== 'number') return null; // 形狀不對 → 誠實回讀不到,不半信半疑加總
total += row.triplet_count;
}
return total;
}
/** KBDB entries 符合條件的總數(limit=1 只拿 total 欄,不搬資料)。null = 讀不到。 */
async function fetchEntryTotal(env: Bindings, filters: Record<string, string>): Promise<number | null> {
const { base, headers } = kbdbBase(env);
@@ -254,6 +279,7 @@ consoleDashboardRouter.get('/console/dashboard-data', async (c) => {
kbdbHealth,
embedStatus,
graphStats,
tripletTotal,
entriesTotal,
wikiCardTotal,
workflowTotal,
@@ -265,7 +291,13 @@ consoleDashboardRouter.get('/console/dashboard-data', async (c) => {
cachedGiteaSprint(c.env, now, (p) => c.executionCtx.waitUntil(p)),
fetchJson<{ ok?: boolean }>(`${kbdbUrl}/health`, kbdbHeaders),
fetchJson<{ enabled?: boolean; pending?: number; embedded?: number }>(`${kbdbUrl}/embed/backfill/status`, kbdbHeaders),
fetchJson<{ total?: number; recent?: { today?: number; this_week?: number } }>(`${graphUrl}/triplets/stats`),
// graph-plugin 只拿來判「圖服務活著沒」(燈號)——數字不從這裡拿,見 fetchTripletTotal。
// headers 一定要帶:plugin 的 /triplets 前綴掛 Bearer 閘,漏帶=永遠 401=永遠假紅燈(#100)。
fetchJson<{ total?: number; recent?: { today?: number; this_week?: number } }>(
`${graphUrl}/triplets/stats`,
graphHeaders(c.env),
),
fetchTripletTotal(c.env, tenant),
// owner_id 一律鎖本租戶:原本不帶 owner 會混到別租戶(實測 459,137 vs leo 的 458,732
fetchEntryTotal(c.env, { owner_id: tenant }),
fetchEntryTotal(c.env, { entry_type: 'wiki_card', owner_id: tenant }),
@@ -400,13 +432,14 @@ consoleDashboardRouter.get('/console/dashboard-data', async (c) => {
embed: embedStatus
? { enabled: embedStatus.enabled === true, embedded: embedStatus.embedded ?? null, pending: embedStatus.pending ?? null }
: null,
graph: graphStats ? { ok: true, triplets: graphStats.total ?? null } : { ok: false, triplets: null },
// ok = plugin 通不通(graphStats 讀得到就是通);triplets = KBDB 真 COUNT(與 plugin 分頁長度無關)
graph: { ok: graphStats !== null, triplets: tripletTotal },
workflow_total: workflowTotal,
},
kb: {
entries_total: entriesTotal,
wiki_card_total: wikiCardTotal,
triplets_total: graphStats?.total ?? null,
triplets_total: tripletTotal,
},
generated_at: new Date(now).toISOString(),
});
@@ -420,15 +453,15 @@ consoleDashboardRouter.get('/console/dashboard-data', async (c) => {
consoleDashboardRouter.get('/console/kb-scale-data', async (c) => {
const tenant = c.env.CONSOLE_TENANT || 'leo';
const { base, headers } = kbdbBase(c.env);
const graphUrl = graphBase(c.env);
const now = Date.now();
const [wikiCards, graphStats, embedStatus] = await Promise.all([
const [wikiCards, tripletTotal, embedStatus] = await Promise.all([
// limit=1 順手拿最新一筆 created_atlist 為 created_at DESC)=「最近寫入時間」
fetchJson<{ total?: number; entries?: { created_at?: string | number }[] }>(
`${base}/entries?${new URLSearchParams({ owner_id: tenant, entry_type: 'wiki_card', limit: '1' }).toString()}`,
headers,
),
fetchJson<{ total?: number }>(`${graphUrl}/triplets/stats`),
// #100:三元組數改讀 KBDB 真 COUNT,不再讀 graph-plugin 的分頁長度(見 fetchTripletTotal 註)
fetchTripletTotal(c.env, tenant),
fetchJson<{ enabled?: boolean; embedded?: number; pending?: number }>(`${base}/embed/backfill/status`, headers),
]);
const latestMs = parseCreatedAtMs(wikiCards?.entries?.[0]?.created_at ?? null);
@@ -436,7 +469,7 @@ consoleDashboardRouter.get('/console/kb-scale-data', async (c) => {
return c.json({
wiki_card_total: typeof wikiCards?.total === 'number' ? wikiCards.total : null,
wiki_card_latest_ago_minutes: latestMs === null ? -1 : agoMinutes(now, latestMs),
triplets_total: typeof graphStats?.total === 'number' ? graphStats.total : null,
triplets_total: tripletTotal,
embedded: embedStatus?.embedded ?? null,
embed_enabled: embedStatus ? embedStatus.enabled === true : null,
generated_at: new Date(now).toISOString(),