portal: 搜尋濾內部型別(value/workflow 雜項列)+總圖頁(Arcrun#39 藏書地圖 GUI 切片)

- filterDeprecatedEntries 加 INTERNAL_ENTRY_TYPES(value/workflow):
  搜尋不再出現無標題雜項列與工作流定義(leo 客戶測試回饋);
  #46 上游修好後隨治標段一併拔除。測試 49/49 綠。
- 新增 GET /portal/data/graph/overview:全租戶 active 三元組 →
  {nodes(degree), edges},D-4 graph 粗閘、kbdbFetch 直讀、上限 500 誠實截斷。
- portal 新增「總圖」頁:nav 隨 graph_allowed 顯示;力導向佈局手刻
  (固定種子=確定性、零外部套件)、紙感樣式(green 節點/amber hub)、
  邊 hover 顯示謂詞、點節點跳該實體圖譜搜尋;頁尾連 00-MAP.md
  (#39「人機共用同一份地圖」的 AI 注入文字版)。
- #39 本體(library_map Template/ingest 重算/MCP 注入)仍歸該 issue SDD。
This commit is contained in:
Claude (總管雲端)
2026-07-18 08:17:22 +00:00
parent 6e0df065fd
commit a82c9bbb82
4 changed files with 219 additions and 4 deletions
+54 -2
View File
@@ -97,14 +97,19 @@ function canReadLibrary(userLibraries: string[], library: string): boolean {
/**
* 搜尋殘影過濾(**Arcrun#46 上游修好前的 portal 端治標**——舊管線的 deprecated 產物還躺在
* KBDB 裡污染搜尋結果;根治=上游清資料/重建索引,那修好後這段可整段拔掉)。
* 濾掉:metadata_json.status === 'deprecated' 的 entry、content 以「(舊管線產物」開頭的 entry
* 濾掉:metadata_json.status === 'deprecated' 的 entry、content 以「(舊管線產物」開頭的 entry
* 內部型別 entryvalueslot 值外漏的無標題雜項列、workflow=工作流定義——都是系統內部件,
* 不是給搜尋用戶看的內容;2026-07-18 leo 客戶測試回饋「搜尋結果偶見無標題雜項列」)。
* metadata_json parse 失敗 → 視為保留(治標不誤殺;壞 metadata ≠ deprecated)。
* 純函式(單測用 export)。
*/
export function filterDeprecatedEntries<T extends { metadata_json?: string | null; content?: string | null }>(
const INTERNAL_ENTRY_TYPES = new Set(['value', 'workflow']);
export function filterDeprecatedEntries<T extends { metadata_json?: string | null; content?: string | null; entry_type?: string | null }>(
entries: T[],
): T[] {
return entries.filter((e) => {
if (e.entry_type && INTERNAL_ENTRY_TYPES.has(e.entry_type)) return false;
try {
const meta = JSON.parse(e.metadata_json ?? 'null') as { status?: unknown } | null;
if (meta && meta.status === 'deprecated') return false;
@@ -235,6 +240,53 @@ portalDataRouter.get('/portal/data/graph/neighbors/:name', (c) =>
}),
);
// GET /portal/data/graph/overview — 書庫總圖(Arcrun#39 藏書地圖的 GUI 資料切片):
// 全租戶 active 三元組 → {nodes:[{name,degree}], edges:[{subject,predicate,object}]}。
// 權限=與 neighbors 同一道 D-4 graph 粗閘;資料直讀 KBDB records(與 search 同 kbdbFetch 路徑,
// 不經 graph plugin、不撞 1042)。邊數上限 500(demo 量級遠低於此;超限誠實截斷並回 truncated)。
portalDataRouter.get('/portal/data/graph/overview', (c) =>
run(c, async () => {
const auth = await requirePortalUser(c);
if (!auth.ok) return auth.res;
const libraries = parseLibraries(auth.user.values.libraries);
if (!(await hasGraphAccess(c.env, libraries))) {
return c.json({ error: '無知識圖譜檢視權限' }, 403);
}
const tenant = portalTenant(c.env);
const res = await kbdbFetch(c.env, `/records/by-template/triplet?owner_id=${encodeURIComponent(tenant)}`);
if (!res.ok) {
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
}
const body = (await res.json().catch(() => null)) as
| { records?: { values?: Record<string, unknown> }[] }
| null;
const records = body && Array.isArray(body.records) ? body.records : [];
const EDGE_CAP = 500;
const seen = new Set<string>();
const edges: { subject: string; predicate: string; object: string }[] = [];
const degree = new Map<string, number>();
let truncated = false;
for (const r of records) {
const v = r && typeof r.values === 'object' && r.values ? r.values : null;
if (!v) continue;
if (v.status === 'deprecated') continue;
const s = typeof v.subject === 'string' ? v.subject.trim() : '';
const o = typeof v.object === 'string' ? v.object.trim() : '';
if (!s || !o) continue;
const p = typeof v.predicate === 'string' ? v.predicate : '';
const key = `${s}${p}${o}`;
if (seen.has(key)) continue;
seen.add(key);
if (edges.length >= EDGE_CAP) { truncated = true; break; }
edges.push({ subject: s, predicate: p, object: o });
degree.set(s, (degree.get(s) ?? 0) + 1);
degree.set(o, (degree.get(o) ?? 0) + 1);
}
const nodes = [...degree.entries()].map(([name, d]) => ({ name, degree: d }));
return c.json({ nodes, edges, node_count: nodes.length, edge_count: edges.length, truncated });
}),
);
// GET /portal/data/chat?question=... — AI 問答(portal-demo-suite)。
// 設計哲學:AI 檢索=用戶手動搜尋同一套——同 search 的 requirePortalUser 閘、同一個租戶資料面,
// 只是把「人下關鍵字」換成「workflow 代查再作答」;前端不因走 AI 多拿任何權限。