@arronqzy/react-blueprint 1.0.1 → 1.0.4

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 (39) hide show
  1. package/README.md +103 -0
  2. package/index.ts +2 -1
  3. package/package.json +7 -6
  4. package/src/BlueprintContextMenu.tsx +5 -3
  5. package/src/BlueprintNodeConfigSidebar.tsx +59 -57
  6. package/src/blueprint.css +21 -15
  7. package/src/components/BlueprintExecutionLogPanel.tsx +32 -30
  8. package/src/components/BlueprintLibrarySelect.tsx +11 -4
  9. package/src/components/BlueprintMetaDialog.tsx +20 -10
  10. package/src/components/BlueprintNodeSwitchTaskDialog.tsx +14 -8
  11. package/src/components/BlueprintPanelToolbar.tsx +50 -42
  12. package/src/components/ClockNodeConfigPanel.tsx +19 -12
  13. package/src/components/FetchNodeConfigPanel.tsx +40 -34
  14. package/src/components/FetchUrlAutocomplete.tsx +3 -1
  15. package/src/components/JsonNodeConfigPanel.tsx +15 -7
  16. package/src/components/LogicNodeConfigPanel.tsx +13 -7
  17. package/src/components/ViewElementMultiSelect.tsx +25 -18
  18. package/src/dev-seed.ts +2 -2
  19. package/src/fetch-config-task-store.ts +8 -2
  20. package/src/graph/blueprint-graph.ts +5 -1
  21. package/src/graph/document.ts +97 -22
  22. package/src/graph/node-summary.ts +32 -18
  23. package/src/graph/sync-nodes.ts +3 -1
  24. package/src/hooks/useBlueprintDebugSession.ts +24 -22
  25. package/src/hooks/useBlueprintFlowColorMode.ts +30 -6
  26. package/src/hooks/useBlueprintPageLifecycle.ts +29 -1
  27. package/src/library/blueprint-io.ts +12 -3
  28. package/src/library/swagger-docs.ts +15 -5
  29. package/src/main.tsx +12 -5
  30. package/src/nodes/AndFlowNode.tsx +7 -5
  31. package/src/nodes/BlueprintFlowNode.tsx +4 -2
  32. package/src/nodes/BlueprintNodeCard.tsx +4 -2
  33. package/src/nodes/BlueprintNodeShell.tsx +4 -2
  34. package/src/nodes/ClockFlowNode.tsx +5 -3
  35. package/src/nodes/FetchFlowNode.tsx +4 -2
  36. package/src/nodes/JsonFlowNode.tsx +4 -2
  37. package/src/nodes/LifecycleFlowNode.tsx +5 -3
  38. package/src/nodes/LogicFlowNode.tsx +4 -2
  39. package/src/runtime/document-to-runnable-graph.ts +9 -6
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useMemo, useState, type ChangeEvent } from "react";
2
2
  import { validateJsonString } from "@arronqzy/blueprint-dsl";
3
3
  import type { JsonNodeConfig } from "@arronqzy/blueprint-dsl";
4
+ import { useI18n } from "@arronqzy/i18n/react";
4
5
 
5
6
  import type { BlueprintGraphNode } from "../graph/document";
6
7
  import { resolveNodeJsonConfig } from "../graph/document";
