@fieldwangai/agentflow 0.1.42 → 0.1.44
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/bin/lib/composer-agent.mjs +99 -6
- package/bin/lib/ui-server.mjs +351 -11
- package/builtin/nodes/display_ascii.md +1 -0
- package/builtin/nodes/display_chart.md +1 -1
- package/builtin/nodes/display_html.md +1 -1
- package/builtin/nodes/display_image.md +1 -1
- package/builtin/nodes/display_markdown.md +1 -0
- package/builtin/nodes/display_mermaid.md +1 -0
- package/builtin/nodes/display_table.md +1 -1
- package/builtin/web-ui/dist/assets/index-CcZ_lCII.js +222 -0
- package/builtin/web-ui/dist/assets/index-DLRqQLBo.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/builtin/web-ui/dist/assets/index-CuOti87V.css +0 -1
- package/builtin/web-ui/dist/assets/index-D9T2uM0l.js +0 -218
|
@@ -50,6 +50,13 @@ function readUserMcpPrivateEnvObject(userId) {
|
|
|
50
50
|
return env;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function readUserMcpPrivateServers(userId) {
|
|
54
|
+
const safe = sanitizeAgentflowUserId(userId);
|
|
55
|
+
if (!safe) return {};
|
|
56
|
+
const data = readJsonObject(path.join(getAgentflowUserDataRoot(safe), "mcp-private.json"));
|
|
57
|
+
return data?.servers && typeof data.servers === "object" && !Array.isArray(data.servers) ? data.servers : {};
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
function pruneCursorMcpPrivateEnvPlaceholders() {
|
|
54
61
|
const filePath = path.join(os.homedir(), ".cursor", "mcp.json");
|
|
55
62
|
const config = readJsonObject(filePath);
|
|
@@ -83,12 +90,98 @@ function pruneCursorMcpPrivateEnvPlaceholders() {
|
|
|
83
90
|
fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
|
|
84
91
|
}
|
|
85
92
|
|
|
93
|
+
function cursorMcpServersFromFile(filePath) {
|
|
94
|
+
const config = readJsonObject(filePath);
|
|
95
|
+
return config?.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
96
|
+
? config.mcpServers
|
|
97
|
+
: {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function materializeWorkspaceCursorMcpPrivateConfig(workspaceRoot, userId) {
|
|
101
|
+
const safe = sanitizeAgentflowUserId(userId);
|
|
102
|
+
if (!safe) return () => {};
|
|
103
|
+
const privateServers = readUserMcpPrivateServers(safe);
|
|
104
|
+
if (!Object.keys(privateServers).length) return () => {};
|
|
105
|
+
|
|
106
|
+
const workspace = path.resolve(workspaceRoot || process.cwd());
|
|
107
|
+
const filePath = path.join(workspace, ".cursor", "mcp.json");
|
|
108
|
+
const globalFilePath = path.join(os.homedir(), ".cursor", "mcp.json");
|
|
109
|
+
const existed = fs.existsSync(filePath);
|
|
110
|
+
const original = existed ? fs.readFileSync(filePath, "utf-8") : "";
|
|
111
|
+
const config = readJsonObject(filePath);
|
|
112
|
+
const localServers = cursorMcpServersFromFile(filePath);
|
|
113
|
+
const globalServers = cursorMcpServersFromFile(globalFilePath);
|
|
114
|
+
const nextServers = { ...localServers };
|
|
115
|
+
let changed = false;
|
|
116
|
+
|
|
117
|
+
for (const [name, privateServer] of Object.entries(privateServers)) {
|
|
118
|
+
const current = nextServers[name] || globalServers[name];
|
|
119
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) continue;
|
|
120
|
+
const privateEnv = privateServer?.env && typeof privateServer.env === "object" && !Array.isArray(privateServer.env) ? privateServer.env : {};
|
|
121
|
+
const privateHeaders = privateServer?.headers && typeof privateServer.headers === "object" && !Array.isArray(privateServer.headers) ? privateServer.headers : {};
|
|
122
|
+
if (!Object.keys(privateEnv).length && !Object.keys(privateHeaders).length) continue;
|
|
123
|
+
|
|
124
|
+
const next = { ...current };
|
|
125
|
+
if (Object.keys(privateEnv).length) {
|
|
126
|
+
const currentEnv = current.env && typeof current.env === "object" && !Array.isArray(current.env) ? current.env : {};
|
|
127
|
+
next.env = { ...currentEnv, ...privateEnv };
|
|
128
|
+
changed = true;
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(privateHeaders).length) {
|
|
131
|
+
const currentHeaders = current.headers && typeof current.headers === "object" && !Array.isArray(current.headers) ? current.headers : {};
|
|
132
|
+
next.headers = { ...currentHeaders, ...privateHeaders };
|
|
133
|
+
changed = true;
|
|
134
|
+
}
|
|
135
|
+
nextServers[name] = next;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!changed) return () => {};
|
|
139
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
140
|
+
fs.writeFileSync(filePath, JSON.stringify({ ...config, mcpServers: nextServers }, null, 2) + "\n", "utf-8");
|
|
141
|
+
|
|
142
|
+
let restored = false;
|
|
143
|
+
return () => {
|
|
144
|
+
if (restored) return;
|
|
145
|
+
restored = true;
|
|
146
|
+
try {
|
|
147
|
+
if (existed) fs.writeFileSync(filePath, original, "utf-8");
|
|
148
|
+
else if (fs.existsSync(filePath)) fs.rmSync(filePath, { force: true });
|
|
149
|
+
} catch {
|
|
150
|
+
// Best-effort restore; do not fail an already-running agent on cleanup.
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
86
155
|
function agentflowUserEnv(userId) {
|
|
87
156
|
const safe = sanitizeAgentflowUserId(userId);
|
|
88
157
|
pruneCursorMcpPrivateEnvPlaceholders();
|
|
89
158
|
return { ...readMergedEnvObject(safe), ...(safe ? readUserMcpPrivateEnvObject(safe) : {}), AGENTFLOW_USER_ID: safe };
|
|
90
159
|
}
|
|
91
160
|
|
|
161
|
+
function runCursorAgentWithPrivateMcp(cliWorkspace, prompt, options, userId) {
|
|
162
|
+
const restore = materializeWorkspaceCursorMcpPrivateConfig(cliWorkspace, userId);
|
|
163
|
+
let handle;
|
|
164
|
+
try {
|
|
165
|
+
handle = runCursorAgentWithPrompt(cliWorkspace, prompt, options);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
restore();
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let restored = false;
|
|
172
|
+
const safeRestore = () => {
|
|
173
|
+
if (restored) return;
|
|
174
|
+
restored = true;
|
|
175
|
+
restore();
|
|
176
|
+
};
|
|
177
|
+
if (handle?.child?.once) {
|
|
178
|
+
handle.child.once("exit", safeRestore);
|
|
179
|
+
handle.child.once("error", safeRestore);
|
|
180
|
+
}
|
|
181
|
+
const finished = Promise.resolve(handle.finished).finally(safeRestore);
|
|
182
|
+
return { ...handle, finished };
|
|
183
|
+
}
|
|
184
|
+
|
|
92
185
|
// ─── script 内容注入辅助 ─────────────────────────────────────────────────
|
|
93
186
|
|
|
94
187
|
/**
|
|
@@ -203,10 +296,10 @@ export function startComposerAgent(opts) {
|
|
|
203
296
|
});
|
|
204
297
|
}
|
|
205
298
|
|
|
206
|
-
return
|
|
299
|
+
return runCursorAgentWithPrivateMcp(cliWs, prompt, {
|
|
207
300
|
...common,
|
|
208
301
|
model: model || undefined,
|
|
209
|
-
});
|
|
302
|
+
}, opts.agentflowUserId);
|
|
210
303
|
}
|
|
211
304
|
|
|
212
305
|
// ─── 为单个 agent 步骤构建 prompt ──────────────────────────────────────────
|
|
@@ -496,12 +589,12 @@ export async function runComposerPostFlowValidationAndRepair(opts) {
|
|
|
496
589
|
setChild(handle.child);
|
|
497
590
|
await handle.finished;
|
|
498
591
|
} else {
|
|
499
|
-
const handle =
|
|
592
|
+
const handle = runCursorAgentWithPrivateMcp(cliWs, agentPrompt, {
|
|
500
593
|
onStreamEvent: stepEmit,
|
|
501
594
|
model: model || undefined,
|
|
502
595
|
force: Boolean(opts.force),
|
|
503
596
|
env,
|
|
504
|
-
});
|
|
597
|
+
}, opts.agentflowUserId || opts.flowContext?.userId);
|
|
505
598
|
setChild(handle.child);
|
|
506
599
|
await handle.finished;
|
|
507
600
|
}
|
|
@@ -769,12 +862,12 @@ export function startComposerMultiStep(opts) {
|
|
|
769
862
|
currentChild = handle.child;
|
|
770
863
|
await handle.finished;
|
|
771
864
|
} else {
|
|
772
|
-
const handle =
|
|
865
|
+
const handle = runCursorAgentWithPrivateMcp(cliWs, agentPrompt, {
|
|
773
866
|
onStreamEvent: stepEmit,
|
|
774
867
|
model: model || undefined,
|
|
775
868
|
force: Boolean(opts.force),
|
|
776
869
|
env,
|
|
777
|
-
});
|
|
870
|
+
}, opts.agentflowUserId || opts.flowContext?.userId);
|
|
778
871
|
currentChild = handle.child;
|
|
779
872
|
await handle.finished;
|
|
780
873
|
}
|
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -108,8 +108,13 @@ const MIME = {
|
|
|
108
108
|
".js": "text/javascript; charset=utf-8",
|
|
109
109
|
".css": "text/css; charset=utf-8",
|
|
110
110
|
".json": "application/json; charset=utf-8",
|
|
111
|
-
".
|
|
111
|
+
".png": "image/png",
|
|
112
|
+
".jpg": "image/jpeg",
|
|
113
|
+
".jpeg": "image/jpeg",
|
|
114
|
+
".gif": "image/gif",
|
|
115
|
+
".webp": "image/webp",
|
|
112
116
|
".svg": "image/svg+xml",
|
|
117
|
+
".ico": "image/x-icon",
|
|
113
118
|
};
|
|
114
119
|
|
|
115
120
|
const RUN_CONFIG_FILENAME = "run-config.json";
|
|
@@ -1125,6 +1130,7 @@ const WORKSPACE_TEXT_EXTS = new Set([
|
|
|
1125
1130
|
".mjs",
|
|
1126
1131
|
".cjs",
|
|
1127
1132
|
]);
|
|
1133
|
+
const WORKSPACE_IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"]);
|
|
1128
1134
|
|
|
1129
1135
|
function resolveWorkspaceFilePath(workspaceRoot, relPath) {
|
|
1130
1136
|
const root = path.resolve(workspaceRoot);
|
|
@@ -1144,9 +1150,59 @@ function workspaceFileIcon(fileName, isDir = false) {
|
|
|
1144
1150
|
if ([".yaml", ".yml", ".json"].includes(ext)) return "data_object";
|
|
1145
1151
|
if (ext === ".css") return "palette";
|
|
1146
1152
|
if (ext === ".html") return "web";
|
|
1153
|
+
if (WORKSPACE_IMAGE_EXTS.has(ext)) return "image";
|
|
1147
1154
|
return "draft";
|
|
1148
1155
|
}
|
|
1149
1156
|
|
|
1157
|
+
function sanitizeWorkspaceUploadName(filename) {
|
|
1158
|
+
const parsed = path.parse(String(filename || "image").replace(/\\/g, "/").split("/").pop() || "image");
|
|
1159
|
+
const stem = (parsed.name || "image")
|
|
1160
|
+
.trim()
|
|
1161
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
1162
|
+
.replace(/^-+|-+$/g, "")
|
|
1163
|
+
.slice(0, 80) || "image";
|
|
1164
|
+
const ext = String(parsed.ext || "").toLowerCase();
|
|
1165
|
+
return `${stem}${WORKSPACE_IMAGE_EXTS.has(ext) ? ext : ".png"}`;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function uniqueWorkspaceRelPath(workspaceRoot, relPath) {
|
|
1169
|
+
let { abs, rel } = resolveWorkspaceFilePath(workspaceRoot, relPath);
|
|
1170
|
+
if (!fs.existsSync(abs)) return { abs, rel };
|
|
1171
|
+
const parsed = path.parse(rel);
|
|
1172
|
+
for (let i = 1; i < 1000; i += 1) {
|
|
1173
|
+
const candidate = path.posix.join(parsed.dir, `${parsed.name}-${i}${parsed.ext}`);
|
|
1174
|
+
const resolved = resolveWorkspaceFilePath(workspaceRoot, candidate);
|
|
1175
|
+
if (!fs.existsSync(resolved.abs)) return resolved;
|
|
1176
|
+
}
|
|
1177
|
+
return { abs, rel };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function workspaceDownloadContentDisposition(relPath) {
|
|
1181
|
+
const fallbackName = path.basename(String(relPath || "download")) || "download";
|
|
1182
|
+
const quotedName = fallbackName.replace(/[\r\n"\\]/g, "_");
|
|
1183
|
+
return `attachment; filename="${quotedName}"; filename*=UTF-8''${encodeURIComponent(fallbackName)}`;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const WORKSPACE_FILE_SKIP_REL_PREFIXES = [
|
|
1187
|
+
".workspace/agentflow/worktrees",
|
|
1188
|
+
".workspace/agentflow/git-repos",
|
|
1189
|
+
".workspace/agentflow/runBuild",
|
|
1190
|
+
".workspace/agentflow/composer-logs",
|
|
1191
|
+
];
|
|
1192
|
+
|
|
1193
|
+
function workspacePathInside(parent, candidate) {
|
|
1194
|
+
const base = path.resolve(parent);
|
|
1195
|
+
const target = path.resolve(candidate);
|
|
1196
|
+
return target === base || target.startsWith(base + path.sep);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function shouldSkipWorkspaceFileRelPath(relPath) {
|
|
1200
|
+
const normalized = String(relPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1201
|
+
return WORKSPACE_FILE_SKIP_REL_PREFIXES.some((prefix) => (
|
|
1202
|
+
normalized === prefix || normalized.startsWith(`${prefix}/`)
|
|
1203
|
+
));
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1150
1206
|
function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget = { count: 0 }) {
|
|
1151
1207
|
if (depth > maxDepth || budget.count > 500) return [];
|
|
1152
1208
|
let entries;
|
|
@@ -1161,6 +1217,7 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
|
|
|
1161
1217
|
if (entry.name.startsWith(".") && entry.name !== ".agents" && entry.name !== ".codex") continue;
|
|
1162
1218
|
const abs = path.join(dir, entry.name);
|
|
1163
1219
|
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
1220
|
+
if (shouldSkipWorkspaceFileRelPath(rel)) continue;
|
|
1164
1221
|
if (entry.isDirectory()) {
|
|
1165
1222
|
if (WORKSPACE_FILE_SKIP_DIRS.has(entry.name)) continue;
|
|
1166
1223
|
budget.count++;
|
|
@@ -1174,7 +1231,7 @@ function readWorkspaceFilesRecursive(dir, root, depth = 0, maxDepth = 3, budget
|
|
|
1174
1231
|
} else if (entry.isFile()) {
|
|
1175
1232
|
if (WORKSPACE_FILE_SKIP_FILES.has(entry.name)) continue;
|
|
1176
1233
|
const ext = path.extname(entry.name).toLowerCase();
|
|
1177
|
-
if (!WORKSPACE_TEXT_EXTS.has(ext)) continue;
|
|
1234
|
+
if (!WORKSPACE_TEXT_EXTS.has(ext) && !WORKSPACE_IMAGE_EXTS.has(ext)) continue;
|
|
1178
1235
|
let size = 0;
|
|
1179
1236
|
try { size = fs.statSync(abs).size; } catch {}
|
|
1180
1237
|
budget.count++;
|
|
@@ -1232,6 +1289,43 @@ function normalizeWorkspaceGraphPayload(payload) {
|
|
|
1232
1289
|
};
|
|
1233
1290
|
}
|
|
1234
1291
|
|
|
1292
|
+
function workspaceRunTouchedNodeIds(result) {
|
|
1293
|
+
const ids = new Set();
|
|
1294
|
+
for (const id of Array.isArray(result?.order) ? result.order : []) {
|
|
1295
|
+
const text = String(id || "").trim();
|
|
1296
|
+
if (text) ids.add(text);
|
|
1297
|
+
}
|
|
1298
|
+
for (const event of Array.isArray(result?.events) ? result.events : []) {
|
|
1299
|
+
const nodeId = String(event?.nodeId || "").trim();
|
|
1300
|
+
if (nodeId) ids.add(nodeId);
|
|
1301
|
+
for (const displayId of Array.isArray(event?.displayNodeIds) ? event.displayNodeIds : []) {
|
|
1302
|
+
const text = String(displayId || "").trim();
|
|
1303
|
+
if (text) ids.add(text);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return ids;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function mergeWorkspaceRunGraph(currentGraph, runGraph, touchedIds) {
|
|
1310
|
+
const current = normalizeWorkspaceGraphPayload(currentGraph || {});
|
|
1311
|
+
const run = normalizeWorkspaceGraphPayload(runGraph || {});
|
|
1312
|
+
const ids = touchedIds instanceof Set ? touchedIds : new Set(touchedIds || []);
|
|
1313
|
+
const instances = { ...(current.instances || {}) };
|
|
1314
|
+
for (const id of ids) {
|
|
1315
|
+
if (run.instances && Object.prototype.hasOwnProperty.call(run.instances, id)) {
|
|
1316
|
+
instances[id] = run.instances[id];
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
...current,
|
|
1321
|
+
version: 1,
|
|
1322
|
+
instances,
|
|
1323
|
+
edges: Array.isArray(current.edges) ? current.edges : [],
|
|
1324
|
+
ui: current.ui && typeof current.ui === "object" ? current.ui : { nodePositions: {} },
|
|
1325
|
+
updatedAt: new Date().toISOString(),
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1235
1329
|
function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {}) {
|
|
1236
1330
|
const flowId = params.flowId != null ? String(params.flowId).trim() : "";
|
|
1237
1331
|
if (!flowId) return { root: path.resolve(workspaceRoot), flowId: "", flowSource: "", archived: false };
|
|
@@ -1952,6 +2046,39 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs) {
|
|
|
1952
2046
|
return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge);
|
|
1953
2047
|
}
|
|
1954
2048
|
|
|
2049
|
+
function workspaceInputValues(graph, nodeId, outputs) {
|
|
2050
|
+
const values = {};
|
|
2051
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
2052
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
2053
|
+
const target = instances[String(nodeId || "")] || {};
|
|
2054
|
+
const inputSlots = Array.isArray(target.input) ? target.input : [];
|
|
2055
|
+
for (const edge of edges) {
|
|
2056
|
+
if (String(edge?.target || "") !== String(nodeId)) continue;
|
|
2057
|
+
const index = workspaceHandleIndex(edge?.targetHandle, "input");
|
|
2058
|
+
const slot = inputSlots[index] || null;
|
|
2059
|
+
const name = String(slot?.name || "").trim();
|
|
2060
|
+
if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
|
|
2061
|
+
const value = workspaceOutputSlotValueForEdge(graph, outputs, edge);
|
|
2062
|
+
if (String(value || "").trim()) values[name] = String(value);
|
|
2063
|
+
}
|
|
2064
|
+
for (const slot of inputSlots) {
|
|
2065
|
+
const name = String(slot?.name || "").trim();
|
|
2066
|
+
if (!name || isWorkspaceSemanticInputSlot(slot) || Object.prototype.hasOwnProperty.call(values, name)) continue;
|
|
2067
|
+
const value = workspaceSlotValue(slot);
|
|
2068
|
+
if (String(value || "").trim()) values[name] = String(value);
|
|
2069
|
+
}
|
|
2070
|
+
return values;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function workspaceResolveBodyPlaceholders(body, inputValues = {}) {
|
|
2074
|
+
const raw = String(body || "");
|
|
2075
|
+
if (!raw.includes("${")) return raw;
|
|
2076
|
+
return raw.replace(/\$\{([A-Za-z_][A-Za-z0-9_-]*)\}/g, (match, name) => {
|
|
2077
|
+
if (!Object.prototype.hasOwnProperty.call(inputValues, name)) return match;
|
|
2078
|
+
return String(inputValues[name] ?? "");
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
|
|
1955
2082
|
function parseWorkspaceSkillKeys(raw) {
|
|
1956
2083
|
const text = String(raw || "").trim();
|
|
1957
2084
|
if (!text) return [];
|
|
@@ -2116,9 +2243,9 @@ function workspaceUpdateDirectDisplays(graph, sourceId, content, outputs = null)
|
|
|
2116
2243
|
return updated;
|
|
2117
2244
|
}
|
|
2118
2245
|
|
|
2119
|
-
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "") {
|
|
2246
|
+
function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock = "", inputValues = {}) {
|
|
2120
2247
|
const instance = graph.instances[nodeId] || {};
|
|
2121
|
-
const body =
|
|
2248
|
+
const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
|
|
2122
2249
|
const label = String(instance.label || nodeId).trim();
|
|
2123
2250
|
const downstreamRequirements = workspaceDownstreamDisplayRequirements(graph, nodeId);
|
|
2124
2251
|
const outputProtocolRequirements = workspaceOutputProtocolRequirements(graph, nodeId);
|
|
@@ -2136,6 +2263,72 @@ function workspaceNodePrompt(graph, nodeId, upstreamText, skillsBlock, mcpBlock
|
|
|
2136
2263
|
].filter(Boolean).join("\n");
|
|
2137
2264
|
}
|
|
2138
2265
|
|
|
2266
|
+
function workspaceDefaultWorktreeRoot(scopedRoot) {
|
|
2267
|
+
return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "worktrees");
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function workspaceShouldAutoCleanupWorktree(scopedRoot, worktreePath, hasExplicitWorktreePath) {
|
|
2271
|
+
if (hasExplicitWorktreePath || !worktreePath) return false;
|
|
2272
|
+
return workspacePathInside(workspaceDefaultWorktreeRoot(scopedRoot), worktreePath);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
function workspaceTrackAutoCleanupWorktree(list, item) {
|
|
2276
|
+
const rawTarget = String(item?.worktreePath || "").trim();
|
|
2277
|
+
if (!rawTarget) return;
|
|
2278
|
+
const target = path.resolve(rawTarget);
|
|
2279
|
+
if (list.some((entry) => path.resolve(entry.worktreePath) === target)) return;
|
|
2280
|
+
list.push({ ...item, worktreePath: target });
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function workspaceUntrackAutoCleanupWorktree(list, worktreePath) {
|
|
2284
|
+
const rawTarget = String(worktreePath || "").trim();
|
|
2285
|
+
if (!rawTarget) return;
|
|
2286
|
+
const target = path.resolve(rawTarget);
|
|
2287
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
2288
|
+
if (path.resolve(list[i].worktreePath) === target) list.splice(i, 1);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function workspaceMarkAutoWorktreeCleaned(graph, entry) {
|
|
2293
|
+
const instance = graph?.instances?.[entry.nodeId];
|
|
2294
|
+
if (!instance) return false;
|
|
2295
|
+
let nextInstance = workspaceSetOutputSlot(instance, "worktreePath", "");
|
|
2296
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "gitContext", "");
|
|
2297
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "workspaceContext", "");
|
|
2298
|
+
graph.instances[entry.nodeId] = nextInstance;
|
|
2299
|
+
return true;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
function workspaceCleanupAutoWorktrees(list, graph, emit) {
|
|
2303
|
+
for (const entry of [...list].reverse()) {
|
|
2304
|
+
try {
|
|
2305
|
+
const result = unloadGitWorktree({
|
|
2306
|
+
repoPath: entry.repoPath,
|
|
2307
|
+
worktreePath: entry.worktreePath,
|
|
2308
|
+
force: false,
|
|
2309
|
+
prune: true,
|
|
2310
|
+
});
|
|
2311
|
+
emit({
|
|
2312
|
+
type: "natural",
|
|
2313
|
+
kind: "status",
|
|
2314
|
+
nodeId: entry.nodeId,
|
|
2315
|
+
text: `已清理临时 worktree:${result.worktreePath}`,
|
|
2316
|
+
});
|
|
2317
|
+
if (workspaceMarkAutoWorktreeCleaned(graph, entry)) {
|
|
2318
|
+
emit({ type: "graph", nodeId: entry.nodeId, graph });
|
|
2319
|
+
}
|
|
2320
|
+
} catch (e) {
|
|
2321
|
+
emit({
|
|
2322
|
+
type: "natural",
|
|
2323
|
+
kind: "warning",
|
|
2324
|
+
nodeId: entry.nodeId,
|
|
2325
|
+
text: `临时 worktree 未自动清理:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
list.splice(0, list.length);
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2139
2332
|
async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
2140
2333
|
const graph = normalizeWorkspaceGraphPayload(payload.graph || {});
|
|
2141
2334
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
@@ -2193,7 +2386,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2193
2386
|
};
|
|
2194
2387
|
let cwd = scopedRoot;
|
|
2195
2388
|
const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
|
|
2389
|
+
const autoCleanupWorktrees = [];
|
|
2196
2390
|
|
|
2391
|
+
try {
|
|
2197
2392
|
for (const nodeId of order) {
|
|
2198
2393
|
throwIfAborted();
|
|
2199
2394
|
const instance = graph.instances[nodeId];
|
|
@@ -2378,13 +2573,23 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2378
2573
|
(gitContext?.repoPath ? path.resolve(gitContext.repoPath) : "");
|
|
2379
2574
|
if (!repoPath) throw new Error("Load Worktree requires repoPath");
|
|
2380
2575
|
const branch = workspaceSlotValue(workspaceSlotByName(instance, "branch")).trim();
|
|
2381
|
-
const
|
|
2576
|
+
const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
|
|
2577
|
+
.find((slot) => String(slot?.name || "") === "worktreePath") || null;
|
|
2578
|
+
const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
|
|
2382
2579
|
const worktreePath = rawWorktreePath ? workspaceResolvePath(cwd, rawWorktreePath) : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : "");
|
|
2580
|
+
const hasExplicitWorktreePath = Boolean(rawWorktreePath) || Boolean(gitContext?.worktreePath);
|
|
2383
2581
|
const previousCwd = cwd;
|
|
2384
2582
|
const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
|
|
2385
2583
|
const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
|
|
2386
2584
|
const pruneMissing = pruneMissingRaw !== "false";
|
|
2387
2585
|
const result = loadGitWorktree({ repoPath, branch, worktreePath, pipelineWorkspace: scopedRoot, force, pruneMissing });
|
|
2586
|
+
if (workspaceShouldAutoCleanupWorktree(scopedRoot, result.worktreePath, hasExplicitWorktreePath)) {
|
|
2587
|
+
workspaceTrackAutoCleanupWorktree(autoCleanupWorktrees, {
|
|
2588
|
+
nodeId,
|
|
2589
|
+
repoPath: result.repoRoot,
|
|
2590
|
+
worktreePath: result.worktreePath,
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2388
2593
|
const outGitContext = buildGitContext({
|
|
2389
2594
|
repoPath: result.repoRoot,
|
|
2390
2595
|
worktreePath: result.worktreePath,
|
|
@@ -2427,6 +2632,7 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2427
2632
|
const pruneRaw = workspaceSlotValue(workspaceSlotByName(instance, "prune")).trim().toLowerCase();
|
|
2428
2633
|
const prune = pruneRaw !== "false";
|
|
2429
2634
|
const result = unloadGitWorktree({ repoPath, worktreePath, force, prune });
|
|
2635
|
+
workspaceUntrackAutoCleanupWorktree(autoCleanupWorktrees, result.worktreePath);
|
|
2430
2636
|
const previousContext = workspaceContext?.previous && typeof workspaceContext.previous === "object" ? workspaceContext.previous : null;
|
|
2431
2637
|
cwd = previousContext?.cwd ? path.resolve(String(previousContext.cwd)) : scopedRoot;
|
|
2432
2638
|
let nextInstance = workspaceSetOutputSlot(instance, "removed", "true");
|
|
@@ -2483,14 +2689,15 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2483
2689
|
|
|
2484
2690
|
const prepareStartedAt = Date.now();
|
|
2485
2691
|
const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs);
|
|
2486
|
-
const
|
|
2692
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs);
|
|
2693
|
+
const body = workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim();
|
|
2487
2694
|
if (defId === "agent_subAgent" && !body && !String(upstreamText || "").trim()) {
|
|
2488
2695
|
throw new Error(`Workspace node ${nodeId} has no task. Fill the node body or connect upstream text.`);
|
|
2489
2696
|
}
|
|
2490
2697
|
const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
|
|
2491
2698
|
const promptSkillsBlock = mergeWorkspaceSkillBlocks(upstreamSkillBlocks, upstreamSkillBlocks ? "" : loadSkillsBlockForKeys(fallbackSelectedSkillKeys));
|
|
2492
2699
|
const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
|
|
2493
|
-
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock);
|
|
2700
|
+
const prompt = workspaceNodePrompt(graph, nodeId, upstreamText, promptSkillsBlock, promptMcpBlock, inputValues);
|
|
2494
2701
|
emitTiming(nodeId, "prepare-agent-prompt", prepareStartedAt, { promptChars: prompt.length, upstreamChars: String(upstreamText || "").length, skillsChars: promptSkillsBlock.length, mcpChars: promptMcpBlock.length });
|
|
2495
2702
|
emit({ type: "natural", kind: "prompt", nodeId, text: prompt });
|
|
2496
2703
|
let content = "";
|
|
@@ -2551,6 +2758,9 @@ async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts =
|
|
|
2551
2758
|
if (slotUpdate.changed || updatedDisplays.length) emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
2552
2759
|
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
2553
2760
|
}
|
|
2761
|
+
} finally {
|
|
2762
|
+
workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
|
|
2763
|
+
}
|
|
2554
2764
|
if (pauseNodeIds.length > 0) {
|
|
2555
2765
|
emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
|
|
2556
2766
|
}
|
|
@@ -2655,6 +2865,47 @@ function parseFlowsImportForm(req) {
|
|
|
2655
2865
|
});
|
|
2656
2866
|
}
|
|
2657
2867
|
|
|
2868
|
+
function parseWorkspaceUploadForm(req) {
|
|
2869
|
+
return new Promise((resolve, reject) => {
|
|
2870
|
+
const bb = busboy({
|
|
2871
|
+
headers: req.headers,
|
|
2872
|
+
limits: { files: 1, fileSize: 10 * 1024 * 1024, parts: 32 },
|
|
2873
|
+
});
|
|
2874
|
+
const fields = {};
|
|
2875
|
+
const chunks = [];
|
|
2876
|
+
let filename = "";
|
|
2877
|
+
let mimeType = "";
|
|
2878
|
+
let gotFile = false;
|
|
2879
|
+
bb.on("field", (name, val) => {
|
|
2880
|
+
fields[String(name || "")] = String(val || "");
|
|
2881
|
+
});
|
|
2882
|
+
bb.on("file", (name, file, info) => {
|
|
2883
|
+
if (name !== "file") {
|
|
2884
|
+
file.resume();
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
gotFile = true;
|
|
2888
|
+
filename = info.filename || "";
|
|
2889
|
+
mimeType = info.mimeType || "";
|
|
2890
|
+
file.on("data", (d) => chunks.push(d));
|
|
2891
|
+
file.on("limit", () => {
|
|
2892
|
+
reject(new Error("FILE_TOO_LARGE"));
|
|
2893
|
+
});
|
|
2894
|
+
});
|
|
2895
|
+
bb.on("finish", () => {
|
|
2896
|
+
resolve({
|
|
2897
|
+
fields,
|
|
2898
|
+
file: Buffer.concat(chunks),
|
|
2899
|
+
filename,
|
|
2900
|
+
mimeType,
|
|
2901
|
+
gotFile,
|
|
2902
|
+
});
|
|
2903
|
+
});
|
|
2904
|
+
bb.on("error", reject);
|
|
2905
|
+
req.pipe(bb);
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2658
2909
|
/** GET 读 flow / nodes / SSE 等 */
|
|
2659
2910
|
function isValidFlowSourceRead(s) {
|
|
2660
2911
|
return s === "builtin" || s === "admin" || s === "user" || s === "workspace";
|
|
@@ -3331,8 +3582,11 @@ export function startUiServer({
|
|
|
3331
3582
|
signal: controller.signal,
|
|
3332
3583
|
onActiveChild: setActiveChild,
|
|
3333
3584
|
});
|
|
3334
|
-
|
|
3335
|
-
|
|
3585
|
+
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
3586
|
+
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
3587
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
3588
|
+
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
3589
|
+
writeEvent({ type: "done", ok: true, path: graphPath, graph: mergedGraph, order: result.order, touchedNodeIds: Array.from(touchedIds), pauseNodeIds: result.pauseNodeIds || [] });
|
|
3336
3590
|
res.end();
|
|
3337
3591
|
} catch (e) {
|
|
3338
3592
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
@@ -3352,8 +3606,11 @@ export function startUiServer({
|
|
|
3352
3606
|
onActiveChild: setActiveChild,
|
|
3353
3607
|
});
|
|
3354
3608
|
const graphPath = workspaceGraphPath(scoped.root);
|
|
3355
|
-
|
|
3356
|
-
|
|
3609
|
+
const currentGraph = readWorkspaceGraph(scoped.root).graph;
|
|
3610
|
+
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
3611
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
3612
|
+
fs.writeFileSync(graphPath, JSON.stringify(mergedGraph, null, 2) + "\n", "utf-8");
|
|
3613
|
+
json(res, 200, { ok: true, path: graphPath, ...result, graph: mergedGraph, touchedNodeIds: Array.from(touchedIds) });
|
|
3357
3614
|
} catch (e) {
|
|
3358
3615
|
if (isWorkspaceRunAbortError(e) || controller.signal.aborted) {
|
|
3359
3616
|
json(res, 200, { ok: false, stopped: true, message: "Workspace run stopped" });
|
|
@@ -3422,6 +3679,41 @@ export function startUiServer({
|
|
|
3422
3679
|
return;
|
|
3423
3680
|
}
|
|
3424
3681
|
|
|
3682
|
+
if (req.method === "GET" && url.pathname === "/api/workspace/file/raw") {
|
|
3683
|
+
try {
|
|
3684
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
3685
|
+
flowId: url.searchParams.get("flowId") || "",
|
|
3686
|
+
flowSource: url.searchParams.get("flowSource") || "user",
|
|
3687
|
+
archived: url.searchParams.get("archived") === "1",
|
|
3688
|
+
}, userCtx);
|
|
3689
|
+
if (scoped.error) {
|
|
3690
|
+
json(res, 400, { error: scoped.error });
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
const { abs, rel } = resolveWorkspaceFilePath(scoped.root, url.searchParams.get("path") || "");
|
|
3694
|
+
if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
3695
|
+
json(res, 404, { error: "File not found" });
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
const ext = path.extname(abs).toLowerCase();
|
|
3699
|
+
const type = MIME[ext] || "application/octet-stream";
|
|
3700
|
+
const data = fs.readFileSync(abs);
|
|
3701
|
+
const headers = {
|
|
3702
|
+
"Content-Type": type,
|
|
3703
|
+
"Content-Length": data.length,
|
|
3704
|
+
"Cache-Control": "no-store",
|
|
3705
|
+
};
|
|
3706
|
+
if (url.searchParams.get("download") === "1") {
|
|
3707
|
+
headers["Content-Disposition"] = workspaceDownloadContentDisposition(rel);
|
|
3708
|
+
}
|
|
3709
|
+
res.writeHead(200, headers);
|
|
3710
|
+
res.end(data);
|
|
3711
|
+
} catch (e) {
|
|
3712
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
3713
|
+
}
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3425
3717
|
if (req.method === "POST" && url.pathname === "/api/workspace/file") {
|
|
3426
3718
|
let payload;
|
|
3427
3719
|
try {
|
|
@@ -3458,6 +3750,54 @@ export function startUiServer({
|
|
|
3458
3750
|
return;
|
|
3459
3751
|
}
|
|
3460
3752
|
|
|
3753
|
+
if (req.method === "POST" && url.pathname === "/api/workspace/upload") {
|
|
3754
|
+
let parsed;
|
|
3755
|
+
try {
|
|
3756
|
+
parsed = await parseWorkspaceUploadForm(req);
|
|
3757
|
+
} catch (e) {
|
|
3758
|
+
json(res, /FILE_TOO_LARGE/.test(String(e.message || e)) ? 413 : 400, { error: (e && e.message) || String(e) });
|
|
3759
|
+
return;
|
|
3760
|
+
}
|
|
3761
|
+
try {
|
|
3762
|
+
if (!parsed.gotFile || !parsed.file.length) {
|
|
3763
|
+
json(res, 400, { error: "Missing upload file" });
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
3767
|
+
flowId: parsed.fields.flowId || "",
|
|
3768
|
+
flowSource: parsed.fields.flowSource || "user",
|
|
3769
|
+
archived: parsed.fields.archived === "1" || parsed.fields.archived === "true" || parsed.fields.flowArchived === "true",
|
|
3770
|
+
}, userCtx);
|
|
3771
|
+
if (scoped.error) {
|
|
3772
|
+
json(res, 400, { error: scoped.error });
|
|
3773
|
+
return;
|
|
3774
|
+
}
|
|
3775
|
+
if (scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
3776
|
+
json(res, 400, { error: "Cannot write to builtin or archived pipeline workspace" });
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
const safeName = sanitizeWorkspaceUploadName(parsed.filename);
|
|
3780
|
+
const ext = path.extname(safeName).toLowerCase();
|
|
3781
|
+
if (!WORKSPACE_IMAGE_EXTS.has(ext) || (parsed.mimeType && !/^image\//i.test(parsed.mimeType))) {
|
|
3782
|
+
json(res, 400, { error: "Only image uploads are supported" });
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
const targetDir = String(parsed.fields.dir || "img").trim().replace(/^[/\\]+/, "") || "img";
|
|
3786
|
+
const target = uniqueWorkspaceRelPath(scoped.root, path.posix.join(targetDir.replace(/\\/g, "/"), safeName));
|
|
3787
|
+
fs.mkdirSync(path.dirname(target.abs), { recursive: true });
|
|
3788
|
+
fs.writeFileSync(target.abs, parsed.file);
|
|
3789
|
+
json(res, 200, {
|
|
3790
|
+
ok: true,
|
|
3791
|
+
path: target.rel,
|
|
3792
|
+
size: parsed.file.length,
|
|
3793
|
+
mimeType: parsed.mimeType,
|
|
3794
|
+
});
|
|
3795
|
+
} catch (e) {
|
|
3796
|
+
json(res, /traversal/i.test(String(e.message || e)) ? 403 : 500, { error: (e && e.message) || String(e) });
|
|
3797
|
+
}
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3461
3801
|
if (req.method === "POST" && url.pathname === "/api/workspace/folder") {
|
|
3462
3802
|
let payload;
|
|
3463
3803
|
try {
|