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:
@@ -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、
|
||||
* 內部型別 entry(value=slot 值外漏的無標題雜項列、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 多拿任何權限。
|
||||
|
||||
Reference in New Issue
Block a user