Merge branch 'main' into feat/deploy-code-component

This commit is contained in:
Leo
2026-07-07 09:39:20 +00:00
8 changed files with 516 additions and 0 deletions
+1
View File
@@ -29,6 +29,7 @@
| `daily-digest` | cron → 多源聚合(KBDB / RSS / 等) → 推送 |
| `parallel-fanout` | 一份輸入分發多 workflow 並行處理 |
| `error-retry` | try_catch + wait + retry 重試外部 API |
| `graph-neighbors` | 同步查詢:撈 KBDB triplet → 記憶體 BFS 找 N 跳鄰居(查詢面工作流,走同步查詢 trigger) |
## 如何用(AI 視角)
@@ -0,0 +1,67 @@
# graph-neighbors
## 解決什麼問題
把 graph plugin 內建的 `GET /graph/neighbors`(同步 request→response 拿鄰居)**改寫成一條 workflow**。
示範「查詢面工作流」:查詢端點不必是框架寫死的 route,可以是一條 `workflow.yaml` —— 撈 triplet
記錄 → 記憶體 BFS → 同步回鄰居。任何唯讀查詢都能這樣泛化成 workflow。
## 依賴的框架能力(本 PR 補上的)
**同步查詢 trigger**cypher-executor `webhooks-named.ts`):現有 named webhook 的 `/trigger`
回的是 `{success,data,trace,duration_ms}` 信封、且只有 POST。查詢面要 **GET + 直接拿最終節點輸出**
新端點同步 `await` 執行 graph → 把 `result.data`(最終節點輸出)本身當 response body 回(非 202):
- `GET /q/:ns/:name`namespace 走 pathinput 走 query string
- `GET /webhooks/named/:name/query`X-Arcrun-API-Key headerinput 走 query string
- `POST /webhooks/named/:name/query`headerinput 走 body
- `POST /webhooks/named/:ns/:name/query`namespace 走 pathinput 走 body
## 怎麼觸發
```bash
# GET(最像原本的 /graph/neighbors)——{你的-cypher-domain}/{你的-kbdb-domain} 換成自己的部署
curl "https://{你的-cypher-domain}/q/{namespace}/graph_neighbors?node=Arcrun&depth=2&template=graph_triplet&namespace={namespace}&kbdb_base=https://{你的-kbdb-domain}"
# POSTheader 認證)
curl -X POST https://{你的-cypher-domain}/webhooks/named/graph_neighbors/query \
-H "X-Arcrun-API-Key: {namespace}" \
-d '{"node":"Arcrun","depth":2,"template":"graph_triplet","namespace":"{namespace}","kbdb_base":"https://{你的-kbdb-domain}"}'
```
> ⚠️ **敏感輸入請用 `POST /query`**`GET /q/` 的參數走 query string,會進 CF / proxy / access log
> 各層日誌;node 名、namespace 等若屬敏感,改走 POST body。
回傳(最終節點輸出本身,非信封):
```json
{ "success": true, "start": "Arcrun", "depth": 2, "directed": false,
"neighbors": [ { "node": "cypher-executor", "predicate": "包含", "from": "Arcrun", "depth": 1 } ],
"count": 1 }
```
## 參數
- `node`(必填):BFS 起點節點名
- `depth`(預設 1):最大跳數
- `template`(必填):triplet 記錄的 base template id(⚠️ 以實際部署的 kbdb-graph-plugin triplet template 為準)
- `namespace`(必填):租戶 owner_idself-hosted 明碼 namespace
- `kbdb_base`(必填):**你自己的 KBDB base URL**(如 `https://kbdb.example.com`)。
workflow 不寫死任何一家的庫——抄示範時帶錯(或照抄別人的值)=查詢與資料流向直接打進別人的庫
KBDB_BASE_URL fallback 同家族坑,勿重蹈)。
- `directed`(預設 false):`true` 只走 subject→object;否則把 triplet 當雙向邊(無向鄰居)
## kbdb_base 該帶哪種 URL1042
cypher-executor 對 kbdb 發 fetch,若打同 zone URL 會踩 CF 1042same-zone self-fetch)。兩條路皆可:
1. 帶 KBDB 的 **custom domain**(跨 zone、走公網前門,天然避開 1042);
2. self-hosted 有 **`global_fetch_strictly_public`** compatibility flagcredential-primitives-wasm
Phase 7cypher wrangler.toml),開了之後 **workers.dev URL 亦可**直接帶。
## ⚠️ 尚未 live 驗(待辦)
- **`code` 零件尚未部署到 leo21c**(另線處理)→ 本工作流無法端到端 live 跑。
workflow.yaml 已寫好放這裡待驗。
- **同步查詢 trigger 本身已驗**`cypher-executor/tests/query-trigger.test.ts`7 測)用內建零件
comp_uppercase,純記憶體、無外部 fetch)證明「確實同步回最終節點輸出而非 202」。
- 上線前另需對一次實際 triplet template id(本檔用 `{{input.template}}` 參數化,未寫死)。
## 對照
記憶體 BFS 對照 `kbdb-graph-plugin``graph-traverse.ts:23-51`triplet 當有向邊
`subject --predicate--> object`,從起點逐跳擴張到 depth 上限,收集首次訪到的節點當鄰居。
## 學到什麼
- 查詢端點可以是 workflow,不必是框架寫死的 route(同步查詢 trigger 讓這件事成立)
- `code` 零件(sandbox inline JS)承載「非 call-api 的純計算」(BFS),不必為此鑄 domain 零件
- 單一 `{{ref}}` pass-through 保留陣列型別 → `{{fetch_triplets.data.records}}` 拿到真陣列餵給 code
@@ -0,0 +1 @@
["graph", "triplet", "bfs", "neighbors", "kbdb", "sync-query", "query-endpoint", "code-node", "common-pattern"]
@@ -0,0 +1,124 @@
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。
# ══════════════════════════════════════════════════════════════════════════════
# ⚠️ 先改成你自己的 KBDB URLself-hosted 必看)
# fetch_triplets 的 base URL 走 {{input.kbdb_base}} 參數(每次呼叫帶),**沒有寫死官方庫**。
# 照抄本示範時,把觸發參數 kbdb_base 換成你自己部署的 KBDB 對外 URL——
# 抄了別人的值=你的查詢與資料流向直接打進別人的庫(KBDB_BASE_URL fallback 同家族坑)。
# ══════════════════════════════════════════════════════════════════════════════
# ── 1042 與 kbdb_base 該帶哪種 URL ──
# cypher-executor 執行 http_request 節點時對 kbdb 發 fetch。若打同 zone URL 會踩 CF 1042
# same-zone self-fetch)。兩條路皆可:
# a) 帶 KBDB 的 custom domain(跨 zone、走公網前門,天然避開 1042);
# b) self-hosted 有 `global_fetch_strictly_public` compatibility flag
# credential-primitives-wasm Phase 7cypher wrangler.toml),開了之後
# workers.dev URL 亦可直接帶。
# ── 觸發(同步查詢 trigger,非 202)──
# GET https://{你的-cypher-domain}/q/{namespace}/graph_neighbors?node=Arcrun&depth=2&template=graph_triplet&namespace={namespace}&kbdb_base=https://{你的-kbdb-domain}
# POST https://{你的-cypher-domain}/webhooks/named/graph_neighbors/query
# -H "X-Arcrun-API-Key: {namespace}"
# -d '{"node":"Arcrun","depth":2,"template":"graph_triplet","namespace":"{namespace}","kbdb_base":"https://{你的-kbdb-domain}"}'
# → 直接回 { success, start, depth, directed, neighbors:[...], count }(最終節點輸出本身)。
# (敏感輸入用 POST /queryGET /q/ 的 query string 會進各層 log,見 description.md。)
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 為準——上線前對一次。
# ⚠️ base URL 由呼叫者帶({{input.kbdb_base}})——不寫死任何一家的庫(見檔頂警示)。
fetch_triplets:
component: http_request
method: GET
url: "{{input.kbdb_base}}/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 硬上限)