@arronqzy/react-blueprint 1.0.0

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.
Files changed (124) hide show
  1. package/.eslintrc.cjs +5 -0
  2. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
  3. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
  4. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
  5. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
  6. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
  7. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
  8. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
  9. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
  10. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
  11. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
  12. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
  13. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
  14. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
  15. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
  16. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
  17. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
  18. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
  19. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
  20. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
  21. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
  22. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
  23. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
  24. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
  25. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
  26. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
  27. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
  28. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
  29. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
  30. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
  31. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
  32. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
  33. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
  34. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
  35. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
  36. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
  37. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
  38. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
  39. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
  40. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
  41. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
  42. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
  43. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
  44. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
  45. package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
  46. package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
  47. package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
  48. package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
  49. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
  50. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
  51. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
  52. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
  53. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
  54. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
  55. package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
  56. package/.turbo/turbo-lint.log +4 -0
  57. package/dist/assets/index-CneNgzWb.css +1 -0
  58. package/dist/assets/index-DsVhHV0-.js +226 -0
  59. package/dist/index.html +14 -0
  60. package/index.html +13 -0
  61. package/index.ts +70 -0
  62. package/package.json +43 -0
  63. package/postcss.config.cjs +6 -0
  64. package/src/BlueprintCanvasContext.tsx +61 -0
  65. package/src/BlueprintContextMenu.tsx +108 -0
  66. package/src/BlueprintNodeConfigSidebar.tsx +358 -0
  67. package/src/blueprint.css +321 -0
  68. package/src/blueprintNodeTypes.ts +20 -0
  69. package/src/components/BlueprintExecutionLogPanel.tsx +427 -0
  70. package/src/components/BlueprintLibrarySelect.tsx +68 -0
  71. package/src/components/BlueprintMetaDialog.tsx +96 -0
  72. package/src/components/BlueprintNodeSwitchTaskDialog.tsx +58 -0
  73. package/src/components/BlueprintPanelToolbar.tsx +664 -0
  74. package/src/components/ClockNodeConfigPanel.tsx +139 -0
  75. package/src/components/FetchNodeConfigPanel.tsx +716 -0
  76. package/src/components/FetchUrlAutocomplete.tsx +190 -0
  77. package/src/components/JsonNodeConfigPanel.tsx +79 -0
  78. package/src/components/LogicNodeConfigPanel.tsx +80 -0
  79. package/src/components/ViewElementMultiSelect.tsx +140 -0
  80. package/src/createBlueprintEdgeTypes.tsx +10 -0
  81. package/src/dev-seed.ts +34 -0
  82. package/src/dev.css +38 -0
  83. package/src/edges/BlueprintSmoothEdge.tsx +35 -0
  84. package/src/fetch-config-task-store.ts +197 -0
  85. package/src/flowCoordinates.ts +19 -0
  86. package/src/flowDefaults.ts +9 -0
  87. package/src/graph/blueprint-graph.ts +261 -0
  88. package/src/graph/document.ts +384 -0
  89. package/src/graph/index.ts +7 -0
  90. package/src/graph/node-summary.ts +88 -0
  91. package/src/graph/node-types.ts +9 -0
  92. package/src/graph/react-flow-adapter.ts +127 -0
  93. package/src/graph/sync-edges.ts +69 -0
  94. package/src/graph/sync-nodes.ts +104 -0
  95. package/src/hooks/useBlueprintDebugSession.ts +515 -0
  96. package/src/hooks/useBlueprintFlowColorMode.ts +8 -0
  97. package/src/hooks/useBlueprintFlowState.ts +486 -0
  98. package/src/hooks/useBlueprintFlowViewport.ts +54 -0
  99. package/src/hooks/useBlueprintNodeSelectionGuard.ts +63 -0
  100. package/src/hooks/useBlueprintPageLifecycle.ts +245 -0
  101. package/src/index.tsx +271 -0
  102. package/src/library/blueprint-io.ts +108 -0
  103. package/src/library/blueprint-library-db.ts +112 -0
  104. package/src/library/execution-log-db.ts +171 -0
  105. package/src/library/execution-log-settings.ts +50 -0
  106. package/src/library/swagger-docs.ts +56 -0
  107. package/src/library/types.ts +35 -0
  108. package/src/main.tsx +43 -0
  109. package/src/nodes/AndFlowNode.tsx +59 -0
  110. package/src/nodes/BlueprintFlowNode.tsx +26 -0
  111. package/src/nodes/BlueprintNodeCard.tsx +169 -0
  112. package/src/nodes/BlueprintNodeShell.tsx +45 -0
  113. package/src/nodes/ClockFlowNode.tsx +67 -0
  114. package/src/nodes/FetchFlowNode.tsx +26 -0
  115. package/src/nodes/JsonFlowNode.tsx +26 -0
  116. package/src/nodes/LifecycleFlowNode.tsx +44 -0
  117. package/src/nodes/LogicFlowNode.tsx +26 -0
  118. package/src/runtime/document-to-runnable-graph.ts +48 -0
  119. package/src/runtime/execution-overlay.ts +169 -0
  120. package/src/types.ts +1 -0
  121. package/src/vite-env.d.ts +1 -0
  122. package/tailwind.config.ts +6 -0
  123. package/tsconfig.json +12 -0
  124. package/vite.config.ts +16 -0
