@arronqzy/blueprint-dsl 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +31 -0
- package/readme.md +32 -0
- package/src/behaviors/default.ts +44 -0
- package/src/blueprint-signal.ts +14 -0
- package/src/clock-config.ts +232 -0
- package/src/core/behavior-registry.ts +32 -0
- package/src/core/behavior.ts +13 -0
- package/src/core/executor.ts +51 -0
- package/src/core/scheduler-loop.ts +13 -0
- package/src/fetch-config.ts +244 -0
- package/src/index.ts +29 -0
- package/src/json-config.ts +37 -0
- package/src/lifecycle.ts +45 -0
- package/src/logic-config.ts +53 -0
- package/src/node-signal.ts +83 -0
- package/src/nodes/definitions.ts +95 -0
- package/src/runtime/blueprint-cycle.ts +136 -0
- package/src/runtime/clock-active-registry.ts +30 -0
- package/src/runtime/clock-scheduler.ts +49 -0
- package/src/runtime/execution-trace.ts +55 -0
- package/src/runtime/graph-runner.ts +1326 -0
- package/src/swagger.ts +192 -0
- package/src/type.ts +34 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { BLUEPRINT_NODE_TYPE } from "../nodes/definitions.js";
|
|
2
|
+
import type { LibraryBlueprintResolver, RunnableGraph } from "./graph-runner.js";
|
|
3
|
+
|
|
4
|
+
export const BLUEPRINT_CALL_STACK_KEY = "__blueprintCallStack";
|
|
5
|
+
|
|
6
|
+
export class BlueprintCycleError extends Error {
|
|
7
|
+
readonly cycleIds: string[];
|
|
8
|
+
|
|
9
|
+
constructor(cycleIds: string[], message: string) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "BlueprintCycleError";
|
|
12
|
+
this.cycleIds = cycleIds;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function collectBlueprintLibraryRefs(graph: RunnableGraph): string[] {
|
|
17
|
+
const ids = new Set<string>();
|
|
18
|
+
for (const node of graph.nodes) {
|
|
19
|
+
if (node.nodeType === BLUEPRINT_NODE_TYPE && node.libraryBlueprintId) {
|
|
20
|
+
ids.add(node.libraryBlueprintId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...ids];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatBlueprintCycleMessage(
|
|
27
|
+
cycleIds: string[],
|
|
28
|
+
resolveName?: (libraryBlueprintId: string) => string
|
|
29
|
+
): string {
|
|
30
|
+
const labels = cycleIds.map((id) => resolveName?.(id) ?? id);
|
|
31
|
+
return `蓝图引用存在死循环:${labels.join(" → ")}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type BlueprintCycleCheckOptions = {
|
|
35
|
+
rootGraph: RunnableGraph;
|
|
36
|
+
/** 当前蓝图对应的蓝图库 id;工作区蓝图未入库时为 null */
|
|
37
|
+
rootLibraryBlueprintId?: string | null;
|
|
38
|
+
resolveLibraryBlueprint: LibraryBlueprintResolver;
|
|
39
|
+
resolveBlueprintName?: (libraryBlueprintId: string) => string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type BlueprintCycleCheckResult =
|
|
43
|
+
| { ok: true }
|
|
44
|
+
| { ok: false; cycleIds: string[]; message: string };
|
|
45
|
+
|
|
46
|
+
export async function detectBlueprintReferenceCycle(
|
|
47
|
+
options: BlueprintCycleCheckOptions
|
|
48
|
+
): Promise<BlueprintCycleCheckResult> {
|
|
49
|
+
const {
|
|
50
|
+
rootGraph,
|
|
51
|
+
rootLibraryBlueprintId,
|
|
52
|
+
resolveLibraryBlueprint,
|
|
53
|
+
resolveBlueprintName,
|
|
54
|
+
} = options;
|
|
55
|
+
|
|
56
|
+
const graphCache = new Map<string, RunnableGraph | null>();
|
|
57
|
+
|
|
58
|
+
const loadGraph = async (libraryBlueprintId: string) => {
|
|
59
|
+
if (!graphCache.has(libraryBlueprintId)) {
|
|
60
|
+
graphCache.set(
|
|
61
|
+
libraryBlueprintId,
|
|
62
|
+
await resolveLibraryBlueprint(libraryBlueprintId)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return graphCache.get(libraryBlueprintId) ?? null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const visit = async (
|
|
69
|
+
path: string[],
|
|
70
|
+
graph: RunnableGraph
|
|
71
|
+
): Promise<string[] | null> => {
|
|
72
|
+
for (const refId of collectBlueprintLibraryRefs(graph)) {
|
|
73
|
+
const cycleIndex = path.indexOf(refId);
|
|
74
|
+
if (cycleIndex !== -1) {
|
|
75
|
+
return [...path.slice(cycleIndex), refId];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const nested = await loadGraph(refId);
|
|
79
|
+
if (!nested) continue;
|
|
80
|
+
|
|
81
|
+
const cycle = await visit([...path, refId], nested);
|
|
82
|
+
if (cycle) return cycle;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const initialPath = rootLibraryBlueprintId ? [rootLibraryBlueprintId] : [];
|
|
88
|
+
const cycleIds = await visit(initialPath, rootGraph);
|
|
89
|
+
|
|
90
|
+
if (!cycleIds) {
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
cycleIds,
|
|
97
|
+
message: formatBlueprintCycleMessage(cycleIds, resolveBlueprintName),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function assertNoBlueprintReferenceCycle(
|
|
102
|
+
options: BlueprintCycleCheckOptions
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const result = await detectBlueprintReferenceCycle(options);
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
throw new BlueprintCycleError(result.cycleIds, result.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function resolveBlueprintCallStack(
|
|
111
|
+
scopeStack: string[] | undefined,
|
|
112
|
+
rootLibraryBlueprintId?: string | null
|
|
113
|
+
): string[] {
|
|
114
|
+
if (scopeStack) return scopeStack;
|
|
115
|
+
return rootLibraryBlueprintId ? [rootLibraryBlueprintId] : [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function detectRuntimeBlueprintCycle(
|
|
119
|
+
callStack: string[],
|
|
120
|
+
targetLibraryBlueprintId: string,
|
|
121
|
+
resolveName?: (libraryBlueprintId: string) => string
|
|
122
|
+
): BlueprintCycleCheckResult {
|
|
123
|
+
const cycleIndex = callStack.indexOf(targetLibraryBlueprintId);
|
|
124
|
+
if (cycleIndex === -1) {
|
|
125
|
+
return { ok: true };
|
|
126
|
+
}
|
|
127
|
+
const cycleIds = [
|
|
128
|
+
...callStack.slice(cycleIndex),
|
|
129
|
+
targetLibraryBlueprintId,
|
|
130
|
+
];
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
cycleIds,
|
|
134
|
+
message: formatBlueprintCycleMessage(cycleIds, resolveName),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const activeClockNodeIds = new Set<string>();
|
|
2
|
+
const listeners = new Set<() => void>();
|
|
3
|
+
|
|
4
|
+
function emit() {
|
|
5
|
+
listeners.forEach((listener) => listener());
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function markClockNodeActive(nodeId: string) {
|
|
9
|
+
if (activeClockNodeIds.has(nodeId)) return;
|
|
10
|
+
activeClockNodeIds.add(nodeId);
|
|
11
|
+
emit();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function markClockNodeInactive(nodeId: string) {
|
|
15
|
+
if (!activeClockNodeIds.delete(nodeId)) return;
|
|
16
|
+
emit();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isClockNodeActive(nodeId: string): boolean {
|
|
20
|
+
return activeClockNodeIds.has(nodeId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getActiveClockNodeIds(): string[] {
|
|
24
|
+
return [...activeClockNodeIds];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function subscribeActiveClockNodes(listener: () => void) {
|
|
28
|
+
listeners.add(listener);
|
|
29
|
+
return () => listeners.delete(listener);
|
|
30
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildClockSignalValue,
|
|
3
|
+
normalizeClockConfig,
|
|
4
|
+
startClockSchedule,
|
|
5
|
+
stopAllClockSchedules,
|
|
6
|
+
stopClockSchedule,
|
|
7
|
+
abortClockSession,
|
|
8
|
+
isClockScheduleActive,
|
|
9
|
+
isClockSessionAborted,
|
|
10
|
+
clearClockSessionAbort,
|
|
11
|
+
validateClockScheduleConfig,
|
|
12
|
+
type ClockNodeConfig,
|
|
13
|
+
} from "../clock-config.js";
|
|
14
|
+
import {
|
|
15
|
+
markClockNodeActive,
|
|
16
|
+
markClockNodeInactive,
|
|
17
|
+
} from "./clock-active-registry.js";
|
|
18
|
+
|
|
19
|
+
export function buildClockSessionKey(scopeId: string, nodeId: string): string {
|
|
20
|
+
return `${scopeId}:${nodeId}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function abortClockNode(scopeId: string, nodeId: string): void {
|
|
24
|
+
abortClockSession(buildClockSessionKey(scopeId, nodeId));
|
|
25
|
+
markClockNodeInactive(nodeId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function scheduleClockOutputs(options: {
|
|
29
|
+
sessionKey: string;
|
|
30
|
+
config: ClockNodeConfig;
|
|
31
|
+
onTick: () => void | Promise<void>;
|
|
32
|
+
onSettled?: () => void;
|
|
33
|
+
}): () => void {
|
|
34
|
+
return startClockSchedule(options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
stopClockSchedule,
|
|
39
|
+
stopAllClockSchedules,
|
|
40
|
+
abortClockSession,
|
|
41
|
+
isClockScheduleActive,
|
|
42
|
+
isClockSessionAborted,
|
|
43
|
+
clearClockSessionAbort,
|
|
44
|
+
validateClockScheduleConfig,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function createClockOutputValue(config: ClockNodeConfig) {
|
|
48
|
+
return buildClockSignalValue(normalizeClockConfig(config));
|
|
49
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Value } from "../type.js";
|
|
2
|
+
import { isFalseSignal, isTrueSignal } from "../node-signal.js";
|
|
3
|
+
|
|
4
|
+
export type ExecutionTraceEntry = {
|
|
5
|
+
id: string;
|
|
6
|
+
nodeId: string;
|
|
7
|
+
nodeLabel?: string;
|
|
8
|
+
nodeType: string;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
finishedAt: number;
|
|
11
|
+
isoTime: string;
|
|
12
|
+
inputs: Record<string, unknown>;
|
|
13
|
+
outputs: Record<string, unknown>;
|
|
14
|
+
error?: string;
|
|
15
|
+
/** 时钟节点第几次 tick(从 1 开始) */
|
|
16
|
+
clockTickIndex?: number;
|
|
17
|
+
/** 时钟节点本次调度总 tick 数 */
|
|
18
|
+
clockTickTotal?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ExecutionRunRecord = {
|
|
22
|
+
runId: string;
|
|
23
|
+
blueprintId: string | null;
|
|
24
|
+
blueprintName: string;
|
|
25
|
+
lifecycleNodeId: string;
|
|
26
|
+
lifecycleNodeLabel?: string;
|
|
27
|
+
lifecyclePhase?: string;
|
|
28
|
+
startedAt: number;
|
|
29
|
+
finishedAt?: number;
|
|
30
|
+
status: "running" | "completed" | "paused" | "failed";
|
|
31
|
+
entries: ExecutionTraceEntry[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function serializeTraceValue(value: Value | undefined): unknown {
|
|
35
|
+
if (value === undefined) return undefined;
|
|
36
|
+
if (isTrueSignal(value)) {
|
|
37
|
+
return {
|
|
38
|
+
kind: "true",
|
|
39
|
+
value: value.value,
|
|
40
|
+
isoTime: value.isoTime,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (isFalseSignal(value)) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "false",
|
|
46
|
+
error: value.error,
|
|
47
|
+
isoTime: value.isoTime,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createTraceEntryId() {
|
|
54
|
+
return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
55
|
+
}
|