@arronqzy/blueprint-dsl 1.0.5 → 1.0.6

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/blueprint-dsl",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Abuilder blueprint DSL runtime",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -37,6 +37,11 @@ export type FetchRequestConfig = {
37
37
  url: string;
38
38
  method?: FetchHttpMethod;
39
39
  headers?: Record<string, string>;
40
+ /**
41
+ * 请求头编辑器原文。可写 JSON,也可整段写成 `{scope?...}`。
42
+ * 运行时优先按该字段解析,避免未加引号的模板在 JSON.parse 时被丢掉。
43
+ */
44
+ headersJson?: string;
40
45
  body?: string;
41
46
  credentials?: RequestCredentials;
42
47
  mode?: RequestMode;
@@ -78,6 +83,7 @@ export function normalizeFetchRequestConfig(
78
83
  ...DEFAULT_FETCH_REQUEST_CONFIG.headers,
79
84
  ...config?.headers,
80
85
  },
86
+ headersJson: config?.headersJson,
81
87
  swaggerEndpoints: config?.swaggerEndpoints,
82
88
  };
83
89
  }
@@ -0,0 +1,295 @@
1
+ import type { FetchRequestConfig } from "./fetch-config.js";
2
+ import {
3
+ evaluateScopeExpression,
4
+ evaluateScopeTemplate,
5
+ hasScopeTemplate,
6
+ stringifyScopeValue,
7
+ } from "./scope-template.js";
8
+ import { isFalseSignal, isTrueSignal } from "./node-signal.js";
9
+
10
+ export type IncomingNodeSource = {
11
+ id: string;
12
+ label?: string;
13
+ name?: string;
14
+ nodeType?: string;
15
+ type?: string;
16
+ role?: string;
17
+ };
18
+
19
+ export type IncomingGraphEdge = {
20
+ source: string;
21
+ target: string;
22
+ sourceHandle?: string;
23
+ targetHandle?: string;
24
+ };
25
+
26
+ export type IncomingNodeScopeEntry = {
27
+ id: string;
28
+ name: string;
29
+ type: string;
30
+ role?: string;
31
+ port: string;
32
+ kind: "true" | "false" | "pending";
33
+ value?: unknown;
34
+ error?: string;
35
+ timestamp?: number;
36
+ isoTime?: string;
37
+ };
38
+
39
+ export type IncomingNodeScope =
40
+ | IncomingNodeScopeEntry
41
+ | Record<string, IncomingNodeScopeEntry>;
42
+
43
+ function readOutputSignal(output: unknown): Pick<
44
+ IncomingNodeScopeEntry,
45
+ "kind" | "value" | "error" | "timestamp" | "isoTime"
46
+ > {
47
+ if (output === undefined) return { kind: "pending" };
48
+ if (isTrueSignal(output)) {
49
+ return {
50
+ kind: "true",
51
+ value: output.value,
52
+ timestamp: output.timestamp,
53
+ isoTime: output.isoTime,
54
+ };
55
+ }
56
+ if (isFalseSignal(output)) {
57
+ return {
58
+ kind: "false",
59
+ error: output.error,
60
+ timestamp: output.timestamp,
61
+ isoTime: output.isoTime,
62
+ };
63
+ }
64
+ if (output && typeof output === "object" && "kind" in output) {
65
+ const rec = output as {
66
+ kind?: string;
67
+ value?: unknown;
68
+ error?: string;
69
+ timestamp?: number;
70
+ isoTime?: string;
71
+ };
72
+ if (rec.kind === "true") {
73
+ return {
74
+ kind: "true",
75
+ value: rec.value,
76
+ timestamp: rec.timestamp,
77
+ isoTime: rec.isoTime,
78
+ };
79
+ }
80
+ if (rec.kind === "false") {
81
+ return {
82
+ kind: "false",
83
+ error: rec.error,
84
+ timestamp: rec.timestamp,
85
+ isoTime: rec.isoTime,
86
+ };
87
+ }
88
+ }
89
+ return { kind: "true", value: output };
90
+ }
91
+
92
+ export function createIncomingNodeScopeEntry(
93
+ node: IncomingNodeSource | undefined,
94
+ port: string,
95
+ output: unknown
96
+ ): IncomingNodeScopeEntry {
97
+ const id = node?.id ?? "in";
98
+ const signal = readOutputSignal(output);
99
+ return {
100
+ id,
101
+ name: node?.label?.trim() || node?.name?.trim() || id,
102
+ type: node?.nodeType || node?.type || "unknown",
103
+ role: node?.role,
104
+ port,
105
+ ...signal,
106
+ };
107
+ }
108
+
109
+ export function buildIncomingNodeScope(
110
+ entries: IncomingNodeScopeEntry[]
111
+ ): IncomingNodeScope | undefined {
112
+ if (entries.length === 0) return undefined;
113
+ if (entries.length === 1) return entries[0];
114
+ const scoped: Record<string, IncomingNodeScopeEntry> = {};
115
+ for (const entry of entries) {
116
+ scoped[entry.id] = entry;
117
+ }
118
+ return scoped;
119
+ }
120
+
121
+ export function collectIncomingNodeScopeEntries(options: {
122
+ targetNodeId: string;
123
+ targetPort?: string;
124
+ nodes: IncomingNodeSource[];
125
+ edges: IncomingGraphEdge[];
126
+ getOutput?: (nodeId: string, port: string) => unknown;
127
+ }): IncomingNodeScopeEntry[] {
128
+ const targetPort = options.targetPort ?? "in";
129
+ const nodeById = new Map(options.nodes.map((node) => [node.id, node]));
130
+ const inbound = options.edges.filter(
131
+ (edge) =>
132
+ edge.target === options.targetNodeId &&
133
+ (edge.targetHandle ?? "in") === targetPort
134
+ );
135
+
136
+ const seen = new Set<string>();
137
+ const entries: IncomingNodeScopeEntry[] = [];
138
+ for (const edge of inbound) {
139
+ const port = edge.sourceHandle ?? "out";
140
+ const key = `${edge.source}:${port}`;
141
+ if (seen.has(key)) continue;
142
+ seen.add(key);
143
+ const output = options.getOutput?.(edge.source, port);
144
+ entries.push(
145
+ createIncomingNodeScopeEntry(nodeById.get(edge.source), port, output)
146
+ );
147
+ }
148
+ return entries;
149
+ }
150
+
151
+ export function resolveFetchIncomingScope(options: {
152
+ fetchNodeId: string;
153
+ inputPort?: string;
154
+ nodes: IncomingNodeSource[];
155
+ edges: IncomingGraphEdge[];
156
+ getOutput?: (nodeId: string, port: string) => unknown;
157
+ fallbackInput?: unknown;
158
+ }): IncomingNodeScope | undefined {
159
+ const entries = collectIncomingNodeScopeEntries({
160
+ targetNodeId: options.fetchNodeId,
161
+ targetPort: options.inputPort,
162
+ nodes: options.nodes,
163
+ edges: options.edges,
164
+ getOutput: options.getOutput,
165
+ });
166
+ if (entries.length > 0) return buildIncomingNodeScope(entries);
167
+ if (options.fallbackInput === undefined) return undefined;
168
+ return createIncomingNodeScopeEntry(
169
+ { id: "in", label: "in", nodeType: "input" },
170
+ options.inputPort ?? "in",
171
+ options.fallbackInput
172
+ );
173
+ }
174
+
175
+ export function latestTraceOutputsByNode(
176
+ entries: Array<{ nodeId: string; outputs?: Record<string, unknown> }>
177
+ ): Record<string, Record<string, unknown>> {
178
+ const map: Record<string, Record<string, unknown>> = {};
179
+ for (const entry of entries) {
180
+ map[entry.nodeId] = entry.outputs ?? {};
181
+ }
182
+ return map;
183
+ }
184
+
185
+ function resolveTemplateString(
186
+ value: string | undefined,
187
+ scope: unknown
188
+ ): string | undefined {
189
+ if (value === undefined) return undefined;
190
+ if (!hasScopeTemplate(value)) return value;
191
+ return evaluateScopeTemplate(value, scope);
192
+ }
193
+
194
+ /** `{scope?...}` 整段表达式,区别于 JSON 对象 `{ "Authorization": "..." }` */
195
+ export function isWholeScopeExpression(text: string): boolean {
196
+ const trimmed = text.trim();
197
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false;
198
+ return trimmed.slice(1, -1).trim().startsWith("scope");
199
+ }
200
+
201
+ function asHeaderRecord(value: unknown): Record<string, string> | undefined {
202
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
203
+ const headers: Record<string, string> = {};
204
+ for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
205
+ if (raw === undefined || raw === null) continue;
206
+ headers[key] = typeof raw === "string" ? raw : stringifyScopeValue(raw);
207
+ }
208
+ return headers;
209
+ }
210
+
211
+ function applyScopeToHeaderRecord(
212
+ headers: Record<string, string>,
213
+ scope: unknown
214
+ ): Record<string, string> {
215
+ if (scope === undefined) return headers;
216
+ return Object.fromEntries(
217
+ Object.entries(headers).map(([key, value]) => [
218
+ evaluateScopeTemplate(String(key), scope),
219
+ evaluateScopeTemplate(value == null ? "" : String(value), scope),
220
+ ])
221
+ );
222
+ }
223
+
224
+ /** 将请求头编辑器原文解析为对象;支持 JSON 内模板,也支持整段 `{scope?...}` */
225
+ export function parseFetchHeadersJson(
226
+ text: string,
227
+ scope?: unknown
228
+ ): Record<string, string> | undefined {
229
+ const trimmed = text.trim();
230
+ if (!trimmed) return {};
231
+ if (isWholeScopeExpression(trimmed)) {
232
+ if (scope === undefined) return undefined;
233
+ const inner = trimmed.slice(1, -1);
234
+ return asHeaderRecord(evaluateScopeExpression(inner, scope));
235
+ }
236
+ try {
237
+ const parsed = asHeaderRecord(JSON.parse(trimmed));
238
+ if (!parsed) return undefined;
239
+ return applyScopeToHeaderRecord(parsed, scope);
240
+ } catch {
241
+ return undefined;
242
+ }
243
+ }
244
+
245
+ export function draftFetchHeadersText(config: FetchRequestConfig): string {
246
+ if (config.headersJson !== undefined) return config.headersJson;
247
+ try {
248
+ return JSON.stringify(config.headers ?? {}, null, 2);
249
+ } catch {
250
+ return "{}";
251
+ }
252
+ }
253
+
254
+ export function resolveFetchHeaders(
255
+ config: FetchRequestConfig,
256
+ scope?: unknown
257
+ ): Record<string, string> | undefined {
258
+ const rawJson = config.headersJson;
259
+ if (rawJson !== undefined && rawJson.trim() !== "") {
260
+ const parsed = parseFetchHeadersJson(rawJson, scope);
261
+ if (parsed) return parsed;
262
+ }
263
+ if (!config.headers) return config.headers;
264
+ return applyScopeToHeaderRecord(config.headers, scope);
265
+ }
266
+
267
+ /** 用上游 scope 解析 fetch 表单中的 URL / Base URL / headers / body */
268
+ export function applyFetchConfigScope(
269
+ config: FetchRequestConfig,
270
+ scope: unknown
271
+ ): FetchRequestConfig {
272
+ if (scope === undefined) {
273
+ const headers = resolveFetchHeaders(config);
274
+ return headers === config.headers ? config : { ...config, headers };
275
+ }
276
+ const headers = resolveFetchHeaders(config, scope);
277
+ return {
278
+ ...config,
279
+ url: resolveTemplateString(config.url, scope) ?? "",
280
+ apiBaseUrl: resolveTemplateString(config.apiBaseUrl, scope),
281
+ body: resolveTemplateString(config.body, scope),
282
+ headers,
283
+ };
284
+ }
285
+
286
+ export function fetchConfigHasScopeTemplate(config: FetchRequestConfig): boolean {
287
+ if (hasScopeTemplate(config.url ?? "")) return true;
288
+ if (hasScopeTemplate(config.apiBaseUrl ?? "")) return true;
289
+ if (hasScopeTemplate(config.body ?? "")) return true;
290
+ if (hasScopeTemplate(config.headersJson ?? "")) return true;
291
+ const headers = config.headers ?? {};
292
+ return Object.entries(headers).some(
293
+ ([key, value]) => hasScopeTemplate(key) || hasScopeTemplate(String(value ?? ""))
294
+ );
295
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,8 @@ export * from "./core/behavior-registry.js";
7
7
  export * from "./core/executor.js";
8
8
  export * from "./behaviors/default.js";
9
9
  export * from "./fetch-config.js";
10
+ export * from "./scope-template.js";
11
+ export * from "./incoming-node-scope.js";
10
12
  export * from "./json-config.js";
11
13
  export * from "./logic-config.js";
12
14
  export * from "./clock-config.js";
@@ -4,6 +4,7 @@ import {
4
4
  normalizeFetchRequestConfig,
5
5
  resolveFetchRequestUrl,
6
6
  } from "../fetch-config.js";
7
+ import { applyFetchConfigScope, resolveFetchIncomingScope } from "../incoming-node-scope.js";
7
8
  import type { JsonNodeConfig } from "../json-config.js";
8
9
  import {
9
10
  DEFAULT_JSON_NODE_CONFIG,
@@ -78,6 +79,8 @@ import {
78
79
  export type RunnableGraphNode = {
79
80
  id: string;
80
81
  nodeType: string;
82
+ label?: string;
83
+ role?: string;
81
84
  lifecyclePhase?: PageLifecyclePhase;
82
85
  /** 蓝图库记录 id,Blueprint 节点引用嵌套蓝图时使用 */
83
86
  libraryBlueprintId?: string;
@@ -1076,7 +1079,15 @@ export class BlueprintGraphRunner {
1076
1079
  return;
1077
1080
  }
1078
1081
 
1079
- const config = normalizeFetchRequestConfig(node.fetchConfig);
1082
+ const incomingScope = runner.buildFetchIncomingScope(
1083
+ token.nodeId,
1084
+ inputPort,
1085
+ input
1086
+ );
1087
+ const config = applyFetchConfigScope(
1088
+ normalizeFetchRequestConfig(node.fetchConfig),
1089
+ incomingScope
1090
+ );
1080
1091
  resolveFetchRequestUrl(config);
1081
1092
 
1082
1093
  const result = await executeFetch(config, {
@@ -1409,6 +1420,27 @@ export class BlueprintGraphRunner {
1409
1420
  );
1410
1421
  }
1411
1422
 
1423
+ private buildFetchIncomingScope(
1424
+ nodeId: string,
1425
+ port: string,
1426
+ fallbackInput: Value | undefined
1427
+ ) {
1428
+ return resolveFetchIncomingScope({
1429
+ fetchNodeId: nodeId,
1430
+ inputPort: port,
1431
+ nodes: this.graph.nodes.map((node) => ({
1432
+ id: node.id,
1433
+ label: node.label,
1434
+ nodeType: node.nodeType,
1435
+ role: node.role,
1436
+ })),
1437
+ edges: this.graph.edges,
1438
+ getOutput: (sourceId, sourcePort) =>
1439
+ this.getNodeOutput(sourceId, sourcePort),
1440
+ fallbackInput,
1441
+ });
1442
+ }
1443
+
1412
1444
  /** 单端口多连线:任一为真则视为真(或语义);支持 triggerNode 注入的入口信号 */
1413
1445
  private resolvePortInputValue(nodeId: string, port: string): Value | undefined {
1414
1446
  const injected = this.injectedInputs.get(`${nodeId}:${port}`);
@@ -0,0 +1,40 @@
1
+ const SCOPE_TEMPLATE_RE = /\{[^}]+\}/;
2
+
3
+ export function hasScopeTemplate(value: string): boolean {
4
+ return SCOPE_TEMPLATE_RE.test(value);
5
+ }
6
+
7
+ export function evaluateScopeExpression(
8
+ expression: string,
9
+ scope: unknown
10
+ ): unknown {
11
+ const trimmed = expression.trim();
12
+ if (!trimmed) return undefined;
13
+ try {
14
+ const fn = new Function("scope", `"use strict"; return (${trimmed});`);
15
+ return fn(scope);
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
20
+
21
+ export function stringifyScopeValue(value: unknown): string {
22
+ if (value === null || value === undefined) return "";
23
+ if (typeof value === "string") return value;
24
+ if (typeof value === "number" || typeof value === "boolean") {
25
+ return String(value);
26
+ }
27
+ try {
28
+ return JSON.stringify(value);
29
+ } catch {
30
+ return String(value);
31
+ }
32
+ }
33
+
34
+ /** 将 `{scope?.a}` 替换为求值结果;无模板时原样返回 */
35
+ export function evaluateScopeTemplate(template: string, scope: unknown): string {
36
+ if (!hasScopeTemplate(template)) return template;
37
+ return template.replace(/\{([^}]+)\}/g, (_match, rawExpr: string) =>
38
+ stringifyScopeValue(evaluateScopeExpression(rawExpr, scope))
39
+ );
40
+ }