@@ -0,0 +1,104 @@
1
+ import type { Connection, Edge, Node } from "@xyflow/react";
2
+
3
+ import type { BlueprintGraph } from "./blueprint-graph";
4
+ import {
5
+ collectPositionUpdates,
6
+ toReactFlowNodes,
7
+ type BlueprintFlowNodeData,
8
+ } from "./react-flow-adapter";
9
+
10
+ /** 从 graph 同步节点时保留 RF 已测量的宽高,否则边无法绘制 */
11
+ export function mergeMeasuredFlowNodes<T extends Node>(
12
+ incoming: T[],
13
+ previous: T[]
14
+ ): T[] {
15
+ const prevById = new Map(previous.map((n) => [n.id, n]));
16
+ return incoming.map((n) => {
17
+ const prev = prevById.get(n.id);
18
+ if (!prev) return n;
19
+ return {
20
+ ...n,
21
+ measured: prev.measured ?? n.measured,
22
+ width: prev.width ?? n.width,
23
+ height: prev.height ?? n.height,
24
+ };
25
+ });
26
+ }
27
+
28
+ export function nodeListSignature(nodes: { id: string }[]): string {
29
+ return nodes
30
+ .map((n) => n.id)
31
+ .sort()
32
+ .join(",");
33
+ }
34
+
35
+ /** 含 role / 配置 / 名称等影响 React Flow 节点展示与端口的字段 */
36
+ export function nodeStructureSignature(
37
+ nodes: Array<{
38
+ id: string;
39
+ role: string;
40
+ label: string;
41
+ configSource?: string;
42
+ lifecyclePhase?: string;
43
+ libraryBlueprintId?: string;
44
+ nodeType?: string;
45
+ fetchConfig?: { url?: string; method?: string; apiBaseUrl?: string; swaggerDocsUrl?: string };
46
+ jsonConfig?: { jsonString?: string };
47
+ logicConfig?: { sourceCode?: string };
48
+ clockConfig?: { intervalSeconds?: number; timeFormat?: string; outputCount?: number; emitImmediately?: boolean };
49
+ }>
50
+ ): string {
51
+ return nodes
52
+ .map(
53
+ (n) =>
54
+ `${n.id}:${n.role}:${n.label}:${n.configSource ?? ""}:${n.lifecyclePhase ?? ""}:${n.libraryBlueprintId ?? ""}:${n.nodeType ?? ""}:${n.fetchConfig?.url ?? ""}:${n.fetchConfig?.method ?? ""}:${n.fetchConfig?.apiBaseUrl ?? ""}:${n.fetchConfig?.swaggerDocsUrl ?? ""}:${n.jsonConfig?.jsonString ?? ""}:${n.logicConfig?.sourceCode ?? ""}:${n.clockConfig?.intervalSeconds ?? ""}:${n.clockConfig?.timeFormat ?? ""}:${n.clockConfig?.outputCount ?? ""}:${n.clockConfig?.emitImmediately ?? ""}`
55
+ )
56
+ .sort()
57
+ .join("|");
58
+ }
59
+
60
+ export function graphToFlowNodes(
61
+ graph: BlueprintGraph,
62
+ selectedNodeId: string | null,
63
+ libraryNameById?: ReadonlyMap<string, string>
64
+ ): Node<BlueprintFlowNodeData>[] {
65
+ const base = toReactFlowNodes(graph.document) as Node<BlueprintFlowNodeData>[];
66
+ return base.map((n) => ({
67
+ ...n,
68
+ selected: false,
69
+ data: {
70
+ ...n.data,
71
+ libraryBlueprintLabel: n.data.libraryBlueprintId
72
+ ? libraryNameById?.get(n.data.libraryBlueprintId)
73
+ : undefined,
74
+ isSelected: selectedNodeId === n.id,
75
+ },
76
+ }));
77
+ }
78
+
79
+ export function resolveConnection(
80
+ connection: Connection,
81
+ rfNodes: Node<BlueprintFlowNodeData>[]
82
+ ): Connection | null {
83
+ const nodeIds = new Set(rfNodes.map((n) => n.id));
84
+ const source = connection.source ?? "";
85
+ const target = connection.target ?? "";
86
+
87
+ if (!source || !target || source === target) return null;
88
+ if (!nodeIds.has(source) || !nodeIds.has(target)) return null;
89
+
90
+ return {
91
+ source,
92
+ target,
93
+ sourceHandle: connection.sourceHandle ?? "out",
94
+ targetHandle: connection.targetHandle ?? "in",
95
+ };
96
+ }
97
+
98
+ export function filterEdgesWithValidNodes(edges: Edge[], nodeIds: Set<string>): Edge[] {
99
+ return edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
100
+ }
101
+
102
+ export function applyFlowNodePositions(graph: BlueprintGraph, rfNodes: Node[]) {
103
+ return graph.applyNodePositions(collectPositionUpdates(rfNodes));
104
+ }
@@ -0,0 +1,515 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import {
3
+ BlueprintGraphRunner,
4
+ detectBlueprintReferenceCycle,
5
+ type ExecutionRunRecord,
6
+ type ExecutionTraceEntry,
7
+ type LibraryBlueprintResolver,
8
+ } from "@arronqzy/blueprint-dsl";
9
+
10
+ import type { BlueprintGraph } from "../graph/blueprint-graph";
11
+ import {
12
+ clearAllExecutionRunRecords,
13
+ downloadExecutionRunExport,
14
+ listExecutionRunRecords,
15
+ purgeExecutionRunsOlderThan,
16
+ putExecutionRunRecord,
17
+ trimExecutionRunRecordsToMax,
18
+ } from "../library/execution-log-db";
19
+ import {
20
+ readExecutionLogSettings,
21
+ retentionCutoffMs,
22
+ writeExecutionLogSettings,
23
+ type ExecutionLogSettings,
24
+ } from "../library/execution-log-settings";
25
+ import { documentToRunnableGraph } from "../runtime/document-to-runnable-graph";
26
+ import { buildExecutionOverlay, shouldHaltDebugOnFalseSignal } from "../runtime/execution-overlay";
27
+ import { edgeListSignature } from "../graph/sync-edges";
28
+ import { nodeStructureSignature } from "../graph/sync-nodes";
29
+
30
+ export type LifecycleNodeOption = {
31
+ id: string;
32
+ label: string;
33
+ phase?: string;
34
+ };
35
+
36
+ export type UseBlueprintDebugSessionOptions = {
37
+ graph: BlueprintGraph;
38
+ blueprintId: string | null;
39
+ blueprintName: string;
40
+ resolveLibraryBlueprint?: LibraryBlueprintResolver;
41
+ libraryNameById?: ReadonlyMap<string, string>;
42
+ /** 检测到蓝图相互引用死循环时回调 */
43
+ onExecutionBlocked?: (message: string) => void;
44
+ /** 视图绑定节点收到真信号时,为关联视图元素写入 scope */
45
+ onViewScopeUpdate?: (
46
+ viewElementIds: string[],
47
+ scope: unknown
48
+ ) => void;
49
+ };
50
+
51
+ function buildNodeLabelMap(graph: BlueprintGraph): Record<string, string> {
52
+ return Object.fromEntries(
53
+ graph.document.nodes.map((node) => [node.id, node.label || node.id])
54
+ );
55
+ }
56
+
57
+ function createRunId() {
58
+ return `run_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
59
+ }
60
+
61
+ export function useBlueprintDebugSession({
62
+ graph,
63
+ blueprintId,
64
+ blueprintName,
65
+ resolveLibraryBlueprint,
66
+ libraryNameById,
67
+ onExecutionBlocked,
68
+ onViewScopeUpdate,
69
+ }: UseBlueprintDebugSessionOptions) {
70
+ const runnerRef = useRef<BlueprintGraphRunner | null>(null);
71
+ const runIdRef = useRef<string | null>(null);
72
+ const runStartedAtRef = useRef<number>(0);
73
+
74
+ const [selectedLifecycleNodeId, setSelectedLifecycleNodeId] = useState<
75
+ string | null
76
+ >(null);
77
+ const [entries, setEntries] = useState<ExecutionTraceEntry[]>([]);
78
+ const [chainComplete, setChainComplete] = useState(false);
79
+ const [logPanelOpen, setLogPanelOpen] = useState(false);
80
+ const [running, setRunning] = useState(false);
81
+ const [settings, setSettings] = useState<ExecutionLogSettings>(() =>
82
+ readExecutionLogSettings()
83
+ );
84
+ const [savedRuns, setSavedRuns] = useState<ExecutionRunRecord[]>([]);
85
+ const [totalSavedRunCount, setTotalSavedRunCount] = useState(0);
86
+
87
+ const lifecycleNodes = useMemo<LifecycleNodeOption[]>(
88
+ () =>
89
+ graph.document.nodes
90
+ .filter((node) => node.role === "lifecycle")
91
+ .map((node) => ({
92
+ id: node.id,
93
+ label: node.label || node.id,
94
+ phase: node.lifecyclePhase,
95
+ })),
96
+ [graph.document.nodes]
97
+ );
98
+
99
+ const hasLifecycleNodes = lifecycleNodes.length > 0;
100
+
101
+ const graphDebugSig = useMemo(
102
+ () =>
103
+ `${nodeStructureSignature(graph.document.nodes)}|${edgeListSignature(graph.document.edges)}`,
104
+ [graph.document.nodes, graph.document.edges]
105
+ );
106
+
107
+ const allowFalseSignalPropagation =
108
+ graph.document.allowFalseSignalPropagation ?? false;
109
+
110
+ const buildRunnableGraph = useCallback(
111
+ () => documentToRunnableGraph(graph.document, { libraryNameById }),
112
+ [graph.document, libraryNameById]
113
+ );
114
+
115
+ const syncRunnerWithLatestGraph = useCallback(() => {
116
+ const runner = runnerRef.current;
117
+ if (!runner?.isDebugSessionActive()) return;
118
+ runner.updateGraph(buildRunnableGraph());
119
+ if (selectedLifecycleNodeId) {
120
+ runner.refreshDebugLifecycleScope(selectedLifecycleNodeId);
121
+ }
122
+ runner.refreshPendingQueueFromLatestGraph();
123
+ }, [buildRunnableGraph, selectedLifecycleNodeId]);
124
+
125
+ const resolveBlueprintName = useCallback(
126
+ (id: string) => libraryNameById?.get(id) ?? id,
127
+ [libraryNameById]
128
+ );
129
+
130
+ const validateBlueprintGraph = useCallback(async () => {
131
+ if (!resolveLibraryBlueprint) return true;
132
+ const result = await detectBlueprintReferenceCycle({
133
+ rootGraph: buildRunnableGraph(),
134
+ rootLibraryBlueprintId: blueprintId,
135
+ resolveLibraryBlueprint,
136
+ resolveBlueprintName,
137
+ });
138
+ if (!result.ok) {
139
+ onExecutionBlocked?.(result.message);
140
+ return false;
141
+ }
142
+ return true;
143
+ }, [
144
+ blueprintId,
145
+ buildRunnableGraph,
146
+ onExecutionBlocked,
147
+ resolveBlueprintName,
148
+ resolveLibraryBlueprint,
149
+ ]);
150
+
151
+ const ensureRunner = useCallback(() => {
152
+ const runner = new BlueprintGraphRunner(buildRunnableGraph(), {
153
+ resolveLibraryBlueprint,
154
+ rootLibraryBlueprintId: blueprintId,
155
+ resolveBlueprintName,
156
+ onViewScopeUpdate,
157
+ });
158
+ runnerRef.current = runner;
159
+ return runner;
160
+ }, [
161
+ blueprintId,
162
+ buildRunnableGraph,
163
+ onViewScopeUpdate,
164
+ resolveBlueprintName,
165
+ resolveLibraryBlueprint,
166
+ ]);
167
+
168
+ const refreshSavedRuns = useCallback(async () => {
169
+ const all = await listExecutionRunRecords();
170
+ setTotalSavedRunCount(all.length);
171
+ const items = blueprintId
172
+ ? all.filter((item) => item.blueprintId === blueprintId)
173
+ : all;
174
+ setSavedRuns(items);
175
+ }, [blueprintId]);
176
+
177
+ useEffect(() => {
178
+ if (!logPanelOpen) return;
179
+ void refreshSavedRuns();
180
+ }, [logPanelOpen, refreshSavedRuns]);
181
+
182
+ const enforceSavedRunLimits = useCallback(async () => {
183
+ const cutoff = retentionCutoffMs(settings.retentionDays);
184
+ await purgeExecutionRunsOlderThan(cutoff);
185
+ await trimExecutionRunRecordsToMax(settings.maxSavedRuns);
186
+ await refreshSavedRuns();
187
+ }, [refreshSavedRuns, settings.maxSavedRuns, settings.retentionDays]);
188
+
189
+ const applyRetention = enforceSavedRunLimits;
190
+
191
+ const clearAllSavedRuns = useCallback(async () => {
192
+ const removed = await clearAllExecutionRunRecords();
193
+ setSavedRuns([]);
194
+ setTotalSavedRunCount(0);
195
+ return removed;
196
+ }, []);
197
+
198
+ const buildRunRecord = useCallback(
199
+ (
200
+ status: ExecutionRunRecord["status"],
201
+ nextEntries: ExecutionTraceEntry[]
202
+ ): ExecutionRunRecord => {
203
+ const lifecycleNode = lifecycleNodes.find(
204
+ (item) => item.id === selectedLifecycleNodeId
205
+ );
206
+ return {
207
+ runId: runIdRef.current ?? createRunId(),
208
+ blueprintId,
209
+ blueprintName,
210
+ lifecycleNodeId: selectedLifecycleNodeId ?? "",
211
+ lifecycleNodeLabel: lifecycleNode?.label,
212
+ lifecyclePhase: lifecycleNode?.phase,
213
+ startedAt: runStartedAtRef.current || Date.now(),
214
+ finishedAt:
215
+ status === "completed" || status === "failed" ? Date.now() : undefined,
216
+ status,
217
+ entries: nextEntries,
218
+ };
219
+ },
220
+ [blueprintId, blueprintName, lifecycleNodes, selectedLifecycleNodeId]
221
+ );
222
+
223
+ const persistRunIfNeeded = useCallback(
224
+ async (record: ExecutionRunRecord) => {
225
+ if (!settings.autoSave) return;
226
+ await putExecutionRunRecord(record);
227
+ await enforceSavedRunLimits();
228
+ },
229
+ [enforceSavedRunLimits, settings.autoSave]
230
+ );
231
+
232
+ const beginRun = useCallback(() => {
233
+ if (!selectedLifecycleNodeId) return null;
234
+ runIdRef.current = createRunId();
235
+ runStartedAtRef.current = Date.now();
236
+ const runner = ensureRunner();
237
+ runner.beginDebugSession(selectedLifecycleNodeId);
238
+ setChainComplete(false);
239
+ return runner;
240
+ }, [ensureRunner, selectedLifecycleNodeId]);
241
+
242
+ useEffect(() => {
243
+ const runner = runnerRef.current;
244
+ if (!runner?.isDebugSessionActive()) return;
245
+ runner.updateGraph(buildRunnableGraph());
246
+ if (selectedLifecycleNodeId) {
247
+ runner.refreshDebugLifecycleScope(selectedLifecycleNodeId);
248
+ }
249
+ runner.refreshPendingQueueFromLatestGraph();
250
+ }, [buildRunnableGraph, graphDebugSig, selectedLifecycleNodeId]);
251
+
252
+ useEffect(() => {
253
+ if (running) return;
254
+ const halted = shouldHaltDebugOnFalseSignal(
255
+ entries,
256
+ allowFalseSignalPropagation
257
+ );
258
+ if (halted) {
259
+ setChainComplete(true);
260
+ return;
261
+ }
262
+ const runner = runnerRef.current;
263
+ if (
264
+ runner?.isDebugSessionActive() &&
265
+ runner.isDebugQueueEmpty() &&
266
+ entries.length > 0
267
+ ) {
268
+ setChainComplete(true);
269
+ }
270
+ }, [allowFalseSignalPropagation, entries, running]);
271
+
272
+ const runAll = useCallback(async () => {
273
+ if (!selectedLifecycleNodeId) return;
274
+ if (!(await validateBlueprintGraph())) return;
275
+ setRunning(true);
276
+ setLogPanelOpen(true);
277
+ try {
278
+ const runner = beginRun();
279
+ if (!runner) return;
280
+ const labels = buildNodeLabelMap(graph);
281
+ const onProgress = (trace: ExecutionTraceEntry[]) => {
282
+ setEntries([...trace]);
283
+ };
284
+ while (runner.isDebugSessionActive() && !runner.isDebugQueueEmpty()) {
285
+ syncRunnerWithLatestGraph();
286
+ await runner.debugStep(labels, onProgress);
287
+ const trace = runner.getTraceEntries();
288
+ setEntries(trace);
289
+ if (shouldHaltDebugOnFalseSignal(trace, allowFalseSignalPropagation)) {
290
+ setChainComplete(true);
291
+ break;
292
+ }
293
+ await new Promise<void>((resolve) => {
294
+ requestAnimationFrame(() => resolve());
295
+ });
296
+ }
297
+ const result = runner.getTraceEntries();
298
+ setChainComplete(
299
+ shouldHaltDebugOnFalseSignal(result, allowFalseSignalPropagation) ||
300
+ (runner.isDebugQueueEmpty() && result.length > 0)
301
+ );
302
+ const record = buildRunRecord("completed", result);
303
+ await persistRunIfNeeded(record);
304
+ } finally {
305
+ setRunning(false);
306
+ }
307
+ }, [allowFalseSignalPropagation, beginRun, buildRunRecord, graph, persistRunIfNeeded, selectedLifecycleNodeId, syncRunnerWithLatestGraph, validateBlueprintGraph]);
308
+
309
+ const stepNext = useCallback(async () => {
310
+ if (
311
+ !selectedLifecycleNodeId ||
312
+ chainComplete ||
313
+ shouldHaltDebugOnFalseSignal(entries, allowFalseSignalPropagation)
314
+ ) {
315
+ return;
316
+ }
317
+ setRunning(true);
318
+ setLogPanelOpen(true);
319
+ try {
320
+ let runner: BlueprintGraphRunner | null = runnerRef.current;
321
+ if (!runner?.isDebugSessionActive()) {
322
+ if (!(await validateBlueprintGraph())) return;
323
+ const started = beginRun();
324
+ if (!started) return;
325
+ runner = started;
326
+ } else {
327
+ syncRunnerWithLatestGraph();
328
+ }
329
+ if (runner.isDebugQueueEmpty()) {
330
+ setChainComplete(true);
331
+ return;
332
+ }
333
+
334
+ const labels = buildNodeLabelMap(graph);
335
+ const onProgress = (trace: ExecutionTraceEntry[]) => {
336
+ setEntries([...trace]);
337
+ };
338
+ const { done, haltedByFalseSignal } = await runner.debugStep(
339
+ labels,
340
+ onProgress
341
+ );
342
+ const nextEntries = runner.getTraceEntries();
343
+ setEntries(nextEntries);
344
+ setChainComplete(done || haltedByFalseSignal === true);
345
+
346
+ if (done) {
347
+ const record = buildRunRecord("completed", nextEntries);
348
+ await persistRunIfNeeded(record);
349
+ }
350
+ } finally {
351
+ setRunning(false);
352
+ }
353
+ }, [
354
+ allowFalseSignalPropagation,
355
+ beginRun,
356
+ buildRunRecord,
357
+ chainComplete,
358
+ entries,
359
+ graph,
360
+ persistRunIfNeeded,
361
+ selectedLifecycleNodeId,
362
+ syncRunnerWithLatestGraph,
363
+ validateBlueprintGraph,
364
+ ]);
365
+
366
+ const stepBack = useCallback(() => {
367
+ if (!selectedLifecycleNodeId) return;
368
+ const runner = runnerRef.current;
369
+ if (!runner?.isDebugSessionActive() || !runner.canDebugStepBack()) return;
370
+
371
+ setRunning(true);
372
+ setLogPanelOpen(true);
373
+ try {
374
+ runner.debugStepBack();
375
+ const nextEntries = runner.getTraceEntries();
376
+ setEntries(nextEntries);
377
+ setChainComplete(
378
+ shouldHaltDebugOnFalseSignal(
379
+ nextEntries,
380
+ allowFalseSignalPropagation
381
+ ) ||
382
+ (runner.isDebugSessionActive() && runner.isDebugQueueEmpty())
383
+ );
384
+ } finally {
385
+ setRunning(false);
386
+ }
387
+ }, [allowFalseSignalPropagation, selectedLifecycleNodeId]);
388
+
389
+ const resetToStart = useCallback(() => {
390
+ if (!selectedLifecycleNodeId || entries.length === 0) return;
391
+ const runner = runnerRef.current;
392
+ if (!runner?.isDebugSessionActive()) return;
393
+
394
+ setRunning(true);
395
+ setLogPanelOpen(true);
396
+ try {
397
+ syncRunnerWithLatestGraph();
398
+ runner.beginDebugSession(selectedLifecycleNodeId);
399
+ setEntries([]);
400
+ setChainComplete(false);
401
+ } finally {
402
+ setRunning(false);
403
+ }
404
+ }, [entries.length, selectedLifecycleNodeId, syncRunnerWithLatestGraph]);
405
+
406
+ const falseSignalHalt = shouldHaltDebugOnFalseSignal(
407
+ entries,
408
+ allowFalseSignalPropagation
409
+ );
410
+ const canResetToStart = entries.length > 0;
411
+ const canStepBack = entries.length > 0;
412
+ const canStepNext =
413
+ Boolean(selectedLifecycleNodeId) && !chainComplete && !falseSignalHalt;
414
+
415
+ const saveCurrentRun = useCallback(async () => {
416
+ if (entries.length === 0) return;
417
+ const record = buildRunRecord(
418
+ runnerRef.current?.isDebugQueueEmpty() ? "completed" : "paused",
419
+ entries
420
+ );
421
+ await putExecutionRunRecord(record);
422
+ await enforceSavedRunLimits();
423
+ }, [buildRunRecord, enforceSavedRunLimits, entries]);
424
+
425
+ const exportCurrentRun = useCallback(() => {
426
+ if (entries.length === 0) return;
427
+ const record = buildRunRecord(
428
+ runnerRef.current?.isDebugQueueEmpty() ? "completed" : "paused",
429
+ entries
430
+ );
431
+ downloadExecutionRunExport(record);
432
+ }, [buildRunRecord, entries]);
433
+
434
+ const updateSettings = useCallback(
435
+ (patch: Partial<ExecutionLogSettings>) => {
436
+ setSettings((prev) => {
437
+ const next = { ...prev, ...patch };
438
+ writeExecutionLogSettings(next);
439
+ return next;
440
+ });
441
+ if (patch.maxSavedRuns !== undefined) {
442
+ const maxSavedRuns = Math.max(1, Math.floor(patch.maxSavedRuns));
443
+ void trimExecutionRunRecordsToMax(maxSavedRuns).then(() =>
444
+ refreshSavedRuns()
445
+ );
446
+ }
447
+ },
448
+ [refreshSavedRuns]
449
+ );
450
+
451
+ const resetSession = useCallback(() => {
452
+ runnerRef.current?.clearDebugSession();
453
+ runnerRef.current = null;
454
+ runIdRef.current = null;
455
+ setEntries([]);
456
+ setChainComplete(false);
457
+ }, []);
458
+
459
+ const abortClock = useCallback((nodeId: string) => {
460
+ runnerRef.current?.abortClockNode(nodeId);
461
+ }, []);
462
+
463
+ const clearLog = resetSession;
464
+
465
+ const selectLifecycleNode = useCallback(
466
+ (nodeId: string | null) => {
467
+ setSelectedLifecycleNodeId(nodeId);
468
+ resetSession();
469
+ },
470
+ [resetSession]
471
+ );
472
+
473
+ const executionOverlay = useMemo(
474
+ () => buildExecutionOverlay(graph, entries),
475
+ [graph, entries]
476
+ );
477
+
478
+ return {
479
+ lifecycleNodes,
480
+ hasLifecycleNodes,
481
+ selectedLifecycleNodeId,
482
+ selectLifecycleNode,
483
+ entries,
484
+ executionOverlay,
485
+ logPanelOpen,
486
+ setLogPanelOpen,
487
+ running,
488
+ runAll,
489
+ resetToStart,
490
+ stepNext,
491
+ stepBack,
492
+ canResetToStart,
493
+ canStepBack,
494
+ canStepNext,
495
+ falseSignalHalt,
496
+ allowFalseSignalPropagation,
497
+ chainComplete,
498
+ saveCurrentRun,
499
+ exportCurrentRun,
500
+ settings,
501
+ updateSettings,
502
+ savedRuns,
503
+ refreshSavedRuns,
504
+ applyRetention,
505
+ clearAllSavedRuns,
506
+ resetSession,
507
+ clearLog,
508
+ totalSavedRunCount,
509
+ abortClock,
510
+ };
511
+ }
512
+
513
+ export type UseBlueprintDebugSessionReturn = ReturnType<
514
+ typeof useBlueprintDebugSession
515
+ >;
@@ -0,0 +1,8 @@
1
+ import { useTheme } from "@arronqzy/ui";
2
+
3
+ /** 与 next-themes 全局主题对齐,勿用 React Flow 的 system(会跟 OS 走) */
4
+ export function useBlueprintFlowColorMode(): "light" | "dark" {
5
+ const { resolvedTheme, theme } = useTheme();
6
+ const current = resolvedTheme ?? theme;
7
+ return current === "dark" ? "dark" : "light";
8
+ }