// arcrun cypher-executor Worker — AI 工作流執行引擎 import { Hono } from 'hono'; import { cors } from 'hono/cors'; import type { ExecutionContext } from '@cloudflare/workers-types'; import type { Bindings } from './types'; import { handleScheduled } from './scheduled'; import { healthRouter } from './routes/health'; import { executeRouter } from './routes/execute'; import { cypherRouter } from './routes/cypher'; import { validateRouter } from './routes/validate'; import { docsRouter } from './routes/docs'; import { webhooksRouter } from './routes/webhooks'; import { webhooksCrudRouter } from './routes/webhooks-crud'; import { webhooksListRouter } from './routes/webhooks-list'; import { recipesRouter } from './routes/recipes'; import { credentialsRouter } from './routes/credentials'; import { webhooksNamedRouter } from './routes/webhooks-named'; import { authRouter } from './routes/auth'; import { resumeRouter } from './routes/resume'; import { executionsRouter } from './routes/executions'; import { initSeedRouter } from './routes/init-seed'; import { kbdbProxyRouter } from './routes/kbdb-proxy'; import { consoleAuthRouter } from './routes/console-auth'; import { consoleDashboardRouter } from './routes/console-dashboard'; import { portalRouter } from './routes/portal'; import { portalDataRouter } from './routes/portal-data'; const app = new Hono<{ Bindings: Bindings }>(); // 全域 CORS(允許 arcrun.dev landing page 帶 credentials 存取) // // 2026-07-21 cypher-ui-split:console/portal UI 搬到 Cloudflare Pages 後,前端與本 API // **不再同源**,故 UI 站的 origin 必須進白名單,否則所有 fetch 會被瀏覽器擋。 // 允許來源=上面兩個 landing + UI_ORIGINS(wrangler.toml [vars] 逗號分隔,實例自填 // 自己的 Pages 網域,如 https://arcrun-console-ui.pages.dev)。 // 刻意不用萬用字元:credentials:true 與 `*` 在 CORS 規格上互斥,且 Authorization/ // X-Arcrun-API-Key 是憑證等級標頭,開全域等於誰都能代打。 const STATIC_ORIGINS = ['https://arcrun.dev', 'https://www.arcrun.dev']; app.use('*', cors({ origin: (origin, c) => { // ⚠️ 非瀏覽器請求(CLI/curl/MCP)沒有 Origin 標頭 → origin 是空字串/undefined。 // 此時必須原樣放行,不能回 null——回 null 會讓 Hono cors 中介層在後續處理拋錯, // 表現為所有 CLI 部署一律 500(2026-07-21 實撞:acr push 全掛,對照組亦然)。 if (!origin) return origin; let extra: string[] = []; try { extra = String((c.env as Record).UI_ORIGINS || '') .split(',').map((s: string) => s.trim()).filter(Boolean); } catch { /* UI_ORIGINS 未設定=只用靜態白名單 */ } // 🔴 2026-08-08 事故根因修復:**同一台實例的 portal 一律自動放行,不再依賴注入**。 // // 那天發生什麼:leo 的 youlin 實例 portal 整個不能用——先是畫面頂端紅字 // 「設定檔沒載入(config.js)」(UI worker 缺 WORKER_SUBDOMAIN),修好之後**登入仍然失敗**。 // 瀏覽器 console 實證: // Access to fetch at '…/portal/login' … blocked by CORS policy: // No 'Access-Control-Allow-Origin' header is present // 真因=這台的 `UI_ORIGINS` 沒被設。 // // 兩次同一個病:**這些變數只有安裝器那條路會注入,任何人手動 `wrangler deploy` 就會漏掉—— // 而漏掉時系統看起來完全正常**(worker 上線、HTTP 200、版本號還是對的), // 只有真人點下去才會發現。leo:「這麼危險的問題已經發生 2 次,不可以再有一次。」 // // ⇒ 治法不是「記得要注入」,是**讓它不需要被注入**: // portal 與本 worker 是同一個 workers.dev 子網域下的兄弟,位址推導得出來。 // **少一個必須注入的變數,就少一個會被漏掉的東西。** // `UI_ORIGINS` 仍然有效(自訂網域/額外前端還是靠它),只是不再是「登得進去」的前提。 const sub = String((c.env as Record).WORKER_SUBDOMAIN || '').trim(); const sibling = sub ? [`https://arcrun-rag-ui.${sub}.workers.dev`] : []; return [...STATIC_ORIGINS, ...sibling, ...extra].includes(origin) ? origin : null; }, allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization', 'X-Arcrun-API-Key'], credentials: true, })); // 掛載所有路由器 app.route('/', docsRouter); app.route('/', healthRouter); app.route('/', executeRouter); app.route('/', cypherRouter); app.route('/', validateRouter); app.route('/', webhooksRouter); app.route('/', webhooksNamedRouter); // 必須在 webhooksCrudRouter 前(避免 /webhooks/:token 攔截 /webhooks/named) app.route('/', webhooksCrudRouter); app.route('/', webhooksListRouter); app.route('/', recipesRouter); app.route('/', credentialsRouter); app.route('/', authRouter); app.route('/', resumeRouter); app.route('/', executionsRouter); // LI SDD M2.1: /executions/* + /workflows/:name/executions app.route('/', initSeedRouter); // 薄殼原則:seed recipe 是 API 行為(rule 07,壓測 §4.1) app.route('/', kbdbProxyRouter); // kbdb-base 9.5:KBDB 資料層 proxy(讓 CLI 透過 cypher 達 KBDB,純轉發) app.route('/', consoleAuthRouter); // Arcrun#3 發現②:console 專用簡單 email+password 登入(單一管理員帳密,非多租戶) app.route('/', consoleDashboardRouter); // T-cockpit ②:駕駛艙 dashboard(聚合 KBDB dash_* entries,無需登入唯讀) app.route('/', portalRouter); // portal-auth P2(#24/#25):RAG Portal 多人授權——用戶模型+認證 API app.route('/', portalDataRouter); // portal-auth P3:/portal/data/* server-side enforce(owner_id+library 注入,安全核心) // Worker 導出(fetch + scheduled) // scheduled handler 對應 wrangler.toml [triggers].crons,每分鐘 tick; // 邏輯在 src/scheduled.ts。對應 SDD: arcrun.md 三-A P1 #3。 export default { fetch: app.fetch, scheduled: handleScheduled, } satisfies ExportedHandler;