diff --git a/cypher-executor/src/routes/portal-data.ts b/cypher-executor/src/routes/portal-data.ts index 7093885..54fdb24 100644 --- a/cypher-executor/src/routes/portal-data.ts +++ b/cypher-executor/src/routes/portal-data.ts @@ -25,9 +25,54 @@ import type { Context } from 'hono'; import type { Bindings } from '../types'; import { kbdbFetch, run, requirePortalUser, parseLibraries, portalTenant, hasGraphAccess, workflowsVisible } from './portal'; import { graphBase } from './kbdb-proxy'; +import { executeWebhookGraph } from '../actions/webhook-handlers'; export const portalDataRouter = new Hono<{ Bindings: Bindings }>(); +// ── tenant workflow in-process 執行(portal-demo-suite)─────────────────────── +// +// 為什麼:RAG self-hosted 實例不部署 kbdb-graph-plugin worker(graphBase 直連會 1042, +// Arcrun#57);graph/chat 這類查詢能力在該實例是以 tenant 的 named workflow 形式存在 +//(WEBHOOKS KV `{tenant}:wf:{name}`,與 /webhooks/named 同一資料源)。 +// 怎麼接:直接 import executeWebhookGraph(webhooks-named.ts trigger/query 端點同一個 +// 執行入口)在 **本 worker 進程內** 執行——絕不 fetch 自己的 hostname +//(global_fetch_strictly_public 下打自己=self-loop,見 /portal/data/workflows 同款註解)。 + +/** 讀 tenant 的 named workflow graph(`{tenant}:wf:{name}`)。不存在/壞 record → null。 */ +async function getTenantWorkflowGraph(env: Bindings, name: string): Promise | null> { + const raw = await env.WEBHOOKS.get(`${portalTenant(env)}:wf:${name}`, 'text'); + if (!raw) return null; + try { + const rec = JSON.parse(raw) as { graph?: Record }; + return rec.graph && typeof rec.graph === 'object' ? rec.graph : null; + } catch { + return null; // 壞 record 視同不存在(呼叫端各自誠實回報) + } +} + +/** workflow 最終節點輸出常見兩形:本體即結果,或再包一層 data(http_request 類零件慣例)。取有目標欄位的那層。 */ +function unwrapWorkflowData(data: unknown, key: string): Record { + const outer = data && typeof data === 'object' ? (data as Record) : {}; + if (key in outer) return outer; + const inner = outer.data; + if (inner && typeof inner === 'object' && key in (inner as Record)) { + return inner as Record; + } + return outer; +} + +/** + * graph_neighbors workflow 輸出 → 與 kbdb-graph-plugin 相同的回應形狀 {neighbors, edges, count} + *(前端 doGraphSearch 讀 neighbors/edges;count=neighbors 數,重算不信 workflow 自報)。 + * 純函式(單測用 export)。 + */ +export function mapGraphWorkflowOutput(data: unknown): { neighbors: unknown[]; edges: unknown[]; count: number } { + const layer = unwrapWorkflowData(data, 'neighbors'); + const neighbors = Array.isArray(layer.neighbors) ? layer.neighbors : []; + const edges = Array.isArray(layer.edges) ? layer.edges : []; + return { neighbors, edges, count: neighbors.length }; +} + /** 越庫/不存在 一律同一句 404(不洩存在性)。 */ function notFound(c: Context<{ Bindings: Bindings }>): Response { return c.json({ error: '找不到這筆資料' }, 404); @@ -106,7 +151,10 @@ portalDataRouter.get('/portal/data/entries/:id', (c) => // GET /portal/data/graph/neighbors/:name — graph 模式(D-4 粗閘): // 只對「擁有 graph 來源庫權限」的用戶開放;無權 → 403(SDD 明定,graph 粗閘是 404 紅線的例外)。 -// 放行後純轉發 kbdb-graph-plugin(token 只在 server 側,同 kbdb-proxy 慣例)。 +// 放行後的資料源二選一(Arcrun#57): +// ① tenant 有 `{tenant}:wf:graph_neighbors` workflow → in-process 執行它(RAG self-hosted +// 實例不部署 kbdb-graph-plugin,直連會 1042),輸出映射成與 plugin 相同形狀; +// ② 沒有 → fallback 原本 graphBase 直連 plugin(Mira/leo21c 實例相容,行為一字不變)。 portalDataRouter.get('/portal/data/graph/neighbors/:name', (c) => run(c, async () => { const auth = await requirePortalUser(c); @@ -115,6 +163,29 @@ portalDataRouter.get('/portal/data/graph/neighbors/:name', (c) => if (!(await hasGraphAccess(c.env, libraries))) { return c.json({ error: '無知識圖譜檢視權限' }, 403); } + + // ① tenant workflow 路徑(存在才走;input:node=path、depth=query 預設 2、namespace/owner=tenant) + const tenant = portalTenant(c.env); + const wfGraph = await getTenantWorkflowGraph(c.env, 'graph_neighbors'); + if (wfGraph) { + const depthRaw = c.req.query('depth') ?? ''; + const depth = /^\d{1,2}$/.test(depthRaw) ? Number(depthRaw) : 2; + const result = await executeWebhookGraph( + c.env, + wfGraph, + { node: c.req.param('name'), depth, namespace: tenant, owner: tenant }, + 'graph_neighbors', + tenant, + c.executionCtx, + ); + if (!result.success) { + // workflow 執行失敗 → 誠實 502(不假裝無關聯) + return c.json({ error: `graph_neighbors workflow 執行失敗:${result.error ?? '未知錯誤'}` }, 502); + } + return c.json(mapGraphWorkflowOutput(result.data)); + } + + // ② plugin fallback(Mira/leo21c 相容) const base = graphBase(c.env); const headers: Record = {}; if (c.env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${c.env.KBDB_INTERNAL_TOKEN}`;