feat(arcrun): implement arcrun MVP — open-source AI workflow engine

Phase 1-5 complete per .agents/specs/u6u-core-mvp/:

**Phase 1 — Cherry-pick & cleanup**
- Create arcrun/ from cypher-executor, credentials, builtins, registry
- Remove 9 InkStone Service Bindings (KBDB, REGISTRY, CLINIC_*, AICEO, MINI_ME)
- Rewrite component-loader: 3-layer (builtin → WASM_BUCKET R2 → error)
- Remove autoPublishMissing.ts, proxy.ts (AICEO), execution-logger.ts (KBDB)
- Clean all KV namespace IDs and InkStone internal URLs from config files

**Phase 2 — contract.yaml completeness**
- Add credentials_required to gmail, google_sheets, telegram, line_notify
- Add config_example to all 21 components with annotated field descriptions

**Phase 3 — Credential injection**
- Add credential-injector.ts: AES-GCM decrypt from CREDENTIALS_KV
- Integrate into GraphExecutor before WASM execution
- Structured errors with repair instructions when credential missing

**Phase 4 — CLI (acr)**
- cli/package.json: arcrun package, bin: acr, deps: commander/js-yaml/chalk/ora
- 8 commands: init, creds push, push, run, validate, parts, list, logs
- Standard mode: writes directly to user's CF KV via CF REST API
- acr init: interactive setup with arcrun.dev API Key registration

**Phase 5 — Open source release prep**
- README.md: 5-minute quickstart, component table, workflow YAML syntax
- CONTRIBUTING.md: TinyGo dev env, component scaffolding, submission flow
- Security audit: no InkStone internal URLs/IDs in committed files
- .gitignore: exclude credentials.yaml, .wrangler, *.wasm

