feat(cypher-executor): 同步查詢 trigger + graph_neighbors 查詢面 workflow 示範

Part 1(框架,cypher-executor webhooks-named.ts):加同步查詢 trigger。
現有 named webhook /trigger 回 {success,data,trace,duration_ms} 信封、只有 POST;
查詢面(console/MCP 打 graph neighbors/traverse)要 GET + 直接拿最終節點輸出當 response。
新端點同步 await 執行 workflow graph → 回 result.data(最終節點輸出)本身當 body(非 202):
  - GET  /q/:ns/:name                    (namespace 走 path,input 走 query string)
  - GET  /webhooks/named/:name/query     (X-Arcrun-API-Key header,input 走 query string)
  - POST /webhooks/named/:name/query     (header,input 走 body)
  - POST /webhooks/named/:ns/:name/query (namespace 走 path,input 走 body)
認證沿用 X-Arcrun-API-Key。誠實(mindset §7):節點失敗回 error+trace(500,非假綠);
paused 工作流無法同步回答 → 409 明講;輸出 5 MiB 硬上限(超過 413);duration 走 header 不污染 body。

Part 2(A 類 workflow.yaml):registry/examples/graph-neighbors/。
http_request 打 base custom domain kbdb.finally.click(避 CF 1042)撈 triplet records
→ code 零件記憶體 BFS(對照 kbdb-graph-plugin graph-traverse.ts)→ 同步回鄰居。
把 graph plugin 內建 GET /graph/neighbors 泛化成查詢面 workflow 的示範。

測試:cypher-executor/tests/query-trigger.test.ts(7 測,全綠)——同步回輸出(非 202)、
GET/POST × header/path 四端點、節點失敗回錯+trace、缺 key 401、不存在 404。
用內建 comp_uppercase(純記憶體)證明 Part 1 同步 trigger 機制本身可用。

