@arronqzy/blueprint-dsl 1.0.7 → 1.0.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/dist/index.cjs +2711 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +739 -0
- package/dist/index.d.ts +739 -0
- package/dist/index.js +2585 -0
- package/dist/index.js.map +1 -0
- package/package.json +16 -7
- package/src/behaviors/default.ts +0 -64
- package/src/blueprint-signal.ts +0 -14
- package/src/clock-config.ts +0 -232
- package/src/core/behavior-registry.ts +0 -32
- package/src/core/behavior.ts +0 -13
- package/src/core/executor.ts +0 -51
- package/src/core/scheduler-loop.ts +0 -13
- package/src/event-config.ts +0 -211
- package/src/fetch-config.ts +0 -281
- package/src/incoming-node-scope.ts +0 -411
- package/src/index.ts +0 -33
- package/src/json-config.ts +0 -37
- package/src/lifecycle.ts +0 -45
- package/src/logic-config.ts +0 -53
- package/src/node-signal.ts +0 -83
- package/src/nodes/definitions.ts +0 -105
- package/src/runtime/blueprint-cycle.ts +0 -136
- package/src/runtime/clock-active-registry.ts +0 -30
- package/src/runtime/clock-scheduler.ts +0 -49
- package/src/runtime/execution-trace.ts +0 -55
- package/src/runtime/graph-runner.ts +0 -1523
- package/src/scope-autocomplete.ts +0 -114
- package/src/scope-template.ts +0 -93
- package/src/swagger.ts +0 -192
- package/src/type.ts +0 -34
package/src/fetch-config.ts
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import type { SwaggerApiEndpoint } from "./swagger.js";
|
|
2
|
-
import { looksLikeJsonText } from "./scope-template.js";
|
|
3
|
-
|
|
4
|
-
export const FETCH_HTTP_METHODS = [
|
|
5
|
-
"GET",
|
|
6
|
-
"POST",
|
|
7
|
-
"PUT",
|
|
8
|
-
"PATCH",
|
|
9
|
-
"DELETE",
|
|
10
|
-
"HEAD",
|
|
11
|
-
"OPTIONS",
|
|
12
|
-
] as const;
|
|
13
|
-
|
|
14
|
-
export type FetchHttpMethod = (typeof FETCH_HTTP_METHODS)[number];
|
|
15
|
-
|
|
16
|
-
export const FETCH_RESPONSE_TYPES = ["json", "text"] as const;
|
|
17
|
-
|
|
18
|
-
export type FetchResponseType = (typeof FETCH_RESPONSE_TYPES)[number];
|
|
19
|
-
|
|
20
|
-
export const FETCH_CREDENTIALS = ["omit", "same-origin", "include"] as const;
|
|
21
|
-
|
|
22
|
-
export const FETCH_MODES = ["cors", "no-cors", "same-origin", "navigate"] as const;
|
|
23
|
-
|
|
24
|
-
export const FETCH_CACHES = [
|
|
25
|
-
"default",
|
|
26
|
-
"no-store",
|
|
27
|
-
"reload",
|
|
28
|
-
"no-cache",
|
|
29
|
-
"force-cache",
|
|
30
|
-
"only-if-cached",
|
|
31
|
-
] as const;
|
|
32
|
-
|
|
33
|
-
export const FETCH_REDIRECTS = ["follow", "error", "manual"] as const;
|
|
34
|
-
|
|
35
|
-
export type FetchUrlInputMode = "swagger" | "manual";
|
|
36
|
-
|
|
37
|
-
export type FetchRequestConfig = {
|
|
38
|
-
url: string;
|
|
39
|
-
method?: FetchHttpMethod;
|
|
40
|
-
headers?: Record<string, string>;
|
|
41
|
-
/**
|
|
42
|
-
* 请求头编辑器原文。可写 JSON,也可整段写成 `{scope?...}`。
|
|
43
|
-
* 运行时优先按该字段解析,避免未加引号的模板在 JSON.parse 时被丢掉。
|
|
44
|
-
*/
|
|
45
|
-
headersJson?: string;
|
|
46
|
-
body?: string;
|
|
47
|
-
credentials?: RequestCredentials;
|
|
48
|
-
mode?: RequestMode;
|
|
49
|
-
cache?: RequestCache;
|
|
50
|
-
redirect?: RequestRedirect;
|
|
51
|
-
responseType?: FetchResponseType;
|
|
52
|
-
timeoutMs?: number;
|
|
53
|
-
/** Swagger / OpenAPI 文档 JSON 地址 */
|
|
54
|
-
swaggerDocsUrl?: string;
|
|
55
|
-
/** API 主机或基础路径,可与接口 path 组合 */
|
|
56
|
-
apiBaseUrl?: string;
|
|
57
|
-
/** 从 Swagger 文档解析出的接口列表 */
|
|
58
|
-
swaggerEndpoints?: SwaggerApiEndpoint[];
|
|
59
|
-
/** 请求 URL 输入方式:接口联想或手动输入 */
|
|
60
|
-
urlInputMode?: FetchUrlInputMode;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export const DEFAULT_FETCH_REQUEST_CONFIG: FetchRequestConfig = {
|
|
64
|
-
url: "",
|
|
65
|
-
method: "GET",
|
|
66
|
-
headers: {},
|
|
67
|
-
body: "",
|
|
68
|
-
credentials: "same-origin",
|
|
69
|
-
mode: "cors",
|
|
70
|
-
cache: "default",
|
|
71
|
-
redirect: "follow",
|
|
72
|
-
responseType: "json",
|
|
73
|
-
timeoutMs: 30000,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/** 合并默认项与节点上保存的 fetch 配置,供运行时发起请求 */
|
|
77
|
-
export function normalizeFetchRequestConfig(
|
|
78
|
-
config?: FetchRequestConfig
|
|
79
|
-
): FetchRequestConfig {
|
|
80
|
-
return {
|
|
81
|
-
...DEFAULT_FETCH_REQUEST_CONFIG,
|
|
82
|
-
...config,
|
|
83
|
-
headers: {
|
|
84
|
-
...DEFAULT_FETCH_REQUEST_CONFIG.headers,
|
|
85
|
-
...config?.headers,
|
|
86
|
-
},
|
|
87
|
-
headersJson: config?.headersJson,
|
|
88
|
-
swaggerEndpoints: config?.swaggerEndpoints,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export type FetchResultValue = {
|
|
93
|
-
status: number;
|
|
94
|
-
statusText: string;
|
|
95
|
-
url: string;
|
|
96
|
-
data: unknown;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
function joinFetchUrl(base: string, path: string): string {
|
|
100
|
-
const normalizedBase = base.replace(/\/+$/, "");
|
|
101
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
102
|
-
return `${normalizedBase}${normalizedPath}`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function getUrlAuthorityPart(url: string): string | null {
|
|
106
|
-
const trimmed = url.trim();
|
|
107
|
-
if (!trimmed || trimmed.startsWith("/")) return null;
|
|
108
|
-
const slashIndex = trimmed.indexOf("/");
|
|
109
|
-
return slashIndex === -1 ? trimmed : trimmed.slice(0, slashIndex);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function isAbsoluteHostAuthority(authority: string): boolean {
|
|
113
|
-
if (/^localhost$/i.test(authority)) return true;
|
|
114
|
-
if (/^localhost:\d+$/i.test(authority)) return true;
|
|
115
|
-
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(authority)) return true;
|
|
116
|
-
if (/^\d{1,3}(?:\.\d{1,3}){3}:\d+$/.test(authority)) return true;
|
|
117
|
-
if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(authority) && authority.includes(".")) {
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
if (/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\.[a-z]{2,}(?::\d+)?$/i.test(authority)) {
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
if (/^[a-z0-9-]+:\d+$/i.test(authority)) return true;
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function hasExplicitUrlScheme(url: string): boolean {
|
|
128
|
-
return /^[a-z][a-z0-9+.-]*:\/\//i.test(url) || url.startsWith("//");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** 解析 fetch 节点的最终请求 URL,避免对已有主机信息的地址重复拼接 apiBaseUrl */
|
|
132
|
-
export function resolveFetchRequestUrl(
|
|
133
|
-
config: Pick<FetchRequestConfig, "url" | "apiBaseUrl">
|
|
134
|
-
): string {
|
|
135
|
-
const rawUrl = config.url?.trim() ?? "";
|
|
136
|
-
if (!rawUrl) {
|
|
137
|
-
throw new Error("请求 URL 未配置");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (hasExplicitUrlScheme(rawUrl)) {
|
|
141
|
-
return rawUrl;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const authority = getUrlAuthorityPart(rawUrl);
|
|
145
|
-
if (authority && isAbsoluteHostAuthority(authority)) {
|
|
146
|
-
return `http://${rawUrl}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const apiBaseUrl = config.apiBaseUrl?.trim();
|
|
150
|
-
if (!apiBaseUrl) {
|
|
151
|
-
return rawUrl;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return joinFetchUrl(apiBaseUrl, rawUrl);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export type ExecuteFetchOptions = {
|
|
158
|
-
signal?: AbortSignal;
|
|
159
|
-
/** 调试模式下 HTTP 非 2xx 仍返回响应体,不抛错 */
|
|
160
|
-
allowHttpError?: boolean;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
function linkAbortSignal(
|
|
164
|
-
controller: AbortController,
|
|
165
|
-
external?: AbortSignal
|
|
166
|
-
): () => void {
|
|
167
|
-
if (!external) return () => {};
|
|
168
|
-
if (external.aborted) {
|
|
169
|
-
controller.abort();
|
|
170
|
-
return () => {};
|
|
171
|
-
}
|
|
172
|
-
const onAbort = () => controller.abort();
|
|
173
|
-
external.addEventListener("abort", onAbort);
|
|
174
|
-
return () => external.removeEventListener("abort", onAbort);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export async function executeFetch(
|
|
178
|
-
config: FetchRequestConfig,
|
|
179
|
-
options?: ExecuteFetchOptions
|
|
180
|
-
): Promise<FetchResultValue> {
|
|
181
|
-
const rawUrl = config.url?.trim();
|
|
182
|
-
if (!rawUrl) {
|
|
183
|
-
throw new Error("请求 URL 未配置");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const url = resolveFetchRequestUrl(config);
|
|
187
|
-
|
|
188
|
-
const method = config.method ?? "GET";
|
|
189
|
-
const hasBody = method !== "GET" && method !== "HEAD";
|
|
190
|
-
const body =
|
|
191
|
-
hasBody && config.body?.trim() ? config.body : undefined;
|
|
192
|
-
|
|
193
|
-
if (body && looksLikeJsonText(body)) {
|
|
194
|
-
try {
|
|
195
|
-
JSON.parse(body);
|
|
196
|
-
} catch (error) {
|
|
197
|
-
throw new Error(
|
|
198
|
-
`请求体不是有效 JSON:${error instanceof Error ? error.message : "格式无效"}`
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const headers: Record<string, string> = { ...(config.headers ?? {}) };
|
|
204
|
-
const hasContentType = Object.keys(headers).some(
|
|
205
|
-
(key) => key.toLowerCase() === "content-type"
|
|
206
|
-
);
|
|
207
|
-
if (body && looksLikeJsonText(body) && !hasContentType) {
|
|
208
|
-
headers["Content-Type"] = "application/json";
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const timeoutMs = config.timeoutMs ?? 30000;
|
|
212
|
-
const controller = new AbortController();
|
|
213
|
-
const unlinkExternal = linkAbortSignal(controller, options?.signal);
|
|
214
|
-
const timer =
|
|
215
|
-
timeoutMs > 0
|
|
216
|
-
? setTimeout(() => controller.abort(), timeoutMs)
|
|
217
|
-
: undefined;
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
const response = await fetch(url, {
|
|
221
|
-
method,
|
|
222
|
-
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
223
|
-
body,
|
|
224
|
-
credentials: config.credentials,
|
|
225
|
-
mode: config.mode,
|
|
226
|
-
cache: config.cache,
|
|
227
|
-
redirect: config.redirect,
|
|
228
|
-
signal: controller.signal,
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
const responseType = config.responseType ?? "json";
|
|
232
|
-
let data: unknown;
|
|
233
|
-
|
|
234
|
-
if (responseType === "text") {
|
|
235
|
-
data = await response.text();
|
|
236
|
-
} else {
|
|
237
|
-
const text = await response.text();
|
|
238
|
-
if (!text) {
|
|
239
|
-
data = null;
|
|
240
|
-
} else {
|
|
241
|
-
try {
|
|
242
|
-
data = JSON.parse(text) as unknown;
|
|
243
|
-
} catch {
|
|
244
|
-
if (options?.allowHttpError) {
|
|
245
|
-
data = text;
|
|
246
|
-
} else {
|
|
247
|
-
throw new Error("响应不是有效的 JSON");
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (!response.ok && !options?.allowHttpError) {
|
|
254
|
-
const detail =
|
|
255
|
-
typeof data === "string"
|
|
256
|
-
? data.slice(0, 200)
|
|
257
|
-
: JSON.stringify(data).slice(0, 200);
|
|
258
|
-
throw new Error(
|
|
259
|
-
`HTTP ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""}`
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
status: response.status,
|
|
265
|
-
statusText: response.statusText,
|
|
266
|
-
url: response.url,
|
|
267
|
-
data,
|
|
268
|
-
};
|
|
269
|
-
} catch (error) {
|
|
270
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
271
|
-
if (options?.signal?.aborted) {
|
|
272
|
-
throw new Error("请求已中止");
|
|
273
|
-
}
|
|
274
|
-
throw new Error(`请求超时(${timeoutMs}ms)`);
|
|
275
|
-
}
|
|
276
|
-
throw error;
|
|
277
|
-
} finally {
|
|
278
|
-
unlinkExternal();
|
|
279
|
-
if (timer) clearTimeout(timer);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
import type { FetchRequestConfig } from "./fetch-config.js";
|
|
2
|
-
import {
|
|
3
|
-
evaluateScopeExpression,
|
|
4
|
-
evaluateScopeTemplate,
|
|
5
|
-
evaluateScopeTemplateInJson,
|
|
6
|
-
hasScopeTemplate,
|
|
7
|
-
looksLikeJsonText,
|
|
8
|
-
stringifyScopeValue,
|
|
9
|
-
} from "./scope-template.js";
|
|
10
|
-
import { isFalseSignal, isTrueSignal } from "./node-signal.js";
|
|
11
|
-
|
|
12
|
-
export type IncomingNodeSource = {
|
|
13
|
-
id: string;
|
|
14
|
-
label?: string;
|
|
15
|
-
name?: string;
|
|
16
|
-
nodeType?: string;
|
|
17
|
-
type?: string;
|
|
18
|
-
role?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type IncomingGraphEdge = {
|
|
22
|
-
source: string;
|
|
23
|
-
target: string;
|
|
24
|
-
sourceHandle?: string;
|
|
25
|
-
targetHandle?: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type IncomingNodeScopeEntry = {
|
|
29
|
-
id: string;
|
|
30
|
-
name: string;
|
|
31
|
-
type: string;
|
|
32
|
-
role?: string;
|
|
33
|
-
port: string;
|
|
34
|
-
kind: "true" | "false" | "pending";
|
|
35
|
-
value?: unknown;
|
|
36
|
-
error?: string;
|
|
37
|
-
timestamp?: number;
|
|
38
|
-
isoTime?: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type IncomingNodeScope =
|
|
42
|
-
| IncomingNodeScopeEntry
|
|
43
|
-
| Record<string, IncomingNodeScopeEntry>;
|
|
44
|
-
|
|
45
|
-
/** 尚无上游数据时,仍可联想 scope 的固定字段 */
|
|
46
|
-
export const FETCH_SCOPE_AUTOCOMPLETE_FALLBACK: IncomingNodeScopeEntry = {
|
|
47
|
-
id: "",
|
|
48
|
-
name: "",
|
|
49
|
-
type: "",
|
|
50
|
-
role: "",
|
|
51
|
-
port: "out",
|
|
52
|
-
kind: "pending",
|
|
53
|
-
value: {},
|
|
54
|
-
error: "",
|
|
55
|
-
timestamp: 0,
|
|
56
|
-
isoTime: "",
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export function resolveFetchScopeAutocompleteRoot(scope: unknown): unknown {
|
|
60
|
-
return scope === undefined ? FETCH_SCOPE_AUTOCOMPLETE_FALLBACK : scope;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function readOutputSignal(output: unknown): Pick<
|
|
64
|
-
IncomingNodeScopeEntry,
|
|
65
|
-
"kind" | "value" | "error" | "timestamp" | "isoTime"
|
|
66
|
-
> {
|
|
67
|
-
if (output === undefined) return { kind: "pending" };
|
|
68
|
-
if (isTrueSignal(output)) {
|
|
69
|
-
return {
|
|
70
|
-
kind: "true",
|
|
71
|
-
value: output.value,
|
|
72
|
-
timestamp: output.timestamp,
|
|
73
|
-
isoTime: output.isoTime,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
if (isFalseSignal(output)) {
|
|
77
|
-
return {
|
|
78
|
-
kind: "false",
|
|
79
|
-
error: output.error,
|
|
80
|
-
timestamp: output.timestamp,
|
|
81
|
-
isoTime: output.isoTime,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
if (output && typeof output === "object" && "kind" in output) {
|
|
85
|
-
const rec = output as {
|
|
86
|
-
kind?: string;
|
|
87
|
-
value?: unknown;
|
|
88
|
-
error?: string;
|
|
89
|
-
timestamp?: number;
|
|
90
|
-
isoTime?: string;
|
|
91
|
-
};
|
|
92
|
-
if (rec.kind === "true") {
|
|
93
|
-
return {
|
|
94
|
-
kind: "true",
|
|
95
|
-
value: rec.value,
|
|
96
|
-
timestamp: rec.timestamp,
|
|
97
|
-
isoTime: rec.isoTime,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
if (rec.kind === "false") {
|
|
101
|
-
return {
|
|
102
|
-
kind: "false",
|
|
103
|
-
error: rec.error,
|
|
104
|
-
timestamp: rec.timestamp,
|
|
105
|
-
isoTime: rec.isoTime,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return { kind: "true", value: output };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function createIncomingNodeScopeEntry(
|
|
113
|
-
node: IncomingNodeSource | undefined,
|
|
114
|
-
port: string,
|
|
115
|
-
output: unknown
|
|
116
|
-
): IncomingNodeScopeEntry {
|
|
117
|
-
const id = node?.id ?? "in";
|
|
118
|
-
const signal = readOutputSignal(output);
|
|
119
|
-
return {
|
|
120
|
-
id,
|
|
121
|
-
name: node?.label?.trim() || node?.name?.trim() || id,
|
|
122
|
-
type: node?.nodeType || node?.type || "unknown",
|
|
123
|
-
role: node?.role,
|
|
124
|
-
port,
|
|
125
|
-
...signal,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function buildIncomingNodeScope(
|
|
130
|
-
entries: IncomingNodeScopeEntry[]
|
|
131
|
-
): IncomingNodeScope | undefined {
|
|
132
|
-
if (entries.length === 0) return undefined;
|
|
133
|
-
if (entries.length === 1) return entries[0];
|
|
134
|
-
const scoped: Record<string, IncomingNodeScopeEntry> = {};
|
|
135
|
-
for (const entry of entries) {
|
|
136
|
-
scoped[entry.id] = entry;
|
|
137
|
-
}
|
|
138
|
-
return scoped;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function collectIncomingNodeScopeEntries(options: {
|
|
142
|
-
targetNodeId: string;
|
|
143
|
-
targetPort?: string;
|
|
144
|
-
nodes: IncomingNodeSource[];
|
|
145
|
-
edges: IncomingGraphEdge[];
|
|
146
|
-
getOutput?: (nodeId: string, port: string) => unknown;
|
|
147
|
-
}): IncomingNodeScopeEntry[] {
|
|
148
|
-
const targetPort = options.targetPort ?? "in";
|
|
149
|
-
const nodeById = new Map(options.nodes.map((node) => [node.id, node]));
|
|
150
|
-
const inbound = options.edges.filter(
|
|
151
|
-
(edge) =>
|
|
152
|
-
edge.target === options.targetNodeId &&
|
|
153
|
-
(edge.targetHandle ?? "in") === targetPort
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const seen = new Set<string>();
|
|
157
|
-
const entries: IncomingNodeScopeEntry[] = [];
|
|
158
|
-
for (const edge of inbound) {
|
|
159
|
-
const port = edge.sourceHandle ?? "out";
|
|
160
|
-
const key = `${edge.source}:${port}`;
|
|
161
|
-
if (seen.has(key)) continue;
|
|
162
|
-
seen.add(key);
|
|
163
|
-
const output = options.getOutput?.(edge.source, port);
|
|
164
|
-
entries.push(
|
|
165
|
-
createIncomingNodeScopeEntry(nodeById.get(edge.source), port, output)
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
return entries;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function resolveFetchIncomingScope(options: {
|
|
172
|
-
fetchNodeId: string;
|
|
173
|
-
inputPort?: string;
|
|
174
|
-
nodes: IncomingNodeSource[];
|
|
175
|
-
edges: IncomingGraphEdge[];
|
|
176
|
-
getOutput?: (nodeId: string, port: string) => unknown;
|
|
177
|
-
fallbackInput?: unknown;
|
|
178
|
-
}): IncomingNodeScope | undefined {
|
|
179
|
-
const entries = collectIncomingNodeScopeEntries({
|
|
180
|
-
targetNodeId: options.fetchNodeId,
|
|
181
|
-
targetPort: options.inputPort,
|
|
182
|
-
nodes: options.nodes,
|
|
183
|
-
edges: options.edges,
|
|
184
|
-
getOutput: options.getOutput,
|
|
185
|
-
});
|
|
186
|
-
if (entries.length > 0) return buildIncomingNodeScope(entries);
|
|
187
|
-
if (options.fallbackInput === undefined) return undefined;
|
|
188
|
-
return createIncomingNodeScopeEntry(
|
|
189
|
-
{ id: "in", label: "in", nodeType: "input" },
|
|
190
|
-
options.inputPort ?? "in",
|
|
191
|
-
options.fallbackInput
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function latestTraceOutputsByNode(
|
|
196
|
-
entries: Array<{ nodeId: string; outputs?: Record<string, unknown> }>
|
|
197
|
-
): Record<string, Record<string, unknown>> {
|
|
198
|
-
const map: Record<string, Record<string, unknown>> = {};
|
|
199
|
-
for (const entry of entries) {
|
|
200
|
-
map[entry.nodeId] = entry.outputs ?? {};
|
|
201
|
-
}
|
|
202
|
-
return map;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function resolveTemplateString(
|
|
206
|
-
value: string | undefined,
|
|
207
|
-
scope: unknown
|
|
208
|
-
): string | undefined {
|
|
209
|
-
if (value === undefined) return undefined;
|
|
210
|
-
if (!hasScopeTemplate(value)) return value;
|
|
211
|
-
return evaluateScopeTemplate(value, scope);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function parseJsonText(
|
|
215
|
-
text: string
|
|
216
|
-
): { ok: true; value: unknown } | { ok: false; error: string } {
|
|
217
|
-
try {
|
|
218
|
-
return { ok: true, value: JSON.parse(text) as unknown };
|
|
219
|
-
} catch (error) {
|
|
220
|
-
return {
|
|
221
|
-
ok: false,
|
|
222
|
-
error: error instanceof Error ? error.message : "JSON 格式无效",
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function validateFetchJsonText(
|
|
228
|
-
text: string,
|
|
229
|
-
options?: { allowEmpty?: boolean }
|
|
230
|
-
): { ok: true } | { ok: false; error: string } {
|
|
231
|
-
const trimmed = text.trim();
|
|
232
|
-
if (!trimmed) {
|
|
233
|
-
if (options?.allowEmpty === false) {
|
|
234
|
-
return { ok: false, error: "内容不能为空" };
|
|
235
|
-
}
|
|
236
|
-
return { ok: true };
|
|
237
|
-
}
|
|
238
|
-
if (isWholeScopeExpression(trimmed)) {
|
|
239
|
-
return { ok: true };
|
|
240
|
-
}
|
|
241
|
-
if (!looksLikeJsonText(trimmed)) {
|
|
242
|
-
return { ok: true };
|
|
243
|
-
}
|
|
244
|
-
const parsed = parseJsonText(trimmed);
|
|
245
|
-
return parsed.ok ? { ok: true } : parsed;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function resolveFetchBody(
|
|
249
|
-
body: string | undefined,
|
|
250
|
-
scope?: unknown
|
|
251
|
-
): string | undefined {
|
|
252
|
-
if (body === undefined) return undefined;
|
|
253
|
-
const trimmed = body.trim();
|
|
254
|
-
if (!trimmed) return body;
|
|
255
|
-
if (scope !== undefined && isWholeScopeExpression(trimmed)) {
|
|
256
|
-
const value = evaluateScopeExpression(trimmed.slice(1, -1), scope);
|
|
257
|
-
if (value === undefined) return "";
|
|
258
|
-
if (typeof value === "string") return value;
|
|
259
|
-
return stringifyScopeValue(value);
|
|
260
|
-
}
|
|
261
|
-
if (scope === undefined || !hasScopeTemplate(body)) return body;
|
|
262
|
-
if (looksLikeJsonText(trimmed)) {
|
|
263
|
-
return evaluateScopeTemplateInJson(body, scope);
|
|
264
|
-
}
|
|
265
|
-
return evaluateScopeTemplate(body, scope);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function getFetchBodyValidationError(
|
|
269
|
-
body: string | undefined,
|
|
270
|
-
resolvedBody?: string
|
|
271
|
-
): string | null {
|
|
272
|
-
const source = (resolvedBody ?? body ?? "").trim();
|
|
273
|
-
if (!source) return null;
|
|
274
|
-
if (isWholeScopeExpression(source)) return null;
|
|
275
|
-
if (!looksLikeJsonText(source)) return null;
|
|
276
|
-
const parsed = parseJsonText(source);
|
|
277
|
-
if (parsed.ok) return null;
|
|
278
|
-
// 仍含未求值的 {scope?...} 时,等上游数据再校验
|
|
279
|
-
if (hasScopeTemplate(source)) return null;
|
|
280
|
-
return parsed.error;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function getFetchHeadersValidationError(
|
|
284
|
-
config: FetchRequestConfig,
|
|
285
|
-
scope?: unknown
|
|
286
|
-
): string | null {
|
|
287
|
-
const raw = (config.headersJson ?? "").trim();
|
|
288
|
-
if (!raw) return null;
|
|
289
|
-
if (isWholeScopeExpression(raw) || hasScopeTemplate(raw)) {
|
|
290
|
-
if (scope === undefined) return null;
|
|
291
|
-
const parsed = parseFetchHeadersJson(raw, scope);
|
|
292
|
-
if (!parsed) return "请求头模板解析后不是有效的 JSON 对象";
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
|
-
const result = validateFetchJsonText(raw);
|
|
296
|
-
if (!result.ok) return result.error;
|
|
297
|
-
const parsed = parseFetchHeadersJson(raw, scope);
|
|
298
|
-
if (!parsed) return "请求头不是有效的 JSON 对象";
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** `{scope?...}` 整段表达式,区别于 JSON 对象 `{ "Authorization": "..." }` */
|
|
303
|
-
export function isWholeScopeExpression(text: string): boolean {
|
|
304
|
-
const trimmed = text.trim();
|
|
305
|
-
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false;
|
|
306
|
-
return trimmed.slice(1, -1).trim().startsWith("scope");
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function asHeaderRecord(value: unknown): Record<string, string> | undefined {
|
|
310
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
311
|
-
const headers: Record<string, string> = {};
|
|
312
|
-
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
313
|
-
if (raw === undefined || raw === null) continue;
|
|
314
|
-
headers[key] = typeof raw === "string" ? raw : stringifyScopeValue(raw);
|
|
315
|
-
}
|
|
316
|
-
return headers;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function applyScopeToHeaderRecord(
|
|
320
|
-
headers: Record<string, string>,
|
|
321
|
-
scope: unknown
|
|
322
|
-
): Record<string, string> {
|
|
323
|
-
if (scope === undefined) return headers;
|
|
324
|
-
return Object.fromEntries(
|
|
325
|
-
Object.entries(headers).map(([key, value]) => [
|
|
326
|
-
evaluateScopeTemplate(String(key), scope),
|
|
327
|
-
evaluateScopeTemplate(value == null ? "" : String(value), scope),
|
|
328
|
-
])
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/** 将请求头编辑器原文解析为对象;支持 JSON 内模板,也支持整段 `{scope?...}` */
|
|
333
|
-
export function parseFetchHeadersJson(
|
|
334
|
-
text: string,
|
|
335
|
-
scope?: unknown
|
|
336
|
-
): Record<string, string> | undefined {
|
|
337
|
-
const trimmed = text.trim();
|
|
338
|
-
if (!trimmed) return {};
|
|
339
|
-
if (isWholeScopeExpression(trimmed)) {
|
|
340
|
-
if (scope === undefined) return undefined;
|
|
341
|
-
const inner = trimmed.slice(1, -1);
|
|
342
|
-
return asHeaderRecord(evaluateScopeExpression(inner, scope));
|
|
343
|
-
}
|
|
344
|
-
let jsonText = trimmed;
|
|
345
|
-
if (scope !== undefined && hasScopeTemplate(trimmed) && looksLikeJsonText(trimmed)) {
|
|
346
|
-
jsonText = evaluateScopeTemplateInJson(trimmed, scope);
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
|
-
const parsed = asHeaderRecord(JSON.parse(jsonText));
|
|
350
|
-
if (!parsed) return undefined;
|
|
351
|
-
if (jsonText === trimmed) {
|
|
352
|
-
return applyScopeToHeaderRecord(parsed, scope);
|
|
353
|
-
}
|
|
354
|
-
return parsed;
|
|
355
|
-
} catch {
|
|
356
|
-
return undefined;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export function draftFetchHeadersText(config: FetchRequestConfig): string {
|
|
361
|
-
if (config.headersJson !== undefined) return config.headersJson;
|
|
362
|
-
try {
|
|
363
|
-
return JSON.stringify(config.headers ?? {}, null, 2);
|
|
364
|
-
} catch {
|
|
365
|
-
return "{}";
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export function resolveFetchHeaders(
|
|
370
|
-
config: FetchRequestConfig,
|
|
371
|
-
scope?: unknown
|
|
372
|
-
): Record<string, string> | undefined {
|
|
373
|
-
const rawJson = config.headersJson;
|
|
374
|
-
if (rawJson !== undefined && rawJson.trim() !== "") {
|
|
375
|
-
const parsed = parseFetchHeadersJson(rawJson, scope);
|
|
376
|
-
if (parsed) return parsed;
|
|
377
|
-
}
|
|
378
|
-
if (!config.headers) return config.headers;
|
|
379
|
-
return applyScopeToHeaderRecord(config.headers, scope);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** 用上游 scope 解析 fetch 表单中的 URL / Base URL / headers / body */
|
|
383
|
-
export function applyFetchConfigScope(
|
|
384
|
-
config: FetchRequestConfig,
|
|
385
|
-
scope: unknown
|
|
386
|
-
): FetchRequestConfig {
|
|
387
|
-
const headers = resolveFetchHeaders(config, scope);
|
|
388
|
-
const body = resolveFetchBody(config.body, scope);
|
|
389
|
-
if (scope === undefined) {
|
|
390
|
-
if (headers === config.headers && body === config.body) return config;
|
|
391
|
-
return { ...config, headers, body };
|
|
392
|
-
}
|
|
393
|
-
return {
|
|
394
|
-
...config,
|
|
395
|
-
url: resolveTemplateString(config.url, scope) ?? "",
|
|
396
|
-
apiBaseUrl: resolveTemplateString(config.apiBaseUrl, scope),
|
|
397
|
-
body,
|
|
398
|
-
headers,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
export function fetchConfigHasScopeTemplate(config: FetchRequestConfig): boolean {
|
|
403
|
-
if (hasScopeTemplate(config.url ?? "")) return true;
|
|
404
|
-
if (hasScopeTemplate(config.apiBaseUrl ?? "")) return true;
|
|
405
|
-
if (hasScopeTemplate(config.body ?? "")) return true;
|
|
406
|
-
if (hasScopeTemplate(config.headersJson ?? "")) return true;
|
|
407
|
-
const headers = config.headers ?? {};
|
|
408
|
-
return Object.entries(headers).some(
|
|
409
|
-
([key, value]) => hasScopeTemplate(key) || hasScopeTemplate(String(value ?? ""))
|
|
410
|
-
);
|
|
411
|
-
}
|