https://claude.ai/code/session_01BnCdSLVH8tUed9VrrPavgT
This commit is contained in:
Claude
2026-04-16 04:06:25 +00:00
commit 2707fca32b
155 changed files with 17413 additions and 0 deletions
@@ -0,0 +1,49 @@
import { BUILTIN_COMPONENTS } from './constants';
import type { Bindings, ComponentRunner } from '../types';
/**
* 建立零件載入器
*
* 三層優先序:
* 1. 內建零件(BUILTIN_COMPONENTS,純本地轉換,不需 R2
* 2. WASM_BUCKET R2 直讀 → {componentId}/{componentId}.wasm
* 3. 找不到 → 結構化錯誤(含 R2 key 與修復說明)
*/
export function createComponentLoader(env: Bindings) {
return async (componentId: string): Promise<ComponentRunner> => {
// 層 1:內建零件(無需 R2
const builtin = BUILTIN_COMPONENTS.get(componentId);
if (builtin) return builtin;
// 層 2:從 WASM_BUCKET R2 讀取
const wasmKey = `${componentId}/${componentId}.wasm`;
const wasmObj = await env.WASM_BUCKET.get(wasmKey);
if (wasmObj) {
const wasmBuffer = await wasmObj.arrayBuffer();
return createWasmRunner(componentId, wasmBuffer, env);
}
// 層 3:找不到
throw new Error(
`零件 ${componentId} 不存在。\n` +
`請確認 ${wasmKey} 已上傳至 WASM_BUCKET。\n` +
`修復:執行 acr parts 查看可用零件清單。`
);
};
}
/**
* 建立 WASM 零件執行器
* 使用 WASI preview1 stdin/stdout JSON I/O 模型
*/
function createWasmRunner(
componentId: string,
wasmBuffer: ArrayBuffer,
_env: Bindings,
): ComponentRunner {
return async (ctx: unknown): Promise<unknown> => {
// 動態 import wasm-executor(避免頂層 import 造成 Worker 啟動問題)
const { executeWasm } = await import('./wasm-executor');
return executeWasm(componentId, wasmBuffer, ctx);
};
}
+54
View File
@@ -0,0 +1,54 @@
import type { ComponentRunner, EdgeType } from '../types';
export const VALID_EDGE_TYPES = new Set([
// 現有
'PIPE', 'IF', 'FOREACH', 'CONTINUE',
// 新增:執行語意
'IS_A', 'ON_SUCCESS', 'ON_FAIL',
// 新增:觸發語意
'ON_CLICK', 'CALLS_SUBFLOW',
// 新增:結構語意(記錄圖結構,不執行)
'CONTAINS', 'HAS_STYLE', 'HAS_BEHAVIOR',
]);
/** 內建零件 ID 集合(不需要查 WASM_BUCKETWorker 記憶體中已有實作)*/
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<string, EdgeType> = {
// 中文語意詞
'完成後': 'PIPE',
'失敗時': 'ON_FAIL',
'對每個': 'FOREACH',
'條件滿足時': 'IF',
// 英文別名
'SUCCESS': 'ON_SUCCESS',
'FAIL': 'ON_FAIL',
'CLICK': 'ON_CLICK',
'SUBFLOW': 'CALLS_SUBFLOW',
};
/**
* 內建零件表(靜態函數,不需要 R2)
* WASM 零件從 WASM_BUCKET R2 直接讀取
*/
export const BUILTIN_COMPONENTS = new Map<string, ComponentRunner>([
['comp_passthrough', (ctx) => ctx],
['comp_uppercase', (ctx) => {
const c = ctx as Record<string, unknown>;
return { ...c, text: String(c.text || '').toUpperCase() };
}],
['comp_counter', (ctx) => {
const c = ctx as Record<string, unknown>;
return { ...c, count: (Number(c.count) || 0) + 1 };
}],
]);
export const SCORE_THRESHOLD = 0.5;
+306
View File
@@ -0,0 +1,306 @@
export const OPENAPI_SPEC = {
openapi: '3.0.3',
info: {
title: 'arcrun cypher-executor API',
description: 'AI Workflow Execution Engine — 透過三元組 Triplet 或圖 Graph 定義工作流,系統執行並回傳結果',
version: '1.0.0',
contact: {
name: 'arcrun',
url: 'https://github.com/arcrun/arcrun',
},
},
servers: [
{ url: 'https://cypher.arcrun.dev', description: 'arcrun.dev Hosted' },
{ url: 'http://localhost:8787', description: 'Local Development' },
],
paths: {
'/': {
get: {
summary: 'Health Check',
tags: ['Health'],
responses: {
'200': {
description: 'Service is running',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
service: { type: 'string' },
version: { type: 'string' },
status: { type: 'string' },
},
},
},
},
},
},
},
},
'/cypher/search': {
post: {
summary: '搜尋工作流需要的零件',
tags: ['Cypher'],
description: '用三元組描述工作流,系統解析並從 Registry 查詢對應零件',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
triplets: {
type: 'array',
items: { type: 'string' },
example: ['start >> 完成後 >> get-data', 'get-data >> 完成後 >> done'],
description: '三元組陣列,格式:\"FROM >> ACTION >> TO\"',
},
auto_publish: {
type: 'boolean',
default: true,
description: '缺失的零件是否自動產生發佈',
},
},
required: ['triplets'],
},
},
},
},
responses: {
'200': {
description: '零件搜尋成功(含版本號和時戳,適合 Markdown 文檔追蹤)',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
version: { type: 'string', example: 'search-v1-20260327-143022', description: '版本號(endpoint-v{major}-{timestamp}' },
timestamp: { type: 'string', format: 'date-time', description: 'ISO 8601 時戳' },
triplets: { type: 'array', items: { type: 'string' }, description: '回送的三元組列表' },
nodes: { type: 'object', description: '搜尋到的零件及其狀態' },
cypher: { type: 'object', description: '工作流圖(null 若有缺失零件)' },
missing: { type: 'array', items: { type: 'string' }, description: '缺失零件列表' },
auto_published: { type: 'object', description: '自動發佈的零件(若 auto_publish=true' },
},
},
},
},
},
'400': { description: '無法解析三元組' },
},
},
},
'/cypher/execute': {
post: {
summary: '執行工作流',
tags: ['Cypher'],
description: '直接執行 triplets,回傳完整執行結果。支援自動發佈缺失零件。',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
triplets: {
type: 'array',
items: { type: 'string' },
description: '三元組陣列,格式:"FROM >> ACTION >> TO"',
},
context: {
type: 'object',
description: '執行上下文,傳入各節點作為初始參數',
},
auto_publish: {
type: 'boolean',
default: true,
description: '缺失的零件是否自動產生臨時實作',
},
},
required: ['triplets'],
},
},
},
},
responses: {
'200': {
description: '執行成功(含版本號和時戳)',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
version: { type: 'string', example: 'execute-v1-20260327-143022', description: '版本號(endpoint-v{major}-{timestamp}' },
timestamp: { type: 'string', format: 'date-time', description: 'ISO 8601 時戳' },
success: { type: 'boolean', enum: [true] },
data: { type: 'object', description: '執行結果' },
trace: { type: 'array', description: '執行跟蹤' },
duration_ms: { type: 'number' },
},
},
},
},
},
'500': {
description: '執行失敗或部份零件缺失(含版本號和時戳)',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
version: { type: 'string', example: 'execute-v1-20260327-143022', description: '版本號(endpoint-v{major}-{timestamp}' },
timestamp: { type: 'string', format: 'date-time', description: 'ISO 8601 時戳' },
success: { type: 'boolean', enum: [false] },
error: { type: 'string' },
missing: { type: 'array', items: { type: 'string' }, description: '無法自動發佈的缺失零件' },
auto_published: {
type: 'object',
description: '自動發佈的零件資訊',
additionalProperties: {
type: 'object',
properties: {
ok: { type: 'boolean' },
componentId: { type: 'string' },
temporary_endpoint: { type: 'string', format: 'uri', description: '臨時實作的 URL' },
implement_by: { type: 'string', format: 'date-time', description: '實作截止時間' },
},
},
},
duration_ms: { type: 'number' },
},
},
},
},
},
},
},
},
'/webhooks': {
post: {
summary: '建立 Webhook',
tags: ['Webhooks'],
description: '將工作流註冊成 Webhook,得到公開 URL',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
triplets: {
type: 'array',
items: { type: 'string' },
},
description: { type: 'string' },
},
},
},
},
},
responses: {
'201': {
description: 'Webhook 建立成功',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
token: { type: 'string' },
webhook_url: { type: 'string', format: 'uri' },
description: { type: 'string' },
created_at: { type: 'string', format: 'date-time' },
},
},
},
},
},
},
},
get: {
summary: '列出所有 Webhooks',
tags: ['Webhooks'],
parameters: [
{
name: 'Authorization',
in: 'header',
required: true,
schema: { type: 'string', example: 'Bearer u6u_xxxxx' },
description: 'API Key 認證',
},
],
responses: {
'200': {
description: 'Webhooks 列表',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
webhooks: {
type: 'array',
items: {
type: 'object',
properties: {
token: { type: 'string' },
description: { type: 'string' },
created_at: { type: 'string', format: 'date-time' },
},
},
},
total: { type: 'number' },
},
},
},
},
},
'401': { description: '未授權' },
},
},
},
'/webhooks/{token}': {
get: {
summary: '查詢單個 Webhook',
tags: ['Webhooks'],
parameters: [
{
name: 'token',
in: 'path',
required: true,
schema: { type: 'string' },
},
],
responses: {
'200': {
description: 'Webhook 資訊',
},
'404': { description: 'Webhook 不存在' },
},
},
delete: {
summary: '刪除 Webhook',
tags: ['Webhooks'],
parameters: [
{
name: 'token',
in: 'path',
required: true,
schema: { type: 'string' },
},
],
responses: {
'200': { description: 'Webhook 已刪除' },
'404': { description: 'Webhook 不存在' },
},
},
},
},
components: {
securitySchemes: {
ApiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'Authorization',
},
},
},
};
+25
View File
@@ -0,0 +1,25 @@
import { z } from 'zod';
// 圖定義的 Zod Schema
export const graphSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
nodes: z.array(z.object({
id: z.string(),
type: z.enum(['Input', 'Component', 'Output']),
componentId: z.string().optional(),
data: z.record(z.unknown()).optional(),
})),
edges: z.array(z.object({
from: z.string(),
to: z.string(),
type: z.enum(['PIPE', 'IF', 'FOREACH', 'CONTINUE']),
condition: z.string().optional(),
iterator: z.string().optional(),
})),
});
export const executeSchema = z.object({
graph: graphSchema,
context: z.record(z.unknown()).default({}),
});
+243
View File
@@ -0,0 +1,243 @@
/**
* WASI preview1 輕量 shim
* 只實作 stdin/stdout/stderr 所需的最小 syscall 集合。
* 其餘 syscall 一律回傳 ENOSYS(76),確保零件無法呼叫網路或檔案系統。
*
* 不依賴任何外部套件(不使用 @cloudflare/workers-wasi)。
* Requirements: 3.1, 3.3
*/
const WASI_ESUCCESS = 0;
const WASI_ENOSYS = 76;
// fd 常數
const FD_STDIN = 0;
const FD_STDOUT = 1;
const FD_STDERR = 2;
export interface WasiShim {
/** WebAssembly.Imports 物件,傳入 WebAssembly.instantiate */
imports: WebAssembly.Imports;
/** 取得 stdout 的完整輸出(合併所有 chunks) */
getStdout(): string;
/** 取得 stderr 的完整輸出 */
getStderr(): string;
/** 注入 WebAssembly.Memoryinstantiate 後呼叫) */
setMemory(memory: WebAssembly.Memory): void;
}
/**
* Host function 注入介面
* 讓 .wasm 零件能透過 host function 呼叫外部服務,而不需要網路 syscall
*/
export interface WasiHostFunctions {
/** HTTP 請求 host function.wasm 呼叫此函數發出 HTTP 請求 */
http_request?: (url: string, method: string, headers: string, body: string) => Promise<string>;
}
/**
* 建立 WASI shim 實例
* @param stdinData - 要寫入 stdin 的 UTF-8 字串(通常是 JSON.stringify(input)
* @param hostFunctions - 可選的 host function 注入(讓 .wasm 呼叫外部服務)
*/
export function createWasiShim(stdinData: string, hostFunctions?: WasiHostFunctions): WasiShim {
const stdinBytes = new TextEncoder().encode(stdinData);
let stdinOffset = 0;
const stdoutChunks: Uint8Array[] = [];
const stderrChunks: Uint8Array[] = [];
let memory: WebAssembly.Memory | null = null;
function getMemoryView(): DataView {
if (!memory) throw new Error('WASI memory not set — call setMemory() after instantiate');
return new DataView(memory.buffer);
}
/**
* fd_write: 將 iovec 陣列的資料寫入 fdstdout=1 或 stderr=2
* iovec 結構:{ buf: i32, buf_len: i32 }(各 4 byteslittle-endian
*/
function fd_write(fd: number, iovs: number, iovs_len: number, nwritten_ptr: number): number {
if (fd !== FD_STDOUT && fd !== FD_STDERR) return WASI_ENOSYS;
const view = getMemoryView();
const buf = memory!.buffer;
let totalWritten = 0;
for (let i = 0; i < iovs_len; i++) {
const iov_base = view.getUint32(iovs + i * 8, true);
const iov_len = view.getUint32(iovs + i * 8 + 4, true);
if (iov_len === 0) continue;
const chunk = new Uint8Array(buf, iov_base, iov_len);
const copy = new Uint8Array(iov_len);
copy.set(chunk);
if (fd === FD_STDOUT) stdoutChunks.push(copy);
else stderrChunks.push(copy);
totalWritten += iov_len;
}
view.setUint32(nwritten_ptr, totalWritten, true);
return WASI_ESUCCESS;
}
/**
* fd_read: 從 stdin 讀取資料到 iovec 陣列
*/
function fd_read(fd: number, iovs: number, iovs_len: number, nread_ptr: number): number {
if (fd !== FD_STDIN) return WASI_ENOSYS;
const view = getMemoryView();
const buf = memory!.buffer;
let totalRead = 0;
for (let i = 0; i < iovs_len; i++) {
const iov_base = view.getUint32(iovs + i * 8, true);
const iov_len = view.getUint32(iovs + i * 8 + 4, true);
if (iov_len === 0) continue;
const remaining = stdinBytes.length - stdinOffset;
if (remaining <= 0) break;
const toCopy = Math.min(iov_len, remaining);
const dest = new Uint8Array(buf, iov_base, toCopy);
dest.set(stdinBytes.subarray(stdinOffset, stdinOffset + toCopy));
stdinOffset += toCopy;
totalRead += toCopy;
}
view.setUint32(nread_ptr, totalRead, true);
return WASI_ESUCCESS;
}
/**
* proc_exit: 零件呼叫 exit(),拋出 Error 中止執行
*/
function proc_exit(code: number): never {
throw new Error(`wasm exit: ${code}`);
}
/**
* random_get: 填充隨機 bytes(使用 Web Crypto API
*/
function random_get(buf_ptr: number, buf_len: number): number {
const view = new Uint8Array(memory!.buffer, buf_ptr, buf_len);
crypto.getRandomValues(view);
return WASI_ESUCCESS;
}
const shim: WasiShim = {
imports: {
wasi_snapshot_preview1: { fd_write,
fd_read,
proc_exit,
random_get,
// 其餘 syscall 回傳 ENOSYS(不允許網路/檔案系統操作)
fd_seek: () => WASI_ENOSYS,
fd_close: () => WASI_ESUCCESS,
fd_fdstat_get: () => WASI_ENOSYS,
fd_prestat_get: () => WASI_ENOSYS,
fd_prestat_dir_name: () => WASI_ENOSYS,
environ_get: () => WASI_ESUCCESS,
environ_sizes_get: (count_ptr: number, size_ptr: number) => {
if (memory) {
const view = getMemoryView();
view.setUint32(count_ptr, 0, true);
view.setUint32(size_ptr, 0, true);
}
return WASI_ESUCCESS;
},
args_get: () => WASI_ESUCCESS,
args_sizes_get: (argc_ptr: number, argv_buf_size_ptr: number) => {
if (memory) {
const view = getMemoryView();
view.setUint32(argc_ptr, 0, true);
view.setUint32(argv_buf_size_ptr, 0, true);
}
return WASI_ESUCCESS;
},
clock_time_get: (id: number, precision: bigint, time_ptr: number) => {
if (memory) {
const view = getMemoryView();
const now = BigInt(Date.now()) * 1_000_000n;
view.setBigUint64(time_ptr, now, true);
}
return WASI_ESUCCESS;
},
clock_res_get: () => WASI_ENOSYS,
poll_oneoff: () => WASI_ENOSYS,
sched_yield: () => WASI_ESUCCESS,
proc_raise: () => WASI_ENOSYS,
sock_accept: () => WASI_ENOSYS,
sock_recv: () => WASI_ENOSYS,
sock_send: () => WASI_ENOSYS,
sock_shutdown: () => WASI_ENOSYS,
path_open: () => WASI_ENOSYS,
path_create_directory: () => WASI_ENOSYS,
path_remove_directory: () => WASI_ENOSYS,
path_rename: () => WASI_ENOSYS,
path_unlink_file: () => WASI_ENOSYS,
path_filestat_get: () => WASI_ENOSYS,
path_readlink: () => WASI_ENOSYS,
path_symlink: () => WASI_ENOSYS,
path_link: () => WASI_ENOSYS,
},
// u6u host functions:讓 .wasm 零件透過 host function 呼叫外部服務
// .wasm 零件用 //go:wasmimport u6u http_request 宣告
u6u: {
http_request: hostFunctions?.http_request
? async (urlPtr: number, urlLen: number, methodPtr: number, methodLen: number,
headersPtr: number, headersLen: number, bodyPtr: number, bodyLen: number,
outPtr: number, outLenPtr: number): Promise<number> => {
if (!memory) return 1;
const buf = memory.buffer;
const dec = new TextDecoder();
const url = dec.decode(new Uint8Array(buf, urlPtr, urlLen));
const method = dec.decode(new Uint8Array(buf, methodPtr, methodLen));
const headers = dec.decode(new Uint8Array(buf, headersPtr, headersLen));
const body = dec.decode(new Uint8Array(buf, bodyPtr, bodyLen));
try {
const result = await hostFunctions!.http_request!(url, method, headers, body);
const encoded = new TextEncoder().encode(result);
// 寫入結果到 outPtr 指向的 buffer
const view = new DataView(buf);
new Uint8Array(buf, outPtr, encoded.length).set(encoded);
view.setUint32(outLenPtr, encoded.length, true);
return 0; // success
} catch {
return 1; // error
}
}
: () => 1, // host function 未注入時回傳錯誤
},
},
setMemory(mem: WebAssembly.Memory) {
memory = mem;
},
getStdout(): string {
if (stdoutChunks.length === 0) return '';
const total = stdoutChunks.reduce((n, c) => n + c.length, 0);
const merged = new Uint8Array(total);
let offset = 0;
for (const chunk of stdoutChunks) {
merged.set(chunk, offset);
offset += chunk.length;
}
return new TextDecoder().decode(merged);
},
getStderr(): string {
if (stderrChunks.length === 0) return '';
const total = stderrChunks.reduce((n, c) => n + c.length, 0);
const merged = new Uint8Array(total);
let offset = 0;
for (const chunk of stderrChunks) {
merged.set(chunk, offset);
offset += chunk.length;
}
return new TextDecoder().decode(merged);
},
};
return shim;
}
+119
View File
@@ -0,0 +1,119 @@
/**
* Tier 1 WASM 執行器
* 從 R2 載入 .wasm,透過 WASI preview1 shim 執行,stdin/stdout JSON I/O。
*
* 快取策略:WebAssembly.Module 快取於 Worker 記憶體(跨請求共享),
* 避免重複編譯。每次執行只重新 instantiate。
*
* Requirements: 3.1, 3.3, 6.6
*/
import { createWasiShim, type WasiHostFunctions } from './wasi-shim';
// Worker 記憶體快取:r2Key → WebAssembly.Module
const moduleCache = new Map<string, WebAssembly.Module>();
export interface WasmExecutorOptions {
/** R2 Bucket binding */
bucket: R2Bucket;
/** R2 物件鍵(例:components/validate_json/v1.wasm */
r2Key: string;
/** 逾時上限(ms),對應 contract.constraints.max_cold_start_ms */
timeoutMs?: number;
/** 可選的 host function 注入(讓 .wasm 呼叫外部服務) */
hostFunctions?: WasiHostFunctions;
}
export interface WasmExecuteResult {
output: unknown;
stdout: string;
stderr: string;
duration_ms: number;
}
/**
* 執行 WASM 零件
* @param input - 傳入零件的 JSON 物件(寫入 stdin
* @param options - 執行選項
*/
export async function executeWasm(
input: unknown,
options: WasmExecutorOptions,
): Promise<WasmExecuteResult> {
const { bucket, r2Key, timeoutMs = 50, hostFunctions } = options;
// ...(其餘不變)
const start = Date.now();
// 1. 取得或編譯 WebAssembly.Module(快取)
let wasmModule = moduleCache.get(r2Key);
if (!wasmModule) {
const obj = await bucket.get(r2Key);
if (!obj) throw new Error(`WASM 零件不存在於 R2${r2Key}`);
const arrayBuffer = await obj.arrayBuffer();
wasmModule = await WebAssembly.compile(arrayBuffer);
moduleCache.set(r2Key, wasmModule);
}
// 2. 建立 WASI shim,注入 stdin 與可選的 host functions
const stdinJson = JSON.stringify(input);
const shim = createWasiShim(stdinJson, hostFunctions);
// 3. instantiate(每次執行都重新 instantiate,shim 狀態是獨立的)
const instance = await WebAssembly.instantiate(wasmModule, shim.imports);
// 4. 注入 memoryWASI fd_read/fd_write 需要存取 memory
const memory = instance.exports.memory as WebAssembly.Memory | undefined;
if (memory) shim.setMemory(memory);
// 5. 執行(帶逾時)
const exports = instance.exports as Record<string, unknown>;
const entryFn = (exports._start ?? exports.main) as (() => void) | undefined;
if (typeof entryFn !== 'function') {
throw new Error(`WASM 零件缺少 _start 或 main exportr2Key: ${r2Key}`);
}
const runWithTimeout = new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`WASM 執行逾時(>${timeoutMs}ms):${r2Key}`));
}, timeoutMs);
try {
entryFn();
clearTimeout(timer);
resolve();
} catch (e) {
clearTimeout(timer);
// proc_exit(0) 拋出 "wasm exit: 0",視為正常結束
if (e instanceof Error && e.message === 'wasm exit: 0') {
resolve();
} else {
reject(e);
}
}
});
await runWithTimeout;
// 6. 讀取 stdoutJSON.parse
const stdout = shim.getStdout().trim();
const stderr = shim.getStderr().trim();
const duration_ms = Date.now() - start;
if (!stdout) {
throw new Error(`WASM 零件沒有輸出(stdout 為空):${r2Key}`);
}
let output: unknown;
try {
output = JSON.parse(stdout);
} catch {
throw new Error(`WASM 零件輸出不是合法 JSON${stdout.slice(0, 200)}`);
}
return { output, stdout, stderr, duration_ms };
}
/** 清除 Module 快取(測試用) */
export function clearModuleCache(): void {
moduleCache.clear();
}