@arronqzy/vue-blueprint 0.1.7 → 0.1.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/vue-blueprint",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Vue 3 blueprint editor for Abuilder (Vue Flow + Ant Design Vue)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,8 +19,8 @@
19
19
  "ant-design-vue": "^4.2.6",
20
20
  "dayjs": "^1.11.13",
21
21
  "vue": "^3.5.13",
22
- "@arronqzy/blueprint-dsl": "1.0.9",
23
- "@arronqzy/i18n": "0.1.7",
22
+ "@arronqzy/blueprint-dsl": "1.0.11",
23
+ "@arronqzy/i18n": "0.1.8",
24
24
  "@arronqzy/vue-rx-store": "0.1.3"
25
25
  },
26
26
  "peerDependencies": {
@@ -14,6 +14,7 @@ import {
14
14
  import FetchNodeConfigPanel from "./components/FetchNodeConfigPanel.vue";
15
15
  import ClockNodeConfigPanel from "./components/ClockNodeConfigPanel.vue";
16
16
  import JsonNodeConfigPanel from "./components/JsonNodeConfigPanel.vue";
17
+ import StorageNodeConfigPanel from "./components/StorageNodeConfigPanel.vue";
17
18
  import LogicNodeConfigPanel from "./components/LogicNodeConfigPanel.vue";
18
19
  import ViewElementMultiSelect from "./components/ViewElementMultiSelect.vue";
19
20
  import ConfigHintIcon from "./components/ConfigHintIcon.vue";
@@ -69,6 +70,7 @@ export type BlueprintNodeConfigSidebarProps = {
69
70
  | "lifecyclePhase"
70
71
  | "fetchConfig"
71
72
  | "jsonConfig"
73
+ | "storageConfig"
72
74
  | "logicConfig"
73
75
  | "clockConfig"
74
76
  | "eventConfig"
@@ -102,6 +104,7 @@ const ROLE_LABEL_KEYS: Record<BlueprintNodeRole, string> = {
102
104
  and: "blueprint.config.roleAnd",
103
105
  fetch: "blueprint.config.roleFetch",
104
106
  json: "blueprint.config.roleJson",
107
+ storage: "blueprint.config.roleStorage",
105
108
  logic: "blueprint.config.roleLogic",
106
109
  clock: "blueprint.config.roleClock",
107
110
  event: "blueprint.config.roleEvent",
@@ -201,6 +204,7 @@ function toggleEventType(eventType: ViewEventType, checked: boolean) {
201
204
  <Select.Option value="lifecycle">{{ t("blueprint.config.configLifecycle") }}</Select.Option>
202
205
  <Select.Option value="fetch">{{ t("blueprint.config.configFetch") }}</Select.Option>
203
206
  <Select.Option value="json">{{ t("blueprint.config.configJson") }}</Select.Option>
207
+ <Select.Option value="storage">{{ t("blueprint.config.configStorage") }}</Select.Option>
204
208
  <Select.Option value="clock">{{ t("blueprint.config.configClock") }}</Select.Option>
205
209
  <Select.Option value="event">{{ t("blueprint.config.configEvent") }}</Select.Option>
206
210
  <Select.Option value="view">{{ t("blueprint.config.configView") }}</Select.Option>
@@ -381,6 +385,14 @@ function toggleEventType(eventType: ViewEventType, checked: boolean) {
381
385
  :node="node"
382
386
  :on-update-node="onUpdateNode"
383
387
  />
388
+ <StorageNodeConfigPanel
389
+ v-if="configSource === 'storage'"
390
+ :node="node"
391
+ :graph-nodes="graphNodes"
392
+ :graph-edges="graphEdges"
393
+ :trace-entries="traceEntries"
394
+ :on-update-node="onUpdateNode"
395
+ />
384
396
  <ClockNodeConfigPanel
385
397
  v-if="configSource === 'clock'"
386
398
  :node="node"
@@ -5,6 +5,7 @@ import BlueprintFlowNode from "./nodes/BlueprintFlowNode.vue";
5
5
  import ClockFlowNode from "./nodes/ClockFlowNode.vue";
6
6
  import FetchFlowNode from "./nodes/FetchFlowNode.vue";
7
7
  import JsonFlowNode from "./nodes/JsonFlowNode.vue";
8
+ import StorageFlowNode from "./nodes/StorageFlowNode.vue";
8
9
  import LifecycleFlowNode from "./nodes/LifecycleFlowNode.vue";
9
10
  import EventFlowNode from "./nodes/EventFlowNode.vue";
10
11
  import LogicFlowNode from "./nodes/LogicFlowNode.vue";
@@ -16,6 +17,7 @@ export const blueprintNodeTypes: Record<string, Component> = {
16
17
  clock: markRaw(ClockFlowNode),
17
18
  fetch: markRaw(FetchFlowNode),
18
19
  json: markRaw(JsonFlowNode),
20
+ storage: markRaw(StorageFlowNode),
19
21
  logic: markRaw(LogicFlowNode),
20
22
  lifecycle: markRaw(LifecycleFlowNode),
21
23
  event: markRaw(EventFlowNode),
@@ -14,7 +14,7 @@ export type BlueprintContextMenuState =
14
14
  clientX: number;
15
15
  clientY: number;
16
16
  nodeId: string;
17
- role: "blueprint" | "logic" | "and" | "lifecycle" | "event" | "fetch" | "json" | "clock";
17
+ role: "blueprint" | "logic" | "and" | "lifecycle" | "event" | "fetch" | "json" | "storage" | "clock";
18
18
  }
19
19
  | {
20
20
  kind: "edge";
@@ -0,0 +1,217 @@
1
+ <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
3
+ import { computed, ref } from "vue";
4
+ import { Checkbox, Input, Select } from "ant-design-vue";
5
+ import type {
6
+ ExecutionTraceEntry,
7
+ StorageKind,
8
+ StorageReadConfig,
9
+ StorageSetConfig,
10
+ } from "@arronqzy/blueprint-dsl";
11
+ import {
12
+ latestTraceOutputsByNode,
13
+ resolveFetchIncomingScope,
14
+ resolveFetchScopeAutocompleteRoot,
15
+ uniqueStorageKinds,
16
+ } from "@arronqzy/blueprint-dsl";
17
+
18
+ import type { BlueprintGraphEdge, BlueprintGraphNode } from "../graph/document";
19
+ import { resolveNodeStorageConfig } from "../graph/document";
20
+ import ConfigHintIcon from "./ConfigHintIcon.vue";
21
+ import ScopeTemplateAutocompleteHost from "./ScopeTemplateAutocompleteHost.vue";
22
+
23
+ const { t } = useI18n();
24
+
25
+ export type StorageNodeConfigPanelProps = {
26
+ node: BlueprintGraphNode;
27
+ graphNodes?: BlueprintGraphNode[];
28
+ graphEdges?: BlueprintGraphEdge[];
29
+ traceEntries?: ExecutionTraceEntry[];
30
+ onUpdateNode: (
31
+ nodeId: string,
32
+ patch: Partial<Pick<BlueprintGraphNode, "storageConfig" | "configSource">>
33
+ ) => void;
34
+ };
35
+
36
+ const props = withDefaults(defineProps<StorageNodeConfigPanelProps>(), {
37
+ graphNodes: () => [],
38
+ graphEdges: () => [],
39
+ traceEntries: () => [],
40
+ });
41
+
42
+ const formRef = ref<HTMLDivElement | null>(null);
43
+ const storageConfig = computed(() => resolveNodeStorageConfig(props.node));
44
+ const incomingScope = computed(() => {
45
+ const outputs = latestTraceOutputsByNode(props.traceEntries);
46
+ return resolveFetchIncomingScope({
47
+ fetchNodeId: props.node.id,
48
+ nodes: props.graphNodes,
49
+ edges: props.graphEdges,
50
+ getOutput: (sourceId, port) => outputs[sourceId]?.[port],
51
+ });
52
+ });
53
+ const autocompleteScope = computed(() =>
54
+ resolveFetchScopeAutocompleteRoot(incomingScope.value)
55
+ );
56
+
57
+ function patchStorageConfig(
58
+ node: BlueprintGraphNode,
59
+ patch: {
60
+ read?: Partial<StorageReadConfig>;
61
+ set?: Partial<StorageSetConfig>;
62
+ }
63
+ ) {
64
+ const current = resolveNodeStorageConfig(node);
65
+ return {
66
+ storageConfig: {
67
+ read: { ...current.read, ...patch.read },
68
+ set: { ...current.set, ...patch.set },
69
+ },
70
+ configSource: "storage" as const,
71
+ };
72
+ }
73
+
74
+ function handleReadStorageChange(value: string) {
75
+ props.onUpdateNode(
76
+ props.node.id,
77
+ patchStorageConfig(props.node, {
78
+ read: { storage: value === "session" ? "session" : "local" },
79
+ })
80
+ );
81
+ }
82
+
83
+ function handleReadKeyChange(value: string) {
84
+ props.onUpdateNode(
85
+ props.node.id,
86
+ patchStorageConfig(props.node, { read: { key: String(value ?? "") } })
87
+ );
88
+ }
89
+
90
+ function handleSetStorageToggle(kind: StorageKind, checked: boolean) {
91
+ const next = new Set(storageConfig.value.set.storages);
92
+ if (checked) next.add(kind);
93
+ else next.delete(kind);
94
+ props.onUpdateNode(
95
+ props.node.id,
96
+ patchStorageConfig(props.node, {
97
+ set: { storages: uniqueStorageKinds([...next]) },
98
+ })
99
+ );
100
+ }
101
+
102
+ function handleSetKeyChange(value: string) {
103
+ props.onUpdateNode(
104
+ props.node.id,
105
+ patchStorageConfig(props.node, { set: { key: String(value ?? "") } })
106
+ );
107
+ }
108
+
109
+ function handleSetValueChange(value: string) {
110
+ props.onUpdateNode(
111
+ props.node.id,
112
+ patchStorageConfig(props.node, { set: { value: String(value ?? "") } })
113
+ );
114
+ }
115
+ </script>
116
+
117
+ <template>
118
+ <div
119
+ ref="formRef"
120
+ class="space-y-2 rounded-md border border-border/70 bg-muted/20 p-2.5"
121
+ >
122
+ <ScopeTemplateAutocompleteHost :scope="autocompleteScope" :container-ref="formRef" />
123
+ <div class="flex items-center gap-1.5">
124
+ <div class="font-medium text-foreground">{{ t("blueprint.config.storageTitle") }}</div>
125
+ <ConfigHintIcon :label="t('blueprint.config.storageTitle')">
126
+ {{ t("blueprint.config.storageHint") }}
127
+ </ConfigHintIcon>
128
+ </div>
129
+
130
+ <div class="space-y-2 rounded-md border border-border/60 bg-background/70 p-2">
131
+ <div class="flex items-center gap-1.5">
132
+ <div class="font-medium text-foreground">{{ t("blueprint.config.storageReadTitle") }}</div>
133
+ <ConfigHintIcon :label="t('blueprint.config.storageReadTitle')">
134
+ {{ t("blueprint.config.storageReadHint") }}
135
+ </ConfigHintIcon>
136
+ </div>
137
+ <label class="block space-y-1">
138
+ <span class="text-muted-foreground">{{ t("blueprint.config.storageTarget") }}</span>
139
+ <Select
140
+ size="small"
141
+ class="w-full"
142
+ :value="storageConfig.read.storage"
143
+ @change="(v) => handleReadStorageChange(String(v))"
144
+ >
145
+ <Select.Option value="session">{{ t("blueprint.config.storageSession") }}</Select.Option>
146
+ <Select.Option value="local">{{ t("blueprint.config.storageLocal") }}</Select.Option>
147
+ </Select>
148
+ </label>
149
+ <label class="block space-y-1">
150
+ <span class="text-muted-foreground">{{ t("blueprint.config.storageKey") }}</span>
151
+ <Input
152
+ size="small"
153
+ :value="storageConfig.read.key"
154
+ spellcheck="false"
155
+ class="font-mono text-[11px]"
156
+ placeholder="key"
157
+ @update:value="(v) => handleReadKeyChange(String(v ?? ''))"
158
+ />
159
+ </label>
160
+ </div>
161
+
162
+ <div class="space-y-2 rounded-md border border-border/60 bg-background/70 p-2">
163
+ <div class="flex items-center gap-1.5">
164
+ <div class="font-medium text-foreground">{{ t("blueprint.config.storageSetTitle") }}</div>
165
+ <ConfigHintIcon :label="t('blueprint.config.storageSetTitle')">
166
+ {{ t("blueprint.config.storageSetHint") }}
167
+ </ConfigHintIcon>
168
+ </div>
169
+ <div class="space-y-1.5">
170
+ <span class="text-muted-foreground">{{ t("blueprint.config.storageTarget") }}</span>
171
+ <label class="flex items-center gap-2">
172
+ <Checkbox
173
+ :checked="storageConfig.set.storages.includes('session')"
174
+ class="mt-0.5"
175
+ @update:checked="(v) => handleSetStorageToggle('session', Boolean(v))"
176
+ />
177
+ <span>{{ t("blueprint.config.storageSession") }}</span>
178
+ </label>
179
+ <label class="flex items-center gap-2">
180
+ <Checkbox
181
+ :checked="storageConfig.set.storages.includes('local')"
182
+ class="mt-0.5"
183
+ @update:checked="(v) => handleSetStorageToggle('local', Boolean(v))"
184
+ />
185
+ <span>{{ t("blueprint.config.storageLocal") }}</span>
186
+ </label>
187
+ </div>
188
+ <label class="block space-y-1">
189
+ <span class="text-muted-foreground">{{ t("blueprint.config.storageKey") }}</span>
190
+ <Input
191
+ size="small"
192
+ :value="storageConfig.set.key"
193
+ spellcheck="false"
194
+ class="font-mono text-[11px]"
195
+ placeholder="key"
196
+ @update:value="(v) => handleSetKeyChange(String(v ?? ''))"
197
+ />
198
+ </label>
199
+ <label class="block space-y-1">
200
+ <span class="inline-flex items-center gap-1 text-muted-foreground">
201
+ {{ t("blueprint.config.storageValue") }}
202
+ <ConfigHintIcon :label="t('blueprint.config.storageValue')">
203
+ {{ t("blueprint.config.storageValueHint") }}
204
+ </ConfigHintIcon>
205
+ </span>
206
+ <Input
207
+ size="small"
208
+ :value="storageConfig.set.value"
209
+ spellcheck="false"
210
+ class="font-mono text-[11px]"
211
+ placeholder="{scope.value}"
212
+ @update:value="(v) => handleSetValueChange(String(v ?? ''))"
213
+ />
214
+ </label>
215
+ </div>
216
+ </div>
217
+ </template>
@@ -41,6 +41,7 @@ export type UseBlueprintDebugSessionOptions = {
41
41
  libraryNameById?: MaybeRefOrGetter<ReadonlyMap<string, string> | undefined>;
42
42
  onExecutionBlocked?: (message: string) => void;
43
43
  onViewScopeUpdate?: (viewElementIds: string[], scope: unknown) => void;
44
+ nameSpace?: MaybeRefOrGetter<string | null | undefined>;
44
45
  };
45
46
 
46
47
  function buildNodeLabelMap(graph: BlueprintGraph): Record<string, string> {
@@ -63,13 +64,20 @@ export function useBlueprintDebugSession(options: UseBlueprintDebugSessionOption
63
64
  const chainComplete = ref(false);
64
65
  const logPanelOpen = ref(false);
65
66
  const running = ref(false);
66
- const settings = ref<ExecutionLogSettings>(readExecutionLogSettings());
67
+ const settings = ref<ExecutionLogSettings>(
68
+ readExecutionLogSettings(toValue(options.nameSpace))
69
+ );
67
70
  const savedRuns = ref<ExecutionRunRecord[]>([]);
68
71
  const totalSavedRunCount = ref(0);
69
72
 
70
73
  const graph = computed(() => toValue(options.graph));
71
74
  const blueprintId = computed(() => toValue(options.blueprintId));
72
75
  const blueprintName = computed(() => toValue(options.blueprintName));
76
+ const nameSpace = computed(() => toValue(options.nameSpace) ?? null);
77
+
78
+ watch(nameSpace, (ns) => {
79
+ settings.value = readExecutionLogSettings(ns);
80
+ });
73
81
 
74
82
  const lifecycleNodes = computed<LifecycleNodeOption[]>(() =>
75
83
  graph.value.document.nodes
@@ -145,7 +153,7 @@ export function useBlueprintDebugSession(options: UseBlueprintDebugSessionOption
145
153
  }
146
154
 
147
155
  async function refreshSavedRuns() {
148
- const all = await listExecutionRunRecords();
156
+ const all = await listExecutionRunRecords(undefined, nameSpace.value);
149
157
  totalSavedRunCount.value = all.length;
150
158
  savedRuns.value = blueprintId.value
151
159
  ? all.filter((item) => item.blueprintId === blueprintId.value)
@@ -158,13 +166,13 @@ export function useBlueprintDebugSession(options: UseBlueprintDebugSessionOption
158
166
 
159
167
  async function enforceSavedRunLimits() {
160
168
  const cutoff = retentionCutoffMs(settings.value.retentionDays);
161
- await purgeExecutionRunsOlderThan(cutoff);
162
- await trimExecutionRunRecordsToMax(settings.value.maxSavedRuns);
169
+ await purgeExecutionRunsOlderThan(cutoff, nameSpace.value);
170
+ await trimExecutionRunRecordsToMax(settings.value.maxSavedRuns, nameSpace.value);
163
171
  await refreshSavedRuns();
164
172
  }
165
173
 
166
174
  async function clearAllSavedRuns() {
167
- const removed = await clearAllExecutionRunRecords();
175
+ const removed = await clearAllExecutionRunRecords(nameSpace.value);
168
176
  savedRuns.value = [];
169
177
  totalSavedRunCount.value = 0;
170
178
  return removed;
@@ -194,7 +202,7 @@ export function useBlueprintDebugSession(options: UseBlueprintDebugSessionOption
194
202
 
195
203
  async function persistRunIfNeeded(record: ExecutionRunRecord) {
196
204
  if (!settings.value.autoSave) return;
197
- await putExecutionRunRecord(record);
205
+ await putExecutionRunRecord(record, nameSpace.value);
198
206
  await enforceSavedRunLimits();
199
207
  }
200
208
 
@@ -356,7 +364,7 @@ export function useBlueprintDebugSession(options: UseBlueprintDebugSessionOption
356
364
  runnerRef.value?.isDebugQueueEmpty() ? "completed" : "paused",
357
365
  entries.value
358
366
  );
359
- await putExecutionRunRecord(record);
367
+ await putExecutionRunRecord(record, nameSpace.value);
360
368
  await enforceSavedRunLimits();
361
369
  }
362
370
 
@@ -373,10 +381,10 @@ export function useBlueprintDebugSession(options: UseBlueprintDebugSessionOption
373
381
  function updateSettings(patch: Partial<ExecutionLogSettings>) {
374
382
  const next = { ...settings.value, ...patch };
375
383
  settings.value = next;
376
- writeExecutionLogSettings(next);
384
+ writeExecutionLogSettings(next, nameSpace.value);
377
385
  if (patch.maxSavedRuns !== undefined) {
378
386
  const maxSavedRuns = Math.max(1, Math.floor(patch.maxSavedRuns));
379
- void trimExecutionRunRecordsToMax(maxSavedRuns).then(() => refreshSavedRuns());
387
+ void trimExecutionRunRecordsToMax(maxSavedRuns, nameSpace.value).then(() => refreshSavedRuns());
380
388
  }
381
389
  }
382
390
 
@@ -8,6 +8,7 @@ import {
8
8
  resolveNodeEventConfig,
9
9
  resolveNodeFetchConfig,
10
10
  resolveNodeJsonConfig,
11
+ resolveNodeStorageConfig,
11
12
  resolveNodeLogicConfig,
12
13
  resolveRunnableNodeType,
13
14
  sanitizeBlueprintDocument,
@@ -197,6 +198,7 @@ export class BlueprintGraph {
197
198
  | "lifecyclePhase"
198
199
  | "fetchConfig"
199
200
  | "jsonConfig"
201
+ | "storageConfig"
200
202
  | "logicConfig"
201
203
  | "clockConfig"
202
204
  | "eventConfig"
@@ -241,6 +243,20 @@ export class BlueprintGraph {
241
243
  ...patch.jsonConfig,
242
244
  };
243
245
  }
246
+ if (patch.storageConfig) {
247
+ nextNode.storageConfig = {
248
+ ...resolveNodeStorageConfig(n),
249
+ ...patch.storageConfig,
250
+ read: {
251
+ ...resolveNodeStorageConfig(n).read,
252
+ ...patch.storageConfig.read,
253
+ },
254
+ set: {
255
+ ...resolveNodeStorageConfig(n).set,
256
+ ...patch.storageConfig.set,
257
+ },
258
+ };
259
+ }
244
260
  if (patch.logicConfig) {
245
261
  nextNode.logicConfig = {
246
262
  ...resolveNodeLogicConfig(n),
@@ -4,6 +4,7 @@ import type {
4
4
  JsonNodeConfig,
5
5
  LogicNodeConfig,
6
6
  PageLifecyclePhase,
7
+ StorageNodeConfig,
7
8
  ViewEventNodeConfig,
8
9
  ViewEventType,
9
10
  } from "@arronqzy/blueprint-dsl";
@@ -16,13 +17,16 @@ import {
16
17
  DEFAULT_JSON_NODE_CONFIG,
17
18
  DEFAULT_LOGIC_NODE_CONFIG,
18
19
  DEFAULT_LOGIC_NODE_TYPE,
20
+ DEFAULT_STORAGE_NODE_CONFIG,
19
21
  DEFAULT_VIEW_EVENT_NODE_CONFIG,
20
22
  EVENT_NODE_TYPE,
21
23
  FETCH_NODE_TYPE,
22
24
  JSON_NODE_TYPE,
23
25
  LIFECYCLE_NODE_TYPE,
24
26
  normalizeClockConfig,
27
+ normalizeStorageConfig,
25
28
  normalizeViewEventConfig,
29
+ STORAGE_NODE_TYPE,
26
30
  VIEW_NODE_TYPE,
27
31
  } from "@arronqzy/blueprint-dsl";
28
32
 
@@ -31,8 +35,9 @@ export type { JsonNodeConfig as BlueprintJsonConfig } from "@arronqzy/blueprint-
31
35
  export type { LogicNodeConfig as BlueprintLogicConfig } from "@arronqzy/blueprint-dsl";
32
36
  export type { ClockNodeConfig as BlueprintClockConfig } from "@arronqzy/blueprint-dsl";
33
37
  export type { ViewEventNodeConfig as BlueprintEventConfig } from "@arronqzy/blueprint-dsl";
38
+ export type { StorageNodeConfig as BlueprintStorageConfig } from "@arronqzy/blueprint-dsl";
34
39
 
35
- export type BlueprintNodeRole = "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "clock" | "event";
40
+ export type BlueprintNodeRole = "blueprint" | "logic" | "and" | "lifecycle" | "fetch" | "json" | "storage" | "clock" | "event";
36
41
 
37
42
  /** 决定右侧配置面板展示哪类配置 */
38
43
  export type BlueprintConfigSource =
@@ -43,6 +48,7 @@ export type BlueprintConfigSource =
43
48
  | "view"
44
49
  | "fetch"
45
50
  | "json"
51
+ | "storage"
46
52
  | "clock"
47
53
  | "event";
48
54
 
@@ -71,6 +77,8 @@ export type BlueprintGraphNode = {
71
77
  fetchConfig?: FetchRequestConfig;
72
78
  /** JSON 节点的 JSON 字符串配置 */
73
79
  jsonConfig?: JsonNodeConfig;
80
+ /** 存储节点的读写配置 */
81
+ storageConfig?: StorageNodeConfig;
74
82
  /** 逻辑节点的 JavaScript 脚本配置 */
75
83
  logicConfig?: LogicNodeConfig;
76
84
  /** 时钟节点的间隔与时间格式配置 */
@@ -108,8 +116,10 @@ export function resolveBlueprintConfigSource(
108
116
  if (node.role === "and") return "and";
109
117
  if (node.role === "fetch") return "fetch";
110
118
  if (node.role === "json") return "json";
119
+ if (node.role === "storage") return "storage";
111
120
  if (node.nodeType === FETCH_NODE_TYPE) return "fetch";
112
121
  if (node.nodeType === JSON_NODE_TYPE) return "json";
122
+ if (node.nodeType === STORAGE_NODE_TYPE) return "storage";
113
123
  if (node.nodeType === CLOCK_NODE_TYPE) return "clock";
114
124
  if (node.nodeType === EVENT_NODE_TYPE) return "event";
115
125
  if (node.nodeType === VIEW_NODE_TYPE) return "view";
@@ -187,6 +197,8 @@ export function resolveRunnableNodeType(
187
197
  return FETCH_NODE_TYPE;
188
198
  case "json":
189
199
  return JSON_NODE_TYPE;
200
+ case "storage":
201
+ return STORAGE_NODE_TYPE;
190
202
  case "logic":
191
203
  return DEFAULT_LOGIC_NODE_TYPE;
192
204
  default:
@@ -210,6 +222,7 @@ export const BLUEPRINT_CONFIG_TYPE_LABEL_KEYS: Record<
210
222
  event: "blueprint.node.typeEvent",
211
223
  fetch: "blueprint.node.typeFetch",
212
224
  json: "blueprint.node.typeJson",
225
+ storage: "blueprint.node.typeStorage",
213
226
  clock: "blueprint.node.typeClock",
214
227
  };
215
228
 
@@ -300,6 +313,15 @@ export function resolveNodeJsonConfig(
300
313
  };
301
314
  }
302
315
 
316
+ export function resolveNodeStorageConfig(
317
+ node: Pick<BlueprintGraphNode, "storageConfig">
318
+ ): StorageNodeConfig {
319
+ return normalizeStorageConfig({
320
+ ...DEFAULT_STORAGE_NODE_CONFIG,
321
+ ...node.storageConfig,
322
+ });
323
+ }
324
+
303
325
  export function resolveNodeLogicConfig(
304
326
  node: Pick<BlueprintGraphNode, "logicConfig">
305
327
  ): LogicNodeConfig {
@@ -399,6 +421,9 @@ export function sanitizeBlueprintDocument(
399
421
  if (next.role === "json") {
400
422
  return { ...next, configSource: "json" as const };
401
423
  }
424
+ if (next.role === "storage") {
425
+ return { ...next, configSource: "storage" as const };
426
+ }
402
427
  if (next.role === "and") {
403
428
  return { ...next, configSource: "and" as const };
404
429
  }
@@ -452,6 +477,7 @@ export function patchNodeConfigSource(
452
477
  logicConfig: undefined,
453
478
  clockConfig: undefined,
454
479
  eventConfig: undefined,
480
+ storageConfig: undefined,
455
481
  };
456
482
  }
457
483
 
@@ -469,6 +495,7 @@ export function patchNodeConfigSource(
469
495
  jsonConfig: undefined,
470
496
  logicConfig: undefined,
471
497
  eventConfig: undefined,
498
+ storageConfig: undefined,
472
499
  };
473
500
  }
474
501
 
@@ -486,6 +513,7 @@ export function patchNodeConfigSource(
486
513
  logicConfig: undefined,
487
514
  clockConfig: undefined,
488
515
  eventConfig: undefined,
516
+ storageConfig: undefined,
489
517
  };
490
518
  }
491
519
 
@@ -503,6 +531,25 @@ export function patchNodeConfigSource(
503
531
  logicConfig: undefined,
504
532
  clockConfig: undefined,
505
533
  eventConfig: undefined,
534
+ storageConfig: undefined,
535
+ };
536
+ }
537
+
538
+ if (configSource === "storage") {
539
+ return {
540
+ role: "storage",
541
+ nodeType: STORAGE_NODE_TYPE,
542
+ configSource: "storage",
543
+ storageConfig: resolveNodeStorageConfig(node),
544
+ lifecyclePhase: undefined,
545
+ viewElementId: undefined,
546
+ viewElementIds: undefined,
547
+ libraryBlueprintId: undefined,
548
+ fetchConfig: undefined,
549
+ jsonConfig: undefined,
550
+ logicConfig: undefined,
551
+ clockConfig: undefined,
552
+ eventConfig: undefined,
506
553
  };
507
554
  }
508
555
 
@@ -520,6 +567,7 @@ export function patchNodeConfigSource(
520
567
  logicConfig: undefined,
521
568
  clockConfig: undefined,
522
569
  eventConfig: undefined,
570
+ storageConfig: undefined,
523
571
  };
524
572
  }
525
573
 
@@ -537,6 +585,7 @@ export function patchNodeConfigSource(
537
585
  logicConfig: resolveNodeLogicConfig(node),
538
586
  clockConfig: undefined,
539
587
  eventConfig: undefined,
588
+ storageConfig: undefined,
540
589
  };
541
590
  }
542
591
 
@@ -554,6 +603,7 @@ export function patchNodeConfigSource(
554
603
  logicConfig: undefined,
555
604
  clockConfig: undefined,
556
605
  eventConfig: undefined,
606
+ storageConfig: undefined,
557
607
  };
558
608
  }
559
609
 
@@ -569,6 +619,7 @@ export function patchNodeConfigSource(
569
619
  jsonConfig: undefined,
570
620
  logicConfig: undefined,
571
621
  clockConfig: undefined,
622
+ storageConfig: undefined,
572
623
  };
573
624
  }
574
625
 
@@ -583,5 +634,6 @@ export function patchNodeConfigSource(
583
634
  logicConfig: undefined,
584
635
  clockConfig: undefined,
585
636
  eventConfig: undefined,
637
+ storageConfig: undefined,
586
638
  };
587
639
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./document";
2
+ export * from "./parse-blueprint-data";
2
3
  export * from "./node-types";
3
4
  export * from "./blueprint-graph";
4
5
  export * from "./vue-flow-adapter";
@@ -9,6 +9,7 @@ import {
9
9
  resolveNodeFetchConfig,
10
10
  resolveNodeJsonConfig,
11
11
  resolveNodeLogicConfig,
12
+ resolveNodeStorageConfig,
12
13
  resolveViewElementIds,
13
14
  resolveNodeEventConfig,
14
15
  getViewEventTypeLabel,
@@ -65,6 +66,29 @@ export function resolveBlueprintNodeSummary(
65
66
  if (!json) return t("blueprint.node.summaryEmptyJson");
66
67
  return truncate(json.replace(/\s+/g, " "), 44);
67
68
  }
69
+ case "storage": {
70
+ const storage = resolveNodeStorageConfig(data);
71
+ const setKey = storage.set.key.trim();
72
+ const readKey = storage.read.key.trim();
73
+ const hasSet = Boolean(setKey) && storage.set.storages.length > 0;
74
+ const hasRead = Boolean(readKey);
75
+ if (hasSet && hasRead) {
76
+ return truncate(
77
+ t("blueprint.node.summaryStorageSetRead", {
78
+ setKey,
79
+ readKey,
80
+ }),
81
+ 44
82
+ );
83
+ }
84
+ if (hasSet) {
85
+ return truncate(t("blueprint.node.summaryStorageSet", { key: setKey }), 44);
86
+ }
87
+ if (hasRead) {
88
+ return truncate(t("blueprint.node.summaryStorageRead", { key: readKey }), 44);
89
+ }
90
+ return t("blueprint.node.summaryStorageUnset");
91
+ }
68
92
  case "logic": {
69
93
  const code = resolveNodeLogicConfig(data).sourceCode?.trim();
70
94
  if (!code) return t("blueprint.node.summaryNoScript");
@@ -4,6 +4,7 @@ export {
4
4
  DEFAULT_LOGIC_NODE_TYPE,
5
5
  LIFECYCLE_NODE_TYPE,
6
6
  JSON_NODE_TYPE,
7
+ STORAGE_NODE_TYPE,
7
8
  CLOCK_NODE_TYPE,
8
9
  AND_NODE_TYPE,
9
10
  EVENT_NODE_TYPE,
@@ -0,0 +1,106 @@
1
+ import {
2
+ sanitizeBlueprintDocument,
3
+ type BlueprintDocument,
4
+ type BlueprintGraphEdge,
5
+ type BlueprintGraphNode,
6
+ type BlueprintNodeRole,
7
+ } from "./document";
8
+
9
+ export type ParseCheckResult = {
10
+ ok: boolean;
11
+ error?: string;
12
+ };
13
+
14
+ const NODE_ROLES = new Set<BlueprintNodeRole>([
15
+ "blueprint",
16
+ "logic",
17
+ "and",
18
+ "lifecycle",
19
+ "fetch",
20
+ "json",
21
+ "storage",
22
+ "clock",
23
+ "event",
24
+ ]);
25
+
26
+ function isRecord(value: unknown): value is Record<string, unknown> {
27
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
28
+ }
29
+
30
+ function extractBlueprintDocument(data: unknown): unknown {
31
+ if (!isRecord(data)) return data;
32
+ if (isRecord(data.blueprintDocument)) return data.blueprintDocument;
33
+ if (isRecord(data.document) && Array.isArray(data.document.nodes)) {
34
+ return data.document;
35
+ }
36
+ return data;
37
+ }
38
+
39
+ function isFiniteNumber(value: unknown): value is number {
40
+ return typeof value === "number" && Number.isFinite(value);
41
+ }
42
+
43
+ function isBlueprintNode(value: unknown): value is BlueprintGraphNode {
44
+ if (!isRecord(value)) return false;
45
+ if (typeof value.id !== "string" || !value.id) return false;
46
+ if (typeof value.role !== "string" || !NODE_ROLES.has(value.role as BlueprintNodeRole)) {
47
+ return false;
48
+ }
49
+ if (!isRecord(value.position) || !isFiniteNumber(value.position.x) || !isFiniteNumber(value.position.y)) {
50
+ return false;
51
+ }
52
+ return true;
53
+ }
54
+
55
+ function isBlueprintEdge(value: unknown): value is BlueprintGraphEdge {
56
+ if (!isRecord(value)) return false;
57
+ return (
58
+ typeof value.id === "string" &&
59
+ value.id.length > 0 &&
60
+ typeof value.source === "string" &&
61
+ value.source.length > 0 &&
62
+ typeof value.target === "string" &&
63
+ value.target.length > 0
64
+ );
65
+ }
66
+
67
+ /**
68
+ * 校验数据能否被蓝图解析(工作区 `blueprintDocument`、蓝图导出 JSON 或裸文档均可)。
69
+ */
70
+ export function parseBlueprintData(
71
+ data: unknown
72
+ ): ParseCheckResult & { value?: BlueprintDocument } {
73
+ if (data == null) {
74
+ return { ok: false, error: "empty" };
75
+ }
76
+ const raw = extractBlueprintDocument(data);
77
+ if (!isRecord(raw)) {
78
+ return { ok: false, error: "invalid-blueprint-data" };
79
+ }
80
+ if (!Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) {
81
+ return { ok: false, error: "invalid-blueprint-data" };
82
+ }
83
+ if (!raw.nodes.every(isBlueprintNode) || !raw.edges.every(isBlueprintEdge)) {
84
+ return { ok: false, error: "invalid-blueprint-data" };
85
+ }
86
+ try {
87
+ const value = sanitizeBlueprintDocument({
88
+ id: typeof raw.id === "string" && raw.id ? raw.id : "default",
89
+ name: typeof raw.name === "string" ? raw.name : undefined,
90
+ nodes: raw.nodes as BlueprintGraphNode[],
91
+ edges: raw.edges as BlueprintGraphEdge[],
92
+ allowFalseSignalPropagation:
93
+ typeof raw.allowFalseSignalPropagation === "boolean"
94
+ ? raw.allowFalseSignalPropagation
95
+ : undefined,
96
+ });
97
+ return { ok: true, value };
98
+ } catch {
99
+ return { ok: false, error: "invalid-blueprint-data" };
100
+ }
101
+ }
102
+
103
+ export function validateBlueprintData(data: unknown): ParseCheckResult {
104
+ const parsed = parseBlueprintData(data);
105
+ return parsed.ok ? { ok: true } : { ok: false, error: parsed.error };
106
+ }
@@ -50,6 +50,10 @@ export function nodeStructureSignature(
50
50
  viewElementIds?: string[];
51
51
  fetchConfig?: { url?: string; method?: string; apiBaseUrl?: string; swaggerDocsUrl?: string };
52
52
  jsonConfig?: { jsonString?: string };
53
+ storageConfig?: {
54
+ read?: { storage?: string; key?: string };
55
+ set?: { storages?: string[]; key?: string; value?: string };
56
+ };
53
57
  logicConfig?: { sourceCode?: string };
54
58
  clockConfig?: { intervalSeconds?: number; timeFormat?: string; outputCount?: number; emitImmediately?: boolean };
55
59
  eventConfig?: { eventTypes?: string[] };
@@ -58,7 +62,7 @@ export function nodeStructureSignature(
58
62
  return nodes
59
63
  .map(
60
64
  (n) =>
61
- `${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 ?? ""}:${(n.eventConfig?.eventTypes ?? []).join(",")}`
65
+ `${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.storageConfig?.read?.storage ?? ""}:${n.storageConfig?.read?.key ?? ""}:${(n.storageConfig?.set?.storages ?? []).join(",")}:${n.storageConfig?.set?.key ?? ""}:${n.storageConfig?.set?.value ?? ""}:${n.logicConfig?.sourceCode ?? ""}:${n.clockConfig?.intervalSeconds ?? ""}:${n.clockConfig?.timeFormat ?? ""}:${n.clockConfig?.outputCount ?? ""}:${n.clockConfig?.emitImmediately ?? ""}:${(n.eventConfig?.eventTypes ?? []).join(",")}`
62
66
  )
63
67
  .sort()
64
68
  .join("|");
@@ -19,6 +19,7 @@ export type BlueprintFlowNodeData = {
19
19
  lifecyclePhase?: BlueprintGraphNode["lifecyclePhase"];
20
20
  fetchConfig?: BlueprintGraphNode["fetchConfig"];
21
21
  jsonConfig?: BlueprintGraphNode["jsonConfig"];
22
+ storageConfig?: BlueprintGraphNode["storageConfig"];
22
23
  clockConfig?: BlueprintGraphNode["clockConfig"];
23
24
  eventConfig?: BlueprintGraphNode["eventConfig"];
24
25
  logicConfig?: BlueprintGraphNode["logicConfig"];
@@ -71,6 +72,8 @@ export function toVueFlowNodes(document: BlueprintDocument): VueFlowNodeView[] {
71
72
  ? "fetch"
72
73
  : n.role === "json"
73
74
  ? "json"
75
+ : n.role === "storage"
76
+ ? "storage"
74
77
  : n.role === "event"
75
78
  ? "event"
76
79
  : "logic",
@@ -93,6 +96,7 @@ export function toVueFlowNodes(document: BlueprintDocument): VueFlowNodeView[] {
93
96
  lifecyclePhase: n.lifecyclePhase,
94
97
  fetchConfig: n.fetchConfig,
95
98
  jsonConfig: n.jsonConfig,
99
+ storageConfig: n.storageConfig,
96
100
  clockConfig: n.clockConfig,
97
101
  eventConfig: n.eventConfig,
98
102
  logicConfig: n.logicConfig,
@@ -1,3 +1,4 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
1
2
  import type {
2
3
  BlueprintLibraryListItem,
3
4
  BlueprintLibraryRecord,
@@ -7,9 +8,9 @@ const DB_NAME = "arronqzy-blueprint-library";
7
8
  const DB_VERSION = 1;
8
9
  const STORE_NAME = "blueprints";
9
10
 
10
- function openDb(): Promise<IDBDatabase> {
11
+ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
11
12
  return new Promise((resolve, reject) => {
12
- const request = indexedDB.open(DB_NAME, DB_VERSION);
13
+ const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
13
14
  request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
14
15
  request.onupgradeneeded = () => {
15
16
  const db = request.result;
@@ -25,9 +26,10 @@ function openDb(): Promise<IDBDatabase> {
25
26
 
26
27
  function runTransaction<T>(
27
28
  mode: IDBTransactionMode,
28
- runner: (store: IDBObjectStore) => IDBRequest<T>
29
+ runner: (store: IDBObjectStore) => IDBRequest<T>,
30
+ nameSpace?: string | null
29
31
  ): Promise<T> {
30
- return openDb().then(
32
+ return openDb(nameSpace).then(
31
33
  (db) =>
32
34
  new Promise<T>((resolve, reject) => {
33
35
  const tx = db.transaction(STORE_NAME, mode);
@@ -53,9 +55,13 @@ function runTransaction<T>(
53
55
  );
54
56
  }
55
57
 
56
- export async function listBlueprintLibrary(): Promise<BlueprintLibraryListItem[]> {
57
- const records = await runTransaction<BlueprintLibraryRecord[]>("readonly", (store) =>
58
- store.getAll()
58
+ export async function listBlueprintLibrary(
59
+ nameSpace?: string | null
60
+ ): Promise<BlueprintLibraryListItem[]> {
61
+ const records = await runTransaction<BlueprintLibraryRecord[]>(
62
+ "readonly",
63
+ (store) => store.getAll(),
64
+ nameSpace
59
65
  );
60
66
  return records
61
67
  .map((record) => ({
@@ -69,26 +75,31 @@ export async function listBlueprintLibrary(): Promise<BlueprintLibraryListItem[]
69
75
  }
70
76
 
71
77
  export async function getBlueprintLibraryRecord(
72
- id: string
78
+ id: string,
79
+ nameSpace?: string | null
73
80
  ): Promise<BlueprintLibraryRecord | null> {
74
- const record = await runTransaction<BlueprintLibraryRecord | undefined>("readonly", (store) =>
75
- store.get(id)
81
+ const record = await runTransaction<BlueprintLibraryRecord | undefined>(
82
+ "readonly",
83
+ (store) => store.get(id),
84
+ nameSpace
76
85
  );
77
86
  return record ?? null;
78
87
  }
79
88
 
80
89
  export async function putBlueprintLibraryRecord(
81
- record: BlueprintLibraryRecord
90
+ record: BlueprintLibraryRecord,
91
+ nameSpace?: string | null
82
92
  ): Promise<BlueprintLibraryRecord> {
83
- await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record));
93
+ await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record), nameSpace);
84
94
  return record;
85
95
  }
86
96
 
87
97
  export async function updateBlueprintLibraryMeta(
88
98
  id: string,
89
- patch: { name?: string; remark?: string }
99
+ patch: { name?: string; remark?: string },
100
+ nameSpace?: string | null
90
101
  ): Promise<BlueprintLibraryRecord | null> {
91
- const existing = await getBlueprintLibraryRecord(id);
102
+ const existing = await getBlueprintLibraryRecord(id, nameSpace);
92
103
  if (!existing) return null;
93
104
 
94
105
  const name = patch.name?.trim() || existing.name;
@@ -103,10 +114,13 @@ export async function updateBlueprintLibraryMeta(
103
114
  },
104
115
  };
105
116
 
106
- await putBlueprintLibraryRecord(record);
117
+ await putBlueprintLibraryRecord(record, nameSpace);
107
118
  return record;
108
119
  }
109
120
 
110
- export async function deleteBlueprintLibraryRecord(id: string): Promise<void> {
111
- await runTransaction<undefined>("readwrite", (store) => store.delete(id));
121
+ export async function deleteBlueprintLibraryRecord(
122
+ id: string,
123
+ nameSpace?: string | null
124
+ ): Promise<void> {
125
+ await runTransaction<undefined>("readwrite", (store) => store.delete(id), nameSpace);
112
126
  }
@@ -1,12 +1,13 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
1
2
  import type { ExecutionRunRecord } from "@arronqzy/blueprint-dsl";
2
3
 
3
4
  const DB_NAME = "arronqzy-blueprint-execution-log";
4
5
  const DB_VERSION = 1;
5
6
  const STORE_NAME = "runs";
6
7
 
7
- function openDb(): Promise<IDBDatabase> {
8
+ function openDb(nameSpace?: string | null): Promise<IDBDatabase> {
8
9
  return new Promise((resolve, reject) => {
9
- const request = indexedDB.open(DB_NAME, DB_VERSION);
10
+ const request = indexedDB.open(appStorageKey(DB_NAME, nameSpace), DB_VERSION);
10
11
  request.onerror = () => reject(request.error ?? new Error("indexeddb-open-failed"));
11
12
  request.onupgradeneeded = () => {
12
13
  const db = request.result;
@@ -22,9 +23,10 @@ function openDb(): Promise<IDBDatabase> {
22
23
 
23
24
  function runTransaction<T>(
24
25
  mode: IDBTransactionMode,
25
- runner: (store: IDBObjectStore) => IDBRequest<T>
26
+ runner: (store: IDBObjectStore) => IDBRequest<T>,
27
+ nameSpace?: string | null
26
28
  ): Promise<T> {
27
- return openDb().then(
29
+ return openDb(nameSpace).then(
28
30
  (db) =>
29
31
  new Promise<T>((resolve, reject) => {
30
32
  const tx = db.transaction(STORE_NAME, mode);
@@ -50,28 +52,55 @@ function runTransaction<T>(
50
52
  );
51
53
  }
52
54
 
55
+ function deleteMany(ids: string[], nameSpace?: string | null): Promise<void> {
56
+ return openDb(nameSpace).then(
57
+ (db) =>
58
+ new Promise<void>((resolve, reject) => {
59
+ const tx = db.transaction(STORE_NAME, "readwrite");
60
+ const store = tx.objectStore(STORE_NAME);
61
+ for (const id of ids) {
62
+ store.delete(id);
63
+ }
64
+ tx.oncomplete = () => {
65
+ db.close();
66
+ resolve();
67
+ };
68
+ tx.onerror = () => {
69
+ db.close();
70
+ reject(tx.error ?? new Error("indexeddb-transaction-failed"));
71
+ };
72
+ })
73
+ );
74
+ }
75
+
53
76
  export async function putExecutionRunRecord(
54
- record: ExecutionRunRecord
77
+ record: ExecutionRunRecord,
78
+ nameSpace?: string | null
55
79
  ): Promise<ExecutionRunRecord> {
56
- await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record));
80
+ await runTransaction<IDBValidKey>("readwrite", (store) => store.put(record), nameSpace);
57
81
  return record;
58
82
  }
59
83
 
60
84
  export async function getExecutionRunRecord(
61
- runId: string
85
+ runId: string,
86
+ nameSpace?: string | null
62
87
  ): Promise<ExecutionRunRecord | null> {
63
88
  const record = await runTransaction<ExecutionRunRecord | undefined>(
64
89
  "readonly",
65
- (store) => store.get(runId)
90
+ (store) => store.get(runId),
91
+ nameSpace
66
92
  );
67
93
  return record ?? null;
68
94
  }
69
95
 
70
96
  export async function listExecutionRunRecords(
71
- blueprintId?: string | null
97
+ blueprintId?: string | null,
98
+ nameSpace?: string | null
72
99
  ): Promise<ExecutionRunRecord[]> {
73
- const records = await runTransaction<ExecutionRunRecord[]>("readonly", (store) =>
74
- store.getAll()
100
+ const records = await runTransaction<ExecutionRunRecord[]>(
101
+ "readonly",
102
+ (store) => store.getAll(),
103
+ nameSpace
75
104
  );
76
105
  const filtered = blueprintId
77
106
  ? records.filter((item) => item.blueprintId === blueprintId)
@@ -79,27 +108,37 @@ export async function listExecutionRunRecords(
79
108
  return filtered.sort((a, b) => b.startedAt - a.startedAt);
80
109
  }
81
110
 
82
- export async function deleteExecutionRunRecord(runId: string): Promise<void> {
83
- await runTransaction<undefined>("readwrite", (store) => store.delete(runId));
111
+ export async function deleteExecutionRunRecord(
112
+ runId: string,
113
+ nameSpace?: string | null
114
+ ): Promise<void> {
115
+ await runTransaction<undefined>("readwrite", (store) => store.delete(runId), nameSpace);
84
116
  }
85
117
 
86
- export async function clearAllExecutionRunRecords(): Promise<number> {
87
- const records = await runTransaction<ExecutionRunRecord[]>("readonly", (store) =>
88
- store.getAll()
118
+ export async function clearAllExecutionRunRecords(
119
+ nameSpace?: string | null
120
+ ): Promise<number> {
121
+ const records = await runTransaction<ExecutionRunRecord[]>(
122
+ "readonly",
123
+ (store) => store.getAll(),
124
+ nameSpace
89
125
  );
90
126
  if (records.length === 0) return 0;
91
127
 
92
- await runTransaction<undefined>("readwrite", (store) => store.clear());
128
+ await runTransaction<undefined>("readwrite", (store) => store.clear(), nameSpace);
93
129
  return records.length;
94
130
  }
95
131
 
96
132
  export async function trimExecutionRunRecordsToMax(
97
- maxCount: number
133
+ maxCount: number,
134
+ nameSpace?: string | null
98
135
  ): Promise<number> {
99
136
  if (!Number.isFinite(maxCount) || maxCount < 1) return 0;
100
137
 
101
- const records = await runTransaction<ExecutionRunRecord[]>("readonly", (store) =>
102
- store.getAll()
138
+ const records = await runTransaction<ExecutionRunRecord[]>(
139
+ "readonly",
140
+ (store) => store.getAll(),
141
+ nameSpace
103
142
  );
104
143
  if (records.length <= maxCount) return 0;
105
144
 
@@ -107,52 +146,29 @@ export async function trimExecutionRunRecordsToMax(
107
146
  const overflow = sorted.slice(maxCount);
108
147
  if (overflow.length === 0) return 0;
109
148
 
110
- await openDb().then(
111
- (db) =>
112
- new Promise<void>((resolve, reject) => {
113
- const tx = db.transaction(STORE_NAME, "readwrite");
114
- const store = tx.objectStore(STORE_NAME);
115
- for (const item of overflow) {
116
- store.delete(item.runId);
117
- }
118
- tx.oncomplete = () => {
119
- db.close();
120
- resolve();
121
- };
122
- tx.onerror = () => {
123
- db.close();
124
- reject(tx.error ?? new Error("indexeddb-transaction-failed"));
125
- };
126
- })
149
+ await deleteMany(
150
+ overflow.map((item) => item.runId),
151
+ nameSpace
127
152
  );
128
153
 
129
154
  return overflow.length;
130
155
  }
131
156
 
132
- export async function purgeExecutionRunsOlderThan(cutoffMs: number): Promise<number> {
133
- const records = await runTransaction<ExecutionRunRecord[]>("readonly", (store) =>
134
- store.getAll()
157
+ export async function purgeExecutionRunsOlderThan(
158
+ cutoffMs: number,
159
+ nameSpace?: string | null
160
+ ): Promise<number> {
161
+ const records = await runTransaction<ExecutionRunRecord[]>(
162
+ "readonly",
163
+ (store) => store.getAll(),
164
+ nameSpace
135
165
  );
136
166
  const stale = records.filter((item) => item.startedAt < cutoffMs);
137
167
  if (stale.length === 0) return 0;
138
168
 
139
- await openDb().then(
140
- (db) =>
141
- new Promise<void>((resolve, reject) => {
142
- const tx = db.transaction(STORE_NAME, "readwrite");
143
- const store = tx.objectStore(STORE_NAME);
144
- for (const item of stale) {
145
- store.delete(item.runId);
146
- }
147
- tx.oncomplete = () => {
148
- db.close();
149
- resolve();
150
- };
151
- tx.onerror = () => {
152
- db.close();
153
- reject(tx.error ?? new Error("indexeddb-transaction-failed"));
154
- };
155
- })
169
+ await deleteMany(
170
+ stale.map((item) => item.runId),
171
+ nameSpace
156
172
  );
157
173
 
158
174
  return stale.length;
@@ -1,3 +1,5 @@
1
+ import { appStorageKey } from "@arronqzy/blueprint-dsl";
2
+
1
3
  export type ExecutionLogSettings = {
2
4
  retentionDays: number;
3
5
  autoSave: boolean;
@@ -13,9 +15,11 @@ export const DEFAULT_EXECUTION_LOG_SETTINGS: ExecutionLogSettings = {
13
15
  maxSavedRuns: 80,
14
16
  };
15
17
 
16
- export function readExecutionLogSettings(): ExecutionLogSettings {
18
+ export function readExecutionLogSettings(
19
+ nameSpace?: string | null
20
+ ): ExecutionLogSettings {
17
21
  try {
18
- const raw = localStorage.getItem(SETTINGS_KEY);
22
+ const raw = localStorage.getItem(appStorageKey(SETTINGS_KEY, nameSpace));
19
23
  if (!raw) return { ...DEFAULT_EXECUTION_LOG_SETTINGS };
20
24
  const parsed = JSON.parse(raw) as Partial<ExecutionLogSettings>;
21
25
  return {
@@ -37,9 +41,15 @@ export function readExecutionLogSettings(): ExecutionLogSettings {
37
41
  }
38
42
  }
39
43
 
40
- export function writeExecutionLogSettings(settings: ExecutionLogSettings) {
44
+ export function writeExecutionLogSettings(
45
+ settings: ExecutionLogSettings,
46
+ nameSpace?: string | null
47
+ ) {
41
48
  try {
42
- localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
49
+ localStorage.setItem(
50
+ appStorageKey(SETTINGS_KEY, nameSpace),
51
+ JSON.stringify(settings)
52
+ );
43
53
  } catch {
44
54
  // ignore storage errors
45
55
  }
@@ -10,7 +10,7 @@ export type BlueprintNodeCardProps = {
10
10
  meta?: string;
11
11
  subtitle?: string;
12
12
  progressLabel?: string;
13
- variant?: "blueprint" | "logic" | "and" | "lifecycle" | "event" | "fetch" | "json" | "clock";
13
+ variant?: "blueprint" | "logic" | "and" | "lifecycle" | "event" | "fetch" | "json" | "storage" | "clock";
14
14
  selected?: boolean;
15
15
  hideLeadingDot?: boolean;
16
16
  };
@@ -61,6 +61,11 @@ const variantStyle = {
61
61
  badge: "bg-teal-500/10 text-teal-700 dark:text-teal-300",
62
62
  dot: "bg-teal-500",
63
63
  },
64
+ storage: {
65
+ accent: "border-l-emerald-500 dark:border-l-emerald-400",
66
+ badge: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
67
+ dot: "bg-emerald-500",
68
+ },
64
69
  clock: {
65
70
  accent: "border-l-rose-500 dark:border-l-rose-400",
66
71
  badge: "bg-rose-500/10 text-rose-700 dark:text-rose-300",
@@ -0,0 +1,29 @@
1
+ <script setup lang="ts">
2
+ import { useI18n } from "@arronqzy/i18n/vue";
3
+ import type { NodeProps } from "@vue-flow/core";
4
+ import { resolveBlueprintNodeTypeLabel } from "../graph/document";
5
+ import { resolveBlueprintNodeSummary } from "../graph/node-summary";
6
+ import { useBlueprintNodeSelect } from "../BlueprintCanvasContext";
7
+ import { resolveBlueprintNodeExecutionTone } from "../runtime/execution-overlay";
8
+ import type { BlueprintFlowNodeData } from "../types";
9
+ import BlueprintNodeShell from "./BlueprintNodeShell.vue";
10
+
11
+ const { t } = useI18n();
12
+
13
+ const props = defineProps<NodeProps<BlueprintFlowNodeData>>();
14
+ const onSelect = useBlueprintNodeSelect();
15
+ const nodeData = props.data;
16
+ </script>
17
+
18
+ <template>
19
+ <BlueprintNodeShell
20
+ :node-id="props.id"
21
+ :label="nodeData.label"
22
+ :meta="resolveBlueprintNodeTypeLabel(nodeData, t)"
23
+ :subtitle="resolveBlueprintNodeSummary(nodeData, t)"
24
+ variant="storage"
25
+ :selected="Boolean(nodeData.isSelected)"
26
+ :execution-tone="resolveBlueprintNodeExecutionTone(nodeData)"
27
+ @select="onSelect"
28
+ />
29
+ </template>
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  FETCH_NODE_TYPE,
3
+ STORAGE_NODE_TYPE,
3
4
  type RunnableGraph,
4
5
  } from "@arronqzy/blueprint-dsl";
5
6
 
6
7
  import {
7
8
  resolveNodeFetchConfig,
9
+ resolveNodeStorageConfig,
8
10
  resolveRunnableNodeType,
9
11
  resolveViewElementIds,
10
12
  type BlueprintDocument,
@@ -34,6 +36,10 @@ export function documentToRunnableGraph(
34
36
  ? resolveNodeFetchConfig(node)
35
37
  : node.fetchConfig,
36
38
  jsonConfig: node.jsonConfig,
39
+ storageConfig:
40
+ nodeType === STORAGE_NODE_TYPE
41
+ ? resolveNodeStorageConfig(node)
42
+ : node.storageConfig,
37
43
  logicConfig: node.logicConfig,
38
44
  clockConfig: node.clockConfig,
39
45
  eventConfig: node.eventConfig,