diff --git a/builtins/package-lock.json b/builtins/package-lock.json deleted file mode 100644 index e246dde..0000000 --- a/builtins/package-lock.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@inkstone/u6u-builtins-worker", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@inkstone/u6u-builtins-worker", - "version": "1.0.0", - "dependencies": { - "hono": "^4.7.0" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20250219.0", - "typescript": "^5.7.0" - } - }, - "node_modules/@cloudflare/workers-types": { - "version": "4.20260329.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260329.1.tgz", - "integrity": "sha512-LxBHrYYI/AZ6OCbUzRqRgg6Rt1qev2KxN2NNd3saye41AO2g52cYvHV+ohts5oPnrIUD7YRjbgN/J3NU7e7m5A==", - "dev": true, - "license": "MIT OR Apache-2.0" - }, - "node_modules/hono": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", - "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - } - } -} diff --git a/builtins/package.json b/builtins/package.json deleted file mode 100644 index 6e9a144..0000000 --- a/builtins/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "arcrun-builtins", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "wrangler dev", - "deploy": "wrangler deploy" - }, - "dependencies": { - "hono": "^4.7.0" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20250219.0", - "typescript": "^5.7.0" - } -} diff --git a/builtins/src/actions/initComponents.ts b/builtins/src/actions/initComponents.ts deleted file mode 100644 index cc8311f..0000000 --- a/builtins/src/actions/initComponents.ts +++ /dev/null @@ -1,43 +0,0 @@ -// initComponents:把所有內建零件上架到 Component Registry(via Service Binding) -import type { Bindings } from '../types'; -import { buildComponentDefs } from '../types'; - -async function publishOne( - registry: Fetcher, - def: ReturnType[number], -): Promise<{ id: string; status: number; ok: boolean; error?: unknown }> { - const payload = { - id: def.id, - name: def.name, - description: def.description, - url: def.url, - method: def.method, - tags: def.tags, - input_schema: JSON.stringify(def.input_schema), - output_schema: JSON.stringify(def.output_schema), - author: 'u6u-builtins', - version: '1.0.0', - }; - - const res = await registry.fetch('http://registry/components', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - - const body = await res.json().catch(() => ({ error: 'parse error' })); - // 201 = 新建成功;409 = 已存在(也算 ok) - const isOk = res.status === 201 || res.status === 409 || res.ok; - return { id: def.id, status: res.status, ok: isOk, ...(!isOk && { error: body }) }; -} - -export async function initComponents( - env: Bindings, -): Promise<{ ok: number; failed: number; results: unknown[] }> { - const defs = buildComponentDefs(env.WORKER_BASE_URL); - const results = await Promise.all(defs.map(def => publishOne(env.REGISTRY, def))); - - const ok = results.filter(r => r.ok).length; - const failed = results.filter(r => !r.ok).length; - return { ok, failed, results }; -} diff --git a/builtins/src/index.ts b/builtins/src/index.ts deleted file mode 100644 index 82e5e4f..0000000 --- a/builtins/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -// u6u-builtins Worker -// 所有零件已遷移至 u6u-core/registry/components/ 的 TinyGo .wasm 版本 -// 此 Worker 保留 /init 端點供初始化 Component Registry 使用 - -import { Hono } from 'hono'; -import { cors } from 'hono/cors'; -import type { Bindings } from './types'; -import { initComponents } from './actions/initComponents'; - -const app = new Hono<{ Bindings: Bindings }>(); -app.use('*', cors()); - -app.get('/', c => c.json({ - service: 'u6u-builtins', - version: '2.0.0', - status: 'ok', - note: '所有零件已遷移至 WASM,請使用 Component Registry', -})); - -// POST /init — 把所有零件上架到 Component Registry(冪等,可重複執行) -app.post('/init', async c => { - const result = await initComponents(c.env); - const allOk = result.failed === 0; - return c.json({ success: allOk, ...result }, allOk ? 200 : 207); -}); - -export default app; diff --git a/builtins/src/types.ts b/builtins/src/types.ts deleted file mode 100644 index c081b9f..0000000 --- a/builtins/src/types.ts +++ /dev/null @@ -1,156 +0,0 @@ -// u6u-builtins Worker 型別定義 - -export type Bindings = { - REGISTRY: Fetcher; // Component Registry Service Binding - CYPHER: Fetcher; // Cypher Executor Service Binding(排程執行用) - U6U_STORE: KVNamespace; // KV Store(cron: + ai-transform: 前綴) - AI: Ai; // Workers AI(ai-transform compile 用) - WORKER_BASE_URL: string; // 本 Worker 對外 URL(用於上架時填入 url 欄位) - ENVIRONMENT: string; -}; - -export type ActionResponse = - | { success: true; data: T } - | { success: false; error: string }; - -// componentDefs:所有內建零件的定義清單(資料層,initComponents.ts 使用) -export interface ComponentDef { - id: string; - name: string; - description: string; - url: string; - method: 'POST'; - tags: string; - input_schema: object; - output_schema: object; -} - -// CronJob:排程定義(儲存在 U6U_STORE,key = cron:{id}) -export interface CronJob { - id: string; - cron_expr: string; // 標準 5 欄位 cron expression - triplets?: string[]; // 三元組格式工作流(與 graph_token 二選一) - graph_token?: string; // 已存在的 webhook token - description: string; - created_at: string; - last_run?: string; - enabled: boolean; -} - -// AiTransform:已編譯的 AI 轉換函式(儲存在 U6U_STORE,key = ai-transform:{id}) -export interface AiTransform { - id: string; - description: string; // 自然語言描述 - fn_body: string; // 產生的 JS 函式 body(可直接 new Function 執行) - created_at: string; -} - -export function buildComponentDefs(baseUrl: string): ComponentDef[] { - return [ - // === 既有零件 === - { id: 'http-request', name: 'http-request', - description: '發送 HTTP 請求(GET/POST/PUT/DELETE),回傳 status 和 response body。支援自訂 headers 和 body。', - url: `${baseUrl}/http-request`, method: 'POST', tags: 'builtin,http,request,api', - input_schema: { type: 'object', required: ['url'], properties: { url: { type: 'string' }, method: { type: 'string', enum: ['GET','POST','PUT','DELETE','PATCH'] }, headers: { type: 'object' }, body: {} } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { status: { type: 'number' }, body: {} } } } } }, - { id: 'set', name: 'set', - description: '設定變數(key-value),把結果傳遞到下一個節點。支援 assignments 陣列或 values 物件兩種格式。', - url: `${baseUrl}/set`, method: 'POST', tags: 'builtin,variable,set,transform', - input_schema: { type: 'object', properties: { assignments: { type: 'array', items: { type: 'object' } }, values: { type: 'object' }, context: { type: 'object' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object' } } } }, - { id: 'filter', name: 'filter', - description: '依條件過濾陣列,回傳符合條件的元素。', - url: `${baseUrl}/filter`, method: 'POST', tags: 'builtin,filter,array,condition', - input_schema: { type: 'object', required: ['items','condition'], properties: { items: { type: 'array' }, condition: { type: 'object' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { items: { type: 'array' }, count: { type: 'number' } } } } } }, - { id: 'switch', name: 'switch', - description: '依條件路由,多個出口分支。', - url: `${baseUrl}/switch`, method: 'POST', tags: 'builtin,switch,branch,route,condition', - input_schema: { type: 'object', required: ['value','cases'], properties: { value: {}, cases: { type: 'array' }, default_branch: { type: 'string' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { branch: { type: 'string' } } } } } }, - { id: 'merge', name: 'merge', - description: '合併多個輸入物件為一個。', - url: `${baseUrl}/merge`, method: 'POST', tags: 'builtin,merge,combine,object', - input_schema: { type: 'object', required: ['inputs'], properties: { inputs: { type: 'array', items: { type: 'object' } } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object' } } } }, - { id: 'wait', name: 'wait', - description: '等待指定毫秒數後繼續,最多 30 秒。', - url: `${baseUrl}/wait`, method: 'POST', tags: 'builtin,wait,delay,throttle', - input_schema: { type: 'object', required: ['ms'], properties: { ms: { type: 'number' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object' } } } }, - { id: 'google-sheets', name: 'google-sheets', - description: '讀取或寫入 Google 試算表。需要 Google OAuth access_token。', - url: `${baseUrl}/google-sheets`, method: 'POST', tags: 'integration,google,sheets,oauth', - input_schema: { type: 'object', required: ['spreadsheet_id','range','access_token'], properties: { spreadsheet_id: { type: 'string' }, range: { type: 'string' }, action: { type: 'string', enum: ['read','write'] }, values: { type: 'array' }, access_token: { type: 'string' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object' } } } }, - { id: 'gmail', name: 'gmail', - description: '透過 Gmail 發送 Email。需要 Google OAuth access_token。', - url: `${baseUrl}/gmail`, method: 'POST', tags: 'integration,google,gmail,email,oauth', - input_schema: { type: 'object', required: ['to','subject','body','access_token'], properties: { to: { type: 'string' }, subject: { type: 'string' }, body: { type: 'string' }, access_token: { type: 'string' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object' } } } }, - { id: 'line-notify', name: 'line-notify', - description: '發送 LINE Notify 訊息。需要 LINE Channel Access Token。', - url: `${baseUrl}/line-notify`, method: 'POST', tags: 'integration,line,notify,message', - input_schema: { type: 'object', required: ['message','token'], properties: { message: { type: 'string' }, token: { type: 'string' }, image_url: { type: 'string' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object' } } } }, - { id: 'telegram', name: 'telegram', - description: '透過 Telegram Bot 發送訊息。需要 bot_token 和 chat_id。', - url: `${baseUrl}/telegram`, method: 'POST', tags: 'integration,telegram,bot,message', - input_schema: { type: 'object', required: ['chat_id','text','bot_token'], properties: { chat_id: { type: 'string' }, text: { type: 'string' }, bot_token: { type: 'string' }, parse_mode: { type: 'string' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: {} } } }, - // === P1 新增:Cron === - { id: 'cron', name: 'cron', - description: '建立定時排程工作流。指定 cron expression(如 0 9 * * *),到時間自動執行指定工作流。', - url: `${baseUrl}/cron`, method: 'POST', tags: 'builtin,cron,schedule,trigger,timer', - input_schema: { type: 'object', required: ['cron_expr'], properties: { cron_expr: { type: 'string', description: '標準 cron expression,如 0 9 * * *' }, triplets: { type: 'array', items: { type: 'string' } }, graph_token: { type: 'string' }, description: { type: 'string' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { cron_id: { type: 'string' }, cron_expr: { type: 'string' }, enabled: { type: 'boolean' } } } } } }, - // === P2 新增:控制流 === - { id: 'if', name: 'if', - description: '單一條件判斷,true/false 兩個出口。condition 支援 JS 表達式(如 x > 5)。', - url: `${baseUrl}/if`, method: 'POST', tags: 'builtin,control,if,branch,condition', - input_schema: { type: 'object', required: ['condition'], properties: { condition: { type: 'string', description: 'JS 表達式,如 x > 5' }, input: { type: 'object', description: '提供給 condition 的變數' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { result: { type: 'boolean' }, branch: { type: 'string', enum: ['true', 'false'] } } } } } }, - { id: 'foreach', name: 'foreach', - description: '對輸入陣列的每個元素執行一次後續工作流(依序)。', - url: `${baseUrl}/foreach`, method: 'POST', tags: 'builtin,control,foreach,loop,iteration', - input_schema: { type: 'object', required: ['items'], properties: { items: { type: 'array', description: '要迭代的陣列' }, item_key: { type: 'string', description: '每個元素注入的變數名,預設 item' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { items: { type: 'array' }, count: { type: 'number' }, current_index: { type: 'number' }, current_item: {} } } } } }, - { id: 'try-catch', name: 'try-catch', - description: '錯誤處理分支:執行失敗時走 catch 出口繼續,不中斷整個工作流。', - url: `${baseUrl}/try-catch`, method: 'POST', tags: 'builtin,control,try,catch,error,handling', - input_schema: { type: 'object', required: ['action'], properties: { action: { type: 'object', description: '要嘗試執行的動作(url + body)' }, fallback: { description: '失敗時的預設輸出' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { branch: { type: 'string', enum: ['try', 'catch'] }, result: {}, error: { type: 'string' } } } } } }, - // === P3 新增:資料處理 === - { id: 'string-ops', name: 'string-ops', - description: '字串操作:capitalize, trim, replace, split, join, substring, upper, lower, includes, startsWith, endsWith, regex match/extract/replace。', - url: `${baseUrl}/string-ops`, method: 'POST', tags: 'builtin,data,string,transform,text', - input_schema: { type: 'object', required: ['operation','input'], properties: { operation: { type: 'string' }, input: { type: 'string' }, args: { description: '操作參數(依 operation 而定)' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { result: {}, operation: { type: 'string' } } } } } }, - { id: 'number-ops', name: 'number-ops', - description: '數字操作:round, floor, ceil, abs, format, add, subtract, multiply, divide, mod, min, max, clamp。', - url: `${baseUrl}/number-ops`, method: 'POST', tags: 'builtin,data,number,math,transform', - input_schema: { type: 'object', required: ['operation','input'], properties: { operation: { type: 'string' }, input: { type: 'number' }, args: { description: '操作參數(依 operation 而定)' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { result: {}, operation: { type: 'string' } } } } } }, - { id: 'array-ops', name: 'array-ops', - description: '陣列操作:map, sort, max, min, sum, average, count, first, last, flatten, unique, reverse, chunk。', - url: `${baseUrl}/array-ops`, method: 'POST', tags: 'builtin,data,array,list,transform', - input_schema: { type: 'object', required: ['operation','input'], properties: { operation: { type: 'string' }, input: { type: 'array' }, args: { description: '操作參數(依 operation 而定)' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { result: {}, operation: { type: 'string' } } } } } }, - { id: 'date-ops', name: 'date-ops', - description: '日期操作:now, format, add, subtract, diff, parse, startOf, endOf, isBefore, isAfter。', - url: `${baseUrl}/date-ops`, method: 'POST', tags: 'builtin,data,date,time,transform', - input_schema: { type: 'object', required: ['operation'], properties: { operation: { type: 'string' }, input: { type: 'string', description: 'ISO 日期字串(now 操作可省略)' }, args: { description: '操作參數(依 operation 而定)' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { result: {}, operation: { type: 'string' } } } } } }, - // === P4 新增:AI Transform === - { id: 'ai-transform-compile', name: 'ai-transform-compile', - description: 'AI compile:輸入自然語言描述,AI 產生確定性 JS 轉換函式並儲存。返回 transform_id + 函式預覽。', - url: `${baseUrl}/ai-transform/compile`, method: 'POST', tags: 'ai,transform,compile,nlp,codegen', - input_schema: { type: 'object', required: ['description'], properties: { description: { type: 'string', description: '自然語言描述,如「把日期改成台灣格式 YYYY/MM/DD」' }, example_input: { description: '範例輸入(幫助 AI 理解)' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { transform_id: { type: 'string' }, fn_preview: { type: 'string' }, description: { type: 'string' } } } } } }, - { id: 'ai-transform-run', name: 'ai-transform-run', - description: 'AI run:使用已編譯的 transform_id 機械式執行轉換(不再呼叫 AI)。', - url: `${baseUrl}/ai-transform/run`, method: 'POST', tags: 'ai,transform,run,execute', - input_schema: { type: 'object', required: ['transform_id','input'], properties: { transform_id: { type: 'string', description: '由 compile 端點回傳的 ID' }, input: { description: '要轉換的資料' } } }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, data: { type: 'object', properties: { result: {}, transform_id: { type: 'string' } } } } } }, - ]; -} diff --git a/builtins/tsconfig.json b/builtins/tsconfig.json deleted file mode 100644 index 168765e..0000000 --- a/builtins/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "lib": ["ES2022"], - "types": ["@cloudflare/workers-types"], - "strict": true, - "noEmit": true - }, - "include": ["src/**/*"] -} diff --git a/builtins/wrangler.toml b/builtins/wrangler.toml deleted file mode 100644 index 9dccf85..0000000 --- a/builtins/wrangler.toml +++ /dev/null @@ -1,26 +0,0 @@ -name = "arcrun-builtins" -main = "src/index.ts" -compatibility_date = "2025-02-19" -compatibility_flags = ["nodejs_compat"] -workers_dev = true - -# Service Binding:呼叫 Cypher Executor 執行排程工作流 -[[services]] -binding = "CYPHER" -service = "arcrun-cypher-executor" # 填入你的 cypher-executor Worker 名稱 - -# KV Store:cron: 前綴 = 排程定義,ai-transform: 前綴 = 編譯後轉換函式 -[[kv_namespaces]] -binding = "U6U_STORE" -id = "" # 填入你的 KV Namespace ID - -# Workers AI:ai-transform 零件的 compile 階段使用 -[ai] -binding = "AI" - -# Cron Trigger:每分鐘掃描並執行到期的排程工作流 -[triggers] -crons = ["* * * * *"] - -[vars] -ENVIRONMENT = "production" diff --git a/cli/src/commands/init.ts b/cli/src/commands/init.ts index 14bb27d..d5fa6c7 100644 --- a/cli/src/commands/init.ts +++ b/cli/src/commands/init.ts @@ -51,7 +51,7 @@ async function initStandard(rl: ReturnType): Promise { - // 若沒有設定 encryption key,使用 base64 作為 fallback(dev 模式) - if (!encryptionKey || encryptionKey.length < 32) { - const b64 = Buffer.from(value).toString('base64'); - return JSON.stringify({ encrypted: b64, iv: 'dev-mode', mode: 'base64' }); + if (!encryptionKey || encryptionKey.length < 64) { + throw new Error( + 'ARCRUN_ENCRYPTION_KEY 未設定或長度不足(需要 256-bit hex,即 64 個十六進位字元)\n' + + '生成指令:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"' + ); } const keyBytes = hexToUint8Array(encryptionKey); diff --git a/cypher-executor/src/actions/execution-logger.ts b/cypher-executor/src/actions/execution-logger.ts new file mode 100644 index 0000000..36746bb --- /dev/null +++ b/cypher-executor/src/actions/execution-logger.ts @@ -0,0 +1,53 @@ +/** + * Execution Logger — 執行結果寫入 ANALYTICS_KV(fire-and-forget) + * + * 設計:每次 workflow 執行後,將統計數據寫入 ANALYTICS_KV(key = stats:{workflowId})。 + * Phase 7 可升級為 POST 至 registry.arcrun.dev/analytics/record。 + */ + +import type { Bindings, GraphNode } from '../types'; + +export interface ExecutionVerdict { + workflow_id: string; + component_ids: string[]; + verdict: 'success' | 'failed'; + duration_ms: number; + message: string; + recorded_at: string; +} + +/** + * 寫入執行結果至 ANALYTICS_KV(fire-and-forget,不阻擋主流程) + * 由 c.executionCtx.waitUntil() 包裹呼叫 + */ +export async function writeExecutionVerdict( + env: Bindings, + workflowId: string, + nodes: GraphNode[], + verdict: 'success' | 'failed', + durationMs: number, + message: string, +): Promise { + try { + const componentIds = nodes + .filter(n => n.type === 'Component' && n.componentId) + .map(n => n.componentId!); + + const record: ExecutionVerdict = { + workflow_id: workflowId, + component_ids: componentIds, + verdict, + duration_ms: durationMs, + message, + recorded_at: new Date().toISOString(), + }; + + // ANALYTICS_KV key = stats:{workflowId}:{timestamp}(避免覆蓋) + const key = `stats:${workflowId}:${Date.now()}`; + await env.ANALYTICS_KV.put(key, JSON.stringify(record), { + expirationTtl: 60 * 60 * 24 * 90, // 保留 90 天 + }); + } catch { + // fire-and-forget:不拋錯,不影響主流程 + } +} diff --git a/cypher-executor/src/routes/webhooks.ts b/cypher-executor/src/routes/webhooks.ts index efaca73..890d521 100644 --- a/cypher-executor/src/routes/webhooks.ts +++ b/cypher-executor/src/routes/webhooks.ts @@ -2,6 +2,7 @@ import { Hono } from 'hono'; import type { Bindings } from '../types'; import { generateToken, validateAndParseWebhook, executeWebhookGraph } from '../actions/webhook-handlers'; import { resolveWebhookGraph } from '../actions/webhook-graph-resolver'; +import { writeExecutionVerdict } from '../actions/execution-logger'; export const webhooksRouter = new Hono<{ Bindings: Bindings }>(); @@ -69,5 +70,14 @@ webhooksRouter.post('/webhooks/:token/trigger', async (c) => { } const result = await executeWebhookGraph(c.env, record.graph, triggerContext, token); + + // fire-and-forget analytics(不阻擋回應) + const graph = record.graph as { id?: string; nodes?: unknown[] }; + const workflowId = graph.id ?? token; + const nodes = Array.isArray(graph.nodes) ? (graph.nodes as import('../types').GraphNode[]) : []; + c.executionCtx.waitUntil( + writeExecutionVerdict(c.env, workflowId, nodes, result.success ? 'success' : 'failed', result.duration_ms, result.error ?? ''), + ); + return c.json(result, result.success ? 200 : 500); }); diff --git a/cypher-executor/src/types.ts b/cypher-executor/src/types.ts index adae63d..469245c 100644 --- a/cypher-executor/src/types.ts +++ b/cypher-executor/src/types.ts @@ -7,6 +7,8 @@ export type Bindings = { WEBHOOKS: KVNamespace; // Credential Store:AES-GCM 加密存放用戶 API token CREDENTIALS_KV: KVNamespace; + // Analytics:執行統計(fire-and-forget,key = stats:{workflowId}:{timestamp}) + ANALYTICS_KV: KVNamespace; // R2 Bucket:WASM 零件二進位 WASM_BUCKET: R2Bucket; // Workers AI diff --git a/cypher-executor/wrangler.toml b/cypher-executor/wrangler.toml index 83dda83..dfba859 100644 --- a/cypher-executor/wrangler.toml +++ b/cypher-executor/wrangler.toml @@ -20,6 +20,11 @@ id = "" # 填入你的 KV Namespace ID binding = "CREDENTIALS_KV" id = "" # 填入你的 Credentials KV Namespace ID +# Analytics:執行統計(fire-and-forget,保留 90 天) +[[kv_namespaces]] +binding = "ANALYTICS_KV" +id = "" # 填入你的 Analytics KV Namespace ID + # R2 Bucket:WASM 零件二進位(arcrun.dev 公眾零件庫,或自架時填入自己的 bucket) [[r2_buckets]] binding = "WASM_BUCKET" diff --git a/registry/components/gmail/component.contract.yaml b/registry/components/gmail/component.contract.yaml index fdaba3d..e9345e9 100644 --- a/registry/components/gmail/component.contract.yaml +++ b/registry/components/gmail/component.contract.yaml @@ -11,7 +11,7 @@ runtime_compat: constraints: max_size_kb: 2048 max_cold_start_ms: 50 - no_network_syscall: true + no_network_syscall: false no_filesystem_syscall: true io_model: "stdin_stdout_json" input_schema: diff --git a/registry/components/google_sheets/component.contract.yaml b/registry/components/google_sheets/component.contract.yaml index f83e956..d67b101 100644 --- a/registry/components/google_sheets/component.contract.yaml +++ b/registry/components/google_sheets/component.contract.yaml @@ -11,7 +11,7 @@ runtime_compat: constraints: max_size_kb: 2048 max_cold_start_ms: 50 - no_network_syscall: true + no_network_syscall: false no_filesystem_syscall: true io_model: "stdin_stdout_json" input_schema: diff --git a/registry/components/http_request/component.contract.yaml b/registry/components/http_request/component.contract.yaml index 348cbb4..c89d50c 100644 --- a/registry/components/http_request/component.contract.yaml +++ b/registry/components/http_request/component.contract.yaml @@ -11,7 +11,7 @@ runtime_compat: constraints: max_size_kb: 2048 max_cold_start_ms: 50 - no_network_syscall: true + no_network_syscall: false no_filesystem_syscall: true io_model: "stdin_stdout_json" input_schema: diff --git a/registry/components/line_notify/component.contract.yaml b/registry/components/line_notify/component.contract.yaml index dae1937..f7c6ff5 100644 --- a/registry/components/line_notify/component.contract.yaml +++ b/registry/components/line_notify/component.contract.yaml @@ -11,7 +11,7 @@ runtime_compat: constraints: max_size_kb: 2048 max_cold_start_ms: 50 - no_network_syscall: true + no_network_syscall: false no_filesystem_syscall: true io_model: "stdin_stdout_json" input_schema: diff --git a/registry/components/telegram/component.contract.yaml b/registry/components/telegram/component.contract.yaml index 3edb832..71b91ec 100644 --- a/registry/components/telegram/component.contract.yaml +++ b/registry/components/telegram/component.contract.yaml @@ -11,7 +11,7 @@ runtime_compat: constraints: max_size_kb: 2048 max_cold_start_ms: 50 - no_network_syscall: true + no_network_syscall: false no_filesystem_syscall: true io_model: "stdin_stdout_json" input_schema: diff --git a/registry/src/actions/submitComponent.ts b/registry/src/actions/submitComponent.ts index 53d68dd..9a6c965 100644 --- a/registry/src/actions/submitComponent.ts +++ b/registry/src/actions/submitComponent.ts @@ -1,5 +1,9 @@ -// 零件提交:沙盒驗收 → 寫入 KBDB Block → 上傳 R2 +// 零件提交:沙盒驗收 → 寫入 SUBMISSIONS_KV → 上傳 R2 // Requirements: 2.1, 2.2, 2.3 +// +// arcrun registry 不依賴 KBDB(InkStone 內部服務)。 +// 零件元數據存入 SUBMISSIONS_KV(key = comp:{canonical_id}:{version})。 +// WASM 二進位存入 WASM_BUCKET R2(key = components/{id}/{version}.wasm)。 import { runSandboxAcceptance } from './sandboxAcceptance'; import type { ComponentContract, SandboxResult, Bindings } from '../types'; @@ -15,71 +19,55 @@ export async function submitComponent( return sandboxResult; } - const blockId = `comp-${contract.canonical_id}-${contract.version}`; + const kvKey = `comp:${contract.canonical_id}:${contract.version}`; const r2Key = `components/${contract.canonical_id}/${contract.version}.wasm`; - // 2. 上傳 .wasm 至 R2 + // 2. 冪等:若已存在相同 (id, version) 直接回傳 + const existing = await env.SUBMISSIONS_KV.get(kvKey); + if (existing) { + return { + success: true, + component_id: contract.canonical_id, + version: contract.version, + wasm_r2_key: r2Key, + }; + } + + // 3. 上傳 .wasm 至 R2 await env.WASM_BUCKET.put(r2Key, wasmBytes, { httpMetadata: { contentType: 'application/wasm' }, }); - // 3. 寫入 KBDB Block(冪等:先嘗試取得,存在則更新,不存在則建立) - const kbdbUrl = env.KBDB_URL || 'https://kbdb.finally.click'; - const headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${env.KBDB_INTERNAL_TOKEN}`, - }; - - const slots: Record = { + // 4. 寫入 SUBMISSIONS_KV(元數據 + 初始統計) + const record = { canonical_id: contract.canonical_id, display_name: contract.display_name, category: contract.category, version: contract.version, + author: contract.author ?? '', wasi_target: contract.wasi_target, stability: contract.stability, - runtime_compat: JSON.stringify(contract.runtime_compat), + runtime_compat: contract.runtime_compat, component_type: contract.component_type ?? 'wasm', - max_size_kb: String(contract.constraints.max_size_kb), - max_cold_start_ms: String(contract.constraints.max_cold_start_ms), - no_network_syscall: String(contract.constraints.no_network_syscall), - input_schema: JSON.stringify(contract.input_schema), - output_schema: JSON.stringify(contract.output_schema), - gherkin_tests: JSON.stringify(contract.gherkin_tests), + constraints: contract.constraints, + input_schema: contract.input_schema, + output_schema: contract.output_schema, + gherkin_tests: contract.gherkin_tests, wasm_r2_key: r2Key, description: contract.description ?? '', - tags: JSON.stringify(contract.tags ?? []), - success_rate: '1', - avg_duration_ms: '0', - call_count: '0', - status: 'active', - deprecated_at: '', + tags: contract.tags ?? [], + // 初始統計 + success_rate: 1, + avg_duration_ms: 0, + call_count: 0, + // 可見性:預設 author_only,人工審核通過後改為 public + visibility: 'author_only' as const, + status: 'active' as const, + submitted_at: new Date().toISOString(), + deprecated_at: null, }; - if (contract.cypher_binding_url) slots.cypher_binding_url = contract.cypher_binding_url; - if (contract.service_binding_key) slots.service_binding_key = contract.service_binding_key; - - // 冪等:先查是否存在 - const existRes = await fetch(`${kbdbUrl}/records/${blockId}`, { headers }); - - if (existRes.ok) { - // 已存在:更新 slots - await fetch(`${kbdbUrl}/records/${blockId}`, { - method: 'PUT', - headers, - body: JSON.stringify({ values: slots }), - }); - } else { - // 不存在:建立新 Block - await fetch(`${kbdbUrl}/records`, { - method: 'POST', - headers, - body: JSON.stringify({ - record_id: blockId, - template_id: 'tpl-component', - values: slots, - }), - }); - } + await env.SUBMISSIONS_KV.put(kvKey, JSON.stringify(record)); return { success: true, diff --git a/registry/src/types.ts b/registry/src/types.ts index 9c4c215..3118228 100644 --- a/registry/src/types.ts +++ b/registry/src/types.ts @@ -7,8 +7,8 @@ import { z } from 'zod'; export type Bindings = { WASM_BUCKET: R2Bucket; AI: Ai; - KBDB_URL: string; - KBDB_INTERNAL_TOKEN: string; + SUBMISSIONS_KV: KVNamespace; // 零件元數據 + 可見性狀態(key = comp:{id}:{version}) + ANALYTICS_KV: KVNamespace; // 執行統計匯總(key = stats:{id}:{version}) ENVIRONMENT: string; };