import type { ComponentRunner, EdgeType } from '../types'; export const VALID_EDGE_TYPES = new Set([ // 現有 'PIPE', 'IF', 'FOREACH', 'CONTINUE', // 新增:執行語意 'IS_A', 'ON_SUCCESS', 'ON_FAIL', // 新增:條件語意(SDD workflow-discovery 3.11)—— 讀上游 if_control/switch 的 branch 'ON_TRUE', 'ON_FALSE', 'ON_BRANCH', // 新增:觸發語意 'ON_CLICK', 'CALLS_SUBFLOW', // 新增:結構語意(記錄圖結構,不執行) 'CONTAINS', 'HAS_STYLE', 'HAS_BEHAVIOR', ]); /** 內建零件 ID 集合(Worker 記憶體中已有實作)*/ export const BUILTIN_IDS = new Set([ 'webhook', 'comp_passthrough', 'comp_uppercase', 'comp_counter', ]); /** 語意邊 → EdgeType 映射(ADR-057 u6u L1:支援中文語意關係詞) * 完成後 → PIPE(成功後觸發下一個) * 失敗時 → CONTINUE(失敗後繼續) * 對每個 → FOREACH(迭代執行) * 條件滿足時 → IF(條件分支) */ export const SEMANTIC_EDGE_MAP: Record = { // 中文語意詞 '完成後': 'PIPE', '失敗時': 'ON_FAIL', '對每個': 'FOREACH', '條件滿足時': 'IF', // 條件分支語意(SDD workflow-discovery 3.11):讓意圖工作流寫得出兩條路 '成立時': 'ON_TRUE', '為真時': 'ON_TRUE', '不成立時': 'ON_FALSE', '為假時': 'ON_FALSE', '否則': 'ON_FALSE', // 英文別名 'SUCCESS': 'ON_SUCCESS', 'FAIL': 'ON_FAIL', 'TRUE': 'ON_TRUE', 'FALSE': 'ON_FALSE', 'ELSE': 'ON_FALSE', 'BRANCH': 'ON_BRANCH', 'CLICK': 'ON_CLICK', 'SUBFLOW': 'CALLS_SUBFLOW', }; /** * wait 零件的等待上限(毫秒)。與 registry/components/wait/component.contract.yaml * 逐字相同 —— 超過此值截斷、不報錯。**不可為了閃避資源上限調小**(Arcrun#101 紅線): * 「等外部系統跟上」是這顆零件存在的理由,把上限砍掉等於把能力換掉。 */ export const WAIT_MAX_MS = 30000; /** * 內建零件表(靜態函數) * WASM 零件 = 各自獨立 Worker,cypher-executor 走 HTTP URL 呼叫(不從 R2 讀) */ export const BUILTIN_COMPONENTS = new Map([ ['comp_passthrough', (ctx) => ctx], ['comp_uppercase', (ctx) => { const c = ctx as Record; return { ...c, text: String(c.text || '').toUpperCase() }; }], ['comp_counter', (ctx) => { const c = ctx as Record; return { ...c, count: (Number(c.count) || 0) + 1 }; }], // ── wait:等待 N 毫秒後繼續(Arcrun#101,2026-08-12)──────────────────────── // // 為什麼「等待」搬進引擎,而不是修那顆 WASM: // // 舊實作是 registry/components/wait/main.go(TinyGo → WASM),用 time.Sleep。 // TinyGo 的 sleep 走 WASI `poll_oneoff`;而每顆 component worker 的 WASI shim 把 // poll_oneoff 實作成 ENOSYS(`.component-builds/*/src/index.ts`:`poll_oneoff: () => 76`) // ⇒ TinyGo 排程器拿不到「睡到某個時間」的手段,退化成迴圈重讀 `clock_time_get` // 自旋等時間到(wasm 內可見 runtime.sleepTicks / sleepQueue / runtime.ticks 符號)。 // // 🔴 到這裡為止是**查得到原始碼的事實**。再往下「所以那個自旋迴圈的結束條件永遠 // 不成立」曾被當成結論寫在這裡,但**寫了測試去證,反而被打臉**:在 // vitest-pool-workers 的 workerd 裡,同步自旋 2553 圈之後 Date.now() 就前進了 // ⇒ 時鐘並沒有全程凍結。 // ⇒ 「為什麼三秒的等待會拖到 35 秒才死」的完整機制**目前仍是推測**, // 證據只有下面 leo 的四次實測。別把它當定論往外傳。 // // 所以症狀不是「等 N 秒花 N 秒 CPU」,而是「不管 ms 填多少都跑到 CPU 上限被砍」。 // leo 2026-08-12 在 youlin stage 實測(只有 input >> wait 兩個節點): // ms=3000 → 38.9s 後 503 / ms=20000 → 34.0s / ms=30000 → 34.9s / 寫死 3000 → 34.8s // 四個值同一個死法、與 ms 無關 —— 3 秒的等待撐到 35 秒才死,就是「迴圈根本沒結束」 // 的證據(若成本與時長成正比,ms=3000 只會花 3 秒 CPU,根本不該死)。 // 也就是說 wait 零件在 Workers 上從來沒有真的等待成功過,不只是貴。 // // 純 WASI 沙箱(stdin→stdout、無 socket、同步呼叫)本來就沒有「不花 CPU 地等」這種 // 東西 —— 會等的只有宿主。故 wait 與 trigger_workflow 同類:**是 orchestrator 的 // 執行排程職責,不是業務邏輯**(rule 02 §2.3 明列「workflow 執行排程」屬 cypher-executor // 合法職責;§2.2 禁的是解密/簽章/template 展開/具體 API 呼叫,等待都不是)。 // 搬進引擎不違反「業務邏輯走 WASM」鐵律。引擎這側 await 一個 timer 只花 wall-clock、 // 不記 CPU ⇒ 等 30 秒與等 3 秒同價(皆 ≈0)。 // // I/O 契約沿用 component.contract.yaml,既有 workflow 的 wait 節點定義不必改: // 吃 ms(必填 > 0)+可選 context;ms > WAIT_MAX_MS 截斷; // 回 { success: true, data: { ...context, waited_ms } };ms <= 0 回 success:false。 // 唯一刻意的放寬:ms 允許數字字串("3000")。WASM 版 json.Unmarshal 進 int 會直接 // 失敗,但 node.data 走 interpolateData 後 `ms: "{{input.delay}}"` 必然是字串 // ⇒ 收字串只會把「本來就跑不動的」變成跑得動,不會改變任何既有成功案例的行為。 ['wait', async (ctx) => { const c = (ctx && typeof ctx === 'object') ? ctx as Record : {}; const requested = typeof c.ms === 'number' ? c.ms : Number(c.ms); if (!Number.isFinite(requested) || requested <= 0) { return { success: false, error: 'ms 必須大於 0' }; } const ms = Math.min(Math.floor(requested), WAIT_MAX_MS); // 這一行就是整張票:await timer ⇒ 只走 wall-clock,不佔請求執行緒、不記 CPU。 await new Promise((resolve) => setTimeout(resolve, ms)); const passthrough = (c.context && typeof c.context === 'object' && !Array.isArray(c.context)) ? c.context as Record : {}; return { success: true, data: { ...passthrough, waited_ms: ms } }; }], ]); export const SCORE_THRESHOLD = 0.5;