@fieldwangai/agentflow 0.1.25
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/LICENSE +21 -0
- package/README.md +201 -0
- package/README.zh-CN.md +201 -0
- package/agents/agentflow-node-executor-code.md +32 -0
- package/agents/agentflow-node-executor-planning.md +32 -0
- package/agents/agentflow-node-executor-requirement.md +32 -0
- package/agents/agentflow-node-executor-test.md +32 -0
- package/agents/agentflow-node-executor-ui.md +32 -0
- package/agents/agentflow-node-executor.md +32 -0
- package/agents/agents.json +8 -0
- package/agents/en/agentflow-node-executor.md +32 -0
- package/agents/zh/agentflow-node-executor.md +32 -0
- package/bin/agentflow.mjs +52 -0
- package/bin/ensure-workspace-reference.mjs +35 -0
- package/bin/lib/agent-runners.mjs +1199 -0
- package/bin/lib/agents-path.mjs +61 -0
- package/bin/lib/api-runner.mjs +361 -0
- package/bin/lib/apply.mjs +852 -0
- package/bin/lib/catalog-agents.mjs +300 -0
- package/bin/lib/catalog-flows.mjs +532 -0
- package/bin/lib/composer-agent.mjs +884 -0
- package/bin/lib/composer-flow-instances.mjs +68 -0
- package/bin/lib/composer-flow-skeleton.mjs +334 -0
- package/bin/lib/composer-flow-validate.mjs +47 -0
- package/bin/lib/composer-log.mjs +197 -0
- package/bin/lib/composer-model-router.mjs +160 -0
- package/bin/lib/composer-node-schema.mjs +299 -0
- package/bin/lib/composer-planner.mjs +749 -0
- package/bin/lib/composer-script-ops.mjs +233 -0
- package/bin/lib/composer-skill-router.mjs +384 -0
- package/bin/lib/flow-import.mjs +305 -0
- package/bin/lib/flow-normalize.mjs +71 -0
- package/bin/lib/flow-write.mjs +395 -0
- package/bin/lib/help.mjs +139 -0
- package/bin/lib/hub-login.mjs +54 -0
- package/bin/lib/hub-publish.mjs +159 -0
- package/bin/lib/hub-remote.mjs +189 -0
- package/bin/lib/hub.mjs +299 -0
- package/bin/lib/i18n.mjs +233 -0
- package/bin/lib/locales/en.json +344 -0
- package/bin/lib/locales/zh.json +344 -0
- package/bin/lib/log.mjs +37 -0
- package/bin/lib/main.mjs +611 -0
- package/bin/lib/model-config.mjs +118 -0
- package/bin/lib/model-lists.mjs +188 -0
- package/bin/lib/node-exec-context.mjs +336 -0
- package/bin/lib/node-execute.mjs +513 -0
- package/bin/lib/normalize-node-tool-command.mjs +97 -0
- package/bin/lib/paths.mjs +216 -0
- package/bin/lib/pipeline-scripts.mjs +41 -0
- package/bin/lib/recent-runs.mjs +173 -0
- package/bin/lib/run-apply-active-lock.mjs +82 -0
- package/bin/lib/run-events.mjs +85 -0
- package/bin/lib/run-node-statuses-from-disk.mjs +85 -0
- package/bin/lib/schedule-config.mjs +227 -0
- package/bin/lib/scheduler.mjs +312 -0
- package/bin/lib/table.mjs +4 -0
- package/bin/lib/terminal.mjs +42 -0
- package/bin/lib/ui-print.mjs +94 -0
- package/bin/lib/ui-server.mjs +2113 -0
- package/bin/lib/workspace-tree.mjs +266 -0
- package/bin/lib/workspace.mjs +180 -0
- package/bin/pipeline/build-node-prompt.mjs +179 -0
- package/bin/pipeline/check-cache.mjs +191 -0
- package/bin/pipeline/check-flow.mjs +543 -0
- package/bin/pipeline/collect-nodes.mjs +212 -0
- package/bin/pipeline/compute-cache-md5.mjs +177 -0
- package/bin/pipeline/ensure-run-dir.mjs +71 -0
- package/bin/pipeline/extract-thinking.mjs +308 -0
- package/bin/pipeline/gc.mjs +129 -0
- package/bin/pipeline/get-env.mjs +83 -0
- package/bin/pipeline/get-exec-id.mjs +145 -0
- package/bin/pipeline/get-ready-nodes.mjs +435 -0
- package/bin/pipeline/get-resolved-values.mjs +337 -0
- package/bin/pipeline/load-key.mjs +62 -0
- package/bin/pipeline/parse-bool.mjs +33 -0
- package/bin/pipeline/parse-flow.mjs +698 -0
- package/bin/pipeline/post-process-control-if.mjs +23 -0
- package/bin/pipeline/post-process-node.mjs +490 -0
- package/bin/pipeline/pre-process-node.mjs +449 -0
- package/bin/pipeline/resolve-inputs.mjs +201 -0
- package/bin/pipeline/run-log.mjs +34 -0
- package/bin/pipeline/run-tool-nodejs.mjs +160 -0
- package/bin/pipeline/save-key.mjs +93 -0
- package/bin/pipeline/snapshot-prior-round.mjs +70 -0
- package/bin/pipeline/validate-flow.mjs +825 -0
- package/bin/pipeline/validate-for-ui.mjs +226 -0
- package/bin/pipeline/validate-script-output.mjs +130 -0
- package/bin/pipeline/write-result.mjs +182 -0
- package/builtin/nodes/agent_subAgent.md +14 -0
- package/builtin/nodes/control_agent_toBool.md +20 -0
- package/builtin/nodes/control_anyOne.md +17 -0
- package/builtin/nodes/control_end.md +11 -0
- package/builtin/nodes/control_if.md +20 -0
- package/builtin/nodes/control_start.md +11 -0
- package/builtin/nodes/control_toBool.md +21 -0
- package/builtin/nodes/provide_file.md +11 -0
- package/builtin/nodes/provide_str.md +11 -0
- package/builtin/nodes/tool_get_env.md +14 -0
- package/builtin/nodes/tool_load_key.md +20 -0
- package/builtin/nodes/tool_nodejs.md +40 -0
- package/builtin/nodes/tool_print.md +14 -0
- package/builtin/nodes/tool_save_key.md +20 -0
- package/builtin/nodes/tool_user_ask.md +23 -0
- package/builtin/nodes/tool_user_check.md +22 -0
- package/builtin/pipelines/module-migrate/flow.yaml +819 -0
- package/builtin/pipelines/module-migrate/scripts/check_imports.mjs +700 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Makefile +362 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp.d +1 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o.d +17 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o.d +5 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o.d +8 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/tree_sitter_kotlin_binding.node.d +1 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/tree_sitter_kotlin_binding.node +0 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/binding.Makefile +6 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/gyp-mac-tool +768 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.Makefile +6 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.target.mk +122 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_except.target.mk +126 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_maybe.target.mk +122 -0
- package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/tree_sitter_kotlin_binding.target.mk +203 -0
- package/builtin/pipelines/new/flow.yaml +545 -0
- package/builtin/pipelines/new/scripts/check-flow.mjs +9 -0
- package/builtin/pipelines/new/scripts/collect-nodes.mjs +211 -0
- package/builtin/pipelines/scripts/adjust-node-positions.mjs +113 -0
- package/builtin/web-ui/dist/agentflow-icon.svg +23 -0
- package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +1 -0
- package/builtin/web-ui/dist/assets/index-DkkhNESc.js +190 -0
- package/builtin/web-ui/dist/index.html +24 -0
- package/package.json +67 -0
- package/reference/flow-control-capabilities.md +274 -0
- package/reference/flow-layout.md +84 -0
- package/reference/flow-prompt-handler-check.md +12 -0
- package/reference/flow-result-semantics.md +14 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 flow.yaml 文本或 zip 解压结果导入流水线目录(user / workspace)。
|
|
3
|
+
*/
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import yaml from "js-yaml";
|
|
7
|
+
import { unzipSync } from "fflate";
|
|
8
|
+
import { resolveFlowDirForWrite, validateUserPipelineId } from "./flow-write.mjs";
|
|
9
|
+
import { normalizeFlowYamlText } from "./flow-normalize.mjs";
|
|
10
|
+
|
|
11
|
+
export const IMPORT_MAX_UNCOMPRESSED_BYTES = 8 * 1024 * 1024;
|
|
12
|
+
export const IMPORT_MAX_FILE_ENTRIES = 500;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} rel
|
|
16
|
+
* @returns {string | null} 规范化后的 posix 相对路径,非法则 null
|
|
17
|
+
*/
|
|
18
|
+
export function sanitizeRelativeEntryPath(rel) {
|
|
19
|
+
if (rel == null || typeof rel !== "string") return null;
|
|
20
|
+
let s = rel.replace(/\\/g, "/").replace(/^\uFEFF/, "");
|
|
21
|
+
while (s.startsWith("/")) s = s.slice(1);
|
|
22
|
+
if (!s || s.includes("\0")) return null;
|
|
23
|
+
if (/^[a-zA-Z]:/.test(s)) return null;
|
|
24
|
+
const parts = s.split("/").filter((p) => p.length > 0);
|
|
25
|
+
for (const p of parts) {
|
|
26
|
+
if (p === "." || p === "..") return null;
|
|
27
|
+
}
|
|
28
|
+
return parts.join("/");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shouldIgnoreZipPath(norm) {
|
|
32
|
+
if (!norm) return true;
|
|
33
|
+
const parts = norm.split("/");
|
|
34
|
+
return parts.some((seg) => seg === "__MACOSX" || seg.startsWith("._"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function flowYamlParentDir(normPath) {
|
|
38
|
+
const lower = normPath.toLowerCase();
|
|
39
|
+
if (lower === "flow.yaml" || lower === "flow.yml") return "";
|
|
40
|
+
const suf = "/flow.yaml";
|
|
41
|
+
const sufYml = "/flow.yml";
|
|
42
|
+
let parent = null;
|
|
43
|
+
if (lower.endsWith(suf)) parent = normPath.slice(0, -suf.length);
|
|
44
|
+
else if (lower.endsWith(sufYml)) parent = normPath.slice(0, -sufYml.length);
|
|
45
|
+
else return null;
|
|
46
|
+
if (parent.includes("/")) return null;
|
|
47
|
+
return parent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {unknown} data
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function isValidFlowRootShape(data) {
|
|
55
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
56
|
+
const o = /** @type {Record<string, unknown>} */ (data);
|
|
57
|
+
if (o.instances != null && typeof o.instances !== "object") return false;
|
|
58
|
+
if (Array.isArray(o.instances)) return false;
|
|
59
|
+
const edges = o.edges;
|
|
60
|
+
const flowEdges = o.flow && typeof o.flow === "object" ? /** @type {any} */ (o.flow).edges : undefined;
|
|
61
|
+
if (edges != null && !Array.isArray(edges)) return false;
|
|
62
|
+
if (flowEdges != null && !Array.isArray(flowEdges)) return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} content
|
|
68
|
+
* @returns {{ ok: true } | { ok: false, error: string }}
|
|
69
|
+
*/
|
|
70
|
+
export function validateImportedFlowYaml(content) {
|
|
71
|
+
if (content == null || typeof content !== "string") {
|
|
72
|
+
return { ok: false, error: "flow.yaml 内容无效" };
|
|
73
|
+
}
|
|
74
|
+
if (Buffer.byteLength(content, "utf8") > IMPORT_MAX_UNCOMPRESSED_BYTES) {
|
|
75
|
+
return { ok: false, error: "flow.yaml 过大" };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const data = yaml.load(content);
|
|
79
|
+
if (!isValidFlowRootShape(data)) {
|
|
80
|
+
return { ok: false, error: "flow.yaml 根结构无效(需含 instances 对象与 edges 数组等)" };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true };
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return { ok: false, error: (e && e.message) || "YAML 解析失败" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {Record<string, Uint8Array>} unzipped
|
|
90
|
+
* @returns {{ ok: true, files: Map<string, Buffer> } | { ok: false, error: string }}
|
|
91
|
+
*/
|
|
92
|
+
export function normalizeZipToPipelineFiles(unzipped) {
|
|
93
|
+
/** @type {Map<string, Buffer>} */
|
|
94
|
+
const raw = new Map();
|
|
95
|
+
let total = 0;
|
|
96
|
+
let count = 0;
|
|
97
|
+
|
|
98
|
+
for (const [rawKey, u8] of Object.entries(unzipped)) {
|
|
99
|
+
if (rawKey.endsWith("/")) continue;
|
|
100
|
+
const norm = sanitizeRelativeEntryPath(rawKey);
|
|
101
|
+
if (!norm || shouldIgnoreZipPath(norm)) continue;
|
|
102
|
+
const size = u8?.length ?? 0;
|
|
103
|
+
total += size;
|
|
104
|
+
if (total > IMPORT_MAX_UNCOMPRESSED_BYTES) {
|
|
105
|
+
return { ok: false, error: "解压后总大小超过限制(8MB)" };
|
|
106
|
+
}
|
|
107
|
+
count += 1;
|
|
108
|
+
if (count > IMPORT_MAX_FILE_ENTRIES) {
|
|
109
|
+
return { ok: false, error: "压缩包内文件数量过多(最多 500 个)" };
|
|
110
|
+
}
|
|
111
|
+
raw.set(norm, Buffer.from(u8));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (raw.size === 0) {
|
|
115
|
+
return { ok: false, error: "压缩包内没有可导入的文件" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @type {Set<string>} */
|
|
119
|
+
const parents = new Set();
|
|
120
|
+
for (const k of raw.keys()) {
|
|
121
|
+
const p = flowYamlParentDir(k);
|
|
122
|
+
if (p !== null) parents.add(p);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (parents.size === 0) {
|
|
126
|
+
return { ok: false, error: "压缩包内未找到 flow.yaml" };
|
|
127
|
+
}
|
|
128
|
+
if (parents.size > 1) {
|
|
129
|
+
return { ok: false, error: "压缩包内存在多个 pipeline(多个 flow.yaml),请分别打包" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const [prefix] = [...parents];
|
|
133
|
+
|
|
134
|
+
for (const k of raw.keys()) {
|
|
135
|
+
const need = prefix === "" ? true : k === prefix || k.startsWith(`${prefix}/`);
|
|
136
|
+
if (!need) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: "ZIP 目录结构无效:存在不属于该流水线目录的文件(请使用单文件夹或根目录 flow.yaml)",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @type {Map<string, Buffer>} */
|
|
145
|
+
const out = new Map();
|
|
146
|
+
const strip = prefix === "" ? "" : `${prefix}/`;
|
|
147
|
+
for (const [k, buf] of raw) {
|
|
148
|
+
const inner = strip ? (k.startsWith(strip) ? k.slice(strip.length) : k) : k;
|
|
149
|
+
if (!inner || inner.endsWith("/")) continue;
|
|
150
|
+
const safe = sanitizeRelativeEntryPath(inner);
|
|
151
|
+
if (!safe) {
|
|
152
|
+
return { ok: false, error: `非法路径: ${inner}` };
|
|
153
|
+
}
|
|
154
|
+
out.set(safe, buf);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const yamlKeys = [...out.keys()].filter(
|
|
158
|
+
(k) => k.toLowerCase() === "flow.yaml" || k.toLowerCase() === "flow.yml",
|
|
159
|
+
);
|
|
160
|
+
if (yamlKeys.length === 0) {
|
|
161
|
+
return { ok: false, error: "归一化后缺少 flow.yaml" };
|
|
162
|
+
}
|
|
163
|
+
if (yamlKeys.length > 1) {
|
|
164
|
+
return { ok: false, error: "流水线目录内不能同时存在多个 flow.yaml / flow.yml" };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const yamlKey = yamlKeys[0];
|
|
168
|
+
if (yamlKey !== "flow.yaml") {
|
|
169
|
+
const body = out.get(yamlKey);
|
|
170
|
+
out.delete(yamlKey);
|
|
171
|
+
out.set("flow.yaml", body);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { ok: true, files: out };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {Map<string, Buffer>} out
|
|
179
|
+
*/
|
|
180
|
+
/**
|
|
181
|
+
* 仅从 zip 推断建议的流水线 ID(单文件夹布局时取文件夹名);不写入磁盘。
|
|
182
|
+
* @param {Buffer} zipBuffer
|
|
183
|
+
* @returns {{ ok: true, suggestedFlowId: string | null } | { ok: false, error: string }}
|
|
184
|
+
*/
|
|
185
|
+
export function suggestFlowIdFromZip(zipBuffer) {
|
|
186
|
+
try {
|
|
187
|
+
const u8 = zipBuffer instanceof Uint8Array ? zipBuffer : new Uint8Array(zipBuffer);
|
|
188
|
+
const unzipped = unzipSync(u8);
|
|
189
|
+
/** @type {Map<string, number>} */
|
|
190
|
+
const raw = new Map();
|
|
191
|
+
let total = 0;
|
|
192
|
+
let count = 0;
|
|
193
|
+
|
|
194
|
+
for (const [rawKey, u8b] of Object.entries(unzipped)) {
|
|
195
|
+
if (rawKey.endsWith("/")) continue;
|
|
196
|
+
const norm = sanitizeRelativeEntryPath(rawKey);
|
|
197
|
+
if (!norm || shouldIgnoreZipPath(norm)) continue;
|
|
198
|
+
const size = u8b?.length ?? 0;
|
|
199
|
+
total += size;
|
|
200
|
+
if (total > IMPORT_MAX_UNCOMPRESSED_BYTES) {
|
|
201
|
+
return { ok: false, error: "解压后总大小超过限制(8MB)" };
|
|
202
|
+
}
|
|
203
|
+
count += 1;
|
|
204
|
+
if (count > IMPORT_MAX_FILE_ENTRIES) {
|
|
205
|
+
return { ok: false, error: "压缩包内文件数量过多(最多 500 个)" };
|
|
206
|
+
}
|
|
207
|
+
raw.set(norm, size);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (raw.size === 0) {
|
|
211
|
+
return { ok: false, error: "压缩包内没有可导入的文件" };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** @type {Set<string>} */
|
|
215
|
+
const parents = new Set();
|
|
216
|
+
for (const k of raw.keys()) {
|
|
217
|
+
const p = flowYamlParentDir(k);
|
|
218
|
+
if (p !== null) parents.add(p);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (parents.size !== 1) {
|
|
222
|
+
return { ok: true, suggestedFlowId: null };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const [prefix] = [...parents];
|
|
226
|
+
if (prefix === "") {
|
|
227
|
+
return { ok: true, suggestedFlowId: null };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const k of raw.keys()) {
|
|
231
|
+
const need = k === prefix || k.startsWith(`${prefix}/`);
|
|
232
|
+
if (!need) {
|
|
233
|
+
return { ok: true, suggestedFlowId: null };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const idCheck = validateUserPipelineId(prefix);
|
|
238
|
+
if (!idCheck.ok) return { ok: true, suggestedFlowId: null };
|
|
239
|
+
return { ok: true, suggestedFlowId: idCheck.flowId };
|
|
240
|
+
} catch (e) {
|
|
241
|
+
return { ok: false, error: (e && e.message) || "ZIP 解析失败" };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {Buffer} zipBuffer
|
|
247
|
+
* @returns {{ ok: true, files: Map<string, Buffer> } | { ok: false, error: string }}
|
|
248
|
+
*/
|
|
249
|
+
export function unzipAndNormalizePipelineZip(zipBuffer) {
|
|
250
|
+
try {
|
|
251
|
+
const u8 = zipBuffer instanceof Uint8Array ? zipBuffer : new Uint8Array(zipBuffer);
|
|
252
|
+
const unzipped = unzipSync(u8);
|
|
253
|
+
return normalizeZipToPipelineFiles(unzipped);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
return { ok: false, error: (e && e.message) || "ZIP 解压失败" };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {string} workspaceRoot
|
|
261
|
+
* @param {string} flowId
|
|
262
|
+
* @param {"user" | "workspace"} flowSource
|
|
263
|
+
* @param {Map<string, Buffer>} filesRelative 相对流水线根,须含 flow.yaml
|
|
264
|
+
* @returns {{ success: true } | { success: false, error: string }}
|
|
265
|
+
*/
|
|
266
|
+
export function writePipelineTree(workspaceRoot, flowId, flowSource, filesRelative) {
|
|
267
|
+
const { flowDir, error } = resolveFlowDirForWrite(workspaceRoot, flowId, flowSource);
|
|
268
|
+
if (error) return { success: false, error };
|
|
269
|
+
if (fs.existsSync(flowDir)) {
|
|
270
|
+
return { success: false, error: "目标目录已存在" };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const yamlBuf = filesRelative.get("flow.yaml");
|
|
274
|
+
if (!yamlBuf) return { success: false, error: "缺少 flow.yaml" };
|
|
275
|
+
const text = yamlBuf.toString("utf8");
|
|
276
|
+
const v = validateImportedFlowYaml(text);
|
|
277
|
+
if (!v.ok) return { success: false, error: v.error };
|
|
278
|
+
const normalizedYaml = normalizeFlowYamlText(text).text;
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
fs.mkdirSync(flowDir, { recursive: true });
|
|
282
|
+
for (const [rel, buf] of filesRelative) {
|
|
283
|
+
const safe = sanitizeRelativeEntryPath(rel);
|
|
284
|
+
if (!safe) return { success: false, error: `非法路径: ${rel}` };
|
|
285
|
+
const abs = path.resolve(path.join(flowDir, ...safe.split("/")));
|
|
286
|
+
const base = path.resolve(flowDir);
|
|
287
|
+
const baseWithSep = base.endsWith(path.sep) ? base : base + path.sep;
|
|
288
|
+
if (abs !== base && !abs.startsWith(baseWithSep)) {
|
|
289
|
+
return { success: false, error: "路径越界" };
|
|
290
|
+
}
|
|
291
|
+
const parent = path.dirname(abs);
|
|
292
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
293
|
+
const payload = safe === "flow.yaml" ? Buffer.from(normalizedYaml, "utf8") : buf;
|
|
294
|
+
fs.writeFileSync(abs, payload);
|
|
295
|
+
}
|
|
296
|
+
return { success: true };
|
|
297
|
+
} catch (e) {
|
|
298
|
+
try {
|
|
299
|
+
fs.rmSync(flowDir, { recursive: true, force: true });
|
|
300
|
+
} catch (_) {
|
|
301
|
+
/* ignore */
|
|
302
|
+
}
|
|
303
|
+
return { success: false, error: (e && e.message) || String(e) };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize flow.yaml text before writing / importing.
|
|
3
|
+
*
|
|
4
|
+
* Legacy / hand-written flows commonly hardcode the pipeline scripts root as
|
|
5
|
+
* `${workspaceRoot}/.workspace/agentflow/pipelines/${flowName}`
|
|
6
|
+
* which only works when the flow is installed under the workspace scope. Hub
|
|
7
|
+
* downloads go to `~/agentflow/pipelines/` (user scope) by default, and builtin
|
|
8
|
+
* flows live inside the package — both break the hardcoded path. Runtime already
|
|
9
|
+
* exposes `${flowDir}` (see bin/pipeline/get-resolved-values.mjs) that resolves
|
|
10
|
+
* to the flow's actual directory regardless of scope, so we rewrite the known
|
|
11
|
+
* bad prefixes to `${flowDir}` on write / import.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const PIPELINE_ROOT_PATTERNS = [
|
|
15
|
+
// ${workspaceRoot}/.workspace/agentflow/pipelines/${flowName}
|
|
16
|
+
/\$\{workspaceRoot\}\/\.workspace\/agentflow\/pipelines\/\$\{flowName\}/g,
|
|
17
|
+
// ${workspaceRoot}/.cursor/agentflow/pipelines/${flowName} (legacy)
|
|
18
|
+
/\$\{workspaceRoot\}\/\.cursor\/agentflow\/pipelines\/\$\{flowName\}/g,
|
|
19
|
+
// ~/agentflow/pipelines/${flowName}
|
|
20
|
+
/~\/agentflow\/pipelines\/\$\{flowName\}/g,
|
|
21
|
+
// $HOME/agentflow/pipelines/${flowName}
|
|
22
|
+
/\$HOME\/agentflow\/pipelines\/\$\{flowName\}/g,
|
|
23
|
+
// ${HOME}/agentflow/pipelines/${flowName}
|
|
24
|
+
/\$\{HOME\}\/agentflow\/pipelines\/\$\{flowName\}/g,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} text
|
|
29
|
+
* @returns {{ text: string, changed: boolean, replacements: number }}
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeFlowYamlText(text) {
|
|
32
|
+
if (typeof text !== "string" || !text) {
|
|
33
|
+
return { text: text ?? "", changed: false, replacements: 0 };
|
|
34
|
+
}
|
|
35
|
+
let out = text;
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const pat of PIPELINE_ROOT_PATTERNS) {
|
|
38
|
+
out = out.replace(pat, () => {
|
|
39
|
+
count += 1;
|
|
40
|
+
return "${flowDir}";
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return { text: out, changed: count > 0, replacements: count };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 识别「flow.yaml 中 tool_nodejs script 路径错」常见模式。
|
|
48
|
+
* 典型触发:script 硬编码 workspace/cursor/user 路径 + `${flowName}`,
|
|
49
|
+
* 但 flow 实际装到其他 scope 时会 `Cannot find module`。
|
|
50
|
+
* @param {string} stderr
|
|
51
|
+
* @returns {string} 空串或可附加到错误信息末尾的修复提示
|
|
52
|
+
*/
|
|
53
|
+
export function buildPipelineScriptPathHint(stderr) {
|
|
54
|
+
const s = typeof stderr === "string" ? stderr : "";
|
|
55
|
+
if (!s) return "";
|
|
56
|
+
const m = s.match(/Cannot find module '([^']+)'/);
|
|
57
|
+
if (!m) return "";
|
|
58
|
+
const missing = m[1];
|
|
59
|
+
const isPipelineScript =
|
|
60
|
+
/\/agentflow\/pipelines\/[^/]+\/scripts\//.test(missing) ||
|
|
61
|
+
/\/\.workspace\/agentflow\/pipelines\//.test(missing) ||
|
|
62
|
+
/\/\.cursor\/agentflow\/pipelines\//.test(missing);
|
|
63
|
+
if (!isPipelineScript) return "";
|
|
64
|
+
return (
|
|
65
|
+
` | Hint: 脚本不存在(${missing})。flow.yaml 的 tool_nodejs.script 可能硬编码了 ` +
|
|
66
|
+
"`${workspaceRoot}/.workspace/agentflow/pipelines/${flowName}/scripts/...`;" +
|
|
67
|
+
"当 flow 装到 ~/agentflow/pipelines/ 或 builtin 时会找不到。" +
|
|
68
|
+
"请改用 `${flowDir}/scripts/xxx.mjs`(${flowDir} 解析到 flow 真实目录,兼容 user/workspace/builtin)。" +
|
|
69
|
+
"AI 自愈无法修复此类模板路径错——必须手改 flow.yaml。"
|
|
70
|
+
);
|
|
71
|
+
}
|