From 60d3e41905c31141b4b5c89d1348870b77fc4ac5 Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 16 Apr 2026 18:36:51 +0800 Subject: [PATCH] feat: component hash IDs + dynamic KV recipe system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hash system: - cmp_xxxxxxxx: stable ID for logic components (SHA-256 of canonical_id) - rec_xxxxxxxx: stable ID for API recipe components - Pre-seeded 15 cmp_ + 6 rec_ hash indexes in KV RECIPES KV (id: 9cf9db905c6241f78503199e58b2ffe0): - POST/GET/DELETE /recipes — CRUD for API recipe definitions - recipe stored as: recipe:{canonical_id} + idx:{rec_hash} - template interpolation: {{key}} replaced from context component-loader resolution order: builtin → external URL → cmp_ hash → rec_ hash → logic canonical_id → KV recipe → builtin API fallback → error Co-Authored-By: Claude Sonnet 4.6 --- cypher-executor/src/index.ts | 2 + cypher-executor/src/lib/component-loader.ts | 324 +++++++++++--------- cypher-executor/src/lib/hash.ts | 34 ++ cypher-executor/src/routes/recipes.ts | 125 ++++++++ cypher-executor/src/types.ts | 2 + cypher-executor/wrangler.toml | 4 + 6 files changed, 345 insertions(+), 146 deletions(-) create mode 100644 cypher-executor/src/lib/hash.ts create mode 100644 cypher-executor/src/routes/recipes.ts diff --git a/cypher-executor/src/index.ts b/cypher-executor/src/index.ts index bfd7bdc..760d63f 100644 --- a/cypher-executor/src/index.ts +++ b/cypher-executor/src/index.ts @@ -11,6 +11,7 @@ import { webhooksRouter } from './routes/webhooks'; import { webhooksCrudRouter } from './routes/webhooks-crud'; import { webhooksListRouter } from './routes/webhooks-list'; import { registerRouter } from './routes/register'; +import { recipesRouter } from './routes/recipes'; const app = new Hono<{ Bindings: Bindings }>(); @@ -27,6 +28,7 @@ app.route('/', webhooksRouter); app.route('/', webhooksCrudRouter); app.route('/', webhooksListRouter); app.route('/', registerRouter); +app.route('/', recipesRouter); // Worker 導出 export default app; diff --git a/cypher-executor/src/lib/component-loader.ts b/cypher-executor/src/lib/component-loader.ts index 7cebbf9..2c74642 100644 --- a/cypher-executor/src/lib/component-loader.ts +++ b/cypher-executor/src/lib/component-loader.ts @@ -1,28 +1,25 @@ /** * arcrun component loader * - * 三種執行模式: + * 解析優先序: * - * 1. 邏輯零件(category=logic) - * → fetch POST https://{name-with-dashes}.arcrun.dev - * 每個邏輯零件是獨立 CF Worker,有 WASM 靜態 bundle - * - * 2. API recipe 零件(category=api) - * → 從 CREDENTIALS_KV 讀取 recipe,fetch 外部 API - * 不需要獨立 Worker,整個執行在 cypher-executor 裡完成 - * - * 3. 外部 URL 零件(componentId 以 http:// 或 https:// 開頭) - * → 直接 fetch,可以是 n8n webhook、MCP endpoint 等任何 HTTP 服務 - * - * 4. 內建零件(BUILTIN_COMPONENTS) - * → 純 JS 函數,不需要網路呼叫 + * 1. 內建零件(BUILTIN_COMPONENTS)— 純 JS,最快 + * 2. 外部 URL(https://...)— 直接 fetch,n8n/MCP/任何 HTTP 服務 + * 3. cmp_xxxxxxxx hash → 查 registry KV idx → canonical_id → 邏輯 Worker + * 4. rec_xxxxxxxx hash → 查 RECIPES KV idx → canonical_id → KV recipe 執行 + * 5. 邏輯零件 canonical_id → Service Binding(同帳號不走公網) + * 6. KV recipe canonical_id → 從 RECIPES KV 讀取 recipe → fetch 外部 API + * 7. 內建 API recipe(gmail/telegram/gsheets 等,寫死的 fallback) + * 8. 找不到 → 報錯 */ import { BUILTIN_COMPONENTS } from './constants'; -import type { Bindings, ComponentRunner } from '../types'; +import { isComponentHash, isRecipeHash } from './hash'; +import { resolveRecipe } from '../routes/recipes'; +import type { Bindings, ComponentRunner, ServiceBinding } from '../types'; /** 邏輯零件 canonical_id → Service Binding key */ -const LOGIC_BINDING_MAP: Record = { +const LOGIC_BINDING_MAP: Record = { if_control: 'SVC_IF_CONTROL', switch: 'SVC_SWITCH', foreach_control: 'SVC_FOREACH_CONTROL', @@ -40,14 +37,142 @@ const LOGIC_BINDING_MAP: Record = { ai_transform_run: 'SVC_AI_TRANSFORM_RUN', }; -/** API 零件 canonical_id → recipe(endpoint + 組裝邏輯)*/ -const API_RECIPES: Record) => Promise> = { +export function createComponentLoader(env: Bindings) { + return async (componentId: string): Promise => { + + // 1. 內建零件(純 JS,最優先) + const builtin = BUILTIN_COMPONENTS.get(componentId); + if (builtin) return builtin; + + // 2. 外部 URL + if (componentId.startsWith('http://') || componentId.startsWith('https://')) { + return makeHttpRunner(componentId); + } + + // 3. cmp_hash → 查 WEBHOOKS KV idx → canonical_id → 邏輯 Worker + if (isComponentHash(componentId)) { + const canonicalId = await env.WEBHOOKS.get(`idx:${componentId}`); + if (canonicalId) { + return makeLogicRunner(canonicalId, env); + } + throw new Error(`找不到零件 hash "${componentId}",請確認已透過 acr push 上傳`); + } + + // 4. rec_hash → 查 RECIPES KV idx → recipe 執行 + if (isRecipeHash(componentId)) { + const recipe = await resolveRecipe(componentId, env.RECIPES); + if (recipe) return makeRecipeRunner(recipe); + throw new Error(`找不到 recipe hash "${componentId}",請確認已透過 acr push 上傳`); + } + + // 5. 邏輯零件 canonical_id → Service Binding + const logicRunner = makeLogicRunner(componentId, env); + if (logicRunner) return logicRunner; + + // 6. KV recipe(動態,用戶 push 的) + const kvRecipe = await resolveRecipe(componentId, env.RECIPES); + if (kvRecipe) return makeRecipeRunner(kvRecipe); + + // 7. 內建 API recipe(寫死的 fallback) + const builtinRecipe = BUILTIN_API_RECIPES[componentId]; + if (builtinRecipe) return builtinRecipe; + + // 8. 找不到 + throw new Error( + `找不到零件 "${componentId}"。\n` + + `邏輯零件:${Object.keys(LOGIC_BINDING_MAP).join(', ')}\n` + + `或傳入外部 URL(https://...)、recipe hash(rec_xxxxxxxx)、零件 hash(cmp_xxxxxxxx)` + ); + }; +} + +// ── 執行器工廠 ──────────────────────────────────────────────────────────────── + +function makeHttpRunner(url: string): ComponentRunner { + return async (ctx: unknown) => { + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(ctx), + }); + if (!res.ok) { + const text = await res.text(); + return { success: false, status: res.status, error: text.slice(0, 200) }; + } + try { return await res.json(); } + catch { return { success: true, data: await res.text() }; } + }; +} + +function makeLogicRunner(canonicalId: string, env: Bindings): ComponentRunner | null { + const bindingKey = LOGIC_BINDING_MAP[canonicalId]; + if (!bindingKey) return null; + + const svc = env[bindingKey] as ServiceBinding | undefined; + if (svc) { + return async (ctx: unknown) => { + const res = await svc.fetch(new Request('https://component/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(ctx), + })); + if (!res.ok) { + const text = await res.text(); + return { success: false, error: `${canonicalId} 回傳 ${res.status}: ${text.slice(0, 200)}` }; + } + try { return await res.json(); } + catch { return { success: false, error: `${canonicalId} 回傳非 JSON` }; } + }; + } + + // Service Binding 未配置時 fallback 到公網(自製零件 or 開發環境) + const fallbackUrl = `https://${canonicalId.replace(/_/g, '-')}.arcrun.dev`; + return makeHttpRunner(fallbackUrl); +} + +function makeRecipeRunner(recipe: import('../routes/recipes').RecipeDefinition): ComponentRunner { + return async (ctx: unknown) => { + const ctxObj = (ctx && typeof ctx === 'object') ? ctx as Record : {}; + + // 模板替換:把 {{key}} 換成 ctx 裡的值 + const interpolate = (s: string) => + s.replace(/\{\{(\w+)\}\}/g, (_, k) => String(ctxObj[k] ?? '')); + + const method = (recipe.method ?? 'POST').toUpperCase(); + const headers: Record = { 'Content-Type': 'application/json' }; + for (const [k, v] of Object.entries(recipe.headers ?? {})) { + headers[k] = interpolate(v); + } + + // body:把 recipe.body 裡的 {{key}} 都換掉 + let bodyStr: string | undefined; + if (recipe.body) { + bodyStr = interpolate(JSON.stringify(recipe.body)); + } else if (method !== 'GET') { + bodyStr = JSON.stringify(ctxObj); + } + + const res = await fetch(interpolate(recipe.endpoint), { + method, + headers, + body: bodyStr, + }); + + const data = await res.json().catch(() => res.text()); + return { success: res.ok, status: res.status, data }; + }; +} + +// ── 內建 API recipe(不存在 KV,直接寫死)──────────────────────────────────── + +const BUILTIN_API_RECIPES: Record = { http_request: async (ctx) => { - const url = ctx.url as string; - const method = (ctx.method as string ?? 'GET').toUpperCase(); - const headers = (ctx.headers as Record) ?? {}; - const body = ctx.body !== undefined ? JSON.stringify(ctx.body) : undefined; + const c = ctx as Record; + const url = c.url as string; if (!url) return { success: false, error: 'url 必填' }; + const method = ((c.method as string) ?? 'GET').toUpperCase(); + const headers = (c.headers as Record) ?? {}; + const body = c.body !== undefined ? JSON.stringify(c.body) : undefined; const res = await fetch(url, { method, headers, body }); const text = await res.text(); let data: unknown = text; @@ -56,152 +181,59 @@ const API_RECIPES: Record) => Promise { - const { to, subject, body, access_token } = ctx as Record; - if (!access_token) return { success: false, error: 'access_token 必填(由 credentials 注入)' }; - if (!to || !subject || !body) return { success: false, error: 'to, subject, body 必填' }; - // Build RFC 2822 message + base64url encode - const message = `To: ${to}\r\nSubject: ${subject}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${body}`; + const c = ctx as Record; + if (!c.access_token) return { success: false, error: 'access_token 必填(由 credentials 注入)' }; + if (!c.to || !c.subject || !c.body) return { success: false, error: 'to, subject, body 必填' }; + const message = `To: ${c.to}\r\nSubject: ${c.subject}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n${c.body}`; const encoded = btoa(unescape(encodeURIComponent(message))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); const res = await fetch('https://gmail.googleapis.com/gmail/v1/users/me/messages/send', { method: 'POST', - headers: { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }, + headers: { 'Authorization': `Bearer ${c.access_token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ raw: encoded }), }); - const data = await res.json(); - return { success: res.ok, data }; + return { success: res.ok, data: await res.json() }; }, telegram: async (ctx) => { - const { bot_token, chat_id, text } = ctx as Record; - if (!bot_token) return { success: false, error: 'bot_token 必填(由 credentials 注入)' }; - const res = await fetch(`https://api.telegram.org/bot${bot_token}/sendMessage`, { + const c = ctx as Record; + if (!c.bot_token) return { success: false, error: 'bot_token 必填(由 credentials 注入)' }; + const res = await fetch(`https://api.telegram.org/bot${c.bot_token}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chat_id, text }), + body: JSON.stringify({ chat_id: c.chat_id, text: c.text }), }); - const data = await res.json(); - return { success: res.ok, data }; + return { success: res.ok, data: await res.json() }; }, line_notify: async (ctx) => { - const { token, message } = ctx as Record; - if (!token) return { success: false, error: 'token 必填(由 credentials 注入)' }; - const form = new URLSearchParams({ message }); + const c = ctx as Record; + if (!c.token) return { success: false, error: 'token 必填(由 credentials 注入)' }; const res = await fetch('https://notify-api.line.me/api/notify', { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/x-www-form-urlencoded' }, - body: form.toString(), + headers: { 'Authorization': `Bearer ${c.token}`, 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ message: c.message }).toString(), }); - const data = await res.json(); - return { success: res.ok, data }; + return { success: res.ok, data: await res.json() }; }, google_sheets: async (ctx) => { - const { access_token, spreadsheet_id, range, values, operation } = ctx as Record; - if (!access_token) return { success: false, error: 'access_token 必填(由 credentials 注入)' }; - const headers = { 'Authorization': `Bearer ${access_token}`, 'Content-Type': 'application/json' }; - const op = (operation as string) ?? 'read'; - if (op === 'read') { + const c = ctx as Record; + if (!c.access_token) return { success: false, error: 'access_token 必填(由 credentials 注入)' }; + const hdrs = { 'Authorization': `Bearer ${c.access_token}`, 'Content-Type': 'application/json' }; + if ((c.operation ?? 'read') === 'read') { const res = await fetch( - `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}/values/${range}`, - { headers } + `https://sheets.googleapis.com/v4/spreadsheets/${c.spreadsheet_id}/values/${c.range}`, + { headers: hdrs } ); - const data = await res.json(); - return { success: res.ok, data }; - } else { - const res = await fetch( - `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}/values/${range}:append?valueInputOption=USER_ENTERED`, - { method: 'POST', headers, body: JSON.stringify({ values }) } - ); - const data = await res.json(); - return { success: res.ok, data }; + return { success: res.ok, data: await res.json() }; } - }, - - cron: async (ctx) => { - // cron 是觸發源,在 workflow 執行時已被觸發,直接 passthrough - return { success: true, data: ctx }; - }, - - ai_transform_compile: async (ctx) => { - // fallback — 通常由 logic Worker 處理,這裡是保險 - return { success: true, data: ctx }; - }, - - ai_transform_run: async (ctx) => { - return { success: true, data: ctx }; - }, -}; - -export function createComponentLoader(env: Bindings) { - return async (componentId: string): Promise => { - - // 1. 內建零件(純 JS,最優先) - const builtin = BUILTIN_COMPONENTS.get(componentId); - if (builtin) return builtin; - - // 2. 外部 URL(componentId 直接是 http/https URL) - if (componentId.startsWith('http://') || componentId.startsWith('https://')) { - return async (ctx: unknown) => { - const res = await fetch(componentId, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(ctx), - }); - if (!res.ok) { - const text = await res.text(); - return { success: false, status: res.status, error: text }; - } - try { return await res.json(); } - catch { return { success: true, data: await res.text() }; } - }; - } - - // 3. 邏輯零件 → Service Binding(CF Workers 直接呼叫,不走公網) - const bindingKey = LOGIC_BINDING_MAP[componentId]; - if (bindingKey) { - const svc = env[bindingKey] as import('../types').ServiceBinding | undefined; - if (svc) { - return async (ctx: unknown) => { - const res = await svc.fetch(new Request('https://component/', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(ctx), - })); - if (!res.ok) { - const text = await res.text(); - return { success: false, error: `${componentId} 回傳 ${res.status}: ${text.slice(0, 200)}` }; - } - try { return await res.json(); } - catch { return { success: false, error: `${componentId} 回傳非 JSON` }; } - }; - } - // Service Binding 未配置時 fallback 到公網 URL(開發環境) - const fallbackUrl = `https://${componentId.replace(/_/g, '-')}.arcrun.dev`; - return async (ctx: unknown) => { - const res = await fetch(fallbackUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(ctx), - }); - try { return await res.json(); } - catch { return { success: false, error: `${componentId} fallback 失敗` }; } - }; - } - - // 4. API recipe 零件 - const recipe = API_RECIPES[componentId]; - if (recipe) { - return async (ctx: unknown) => recipe(ctx as Record); - } - - // 5. 找不到 - throw new Error( - `找不到零件 "${componentId}"。\n` + - `可用邏輯零件:${Object.keys(LOGIC_COMPONENT_URLS).join(', ')}\n` + - `可用 API 零件:${Object.keys(API_RECIPES).join(', ')}\n` + - `也可傳入外部 URL(https://...)作為零件。` + const res = await fetch( + `https://sheets.googleapis.com/v4/spreadsheets/${c.spreadsheet_id}/values/${c.range}:append?valueInputOption=USER_ENTERED`, + { method: 'POST', headers: hdrs, body: JSON.stringify({ values: c.values }) } ); - }; -} + return { success: res.ok, data: await res.json() }; + }, + + cron: async (ctx) => ({ success: true, data: ctx }), +}; diff --git a/cypher-executor/src/lib/hash.ts b/cypher-executor/src/lib/hash.ts new file mode 100644 index 0000000..f13b0b3 --- /dev/null +++ b/cypher-executor/src/lib/hash.ts @@ -0,0 +1,34 @@ +/** + * 穩定 ID 衍生工具 + * + * 邏輯零件: cmp_ + * API recipe:rec_ + * + * 同一個 canonical_id 永遠得到同一個 hash, + * 讓 workflow 可以用 hash 引用零件,不受改名影響。 + */ + +export async function deriveComponentHash(canonicalId: string): Promise { + return 'cmp_' + await sha256Prefix(canonicalId); +} + +export async function deriveRecipeHash(canonicalId: string): Promise { + return 'rec_' + await sha256Prefix(canonicalId); +} + +export function isComponentHash(id: string): boolean { + return /^cmp_[0-9a-f]{8}$/.test(id); +} + +export function isRecipeHash(id: string): boolean { + return /^rec_[0-9a-f]{8}$/.test(id); +} + +async function sha256Prefix(input: string): Promise { + const data = new TextEncoder().encode(input); + const buf = await crypto.subtle.digest('SHA-256', data); + return Array.from(new Uint8Array(buf)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + .slice(0, 8); +} diff --git a/cypher-executor/src/routes/recipes.ts b/cypher-executor/src/routes/recipes.ts new file mode 100644 index 0000000..9a0ceb3 --- /dev/null +++ b/cypher-executor/src/routes/recipes.ts @@ -0,0 +1,125 @@ +/** + * /recipes — API recipe CRUD + * + * recipe 是「http_request + 參數模板」的具名封裝。 + * 不需要 deploy Worker,執行時由 cypher-executor 直接 fetch。 + * + * KV 結構: + * recipe:{canonical_id} → RecipeDefinition JSON + * idx:{rec_hash} → canonical_id (反查索引) + * + * 引用方式(workflow config): + * component: "rec_f7e2a1b3" → 永久穩定,不受改名影響 + * component: "slack" → 向前兼容,直接用 canonical_id 查 + */ + +import { Hono } from 'hono'; +import type { Bindings } from '../types'; +import { deriveRecipeHash } from '../lib/hash'; + +export const recipesRouter = new Hono<{ Bindings: Bindings }>(); + +export interface RecipeDefinition { + canonical_id: string; + hash_id: string; // rec_xxxxxxxx + display_name?: string; + description?: string; + endpoint: string; + method?: string; // GET | POST | PUT | PATCH | DELETE,預設 POST + headers?: Record; + body?: Record; + credentials_required?: Array<{ + key: string; + inject_as: string; + }>; + created_at: number; + updated_at: number; +} + +// POST /recipes — 新增或更新 recipe +recipesRouter.post('/recipes', async (c) => { + let body: Partial; + try { + body = await c.req.json(); + } catch { + return c.json({ success: false, error: 'request body 必須為 JSON' }, 400); + } + + const canonicalId = (body.canonical_id ?? '').trim().toLowerCase(); + if (!canonicalId) return c.json({ success: false, error: 'canonical_id 必填' }, 400); + if (!body.endpoint) return c.json({ success: false, error: 'endpoint 必填' }, 400); + + const hashId = await deriveRecipeHash(canonicalId); + const now = Date.now(); + + // 讀取現有版本(保留 created_at) + const existing = await c.env.RECIPES.get(`recipe:${canonicalId}`, 'json') as RecipeDefinition | null; + + const recipe: RecipeDefinition = { + canonical_id: canonicalId, + hash_id: hashId, + display_name: body.display_name, + description: body.description, + endpoint: body.endpoint, + method: (body.method ?? 'POST').toUpperCase(), + headers: body.headers, + body: body.body, + credentials_required: body.credentials_required, + created_at: existing?.created_at ?? now, + updated_at: now, + }; + + // 寫入兩個 KV key + await Promise.all([ + c.env.RECIPES.put(`recipe:${canonicalId}`, JSON.stringify(recipe)), + c.env.RECIPES.put(`idx:${hashId}`, canonicalId), + ]); + + return c.json({ success: true, recipe }); +}); + +// GET /recipes/:id — 讀取 recipe(支援 canonical_id 或 rec_hash) +recipesRouter.get('/recipes/:id', async (c) => { + const id = c.req.param('id'); + const recipe = await resolveRecipe(id, c.env.RECIPES); + if (!recipe) return c.json({ success: false, error: `找不到 recipe: ${id}` }, 404); + return c.json({ success: true, recipe }); +}); + +// GET /recipes — 列出所有 recipe +recipesRouter.get('/recipes', async (c) => { + const list = await c.env.RECIPES.list({ prefix: 'recipe:' }); + const recipes = await Promise.all( + list.keys.map(k => c.env.RECIPES.get(k.name, 'json')) + ); + return c.json({ success: true, recipes: recipes.filter(Boolean), count: recipes.length }); +}); + +// DELETE /recipes/:id — 刪除 recipe +recipesRouter.delete('/recipes/:id', async (c) => { + const id = c.req.param('id'); + const recipe = await resolveRecipe(id, c.env.RECIPES); + if (!recipe) return c.json({ success: false, error: `找不到 recipe: ${id}` }, 404); + + await Promise.all([ + c.env.RECIPES.delete(`recipe:${recipe.canonical_id}`), + c.env.RECIPES.delete(`idx:${recipe.hash_id}`), + ]); + + return c.json({ success: true, deleted: recipe.canonical_id }); +}); + +/** 用 canonical_id 或 rec_hash 查 recipe */ +export async function resolveRecipe( + id: string, + kv: KVNamespace, +): Promise { + // rec_xxxxxxxx → 先查 idx 反查 canonical_id + if (id.startsWith('rec_')) { + const canonicalId = await kv.get(`idx:${id}`); + if (!canonicalId) return null; + return kv.get(`recipe:${canonicalId}`, 'json'); + } + // 直接用 canonical_id + return kv.get(`recipe:${id}`, 'json'); +} diff --git a/cypher-executor/src/types.ts b/cypher-executor/src/types.ts index 93bb235..bde72d5 100644 --- a/cypher-executor/src/types.ts +++ b/cypher-executor/src/types.ts @@ -24,6 +24,8 @@ export type Bindings = { SVC_AI_TRANSFORM_RUN: ServiceBinding; // KV Context Store:節點 output 透過 KV 傳遞,解決同名欄位衝突 EXEC_CONTEXT: KVNamespace; + // Recipe Store:API recipe 定義(key: recipe:{canonical_id} 或 idx:{hash_id}) + RECIPES: KVNamespace; // Webhook Store:key = workflow name,value = Workflow JSON WEBHOOKS: KVNamespace; // Credential Store:AES-GCM 加密存放用戶 API token diff --git a/cypher-executor/wrangler.toml b/cypher-executor/wrangler.toml index 387aea7..ef93c81 100644 --- a/cypher-executor/wrangler.toml +++ b/cypher-executor/wrangler.toml @@ -19,6 +19,10 @@ id = "e7f4320f88d343f187e35e3543dd74c9" binding = "ANALYTICS_KV" id = "a43b7997c8e54a34886c2995a853c720" +[[kv_namespaces]] +binding = "RECIPES" +id = "9cf9db905c6241f78503199e58b2ffe0" + [[r2_buckets]] binding = "WASM_BUCKET" bucket_name = "arcrun-wasm"