feat(console): library-map M5 — 總庫搜尋頁藏書地圖首頁區塊(R4)

library-map SDD M5(Arcrun#39):console 首頁從空白搜尋框變全館地圖。

落點裁定:console 是 hash-routing 單頁殼,home 隨 profile(full=駕駛艙、
rag=總庫搜尋)——地圖放總庫搜尋頁搜尋框上方:rag profile 該頁即首頁;
full profile 它是全館入口,「點庫卡片→進該庫搜尋」動線同頁最短,一份實作
兩 profile 全吃。

- console.ts:藏書地圖區塊(lm-section)——每庫一張卡(庫名+narrative+
  top entities 前 5 帶 degree+triplet_count+commit_hash/updated 更新時間感)、
  跨庫 bridges 有資料才列一小區;兩段式渲染(全館列表先出卡,逐庫詳圖補
  degree/bridges,詳圖失敗維持名字版不擋);點庫卡片=設 library filter
  (/kbdb/search 既有 library 參數,不新造)進庫搜尋,附清除鈕搜回全館;
  /map 空陣列或錯誤=誠實空狀態(指引 POST /map/recompute backfill),
  不擋搜尋等其他功能。品牌沿 CONSOLE_BRAND 變數(#21),零外部庫。
- kbdb-proxy.ts:GET /kbdb/map+/kbdb/map/:library 純轉發(rule 07,聚合真身
  在 kbdb base M2)。owner_id 不強制注入只選擇性透傳(同 M4 MCP kbdb_get_map
  語意;現行 backfill map block owner=NULL,強制注入租戶=首頁永遠假空狀態);
  仍要求 X-Arcrun-API-Key 同閘;KBDB 不可達誠實 502。
- 測試:kbdb-map-proxy.test.ts 9 測(租戶閘/轉發/owner 透傳/空陣列/404/502)+
  console-library-map-page.test.ts 6 測(route 200/區塊在搜尋框上方/資料源接線/
  library 參數/誠實空狀態文案/品牌變數)——15/15 過。
- 誠實回報:tsc 16 行 ExecutionContext.tracing 錯誤與 executor.test 1 失敗=
  main 基線既有(git stash 對照實證),非本分支引入。

merge 後需 gated redeploy cypher-executor(leo 閘)。關聯 #39。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUmjwkHLVBHM3ydhT1WSW3
This commit is contained in:
Claude
2026-07-19 09:34:28 +00:00
parent 289cd495b4
commit 10dda9e6f5
5 changed files with 369 additions and 3 deletions
+38
View File
@@ -218,6 +218,44 @@ kbdbProxyRouter.get('/kbdb/graph/neighbors/:name', async (c) => {
}
});
// ── map(藏書地圖,library-map SDD M5/R4console 首頁全館地圖)──────────────────
//
// 純轉發(rule 07):聚合真身在 KBDB 基本盤(kbdb/src/routes/map.tsM2 已 merge)。
// owner_id **不強制注入、只選擇性透傳**——與 MCP kbdb_get_mapM4)同義,理由兩層:
// 1. map block 是庫級聚合摘要(narrativetop entities+規模),非租戶私資料;
// 2. 現行 backfill recompute 未帶 ownermap block owner_id=NULL)——若比照 search
// 強制注入租戶,`e.owner_id = ?` 會把 NULL 列全濾掉 → 首頁永遠假空狀態(不誠實)。
// 仍要求 X-Arcrun-API-Key(與本檔其他端點同閘;console 登入後才有 tenant 字串)。
// GET /kbdb/map — 全館地圖(每庫一行:librarynarrativetop 3 entitiestriplet_countupdated_at)。
kbdbProxyRouter.get('/kbdb/map', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const owner = c.req.query('owner_id');
const qs = owner ? `?owner_id=${encodeURIComponent(owner)}` : '';
try {
const res = await fetch(`${base}/map${qs}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
} catch (e) {
// KBDB 不可達 → 誠實回報(前端顯示「地圖服務不可達」,不擋首頁其他功能)
return c.json({ success: false, error: `KBDB 不可達(${base}):${e instanceof Error ? e.message : String(e)}` }, 502);
}
});
// GET /kbdb/map/:library — 該庫詳圖(完整 top_entities 帶 degreerelation_profilebridgescommit_hash)。
kbdbProxyRouter.get('/kbdb/map/:library', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);
const { base, headers } = kbdbBase(c.env);
const owner = c.req.query('owner_id');
const qs = owner ? `?owner_id=${encodeURIComponent(owner)}` : '';
try {
const res = await fetch(`${base}/map/${encodeURIComponent(c.req.param('library'))}${qs}`, { headers });
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
} catch (e) {
return c.json({ success: false, error: `KBDB 不可達(${base}):${e instanceof Error ? e.message : String(e)}` }, 502);
}
});
// PATCH /kbdb/entries/:id — 更新單筆 entry。owner_id 不可被改(剝除 caller 自帶的 owner_id)。
kbdbProxyRouter.patch('/kbdb/entries/:id', async (c) => {
if (!tenant(c)) return c.json(NEED_KEY, 401);