@@ -27,6 +28,7 @@ export function JsonNodeConfigPanel({
27
28
  node,
28
29
  onUpdateNode,
29
30
  }: JsonNodeConfigPanelProps) {
31
+ const { t } = useI18n();
30
32
  const jsonConfig = resolveNodeJsonConfig(node);
31
33
  const [draftError, setDraftError] = useState<string | null>(null);
32
34
 
@@ -51,15 +53,17 @@ export function JsonNodeConfigPanel({
51
53
 
52
54
  return (
53
55
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
54
- <div className="font-medium text-foreground">JSON 节点</div>
56
+ <div className="font-medium text-foreground">
57
+ {t("blueprint.config.jsonTitle")}
58
+ </div>
55
59
  <p className="text-[11px] text-muted-foreground">
56
- 收到<strong>真信号</strong>后,将下方 JSON 解析为 JavaScript object(或
57
- array)并从输出口发出<strong>真信号</strong>;解析失败则发出
58
- <strong>假信号</strong>。
60
+ {t("blueprint.config.jsonHint")}
59
61
  </p>
60
62
 
61
63
  <label className="block space-y-1">
62
- <span className="text-muted-foreground">JSON 内容</span>
64
+ <span className="text-muted-foreground">
65
+ {t("blueprint.config.jsonContent")}
66
+ </span>
63
67
  <textarea
64
68
  value={jsonConfig.jsonString}
65
69
  onChange={handleChange}
@@ -69,9 +73,13 @@ export function JsonNodeConfigPanel({
69
73
  placeholder={'{\n "key": "value"\n}'}
70
74
  />
71
75
  {parseError ? (
72
- <p className="text-[11px] text-destructive">JSON 格式错误:{parseError}</p>
76
+ <p className="text-[11px] text-destructive">
77
+ {t("blueprint.config.jsonFormatError", { error: parseError })}
78
+ </p>
73
79
  ) : (
74
- <p className="text-[11px] text-muted-foreground">JSON 格式正确</p>
80
+ <p className="text-[11px] text-muted-foreground">
81
+ {t("blueprint.config.jsonFormatOk")}
82
+ </p>
75
83
  )}
76
84
  </label>
77
85
  </div>
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useMemo, useState, type ChangeEvent } from "react";
2
2
  import { validateLogicSourceCode } from "@arronqzy/blueprint-dsl";
3
3
  import type { LogicNodeConfig } from "@arronqzy/blueprint-dsl";
4
+ import { useI18n } from "@arronqzy/i18n/react";
4
5
 
5
6
  import type { BlueprintGraphNode } from "../graph/document";
6
7
  import { resolveNodeLogicConfig } from "../graph/document";
@@ -27,6 +28,7 @@ export function LogicNodeConfigPanel({
27
28
  node,
28
29
  onUpdateNode,
29
30
  }: LogicNodeConfigPanelProps) {
31
+ const { t } = useI18n();
30
32
  const logicConfig = resolveNodeLogicConfig(node);
31
33
  const [draftError, setDraftError] = useState<string | null>(null);
32
34
 
@@ -50,15 +52,17 @@ export function LogicNodeConfigPanel({
50
52
 
51
53
  return (
52
54
  <div className="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5">
53
- <div className="font-medium text-foreground">逻辑节点</div>
55
+ <div className="font-medium text-foreground">
56
+ {t("blueprint.config.logicTitle")}
57
+ </div>
54
58
  <p className="text-[11px] text-muted-foreground">
55
- 收到<strong>真信号</strong>后,将输入数据传入下方 <code>update</code>{" "}
56
- 函数并执行;返回值作为<strong>真信号</strong>输出。代码语法错误或运行时报错则发出
57
- <strong>假信号</strong>(错误信息为输出值)。
59
+ {t("blueprint.config.logicHint")}
58
60
  </p>
59
61
 
60
62
  <label className="block space-y-1">
61
- <span className="text-muted-foreground">JavaScript 代码</span>
63
+ <span className="text-muted-foreground">
64
+ {t("blueprint.config.jsCode")}
65
+ </span>
62
66
  <textarea
63
67
  value={logicConfig.sourceCode}
64
68
  onChange={handleChange}
@@ -69,10 +73,12 @@ export function LogicNodeConfigPanel({
69
73
  />
70
74
  {parseError ? (
71
75
  <p className="text-[11px] text-destructive">
72
- JavaScript 错误:{parseError}
76
+ {t("blueprint.config.jsError", { error: parseError })}
73
77
  </p>
74
78
  ) : (
75
- <p className="text-[11px] text-muted-foreground">JavaScript 语法正确</p>
79
+ <p className="text-[11px] text-muted-foreground">
80
+ {t("blueprint.config.jsSyntaxOk")}
81
+ </p>
76
82
  )}
77
83
  </label>
78
84
  </div>
@@ -12,6 +12,7 @@ import {
12
12
  PopoverContent,
13
13
  PopoverTrigger,
14
14
  } from "@arronqzy/ui";
15
+ import { useI18n } from "@arronqzy/i18n/react";
15
16
 
16
17
  export type ViewElementMultiSelectOption = {
17
18
  id: string;
@@ -45,35 +46,38 @@ function ChevronDownIcon({ className }: { className?: string }) {
45
46
  );
46
47
  }
47
48
 
48
- function resolveTriggerText(
49
- value: string[],
50
- labelById: Map<string, string>,
51
- placeholder: string
52
- ): string {
53
- if (value.length === 0) return placeholder;
54
- const labels = value.map((id) => labelById.get(id) ?? id);
55
- if (labels.length === 1) return labels[0]!;
56
- const joined = labels.join("、");
57
- if (joined.length <= 26) return joined;
58
- return `已选 ${value.length} 项`;
59
- }
60
-
61
49
  export function ViewElementMultiSelect({
62
50
  options,
63
51
  value,
64
52
  onChange,
65
- placeholder = "选择视图节点",
53
+ placeholder,
66
54
  disabled = false,
67
55
  }: ViewElementMultiSelectProps) {
56
+ const { t } = useI18n();
68
57
  const [open, setOpen] = useState(false);
58
+ const resolvedPlaceholder =
59
+ placeholder ?? t("blueprint.config.selectViewNode");
69
60
  const labelById = useMemo(
70
61
  () => new Map(options.map((opt) => [opt.id, opt.label])),
71
62
  [options]
72
63
  );
73
64
  const selectedSet = useMemo(() => new Set(value), [value]);
74
- const triggerText = resolveTriggerText(value, labelById, placeholder);
75
65
  const emptyOptions = options.length === 0;
76
66
 
67
+ let triggerText = resolvedPlaceholder;
68
+ if (value.length > 0) {
69
+ const labels = value.map((id) => labelById.get(id) ?? id);
70
+ if (labels.length === 1) {
71
+ triggerText = labels[0]!;
72
+ } else {
73
+ const joined = labels.join("、");
74
+ triggerText =
75
+ joined.length <= 26
76
+ ? joined
77
+ : t("blueprint.config.selectedCount", { count: value.length });
78
+ }
79
+ }
80
+
77
81
  const toggle = (id: string) => {
78
82
  onChange(
79
83
  selectedSet.has(id)
@@ -97,7 +101,7 @@ export function ViewElementMultiSelect({
97
101
  )}
98
102
  >
99
103
  <span className="min-w-0 flex-1 truncate text-left">
100
- {emptyOptions ? "视图画布暂无节点" : triggerText}
104
+ {emptyOptions ? t("blueprint.config.noViewOnCanvas") : triggerText}
101
105
  </span>
102
106
  <ChevronDownIcon className="h-4 w-4 shrink-0 opacity-50" />
103
107
  </Button>
@@ -107,9 +111,12 @@ export function ViewElementMultiSelect({
107
111
  align="start"
108
112
  >
109
113
  <Command>
110
- <CommandInput placeholder="搜索视图节点…" className="h-9 text-xs" />
114
+ <CommandInput
115
+ placeholder={t("blueprint.config.searchViewNodes")}
116
+ className="h-9 text-xs"
117
+ />
111
118
  <CommandList>
112
- <CommandEmpty>无匹配节点</CommandEmpty>
119
+ <CommandEmpty>{t("blueprint.config.noMatchingNodes")}</CommandEmpty>
113
120
  <CommandGroup>
114
121
  {options.map((opt) => {
115
122
  const selected = selectedSet.has(opt.id);
package/src/dev-seed.ts CHANGED
@@ -11,9 +11,9 @@ import {
11
11
  /** DevApp 初始图:两个节点 + 一条连线,便于直接验证边是否渲染 */
12
12
  export function createDevBlueprintGraph(): BlueprintGraph {
13
13
  let graph = BlueprintGraph.empty();
14
- graph = graph.addBlueprintNode({ x: 40, y: 80 }, "节点 A");
14
+ graph = graph.addBlueprintNode({ x: 40, y: 80 }, "Node A");
15
15
  const nodeA = graph.document.nodes[0]!;
16
- graph = graph.addBlueprintNode({ x: 300, y: 80 }, "节点 B");
16
+ graph = graph.addBlueprintNode({ x: 300, y: 80 }, "Node B");
17
17
  const nodeB = graph.document.nodes[1]!;
18
18
 
19
19
  const withEdge = addEdge(
@@ -6,6 +6,7 @@ import type {
6
6
  ParsedSwaggerDocument,
7
7
  } from "@arronqzy/blueprint-dsl";
8
8
  import { executeFetch } from "@arronqzy/blueprint-dsl";
9
+ import { resolveLocale, tForLocale } from "@arronqzy/i18n";
9
10
 
10
11
  import { loadSwaggerDocument } from "./library/swagger-docs";
11
12
 
@@ -143,7 +144,9 @@ export function startSwaggerLoadTask(params: {
143
144
  }
144
145
 
145
146
  const message =
146
- error instanceof Error ? error.message : "Swagger 文档解析失败";
147
+ error instanceof Error
148
+ ? error.message
149
+ : tForLocale(resolveLocale())("blueprint.config.swaggerParseFailed");
147
150
  swaggerTasks.set(params.nodeId, {
148
151
  record: { status: "error", error: message },
149
152
  abortController,
@@ -186,7 +189,10 @@ export function startFetchDebugTask(params: {
186
189
  return;
187
190
  }
188
191
 
189
- const message = error instanceof Error ? error.message : "请求失败";
192
+ const message =
193
+ error instanceof Error
194
+ ? error.message
195
+ : tForLocale(resolveLocale())("blueprint.config.requestFailed");
190
196
  fetchDebugTasks.set(params.nodeId, {
191
197
  record: { status: "error", error: message },
192
198
  abortController,
@@ -8,6 +8,7 @@ import {
8
8
  resolveNodeFetchConfig,
9
9
  resolveNodeJsonConfig,
10
10
  resolveNodeLogicConfig,
11
+ resolveRunnableNodeType,
11
12
  sanitizeBlueprintDocument,
12
13
  type BlueprintDocument,
13
14
  type BlueprintGraphEdge,
@@ -250,7 +251,10 @@ export class BlueprintGraph {
250
251
  ...patch.clockConfig,
251
252
  };
252
253
  }
253
- return nextNode;
254
+ return {
255
+ ...nextNode,
256
+ nodeType: resolveRunnableNodeType(nextNode),
257
+ };
254
258
  }),
255
259
  });
256
260
  }
@@ -89,48 +89,116 @@ export function pruneViewElementIds(
89
89
  export function resolveBlueprintConfigSource(
90
90
  node: Pick<
91
91
  BlueprintGraphNode,
92
- "role" | "configSource" | "viewElementId" | "viewElementIds"
92
+ "role" | "nodeType" | "configSource" | "viewElementId" | "viewElementIds"
93
93
  >
94
94
  ): BlueprintConfigSource {
95
95
  if (node.configSource) return node.configSource;
96
- if (resolveViewElementIds(node).length > 0) return "view";
97
96
  if (node.role === "lifecycle") return "lifecycle";
98
97
  if (node.role === "clock") return "clock";
99
98
  if (node.role === "and") return "and";
100
99
  if (node.role === "fetch") return "fetch";
101
100
  if (node.role === "json") return "json";
101
+ if (node.nodeType === FETCH_NODE_TYPE) return "fetch";
102
+ if (node.nodeType === JSON_NODE_TYPE) return "json";
103
+ if (node.nodeType === CLOCK_NODE_TYPE) return "clock";
104
+ if (node.nodeType === VIEW_NODE_TYPE) return "view";
105
+ if (resolveViewElementIds(node).length > 0) return "view";
102
106
  return node.role === "logic" ? "logic" : "blueprint";
103
107
  }
104
108
 
105
- /** 画布节点顶部(拖拽区)展示的配置类型文案 */
106
- export const BLUEPRINT_CONFIG_TYPE_LABELS: Record<
109
+ /** 可执行图节点类型:与配置类型对齐,避免文档里残留旧 nodeType */
110
+ export function resolveRunnableNodeType(
111
+ node: Pick<
112
+ BlueprintGraphNode,
113
+ "role" | "nodeType" | "configSource" | "viewElementId" | "viewElementIds"
114
+ >
115
+ ): string {
116
+ switch (resolveBlueprintConfigSource(node)) {
117
+ case "view":
118
+ return VIEW_NODE_TYPE;
119
+ case "lifecycle":
120
+ return LIFECYCLE_NODE_TYPE;
121
+ case "clock":
122
+ return CLOCK_NODE_TYPE;
123
+ case "and":
124
+ return AND_NODE_TYPE;
125
+ case "fetch":
126
+ return FETCH_NODE_TYPE;
127
+ case "json":
128
+ return JSON_NODE_TYPE;
129
+ case "logic":
130
+ return DEFAULT_LOGIC_NODE_TYPE;
131
+ default:
132
+ return BLUEPRINT_NODE_TYPE;
133
+ }
134
+ }
135
+
136
+ import type { TranslateFn } from "@arronqzy/i18n";
137
+ import { resolveLocale, tForLocale } from "@arronqzy/i18n";
138
+
139
+ /** 画布节点顶部(拖拽区)配置类型对应的 i18n key */
140
+ export const BLUEPRINT_CONFIG_TYPE_LABEL_KEYS: Record<
107
141
  BlueprintConfigSource,
108
142
  string
109
143
  > = {
110
- blueprint: "蓝图",
111
- logic: "逻辑",
112
- and: "并运算",
113
- lifecycle: "生命周期",
114
- view: "视图",
115
- fetch: "数据源获取",
116
- json: "JSON 节点",
117
- clock: "时钟",
144
+ blueprint: "blueprint.node.typeBlueprint",
145
+ logic: "blueprint.node.typeLogic",
146
+ and: "blueprint.node.typeAnd",
147
+ lifecycle: "blueprint.node.typeLifecycle",
148
+ view: "blueprint.node.typeView",
149
+ fetch: "blueprint.node.typeFetch",
150
+ json: "blueprint.node.typeJson",
151
+ clock: "blueprint.node.typeClock",
118
152
  };
119
153
 
154
+ export const BLUEPRINT_LIFECYCLE_PHASE_KEYS: Record<PageLifecyclePhase, string> =
155
+ {
156
+ created: "blueprint.node.phaseCreated",
157
+ beforeMount: "blueprint.node.phaseBeforeMount",
158
+ mounted: "blueprint.node.phaseMounted",
159
+ updated: "blueprint.node.phaseUpdated",
160
+ beforeDestroy: "blueprint.node.phaseBeforeDestroy",
161
+ destroy: "blueprint.node.phaseDestroy",
162
+ activated: "blueprint.node.phaseActivated",
163
+ deactivated: "blueprint.node.phaseDeactivated",
164
+ blueprintActivated: "blueprint.node.phaseBlueprintActivated",
165
+ };
166
+
167
+ function defaultTranslate(): TranslateFn {
168
+ return tForLocale(resolveLocale());
169
+ }
170
+
171
+ export function getBlueprintNodeTypeLabel(
172
+ t: TranslateFn,
173
+ type: BlueprintConfigSource
174
+ ): string {
175
+ return t(BLUEPRINT_CONFIG_TYPE_LABEL_KEYS[type]);
176
+ }
177
+
178
+ export function getLifecyclePhaseLabel(
179
+ t: TranslateFn,
180
+ phase: PageLifecyclePhase
181
+ ): string {
182
+ const key = BLUEPRINT_LIFECYCLE_PHASE_KEYS[phase];
183
+ return key ? t(key) : phase;
184
+ }
185
+
120
186
  export function resolveBlueprintNodeTypeLabel(
121
187
  node: Pick<
122
188
  BlueprintGraphNode,
123
- "role" | "configSource" | "viewElementId" | "viewElementIds"
124
- >
189
+ "role" | "nodeType" | "configSource" | "viewElementId" | "viewElementIds"
190
+ >,
191
+ t: TranslateFn = defaultTranslate()
125
192
  ): string {
126
- return BLUEPRINT_CONFIG_TYPE_LABELS[resolveBlueprintConfigSource(node)];
193
+ return getBlueprintNodeTypeLabel(t, resolveBlueprintConfigSource(node));
127
194
  }
128
195
 
129
196
  /** 新建节点时的默认显示名称(与类型文案一致) */
130
197
  export function defaultBlueprintNodeLabel(
131
- configSource: BlueprintConfigSource
198
+ configSource: BlueprintConfigSource,
199
+ t: TranslateFn = defaultTranslate()
132
200
  ): string {
133
- return BLUEPRINT_CONFIG_TYPE_LABELS[configSource];
201
+ return getBlueprintNodeTypeLabel(t, configSource);
134
202
  }
135
203
 
136
204
  export function resolveNodeFetchConfig(
@@ -221,11 +289,9 @@ export function sanitizeBlueprintDocument(
221
289
  viewElementId: undefined,
222
290
  };
223
291
  }
224
- if (
225
- resolveBlueprintConfigSource(next) === "view" &&
226
- next.nodeType !== VIEW_NODE_TYPE
227
- ) {
228
- next = { ...next, nodeType: VIEW_NODE_TYPE };
292
+ const runnableNodeType = resolveRunnableNodeType(next);
293
+ if (next.nodeType !== runnableNodeType) {
294
+ next = { ...next, nodeType: runnableNodeType };
229
295
  }
230
296
  if (next.role === "lifecycle") {
231
297
  return { ...next, configSource: "lifecycle" as const };
@@ -233,6 +299,15 @@ export function sanitizeBlueprintDocument(
233
299
  if (next.role === "clock") {
234
300
  return { ...next, configSource: "clock" as const };
235
301
  }
302
+ if (next.role === "fetch") {
303
+ return { ...next, configSource: "fetch" as const };
304
+ }
305
+ if (next.role === "json") {
306
+ return { ...next, configSource: "json" as const };
307
+ }
308
+ if (next.role === "and") {
309
+ return { ...next, configSource: "and" as const };
310
+ }
236
311
  if (next.configSource === "lifecycle" || next.configSource === "clock") {
237
312
  return { ...next, configSource: undefined };
238
313
  }
@@ -1,9 +1,9 @@
1
- import {
2
- PAGE_LIFECYCLE_LABELS,
3
- resolveFetchRequestUrl,
4
- } from "@arronqzy/blueprint-dsl";
1
+ import { resolveFetchRequestUrl } from "@arronqzy/blueprint-dsl";
2
+ import type { TranslateFn } from "@arronqzy/i18n";
3
+ import { resolveLocale, tForLocale } from "@arronqzy/i18n";
5
4
 
6
5
  import {
6
+ getLifecyclePhaseLabel,
7
7
  resolveBlueprintConfigSource,
8
8
  resolveNodeClockConfig,
9
9
  resolveNodeFetchConfig,
@@ -19,21 +19,26 @@ function truncate(text: string, max: number): string {
19
19
  return `${trimmed.slice(0, Math.max(0, max - 1))}…`;
20
20
  }
21
21
 
22
+ function defaultTranslate(): TranslateFn {
23
+ return tForLocale(resolveLocale());
24
+ }
25
+
22
26
  /** 画布节点正文区展示的配置摘要 */
23
27
  export function resolveBlueprintNodeSummary(
24
- data: BlueprintFlowNodeData
28
+ data: BlueprintFlowNodeData,
29
+ t: TranslateFn = defaultTranslate()
25
30
  ): string | undefined {
26
31
  const configSource = resolveBlueprintConfigSource(data);
27
32
 
28
33
  switch (configSource) {
29
34
  case "fetch": {
30
35
  const config = resolveNodeFetchConfig(data);
31
- if (!config.url?.trim()) return "未配置 URL";
36
+ if (!config.url?.trim()) return t("blueprint.node.summaryNoUrl");
32
37
  let url = config.url.trim();
33
38
  try {
34
39
  url = resolveFetchRequestUrl(config);
35
40
  } catch {
36
- /* 保留原始输入 */
41
+ /* keep raw input */
37
42
  }
38
43
  const method = config.method ?? "GET";
39
44
  return truncate(`${method} ${url}`, 44);
@@ -41,20 +46,26 @@ export function resolveBlueprintNodeSummary(
41
46
  case "clock": {
42
47
  const clock = resolveNodeClockConfig(data);
43
48
  const parts = [
44
- `${clock.outputCount} 次`,
45
- clock.intervalSeconds > 0 ? `间隔 ${clock.intervalSeconds}s` : null,
46
- clock.emitImmediately ? "立即首发" : "延迟首发",
49
+ t("blueprint.node.summaryOutputCount", { count: clock.outputCount }),
50
+ clock.intervalSeconds > 0
51
+ ? t("blueprint.node.summaryInterval", {
52
+ seconds: clock.intervalSeconds,
53
+ })
54
+ : null,
55
+ clock.emitImmediately
56
+ ? t("blueprint.node.summaryEmitImmediate")
57
+ : t("blueprint.node.summaryEmitDelayed"),
47
58
  ].filter(Boolean);
48
59
  return parts.join(" · ");
49
60
  }
50
61
  case "json": {
51
62
  const json = resolveNodeJsonConfig(data).jsonString?.trim();
52
- if (!json) return "空 JSON";
63
+ if (!json) return t("blueprint.node.summaryEmptyJson");
53
64
  return truncate(json.replace(/\s+/g, " "), 44);
54
65
  }
55
66
  case "logic": {
56
67
  const code = resolveNodeLogicConfig(data).sourceCode?.trim();
57
- if (!code) return "未配置脚本";
68
+ if (!code) return t("blueprint.node.summaryNoScript");
58
69
  const firstLine =
59
70
  code.split("\n").find((line) => line.trim())?.trim() ?? code;
60
71
  return truncate(firstLine, 44);
@@ -66,22 +77,25 @@ export function resolveBlueprintNodeSummary(
66
77
  if (data.libraryBlueprintId) {
67
78
  return truncate(data.libraryBlueprintId, 44);
68
79
  }
69
- return "未选择蓝图";
80
+ return t("blueprint.node.summaryNoBlueprint");
70
81
  }
71
82
  case "lifecycle": {
72
83
  const phase = data.lifecyclePhase ?? "mounted";
73
- return PAGE_LIFECYCLE_LABELS[phase] ?? phase;
84
+ return getLifecyclePhaseLabel(t, phase);
74
85
  }
75
86
  case "view": {
76
87
  const ids = resolveViewElementIds(data);
77
- if (ids.length === 0) return "未关联视图节点";
88
+ if (ids.length === 0) return t("blueprint.node.summaryNoView");
78
89
  if (ids.length === 1) {
79
- return truncate(`视图节点 ${ids[0]}`, 44);
90
+ return truncate(
91
+ t("blueprint.node.summaryViewOne", { id: ids[0] }),
92
+ 44
93
+ );
80
94
  }
81
- return `已关联 ${ids.length} 个视图节点`;
95
+ return t("blueprint.node.summaryViewMany", { count: ids.length });
82
96
  }
83
97
  case "and":
84
- return "两路输入均为真时输出";
98
+ return t("blueprint.node.summaryAnd");
85
99
  default:
86
100
  return undefined;
87
101
  }
@@ -42,6 +42,8 @@ export function nodeStructureSignature(
42
42
  lifecyclePhase?: string;
43
43
  libraryBlueprintId?: string;
44
44
  nodeType?: string;
45
+ viewElementId?: string;
46
+ viewElementIds?: string[];
45
47
  fetchConfig?: { url?: string; method?: string; apiBaseUrl?: string; swaggerDocsUrl?: string };
46
48
  jsonConfig?: { jsonString?: string };
47
49
  logicConfig?: { sourceCode?: string };
@@ -51,7 +53,7 @@ export function nodeStructureSignature(
51
53
  return nodes
52
54
  .map(
53
55
  (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 ?? ""}`
56
+ `${n.id}:${n.role}:${n.label}:${n.configSource ?? ""}:${n.lifecyclePhase ?? ""}:${n.libraryBlueprintId ?? ""}:${n.nodeType ?? ""}:${(n.viewElementIds ?? (n.viewElementId ? [n.viewElementId] : [])).join(",")}:${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
57
  )
56
58
  .sort()
57
59
  .join("|");
@@ -112,16 +112,22 @@ export function useBlueprintDebugSession({
112
112
  [graph.document, libraryNameById]
113
113
  );
114
114
 
115
- const syncRunnerWithLatestGraph = useCallback(() => {
115
+ const syncRunnerGraphConfig = useCallback(() => {
116
116
  const runner = runnerRef.current;
117
117
  if (!runner?.isDebugSessionActive()) return;
118
118
  runner.updateGraph(buildRunnableGraph());
119
119
  if (selectedLifecycleNodeId) {
120
120
  runner.refreshDebugLifecycleScope(selectedLifecycleNodeId);
121
121
  }
122
- runner.refreshPendingQueueFromLatestGraph();
123
122
  }, [buildRunnableGraph, selectedLifecycleNodeId]);
124
123
 
124
+ const syncRunnerWithLatestGraph = useCallback(() => {
125
+ syncRunnerGraphConfig();
126
+ const runner = runnerRef.current;
127
+ if (!runner?.isDebugSessionActive()) return;
128
+ runner.refreshPendingQueueFromLatestGraph();
129
+ }, [syncRunnerGraphConfig]);
130
+
125
131
  const resolveBlueprintName = useCallback(
126
132
  (id: string) => libraryNameById?.get(id) ?? id,
127
133
  [libraryNameById]
@@ -242,12 +248,9 @@ export function useBlueprintDebugSession({
242
248
  useEffect(() => {
243
249
  const runner = runnerRef.current;
244
250
  if (!runner?.isDebugSessionActive()) return;
245
- runner.updateGraph(buildRunnableGraph());
246
- if (selectedLifecycleNodeId) {
247
- runner.refreshDebugLifecycleScope(selectedLifecycleNodeId);
248
- }
251
+ syncRunnerGraphConfig();
249
252
  runner.refreshPendingQueueFromLatestGraph();
250
- }, [buildRunnableGraph, graphDebugSig, selectedLifecycleNodeId]);
253
+ }, [graphDebugSig, syncRunnerGraphConfig]);
251
254
 
252
255
  useEffect(() => {
253
256
  if (running) return;
@@ -277,24 +280,14 @@ export function useBlueprintDebugSession({
277
280
  try {
278
281
  const runner = beginRun();
279
282
  if (!runner) return;
283
+ runner.updateGraph(buildRunnableGraph());
284
+ runner.refreshDebugLifecycleScope(selectedLifecycleNodeId);
280
285
  const labels = buildNodeLabelMap(graph);
281
286
  const onProgress = (trace: ExecutionTraceEntry[]) => {
282
287
  setEntries([...trace]);
283
288
  };
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();
289
+ const result = await runner.debugRunAll(labels, onProgress);
290
+ setEntries(result);
298
291
  setChainComplete(
299
292
  shouldHaltDebugOnFalseSignal(result, allowFalseSignalPropagation) ||
300
293
  (runner.isDebugQueueEmpty() && result.length > 0)
@@ -304,7 +297,16 @@ export function useBlueprintDebugSession({
304
297
  } finally {
305
298
  setRunning(false);
306
299
  }
307
- }, [allowFalseSignalPropagation, beginRun, buildRunRecord, graph, persistRunIfNeeded, selectedLifecycleNodeId, syncRunnerWithLatestGraph, validateBlueprintGraph]);
300
+ }, [
301
+ allowFalseSignalPropagation,
302
+ beginRun,
303
+ buildRunRecord,
304
+ buildRunnableGraph,
305
+ graph,
306
+ persistRunIfNeeded,
307
+ selectedLifecycleNodeId,
308
+ validateBlueprintGraph,
309
+ ]);
308
310
 
309
311
  const stepNext = useCallback(async () => {
310
312
  if (