portal 圖譜模式修復(#57):優先 in-process 執行 tenant graph_neighbors workflow
RAG self-hosted 實例不部署 kbdb-graph-plugin,graphBase 直連必 1042。改為:
WEBHOOKS KV 有 {tenant}:wf:graph_neighbors 時直接 import executeWebhookGraph
(webhooks-named trigger/query 同一執行入口)在本進程執行——絕不 fetch 自己
hostname(self-loop);輸出映射成與 plugin 相同形狀 {neighbors, edges, count}。
沒有該 workflow → fallback 原 graphBase 直連(Mira/leo21c 行為一字不變)。
input:node=path、depth=query(預設 2)、namespace/owner=tenant。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BH7LdhuCdVUHfHXbM7N8r5
This commit is contained in:
@@ -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<Record<string, unknown> | 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<string, unknown> };
|
||||
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<string, unknown> {
|
||||
const outer = data && typeof data === 'object' ? (data as Record<string, unknown>) : {};
|
||||
if (key in outer) return outer;
|
||||
const inner = outer.data;
|
||||
if (inner && typeof inner === 'object' && key in (inner as Record<string, unknown>)) {
|
||||
return inner as Record<string, unknown>;
|
||||
}
|
||||
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<string, string> = {};
|
||||
if (c.env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${c.env.KBDB_INTERNAL_TOKEN}`;
|
||||
|
||||
Reference in New Issue
Block a user