待驗:graph_neighbors 需 code 零件部署 leo21c 後才能 live 端到端(另線處理);
triplet template id 上線前對一次(workflow 已參數化未寫死)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
This commit is contained in:
2026-07-07 08:16:17 +00:00
parent b13b4dfe49
commit 313aeb13bf
6 changed files with 435 additions and 0 deletions
@@ -0,0 +1,112 @@
name: graph_neighbors
description: >
同步查詢:給一個節點 → 從 KBDB triplet 記錄建鄰接表 → 記憶體 BFS 找 N 跳鄰居 → 同步回鄰居清單。
這是「查詢面工作流」示範:用同步查詢 triggerGET /q/:ns/graph_neighbors 或
POST /webhooks/named/:name/query),把 workflow 最終節點輸出直接當 HTTP response 拿回,
取代 graph plugin 內建的 GET /graph/neighbors。對照 kbdb-graph-plugin graph-traverse.ts 的記憶體 BFS。
# ── 為什麼走 base custom domainkbdb.finally.click)而非 workers.dev ──
# cypher-executor 執行 http_request 節點時對 kbdb 發 fetch。若打 *.uncle6-me.workers.dev 同 zone
# 會踩 CF 1042same-zone self-fetch)。打 base 的對外 custom domain kbdb.finally.click 屬跨 zone
# 前門公網進出,避開 1042(同 credential-primitives-wasm Phase 7 的 global_fetch_strictly_public 精神)。
# ── 觸發(同步查詢 trigger,非 202)──
# GET https://cypher.arcrun.dev/q/{namespace}/graph_neighbors?node=Arcrun&depth=2&template=graph_triplet&namespace={namespace}
# POST https://cypher.arcrun.dev/webhooks/named/graph_neighbors/query
# -H "X-Arcrun-API-Key: {namespace}"
# -d '{"node":"Arcrun","depth":2,"template":"graph_triplet","namespace":"{namespace}"}'
# → 直接回 { success, start, depth, directed, neighbors:[...], count }(最終節點輸出本身)。
flow:
- "input >> ON_SUCCESS >> fetch_triplets"
- "fetch_triplets >> ON_SUCCESS >> bfs_neighbors"
config:
# 1) 撈本租戶的 triplet 記錄。triplet = base 萬用表的一個 templategraph plugin 寫入),
# slots = subject / predicate / object。base 端點:GET /records/by-template/:template?owner_id=
# 回 { success, records:[{ record_id, values:{subject,predicate,object} }], count }。
# ⚠️ template 名(此處 {{input.template}},預設由呼叫者帶 graph_triplet)以實際部署的
# kbdb-graph-plugin triplet template id 為準——上線前對一次。
fetch_triplets:
component: http_request
method: GET
url: "https://kbdb.finally.click/records/by-template/{{input.template}}?owner_id={{input.namespace}}"
headers:
Accept: "application/json"
# 2) ★ 記憶體 BFS(通用 code 零件,sandbox inline JS,無 LLM、無 fs/網路,stdin→stdout JSON)。
# 對照 kbdb-graph-plugin graph-traverse.ts:23-51 的記憶體 BFS:把 triplet 當有向邊
# subject --predicate--> object 建鄰接表,從 start 逐跳擴張到 depth 上限,收集新訪節點當鄰居。
# directed=false(預設)時把邊當雙向(無向圖鄰居);directed=true 只走 subject→object。
bfs_neighbors:
component: code
code: |
// graph_neighbors — 記憶體 BFS 找 N 跳鄰居(純函式、決定性、零 token)。
// input(由下方 input: 映射解析後注入):
// records[] : triplet 記錄({ values:{subject,predicate,object} } 或扁平 {subject,predicate,object}
// start : 起點節點名(字串)
// depth : 最大跳數(字串或數字,來自 query string 時是字串)
// directed : "true" 只走 subject→object;否則當無向
const records = Array.isArray(input.records) ? input.records : [];
const start = String(input.start == null ? '' : input.start);
const maxDepth = Math.max(1, parseInt(String(input.depth == null ? 1 : input.depth), 10) || 1);
const directed = String(input.directed == null ? '' : input.directed) === 'true';
if (!start) {
return { success: false, error: 'graph_neighbors 缺 startnode)參數' };
}
// 建鄰接表:subject --predicate--> object。無向時同時加反向邊。
const adj = new Map();
function addEdge(from, to, predicate) {
if (!adj.has(from)) adj.set(from, []);
adj.get(from).push({ node: to, predicate: predicate });
}
for (const r of records) {
const v = (r && typeof r === 'object' && r.values && typeof r.values === 'object') ? r.values : r;
if (!v || typeof v !== 'object') continue;
const s = v.subject, p = v.predicate, o = v.object;
if (!s || !o) continue;
addEdge(s, o, p);
if (!directed) addEdge(o, s, p);
}
// BFS:一層一跳,收集首次訪到的節點當鄰居(記 depth / 來源 / 關係)。
const visited = new Set([start]);
let frontier = [start];
const neighbors = [];
for (let d = 1; d <= maxDepth; d++) {
const next = [];
for (const cur of frontier) {
const outs = adj.get(cur) || [];
for (const e of outs) {
if (visited.has(e.node)) continue;
visited.add(e.node);
neighbors.push({ node: e.node, predicate: e.predicate, from: cur, depth: d });
next.push(e.node);
}
}
frontier = next;
if (frontier.length === 0) break;
}
return {
success: true,
start: start,
depth: maxDepth,
directed: directed,
neighbors: neighbors,
count: neighbors.length,
};
# input 映射:{{...}} 對 workflow context 展開後注入 code 沙箱的 `input` 變數。
# {{input.X}} 的 input = 上游 input 節點輸出(=觸發 context);{{fetch_triplets.data.records}}
# = http_request 回應 body 的 records 陣列(單一 ref pass-through 保留陣列型別)。
input:
records: "{{fetch_triplets.data.records}}"
start: "{{input.node}}"
depth: "{{input.depth}}"
directed: "{{input.directed}}"
limits:
timeout_ms: 3000 # 純 CPU BFS,充裕
max_output_bytes: 2097152 # 鄰居清單上限 2 MiB(呼叫端同步查詢輸出也有 5 MiB 硬上限)