arcrun — AI workflow execution engine (clean history)
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。 此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在 richblack/arcrun 與本地 backup 分支)。含: - acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe) - recipe push 把關(資料外流提醒 + 打通檢查) - 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1) - CLI / cypher-executor / registry / 完整 SDD Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { SEMANTIC_EDGE_MAP, VALID_EDGE_TYPES } from '../lib/constants';
|
||||
import type { EdgeType } from '../types';
|
||||
|
||||
export type ParsedTriplets = {
|
||||
edges: Array<{ from: string; to: string; label: string }>;
|
||||
nodeNames: Set<string>;
|
||||
/** 出現在 from 但不出現在任何 to 的節點(事件源 / 起始點) */
|
||||
sourceNodes: Set<string>;
|
||||
/** 出現在 to 但不出現在任何 from 的節點(終點)*/
|
||||
sinkNodes: Set<string>;
|
||||
};
|
||||
|
||||
export type NodeRole = 'Input' | 'Component' | 'Output';
|
||||
|
||||
/**
|
||||
* 解析後的零件 URI
|
||||
* 支援格式:
|
||||
* component://validate_json
|
||||
* component://validate_json@stable
|
||||
* component://validate_json@pinned:v1
|
||||
* workflow://wf_save_to_db
|
||||
* ui://u6u-btn
|
||||
* style://glow-effect
|
||||
*/
|
||||
export interface ResolvedComponentId {
|
||||
type: 'component' | 'workflow' | 'ui' | 'style';
|
||||
canonicalId: string;
|
||||
stability: 'floating' | 'stable' | 'pinned';
|
||||
pinnedVersion?: string;
|
||||
/** 原始 URI 字串 */
|
||||
raw: string;
|
||||
}
|
||||
|
||||
/** 解析零件 URI 協議 */
|
||||
export function resolveComponentId(uri: string): ResolvedComponentId {
|
||||
const raw = uri.trim();
|
||||
|
||||
// 解析協議前綴
|
||||
let type: ResolvedComponentId['type'] = 'component';
|
||||
let rest = raw;
|
||||
|
||||
if (raw.startsWith('component://')) {
|
||||
type = 'component';
|
||||
rest = raw.slice('component://'.length);
|
||||
} else if (raw.startsWith('workflow://')) {
|
||||
type = 'workflow';
|
||||
rest = raw.slice('workflow://'.length);
|
||||
} else if (raw.startsWith('ui://')) {
|
||||
type = 'ui';
|
||||
rest = raw.slice('ui://'.length);
|
||||
} else if (raw.startsWith('style://')) {
|
||||
type = 'style';
|
||||
rest = raw.slice('style://'.length);
|
||||
}
|
||||
|
||||
// 解析穩定性標籤
|
||||
// component://id@stable
|
||||
// component://id@pinned:v1
|
||||
let canonicalId = rest;
|
||||
let stability: ResolvedComponentId['stability'] = 'floating';
|
||||
let pinnedVersion: string | undefined;
|
||||
|
||||
const atIdx = rest.indexOf('@');
|
||||
if (atIdx > 0) {
|
||||
canonicalId = rest.slice(0, atIdx);
|
||||
const tag = rest.slice(atIdx + 1);
|
||||
if (tag === 'stable') {
|
||||
stability = 'stable';
|
||||
} else if (tag.startsWith('pinned:')) {
|
||||
stability = 'pinned';
|
||||
pinnedVersion = tag.slice('pinned:'.length);
|
||||
}
|
||||
}
|
||||
|
||||
return { type, canonicalId, stability, pinnedVersion, raw };
|
||||
}
|
||||
|
||||
/** 解析 triplets 字串陣列,回傳節點與邊的結構 */
|
||||
export function parseTriplets(rawTriplets: unknown[]): ParsedTriplets | null {
|
||||
const edges: Array<{ from: string; to: string; label: string }> = [];
|
||||
const nodeNames = new Set<string>();
|
||||
const fromSet = new Set<string>();
|
||||
const toSet = new Set<string>();
|
||||
|
||||
for (const line of rawTriplets) {
|
||||
if (typeof line !== 'string') continue;
|
||||
const parts = line.split('>>').map((s: string) => s.trim());
|
||||
if (parts.length !== 3) continue;
|
||||
const [from, action, to] = parts;
|
||||
edges.push({ from, to, label: action });
|
||||
nodeNames.add(from);
|
||||
nodeNames.add(to);
|
||||
fromSet.add(from);
|
||||
toSet.add(to);
|
||||
}
|
||||
|
||||
if (nodeNames.size === 0) return null;
|
||||
|
||||
const sourceNodes = new Set([...fromSet].filter(n => !toSet.has(n)));
|
||||
const sinkNodes = new Set([...toSet].filter(n => !fromSet.has(n)));
|
||||
return { edges, nodeNames, sourceNodes, sinkNodes };
|
||||
}
|
||||
|
||||
/** 保留字節點名稱 — 明確宣告為 Input 或 Output 端點 */
|
||||
const INPUT_NAMES = new Set(['input', 'trigger', 'webhook', 'start']);
|
||||
const OUTPUT_NAMES = new Set(['output', 'result', 'end', 'done']);
|
||||
|
||||
/** 根據節點在圖中的位置決定其 type
|
||||
*
|
||||
* 規則:
|
||||
* - 名稱在 INPUT_NAMES → Input(無論位置)
|
||||
* - 名稱在 OUTPUT_NAMES → Output(無論位置)
|
||||
* - sourceNode(只出現在 from)且名稱不在 INPUT_NAMES → Component(例如 cron 作為觸發源)
|
||||
* - sinkNode(只出現在 to)且名稱不在 OUTPUT_NAMES → Component(最常見情況:最後一個實際零件)
|
||||
* - 其他中間節點 → Component
|
||||
*/
|
||||
export function resolveNodeRole(name: string, parsed: ParsedTriplets): NodeRole {
|
||||
if (INPUT_NAMES.has(name.toLowerCase())) return 'Input';
|
||||
if (OUTPUT_NAMES.has(name.toLowerCase())) return 'Output';
|
||||
if (parsed.sourceNodes.has(name)) return 'Input';
|
||||
return 'Component';
|
||||
}
|
||||
|
||||
/** 將 edge label 轉換為合法 EdgeType
|
||||
* 優先序:VALID_EDGE_TYPES(完整匹配)→ SEMANTIC_EDGE_MAP(語意別名)→ 預設 PIPE */
|
||||
export function toEdgeType(label: string): EdgeType {
|
||||
const upper = label.toUpperCase();
|
||||
if (VALID_EDGE_TYPES.has(upper)) return upper as EdgeType;
|
||||
return (SEMANTIC_EDGE_MAP[label] ?? SEMANTIC_EDGE_MAP[upper] ?? 'PIPE') as EdgeType;
|
||||
}
|
||||
Reference in New Issue
Block a user